From 97371daeffa340657dc8c600703c67d3c8926556 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 3 Aug 2026 17:12:26 -0600 Subject: [PATCH 1/3] test: Guard that import ldclient does not require aiohttp --- ldclient/testing/test_import_safety.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 ldclient/testing/test_import_safety.py diff --git a/ldclient/testing/test_import_safety.py b/ldclient/testing/test_import_safety.py new file mode 100644 index 00000000..1139a28e --- /dev/null +++ b/ldclient/testing/test_import_safety.py @@ -0,0 +1,30 @@ +import subprocess +import sys + + +def test_import_ldclient_does_not_require_aiohttp(): + """A bare ``import ldclient`` must not import aiohttp. + + aiohttp is an optional dependency (the ``async`` extra); sync-only installs + do not have it. The async code keeps its aiohttp-importing pieces (the async + client and HTTP transport) off the eager import path, so a sync-only user + can still ``import ldclient``. ``import ldclient`` also loads + ``ldclient.migrations`` (see ``ldclient/__init__.py``), so this covers the + async migration surface too. + + This guards the invariant across all async work: no change may make + ``import ldclient`` pull in aiohttp. A fresh interpreter is used so the + result is not affected by other tests that already imported aiohttp. + """ + code = "import ldclient, sys; sys.exit(1 if 'aiohttp' in sys.modules else 0)" + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, ( + "importing ldclient pulled in aiohttp. Keep aiohttp off the eager " + "import path (import async_client / transport lazily).\n" + "stderr:\n%s" % result.stderr + ) From 916196a818605c779a41f74bca57680b3fb832f6 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 3 Aug 2026 17:19:09 -0600 Subject: [PATCH 2/3] test: Move import-safety guard into test_aio.py --- ldclient/testing/test_aio.py | 27 +++++++++++++++++++++++ ldclient/testing/test_import_safety.py | 30 -------------------------- 2 files changed, 27 insertions(+), 30 deletions(-) delete mode 100644 ldclient/testing/test_import_safety.py diff --git a/ldclient/testing/test_aio.py b/ldclient/testing/test_aio.py index 6bf8d8ab..b62dc24c 100644 --- a/ldclient/testing/test_aio.py +++ b/ldclient/testing/test_aio.py @@ -6,6 +6,8 @@ """ import asyncio +import subprocess +import sys import threading import time @@ -522,3 +524,28 @@ def test_sse_factory_proxy_precedence(self): assert AsyncSSEFactory(cfg)._proxy == 'http://cfg-proxy:9000' # Neither set -> None (per-URL env fallback happens in create()). assert AsyncSSEFactory(Config('sdk-key'))._proxy is None + + +class TestImportSafety: + def test_import_ldclient_does_not_require_aiohttp(self): + """A bare ``import ldclient`` must not import aiohttp. + + aiohttp is an optional dependency (the ``async`` extra), so sync-only + installs do not have it. The async code keeps its aiohttp-importing + pieces (the async client and HTTP transport) off the eager import path. + ``import ldclient`` also loads ``ldclient.migrations``, so this covers + the async migration surface too. A fresh interpreter is used so the + result is not affected by other tests that already imported aiohttp. + """ + code = "import ldclient, sys; sys.exit(1 if 'aiohttp' in sys.modules else 0)" + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, ( + "importing ldclient pulled in aiohttp. Keep aiohttp off the eager " + "import path (import async_client / transport lazily).\n" + "stderr:\n%s" % result.stderr + ) diff --git a/ldclient/testing/test_import_safety.py b/ldclient/testing/test_import_safety.py deleted file mode 100644 index 1139a28e..00000000 --- a/ldclient/testing/test_import_safety.py +++ /dev/null @@ -1,30 +0,0 @@ -import subprocess -import sys - - -def test_import_ldclient_does_not_require_aiohttp(): - """A bare ``import ldclient`` must not import aiohttp. - - aiohttp is an optional dependency (the ``async`` extra); sync-only installs - do not have it. The async code keeps its aiohttp-importing pieces (the async - client and HTTP transport) off the eager import path, so a sync-only user - can still ``import ldclient``. ``import ldclient`` also loads - ``ldclient.migrations`` (see ``ldclient/__init__.py``), so this covers the - async migration surface too. - - This guards the invariant across all async work: no change may make - ``import ldclient`` pull in aiohttp. A fresh interpreter is used so the - result is not affected by other tests that already imported aiohttp. - """ - code = "import ldclient, sys; sys.exit(1 if 'aiohttp' in sys.modules else 0)" - result = subprocess.run( - [sys.executable, "-c", code], - capture_output=True, - text=True, - ) - - assert result.returncode == 0, ( - "importing ldclient pulled in aiohttp. Keep aiohttp off the eager " - "import path (import async_client / transport lazily).\n" - "stderr:\n%s" % result.stderr - ) From f8a06a6c2ed2accaef4f26c5fa16ef8ec1abb015 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 4 Aug 2026 09:44:13 -0600 Subject: [PATCH 3/3] docs: Trim the import-safety test docstring --- ldclient/testing/test_aio.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ldclient/testing/test_aio.py b/ldclient/testing/test_aio.py index b62dc24c..6f75754a 100644 --- a/ldclient/testing/test_aio.py +++ b/ldclient/testing/test_aio.py @@ -528,14 +528,9 @@ def test_sse_factory_proxy_precedence(self): class TestImportSafety: def test_import_ldclient_does_not_require_aiohttp(self): - """A bare ``import ldclient`` must not import aiohttp. - - aiohttp is an optional dependency (the ``async`` extra), so sync-only - installs do not have it. The async code keeps its aiohttp-importing - pieces (the async client and HTTP transport) off the eager import path. - ``import ldclient`` also loads ``ldclient.migrations``, so this covers - the async migration surface too. A fresh interpreter is used so the - result is not affected by other tests that already imported aiohttp. + """A bare ``import ldclient`` must not import anything that requires an + optional dependency. aiohttp (the ``async`` extra) is optional, so a + sync-only install must still be able to import ldclient. """ code = "import ldclient, sys; sys.exit(1 if 'aiohttp' in sys.modules else 0)" result = subprocess.run(