pytest_pg 0.0.28a1__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_pg/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ from importlib.metadata import version as _get_version
2
+
3
+ from .fixtures import PG, pg, pg_11, pg_12, pg_13, pg_14, pg_15, pg_16, pg_17, pg_18, run_pg
4
+
5
+ __all__: tuple[str, ...] = (
6
+ "PG",
7
+ "run_pg",
8
+ "pg",
9
+ "pg_11",
10
+ "pg_12",
11
+ "pg_13",
12
+ "pg_14",
13
+ "pg_15",
14
+ "pg_16",
15
+ "pg_17",
16
+ "pg_18",
17
+ )
18
+
19
+ __version__ = _get_version("pytest_pg")
pytest_pg/fixtures.py ADDED
@@ -0,0 +1,139 @@
1
+ import contextlib
2
+ import dataclasses
3
+ import time
4
+ import uuid
5
+ from collections.abc import Generator
6
+
7
+ import docker
8
+ import docker.errors
9
+ import pytest
10
+
11
+ from .utils import find_unused_local_port, is_pg_ready, resolve_docker_host
12
+
13
+ LOCALHOST = "127.0.0.1"
14
+ DEFAULT_PG_USER = "postgres"
15
+ DEFAULT_PG_PASSWORD = "mysecretpassword"
16
+ DEFAULT_PG_DATABASE = "postgres"
17
+
18
+
19
+ @dataclasses.dataclass(frozen=True)
20
+ class PG:
21
+ host: str
22
+ port: int
23
+ user: str
24
+ password: str
25
+ database: str
26
+
27
+
28
+ @contextlib.contextmanager
29
+ def run_pg(image: str, ready_timeout: float = 30.0) -> Generator[PG, None, None]:
30
+ docker_client = docker.APIClient(base_url=resolve_docker_host(), version="auto")
31
+ try:
32
+ docker_client.inspect_image(image)
33
+ except docker.errors.ImageNotFound:
34
+ docker_client.pull(image)
35
+
36
+ unused_port = find_unused_local_port()
37
+
38
+ postgresql_data_path = "/var/lib/postgresql/data"
39
+
40
+ container = docker_client.create_container(
41
+ image=image,
42
+ name=f"pytest-pg-{uuid.uuid4()}",
43
+ ports=[5432],
44
+ detach=True,
45
+ host_config=docker_client.create_host_config(
46
+ port_bindings={5432: (LOCALHOST, unused_port)}, tmpfs=[postgresql_data_path]
47
+ ),
48
+ environment={
49
+ "POSTGRES_HOST_AUTH_METHOD": "trust",
50
+ "PGDATA": postgresql_data_path,
51
+ "POSTGRES_INITDB_ARGS": "--no-sync",
52
+ },
53
+ command="-c fsync=off -c full_page_writes=off -c synchronous_commit=off -c bgwriter_lru_maxpages=0 -c jit=off",
54
+ )
55
+
56
+ try:
57
+ docker_client.start(container=container["Id"])
58
+
59
+ started_at = time.monotonic()
60
+
61
+ while time.monotonic() - started_at < ready_timeout:
62
+ if is_pg_ready(
63
+ host=LOCALHOST,
64
+ port=unused_port,
65
+ database=DEFAULT_PG_DATABASE,
66
+ user=DEFAULT_PG_USER,
67
+ password=DEFAULT_PG_PASSWORD,
68
+ ):
69
+ break
70
+
71
+ time.sleep(0.05)
72
+ else:
73
+ container_logs = docker_client.logs(container["Id"]).decode()
74
+ pytest.fail(f"Failed to start postgres using {image} in {ready_timeout} seconds: {container_logs}")
75
+
76
+ yield PG(
77
+ host=LOCALHOST,
78
+ port=unused_port,
79
+ user=DEFAULT_PG_USER,
80
+ password=DEFAULT_PG_PASSWORD,
81
+ database=DEFAULT_PG_DATABASE,
82
+ )
83
+ finally:
84
+ docker_client.kill(container=container["Id"])
85
+ docker_client.remove_container(container["Id"], v=True)
86
+
87
+
88
+ @pytest.fixture(scope="session")
89
+ def pg() -> Generator[PG, None, None]:
90
+ with run_pg("postgres:latest") as pg:
91
+ yield pg
92
+
93
+
94
+ @pytest.fixture(scope="session")
95
+ def pg_11() -> Generator[PG, None, None]:
96
+ with run_pg("postgres:11") as pg:
97
+ yield pg
98
+
99
+
100
+ @pytest.fixture(scope="session")
101
+ def pg_12() -> Generator[PG, None, None]:
102
+ with run_pg("postgres:12") as pg:
103
+ yield pg
104
+
105
+
106
+ @pytest.fixture(scope="session")
107
+ def pg_13() -> Generator[PG, None, None]:
108
+ with run_pg("postgres:13") as pg:
109
+ yield pg
110
+
111
+
112
+ @pytest.fixture(scope="session")
113
+ def pg_14() -> Generator[PG, None, None]:
114
+ with run_pg("postgres:14") as pg:
115
+ yield pg
116
+
117
+
118
+ @pytest.fixture(scope="session")
119
+ def pg_15() -> Generator[PG, None, None]:
120
+ with run_pg("postgres:15") as pg:
121
+ yield pg
122
+
123
+
124
+ @pytest.fixture(scope="session")
125
+ def pg_16() -> Generator[PG, None, None]:
126
+ with run_pg("postgres:16") as pg:
127
+ yield pg
128
+
129
+
130
+ @pytest.fixture(scope="session")
131
+ def pg_17() -> Generator[PG, None, None]:
132
+ with run_pg("postgres:17") as pg:
133
+ yield pg
134
+
135
+
136
+ @pytest.fixture(scope="session")
137
+ def pg_18() -> Generator[PG, None, None]:
138
+ with run_pg("postgres:18") as pg:
139
+ yield pg
pytest_pg/py.typed ADDED
@@ -0,0 +1 @@
1
+ Marker
pytest_pg/utils.py ADDED
@@ -0,0 +1,118 @@
1
+ import asyncio
2
+ import os
3
+ import shutil
4
+ import socket
5
+ import subprocess
6
+ from typing import Any, Protocol
7
+
8
+
9
+ class IsReadyFunc(Protocol):
10
+ def __call__(
11
+ self,
12
+ *,
13
+ host: str,
14
+ port: int,
15
+ database: str,
16
+ user: str,
17
+ password: str,
18
+ ) -> bool: ...
19
+
20
+
21
+ def _try_get_is_postgres_ready_based_on_psycopg2() -> IsReadyFunc | None:
22
+ try:
23
+ # noinspection PyPackageRequirements
24
+ import psycopg2
25
+
26
+ def _is_postgres_ready(**params: Any) -> bool:
27
+ try:
28
+ with psycopg2.connect(**params, connect_timeout=5):
29
+ return True
30
+ except psycopg2.OperationalError:
31
+ return False
32
+
33
+ return _is_postgres_ready
34
+ except ImportError:
35
+ return None
36
+
37
+
38
+ def _try_get_is_postgres_ready_based_on_psycopg3() -> IsReadyFunc | None:
39
+ try:
40
+ import psycopg # pyright: ignore[reportMissingImports]
41
+
42
+ def _is_postgres_ready(**params: Any) -> bool:
43
+ try:
44
+ params.pop("database")
45
+ with psycopg.connect(**params, connect_timeout=5):
46
+ return True
47
+ except psycopg.OperationalError:
48
+ return False
49
+
50
+ return _is_postgres_ready
51
+ except ImportError:
52
+ return None
53
+
54
+
55
+ def _try_get_is_postgres_ready_based_on_asyncpg() -> IsReadyFunc | None:
56
+ try:
57
+ # noinspection PyPackageRequirements
58
+ import asyncpg # pyright: ignore[reportMissingImports]
59
+
60
+ def _is_postgres_ready(**params: Any) -> bool:
61
+ async def _is_postgres_ready_async() -> bool:
62
+ try:
63
+ connection = await asyncpg.connect(**params, timeout=5.0)
64
+ await connection.close()
65
+ return True
66
+ except (asyncpg.exceptions.PostgresError, OSError):
67
+ return False
68
+
69
+ return asyncio.run(_is_postgres_ready_async())
70
+
71
+ return _is_postgres_ready
72
+
73
+ except ImportError:
74
+ return None
75
+
76
+
77
+ def _get_dummy_is_postgresql_ready() -> IsReadyFunc:
78
+ def _is_postgres_ready(**_: Any) -> bool:
79
+ return True
80
+
81
+ return _is_postgres_ready
82
+
83
+
84
+ is_pg_ready = (
85
+ _try_get_is_postgres_ready_based_on_asyncpg()
86
+ or _try_get_is_postgres_ready_based_on_psycopg2()
87
+ or _try_get_is_postgres_ready_based_on_psycopg3()
88
+ or _get_dummy_is_postgresql_ready()
89
+ )
90
+
91
+
92
+ def find_unused_local_port() -> int:
93
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
94
+ s.bind(("127.0.0.1", 0))
95
+ return s.getsockname()[1] # type: ignore
96
+
97
+
98
+ def resolve_docker_host() -> str | None:
99
+ # The `docker` Python SDK doesn't read `docker context` like the CLI does.
100
+ # Hand DOCKER_HOST to it from the active context so non-default setups
101
+ # (Colima, custom contexts) work without the user exporting it manually.
102
+ # No-op when the docker CLI is missing or DOCKER_HOST is already set.
103
+ host = os.environ.get("DOCKER_HOST")
104
+ if host:
105
+ return host
106
+ if shutil.which("docker") is None:
107
+ return None
108
+ try:
109
+ result = subprocess.run(
110
+ ["docker", "context", "inspect", "--format", "{{.Endpoints.docker.Host}}"],
111
+ capture_output=True,
112
+ text=True,
113
+ check=True,
114
+ timeout=5,
115
+ )
116
+ except (OSError, subprocess.SubprocessError):
117
+ return None
118
+ return result.stdout.strip() or None
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest_pg
3
+ Version: 0.0.28a1
4
+ Summary: A tiny plugin for pytest which runs PostgreSQL in Docker
5
+ Project-URL: Homepage, https://github.com/anna-money/pytest-pg
6
+ Author-email: Yury Pliner <yury.pliner@gmail.com>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Framework :: Pytest
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: docker>=7.0.0
23
+ Requires-Dist: pytest>=8.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pyright==1.1.407; extra == 'dev'
26
+ Requires-Dist: pytest==9.0.2; extra == 'dev'
27
+ Requires-Dist: ruff==0.14.10; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # pytest-pg
31
+
32
+ A pytest plugin that provides session-scoped fixtures for running PostgreSQL inside Docker containers.
33
+ It automatically spins up a container, waits for PostgreSQL to become ready, exposes connection details
34
+ (`host`, `port`, `user`, `password`, `database`) via a `PG` dataclass, and tears the container down after
35
+ the test session. Pre-built fixtures are available for PostgreSQL versions 11 through 18 as well as `latest`.
36
+
37
+ Readiness checks work with any of the common drivers — asyncpg, psycopg2, or psycopg3.
38
+
39
+ To speed up tests, pytest-pg does the following tweaks:
40
+
41
+ 1. fsync=off
42
+ 2. full_page_writes=off
43
+ 3. synchronous_commit=off
44
+ 4. jit=off
45
+ 5. bgwriter_lru_maxpages=0
46
+ 6. data directory is mounted to a tmpfs
47
+
48
+
49
+ # How to use?
50
+
51
+ You can use the following fixtures:
52
+
53
+ * `pg` – the latest PostgreSQL image available
54
+ * `pg_11` – PostgreSQL 11
55
+ * `pg_12` – PostgreSQL 12
56
+ * `pg_13` – PostgreSQL 13
57
+ * `pg_14` – PostgreSQL 14
58
+ * `pg_15` – PostgreSQL 15
59
+ * `pg_16` – PostgreSQL 16
60
+ * `pg_17` – PostgreSQL 17
61
+ * `pg_18` – PostgreSQL 18
62
+
63
+ ```python
64
+ import asyncpg
65
+
66
+
67
+ async def test_asyncpg_query(pg):
68
+ conn = await asyncpg.connect(
69
+ user=pg.user,
70
+ password=pg.password,
71
+ database=pg.database,
72
+ host=pg.host,
73
+ port=pg.port,
74
+ )
75
+
76
+ await conn.execute("CREATE TABLE test_table (id serial PRIMARY KEY, value text);")
77
+ await conn.execute("INSERT INTO test_table (value) VALUES ($1)", "hello")
78
+ row = await conn.fetchrow("SELECT value FROM test_table WHERE id = $1", 1)
79
+
80
+ assert row["value"] == "hello"
81
+
82
+ await conn.close()
83
+ ```
84
+
85
+
86
+ Also `run_pg` context manager is available, you can use it to create your own fixture, using docker image you need:
87
+
88
+ ```python
89
+ import os
90
+
91
+ import pytest
92
+ import pytest_pg
93
+
94
+
95
+ @pytest.fixture(scope='session', autouse=True)
96
+ def postgres_env_vars() -> Generator[None]:
97
+ docker_image = 'postgres:18'
98
+ with pytest_pg.run_pg(docker_image) as pg:
99
+ os.environ['POSTGRES_USER'] = pg.user
100
+ os.environ['POSTGRES_PASSWORD'] = pg.password
101
+ os.environ['POSTGRES_HOST'] = pg.host
102
+ os.environ['POSTGRES_PORT'] = str(pg.port)
103
+ os.environ['POSTGRES_DBNAME'] = pg.database
104
+ yield
105
+
106
+
107
+ # or like so:
108
+ @pytest.fixture(scope='session', autouse=True)
109
+ def postgres_env_vars(pg_18: pytest_pg.PG) -> Generator[None]:
110
+ os.environ['POSTGRES_USER'] = pg_18.user
111
+ os.environ['POSTGRES_PASSWORD'] = pg_18.password
112
+ os.environ['POSTGRES_HOST'] = pg_18.host
113
+ os.environ['POSTGRES_PORT'] = str(pg_18.port)
114
+ os.environ['POSTGRES_DBNAME'] = pg_18.database
115
+ yield
116
+ ```
@@ -0,0 +1,9 @@
1
+ pytest_pg/__init__.py,sha256=UkijuJE4fi_pOvZ1D1qwiqRSmU5LDXWoyX8fa-KKH04,360
2
+ pytest_pg/fixtures.py,sha256=9tknPHXI0tAVlJNRK_XZZVUjxgUPBGYG0R7nJiMt18w,3694
3
+ pytest_pg/py.typed,sha256=E84IaZyFwfLqvXjOVW4LS6WH7QOaKEFpNh9TFyzHNQc,6
4
+ pytest_pg/utils.py,sha256=KkulNEwm_u8_-iSiWOMjE1x65jGSGttpBCe0rOXJj6c,3410
5
+ pytest_pg-0.0.28a1.dist-info/METADATA,sha256=s0kWfRqLSbW6ysxkJQQ6QMLrmSe8IXXjvWK2OKTel90,3719
6
+ pytest_pg-0.0.28a1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ pytest_pg-0.0.28a1.dist-info/entry_points.txt,sha256=YItrIbbORp9wVDzM2aXaYi6XDi7Qw38rD3RaqAoZLe8,33
8
+ pytest_pg-0.0.28a1.dist-info/licenses/LICENSE,sha256=aVmBXoo4joWuu-u1A14wOPPZKjKO4JZUOo4LuX4RQqQ,1068
9
+ pytest_pg-0.0.28a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ pytest_pg = pytest_pg
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Yury Pliner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.