From 8337b67351a45c63acf4151eca14688e39c72a84 Mon Sep 17 00:00:00 2001 From: Sean Whalen <44679+seanthegeek@users.noreply.github.com> Date: Fri, 12 Jun 2026 21:48:20 -0400 Subject: [PATCH] Cover both arms of the optional psycopg import in postgres.py (#799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module-level try/except import is environment-dependent: with psycopg installed the ImportError fallback never runs, and without it (CI's test job) the successful-import arm never completes — so Codecov flags one side or the other no matter where coverage is measured (it flagged the import line on master right after #798 merged). Exercise both arms explicitly: execute the module's source into a fresh, throwaway module object (importlib.util.module_from_spec + exec_module) under a patched sys.modules — a None entry forces ImportError, fake module entries force the success path — and assert on the psycopg / psycopg_json bindings each arm produces. The throwaway-module approach (rather than importlib.reload) leaves the canonical parsedmarc.postgres untouched, so the identity of PostgreSQLError / AlreadySaved held by the rest of the test module is preserved. Verified covered in both environments: with the venv's real psycopg, and with psycopg hidden via a PYTHONPATH shim to simulate CI; the import block reports no missing lines either way. Co-authored-by: Claude Fable 5 --- tests/test_postgres.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_postgres.py b/tests/test_postgres.py index 4b05d21..3065220 100644 --- a/tests/test_postgres.py +++ b/tests/test_postgres.py @@ -7,7 +7,9 @@ the bound parameters that a real PostgreSQL server would receive, plus the real-sample round trip, so the tests fail if the dict-key mapping regresses. """ +import importlib.util import os +import sys import unittest from glob import glob from typing import cast @@ -47,6 +49,51 @@ def tearDownModule(): _types_patcher.stop() +class TestPsycopgImportFallback(unittest.TestCase): + """The module-level psycopg import has two arms — psycopg installed + and psycopg missing — and a normal test run only ever executes the + arm that matches the environment. Re-execute the module's source + with sys.modules manipulated to force each arm, so both are + exercised (and covered) whether or not psycopg is installed.""" + + def _exec_fresh_module_with(self, modules): + """Execute parsedmarc.postgres's source into a fresh, throwaway + module object under a patched sys.modules, and return the + (psycopg, psycopg_json) it bound. The canonical module in + sys.modules is untouched, so the identity of its classes + (PostgreSQLError, AlreadySaved, ...) that other tests hold + references to is preserved.""" + spec = importlib.util.find_spec("parsedmarc.postgres") + assert spec is not None and spec.loader is not None + mod = importlib.util.module_from_spec(spec) + with patch.dict(sys.modules, modules): + spec.loader.exec_module(mod) + return mod.psycopg, mod.psycopg_json + + def test_missing_psycopg_falls_back_to_none(self): + """A None entry in sys.modules makes ``import psycopg`` raise + ImportError, exercising the fallback arm.""" + psycopg_mod, json_mod = self._exec_fresh_module_with( + {"psycopg": None, "psycopg.types": None, "psycopg.types.json": None} + ) + self.assertIsNone(psycopg_mod) + self.assertIsNone(json_mod) + + def test_installed_psycopg_binds_module_and_json_submodule(self): + fake_json = MagicMock() + fake_types = MagicMock(json=fake_json) + fake_psycopg = MagicMock(types=fake_types) + psycopg_mod, json_mod = self._exec_fresh_module_with( + { + "psycopg": fake_psycopg, + "psycopg.types": fake_types, + "psycopg.types.json": fake_json, + } + ) + self.assertIs(psycopg_mod, fake_psycopg) + self.assertIs(json_mod, fake_json) + + class TestPostgreSQLHelpers(unittest.TestCase): """Unit tests for the pure helper functions in parsedmarc.postgres."""