pytest-dblift 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_dblift/__init__.py +3 -0
- pytest_dblift/_client.py +71 -0
- pytest_dblift/fixtures.py +84 -0
- pytest_dblift/plugin.py +31 -0
- pytest_dblift-0.1.0.dist-info/METADATA +92 -0
- pytest_dblift-0.1.0.dist-info/RECORD +9 -0
- pytest_dblift-0.1.0.dist-info/WHEEL +5 -0
- pytest_dblift-0.1.0.dist-info/entry_points.txt +2 -0
- pytest_dblift-0.1.0.dist-info/top_level.txt +1 -0
pytest_dblift/_client.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""URL resolution and DBLiftClient construction for pytest-dblift fixtures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from api import DBLiftClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _worker_id(config: pytest.Config) -> str:
|
|
14
|
+
"""Return xdist worker id ('gw0', ...) or 'master' when not under xdist."""
|
|
15
|
+
workerinput = getattr(config, "workerinput", None)
|
|
16
|
+
if workerinput:
|
|
17
|
+
return workerinput.get("workerid", "master")
|
|
18
|
+
return "master"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def default_sqlite_file_url(
|
|
22
|
+
tmp_path_factory: pytest.TempPathFactory, config: pytest.Config | None = None
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Session-scoped temp SQLite file URL. Under xdist, suffix the filename with the worker id."""
|
|
25
|
+
base = tmp_path_factory.mktemp("dblift_pytest", numbered=True)
|
|
26
|
+
wid = _worker_id(config) if config is not None else "master"
|
|
27
|
+
if wid != "master":
|
|
28
|
+
db_path = base / f"test_{wid}.db"
|
|
29
|
+
else:
|
|
30
|
+
db_path = base / "test.db"
|
|
31
|
+
return f"sqlite:///{db_path}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_dblift_config(
|
|
35
|
+
pytestconfig: pytest.Config,
|
|
36
|
+
*,
|
|
37
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Build config dict from CLI options + defaults.
|
|
40
|
+
|
|
41
|
+
Returns dict with 'url' and 'migrations_dir' (absolute path str).
|
|
42
|
+
A relative migrations dir is resolved against pytest rootdir.
|
|
43
|
+
"""
|
|
44
|
+
url = pytestconfig.getoption("--dblift-url")
|
|
45
|
+
if not url:
|
|
46
|
+
url = default_sqlite_file_url(tmp_path_factory, pytestconfig)
|
|
47
|
+
|
|
48
|
+
raw_mig = pytestconfig.getoption("--dblift-migrations-dir") or "migrations"
|
|
49
|
+
rootdir = getattr(pytestconfig, "rootdir", None) or Path.cwd()
|
|
50
|
+
rootdir = Path(rootdir)
|
|
51
|
+
mig_path = Path(raw_mig)
|
|
52
|
+
if not mig_path.is_absolute():
|
|
53
|
+
mig_path = (rootdir / mig_path).resolve()
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"url": url,
|
|
57
|
+
"migrations_dir": str(mig_path),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_dblift_client(
|
|
62
|
+
engine: Any,
|
|
63
|
+
*,
|
|
64
|
+
migrations_dir: str | Path | list[str | Path] | None,
|
|
65
|
+
schema: str | None = None,
|
|
66
|
+
) -> DBLiftClient:
|
|
67
|
+
return DBLiftClient.from_sqlalchemy(
|
|
68
|
+
engine,
|
|
69
|
+
migrations_dir=migrations_dir,
|
|
70
|
+
schema=schema,
|
|
71
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""pytest-dblift fixtures.
|
|
2
|
+
|
|
3
|
+
Session scope for config/engine/client. Function scope for migrate/clean/validate/undo.
|
|
4
|
+
No autouse: tests opt in by requesting a fixture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Iterator
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from sqlalchemy import create_engine
|
|
13
|
+
|
|
14
|
+
from api import DBLiftClient
|
|
15
|
+
|
|
16
|
+
from ._client import create_dblift_client, resolve_dblift_config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="session")
|
|
20
|
+
def dblift_config(
|
|
21
|
+
pytestconfig: pytest.Config, tmp_path_factory: pytest.TempPathFactory
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Session config from CLI or temp SQLite. Overridable in consumer conftest.py."""
|
|
24
|
+
return resolve_dblift_config(pytestconfig, tmp_path_factory=tmp_path_factory)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="session")
|
|
28
|
+
def dblift_engine(dblift_config: dict[str, Any]) -> Iterator[Any]:
|
|
29
|
+
engine = create_engine(dblift_config["url"])
|
|
30
|
+
yield engine
|
|
31
|
+
engine.dispose()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(scope="session")
|
|
35
|
+
def dblift_client(dblift_engine: Any, dblift_config: dict[str, Any]) -> Iterator[DBLiftClient]:
|
|
36
|
+
client = create_dblift_client(
|
|
37
|
+
dblift_engine,
|
|
38
|
+
migrations_dir=dblift_config.get("migrations_dir"),
|
|
39
|
+
schema=dblift_config.get("schema"),
|
|
40
|
+
)
|
|
41
|
+
yield client
|
|
42
|
+
client.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def dblift_migrated_db(dblift_client: DBLiftClient) -> Iterator[DBLiftClient]:
|
|
47
|
+
result = dblift_client.migrate()
|
|
48
|
+
assert getattr(result, "success", False), (
|
|
49
|
+
f"migrate failed: {getattr(result, 'error_message', result)}"
|
|
50
|
+
)
|
|
51
|
+
yield dblift_client
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def dblift_empty_db(dblift_client: DBLiftClient) -> Iterator[DBLiftClient]:
|
|
56
|
+
result = dblift_client.clean(clean_enabled=True)
|
|
57
|
+
assert getattr(result, "success", False), (
|
|
58
|
+
f"clean failed: {getattr(result, 'error_message', result)}"
|
|
59
|
+
)
|
|
60
|
+
yield dblift_client
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.fixture
|
|
64
|
+
def dblift_validate(dblift_client: DBLiftClient) -> Callable[..., Any]:
|
|
65
|
+
def _run_validate(**kwargs: Any) -> Any:
|
|
66
|
+
result = dblift_client.validate(**kwargs)
|
|
67
|
+
assert getattr(result, "success", False), (
|
|
68
|
+
f"validate failed: {getattr(result, 'error_message', result)}"
|
|
69
|
+
)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
return _run_validate
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def dblift_undo(dblift_client: DBLiftClient) -> Callable[..., Any]:
|
|
77
|
+
def _run_undo(**kwargs: Any) -> Any:
|
|
78
|
+
result = dblift_client.undo(**kwargs)
|
|
79
|
+
assert getattr(result, "success", False), (
|
|
80
|
+
f"undo failed: {getattr(result, 'error_message', result)}"
|
|
81
|
+
)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
return _run_undo
|
pytest_dblift/plugin.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""pytest11 entry: CLI options and fixture loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
pytest_plugins = ["pytest_dblift.fixtures"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
11
|
+
group = parser.getgroup("dblift", "dblift pytest integration")
|
|
12
|
+
group.addoption(
|
|
13
|
+
"--dblift-url",
|
|
14
|
+
action="store",
|
|
15
|
+
default=None,
|
|
16
|
+
help="Database URL for dblift (e.g. sqlite:////tmp/test.db or postgresql+psycopg://...). "
|
|
17
|
+
"Used when no dblift_config fixture override is provided.",
|
|
18
|
+
)
|
|
19
|
+
group.addoption(
|
|
20
|
+
"--dblift-migrations-dir",
|
|
21
|
+
action="store",
|
|
22
|
+
default="migrations",
|
|
23
|
+
help="Path to the migrations directory. Defaults to migrations.",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
28
|
+
config.addinivalue_line(
|
|
29
|
+
"markers",
|
|
30
|
+
"dblift: marks tests as using dblift fixtures (provided by pytest-dblift)",
|
|
31
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-dblift
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pytest plugin for DBLift migrations
|
|
5
|
+
Author: DBLift
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/dblift/dblift
|
|
8
|
+
Classifier: Framework :: Pytest
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: dblift>=3.9
|
|
16
|
+
Requires-Dist: pytest>=7.3
|
|
17
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-xdist>=3.3; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# pytest-dblift
|
|
22
|
+
|
|
23
|
+
pytest plugin for [DBLift](https://github.com/dblift/dblift). It applies your migrations in tests and exposes a `DBLiftClient`.
|
|
24
|
+
|
|
25
|
+
This is a **separate PyPI package**, not `dblift[pytest]`.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
SQLite (default, stdlib driver):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pytest-dblift
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Any other engine: install the matching dblift extra so the native driver is present. The plugin does not install drivers and does not open a second connection — it calls `create_engine(url)` then `DBLiftClient.from_sqlalchemy`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pytest-dblift "dblift[postgresql]"
|
|
39
|
+
pytest --dblift-url "postgresql+psycopg://user:pass@localhost/app_test"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
def test_schema(dblift_migrated_db, dblift_client):
|
|
46
|
+
info = dblift_client.info()
|
|
47
|
+
assert info.pending_migrations == []
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`dblift_migrated_db` applies pending migrations (function scope). `dblift_client` is a session-scoped `DBLiftClient`. No fixture is autouse: request what you need.
|
|
51
|
+
|
|
52
|
+
## Fixtures
|
|
53
|
+
|
|
54
|
+
| Fixture | Scope | Role |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `dblift_config` | session | Dict with `url`, `migrations_dir`, optional `schema`. Override in `conftest.py`. |
|
|
57
|
+
| `dblift_engine` | session | SQLAlchemy engine from that URL. Override to inject your app engine. |
|
|
58
|
+
| `dblift_client` | session | `DBLiftClient.from_sqlalchemy(...)`. |
|
|
59
|
+
| `dblift_migrated_db` | function | `client.migrate()`, then yield the client. |
|
|
60
|
+
| `dblift_empty_db` | function | `client.clean(clean_enabled=True)`, then yield the client. |
|
|
61
|
+
| `dblift_validate` | function | Callable: `dblift_validate(**kwargs)` runs `client.validate` and asserts success. |
|
|
62
|
+
| `dblift_undo` | function | Callable: `dblift_undo(**kwargs)` runs `client.undo` and asserts success. Does not migrate. |
|
|
63
|
+
|
|
64
|
+
Undo uses companion `U*` scripts (same as dblift). There is no `undo()` function inside a `V*.py` file.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
def test_rollback(dblift_migrated_db, dblift_undo):
|
|
68
|
+
result = dblift_undo(target_version="0")
|
|
69
|
+
assert result.success
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Override config in `tests/conftest.py`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import pytest
|
|
76
|
+
|
|
77
|
+
@pytest.fixture(scope="session")
|
|
78
|
+
def dblift_config():
|
|
79
|
+
return {
|
|
80
|
+
"url": "postgresql+psycopg://user:pass@localhost/app_test",
|
|
81
|
+
"migrations_dir": "migrations",
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## CLI options
|
|
86
|
+
|
|
87
|
+
| Option | What it does |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `--dblift-url` | Database URL when `dblift_config` is not overridden. Default: a temp SQLite **file**. |
|
|
90
|
+
| `--dblift-migrations-dir` | One migrations directory (not a comma-separated list). Default: `migrations`. |
|
|
91
|
+
|
|
92
|
+
pytest-xdist: only the default SQLite file is per-worker (`test_gw0.db`, …). A URL you pass with `--dblift-url` is used as-is.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pytest_dblift/__init__.py,sha256=ug4A_sjForoM-WH-Yz0oG1mr2ZsKpJTWlJRaNHFAXMg,81
|
|
2
|
+
pytest_dblift/_client.py,sha256=788Z9jNmKVUnfw9v5O0XlD35tlapKROtNhUHx7Z0Z0w,2111
|
|
3
|
+
pytest_dblift/fixtures.py,sha256=YwtHGpDtbRRfNomwovuPly4UIRchgc6Dr5Aco_06GTI,2535
|
|
4
|
+
pytest_dblift/plugin.py,sha256=jSqJpEyiciPRWvocyxLxfywmdfwsxdQXQRvQpClUoAA,920
|
|
5
|
+
pytest_dblift-0.1.0.dist-info/METADATA,sha256=PoLjl35UfMUtr4H2NDIwWBc_ziT9zbkZSzPDBMYZgOM,3288
|
|
6
|
+
pytest_dblift-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
pytest_dblift-0.1.0.dist-info/entry_points.txt,sha256=YxMThYqLZg4KkQg2d6bA3sPHZPkACTBgaQMwMK870ms,41
|
|
8
|
+
pytest_dblift-0.1.0.dist-info/top_level.txt,sha256=jJf1n0xrr0N-iEXJYOFdEtgenXQrivW5FZps5PMEvZY,14
|
|
9
|
+
pytest_dblift-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_dblift
|