From 4d90ca1cc2d0ecb2d329adf3573de77892215088 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Fri, 17 Jul 2026 07:41:04 +0200 Subject: [PATCH] improve --- README.md | 8 ++++++++ pgdevkit/testdb/api.py | 4 ++-- pgdevkit/testdb/constants.py | 17 +++++++++++++++++ pgdevkit/testdb/container.py | 10 ++-------- tests/conftest.py | 14 ++++++++++---- tests/db/test_crud.py | 4 ++-- tests/test_fetch_missing.py | 4 ++-- tests/test_fetch_missing_cli.py | 4 ++-- tests/testdb/test_api.py | 2 +- tests/testdb/test_container.py | 5 +---- tests/testdb/test_query.py | 4 ++-- tests/testdb/test_schema.py | 4 ++-- 12 files changed, 51 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 3c62aef..1e5136e 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,14 @@ is already reachable at that address and skips container management if so. Set `PGDEVKIT_SKIP_CONTAINER=1` to always assume it's already there and skip that check too. +To point at a local Postgres install instead of the podman container — +useful when podman isn't available, or you'd rather use peer authentication +as the current OS user — set `PGDEVKIT_TESTDB_HOST` to the unix socket +directory (e.g. `/var/run/postgresql`) and `PGDEVKIT_TESTDB_PASSWORD=""`. +The role named by `PGDEVKIT_TESTDB_USER` must exist and match your OS user +(`CREATE ROLE SUPERUSER LOGIN;`) and `pg_hba.conf` must allow `peer` +auth for local connections (Debian/Ubuntu Postgres ships this by default). + ## `pgdevkit.db` — helpers for application code Install with the `db` extra: `pip install pgdevkit[db]`. diff --git a/pgdevkit/testdb/api.py b/pgdevkit/testdb/api.py index 2337c9d..9b1fb0c 100644 --- a/pgdevkit/testdb/api.py +++ b/pgdevkit/testdb/api.py @@ -14,11 +14,11 @@ def _admin_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres?connect_timeout=10" + return constants.conninfo("postgres", connect_timeout=10) def _db_dsn(db_name: str) -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{db_name}?connect_timeout=10" + return constants.conninfo(db_name, connect_timeout=10) def _resolve(project_root: Path | None) -> tuple[ProjectConfig, str]: diff --git a/pgdevkit/testdb/constants.py b/pgdevkit/testdb/constants.py index a9c6a64..841552e 100644 --- a/pgdevkit/testdb/constants.py +++ b/pgdevkit/testdb/constants.py @@ -2,6 +2,8 @@ import os +from psycopg.conninfo import make_conninfo + CONTAINER_NAME = "pgdevkit-postgres" IMAGE = "pgvector/pgvector:pg18-trixie" HOST = os.environ.get("PGDEVKIT_TESTDB_HOST", "localhost") @@ -9,3 +11,18 @@ USER = os.environ.get("PGDEVKIT_TESTDB_USER", "postgres") PASSWORD = os.environ.get("PGDEVKIT_TESTDB_PASSWORD", "testpwd") PG_SPEED_FLAGS = ["-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] + + +def conninfo(dbname: str, *, connect_timeout: int | None = None) -> str: + """Build a libpq conninfo string from HOST/PORT/USER/PASSWORD. + + PASSWORD is omitted when empty (PGDEVKIT_TESTDB_PASSWORD="") so HOST can + be pointed at a unix socket directory (e.g. /var/run/postgresql) and + authenticate via peer auth as the current OS user instead of a password. + """ + params: dict[str, str | int] = {"host": HOST, "port": PORT, "user": USER, "dbname": dbname} + if PASSWORD: + params["password"] = PASSWORD + if connect_timeout is not None: + params["connect_timeout"] = connect_timeout + return make_conninfo(**params) diff --git a/pgdevkit/testdb/container.py b/pgdevkit/testdb/container.py index 48fa9e7..2319f23 100644 --- a/pgdevkit/testdb/container.py +++ b/pgdevkit/testdb/container.py @@ -21,10 +21,7 @@ def _available(timeout: float = 3.0) -> bool: # libpq's connect_timeout is whole seconds; anything below 1 means "wait # indefinitely" instead of a short timeout, so it's clamped up to 1. connect_timeout = max(1, round(timeout)) - dsn = ( - f"postgresql://{constants.USER}:{constants.PASSWORD}" - f"@{constants.HOST}:{constants.PORT}/postgres?connect_timeout={connect_timeout}" - ) + dsn = constants.conninfo("postgres", connect_timeout=connect_timeout) try: with psycopg.connect(dsn): return True @@ -62,10 +59,7 @@ def _create_container() -> None: def _wait_ready(timeout: float = 30.0) -> None: deadline = time.monotonic() + timeout - dsn = ( - f"postgresql://{constants.USER}:{constants.PASSWORD}" - f"@{constants.HOST}:{constants.PORT}/postgres?connect_timeout=2" - ) + dsn = constants.conninfo("postgres", connect_timeout=2) last_error: Exception | None = None while time.monotonic() < deadline: try: diff --git a/tests/conftest.py b/tests/conftest.py index 7bf0783..51ff586 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import psycopg import pytest +from psycopg.conninfo import make_conninfo from pgdevkit.testdb import ensure_testdb @@ -11,10 +12,15 @@ @pytest.fixture(scope="session") def postgres_dsn() -> str: env = ensure_testdb() - return ( - f"postgresql://{env['PGDEVKIT_POSTGRES_USER']}:{env['PGDEVKIT_POSTGRES_PASSWORD']}" - f"@{env['PGDEVKIT_POSTGRES_HOST']}:{env['PGDEVKIT_POSTGRES_PORT']}/{env['PGDEVKIT_POSTGRES_DB']}" - ) + params = { + "host": env["PGDEVKIT_POSTGRES_HOST"], + "port": env["PGDEVKIT_POSTGRES_PORT"], + "user": env["PGDEVKIT_POSTGRES_USER"], + "dbname": env["PGDEVKIT_POSTGRES_DB"], + } + if env["PGDEVKIT_POSTGRES_PASSWORD"]: + params["password"] = env["PGDEVKIT_POSTGRES_PASSWORD"] + return make_conninfo(**params) @pytest.fixture diff --git a/tests/db/test_crud.py b/tests/db/test_crud.py index 1d1cab1..2fdeb1a 100644 --- a/tests/db/test_crud.py +++ b/tests/db/test_crud.py @@ -26,7 +26,7 @@ def _admin_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres" + return constants.conninfo("postgres") class Widget(PostgresTableModel): @@ -49,7 +49,7 @@ async def pool(monkeypatch: pytest.MonkeyPatch): with psycopg.connect(_admin_dsn(), autocommit=True) as con: con.execute(f'DROP DATABASE IF EXISTS "{TEST_DB}"') con.execute(f'CREATE DATABASE "{TEST_DB}"') - with psycopg.connect(f"{_admin_dsn().rsplit('/', 1)[0]}/{TEST_DB}", autocommit=True) as con: + with psycopg.connect(constants.conninfo(TEST_DB), autocommit=True) as con: con.execute("CREATE TABLE widget (id serial PRIMARY KEY, name text NOT NULL)") monkeypatch.setenv(f"{ENV_PREFIX}HOST", constants.HOST) diff --git a/tests/test_fetch_missing.py b/tests/test_fetch_missing.py index e666230..a9b46c3 100644 --- a/tests/test_fetch_missing.py +++ b/tests/test_fetch_missing.py @@ -14,11 +14,11 @@ def _admin_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres" + return constants.conninfo("postgres") def _db_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{TEST_DB}" + return constants.conninfo(TEST_DB) def test_layer_folder_for_matches_stripped_sort_prefix(tmp_path: Path): diff --git a/tests/test_fetch_missing_cli.py b/tests/test_fetch_missing_cli.py index 0dd77c3..95d15a1 100644 --- a/tests/test_fetch_missing_cli.py +++ b/tests/test_fetch_missing_cli.py @@ -16,11 +16,11 @@ def _admin_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres" + return constants.conninfo("postgres") def _db_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{TEST_DB}" + return constants.conninfo(TEST_DB) @requires_podman diff --git a/tests/testdb/test_api.py b/tests/testdb/test_api.py index 25a34e5..65e8c73 100644 --- a/tests/testdb/test_api.py +++ b/tests/testdb/test_api.py @@ -14,7 +14,7 @@ def _admin_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres" + return constants.conninfo("postgres") @requires_podman diff --git a/tests/testdb/test_container.py b/tests/testdb/test_container.py index 69279fb..754a573 100644 --- a/tests/testdb/test_container.py +++ b/tests/testdb/test_container.py @@ -50,10 +50,7 @@ def _fail(*a, **k): def _admin_dsn() -> str: - return ( - f"postgresql://{constants.USER}:{constants.PASSWORD}" - f"@{constants.HOST}:{constants.PORT}/postgres?connect_timeout=5" - ) + return constants.conninfo("postgres", connect_timeout=5) @requires_podman diff --git a/tests/testdb/test_query.py b/tests/testdb/test_query.py index 17fcfe8..a317657 100644 --- a/tests/testdb/test_query.py +++ b/tests/testdb/test_query.py @@ -37,11 +37,11 @@ def test_split_statements_handles_tagged_dollar_quotes(): def _admin_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres" + return constants.conninfo("postgres") def _db_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{TEST_DB}" + return constants.conninfo(TEST_DB) @pytest.fixture diff --git a/tests/testdb/test_schema.py b/tests/testdb/test_schema.py index edf70d0..18f3d31 100644 --- a/tests/testdb/test_schema.py +++ b/tests/testdb/test_schema.py @@ -15,11 +15,11 @@ def _admin_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres" + return constants.conninfo("postgres") def _db_dsn() -> str: - return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{TEST_DB}" + return constants.conninfo(TEST_DB) @pytest.fixture