From 062911dec171d703d2717841cfdf851c162f629d Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Mon, 26 Oct 2026 12:51:03 +0100 Subject: [PATCH 1/5] Look up each module name without a package version only once --- aikido_zen/sinks/builtins_import.py | 7 ++++++- aikido_zen/sinks/tests/builtins_import_test.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/aikido_zen/sinks/builtins_import.py b/aikido_zen/sinks/builtins_import.py index fe6a00a2c..914bcbbd9 100644 --- a/aikido_zen/sinks/builtins_import.py +++ b/aikido_zen/sinks/builtins_import.py @@ -6,6 +6,9 @@ running_import_scan = contextvars.ContextVar("running_import_scan", default=False) +# version() scans all of sys.path, so names it cannot find (re, json) are looked up once. +names_without_version = set() + @after def _import(func, instance, args, kwargs, return_value): @@ -30,7 +33,7 @@ def _import(func, instance, args, kwargs, return_value): # Avoid circular dependencies, this is a double safety-check for if contextvar check fails. return - if PackagesStore.get_package(name): + if name in names_without_version or PackagesStore.get_package(name): return version = None @@ -40,6 +43,8 @@ def _import(func, instance, args, kwargs, return_value): pass if version: PackagesStore.add_package(name, version) + else: + names_without_version.add(name) finally: running_import_scan.set(False) diff --git a/aikido_zen/sinks/tests/builtins_import_test.py b/aikido_zen/sinks/tests/builtins_import_test.py index 9953417c3..a2261ee4d 100644 --- a/aikido_zen/sinks/tests/builtins_import_test.py +++ b/aikido_zen/sinks/tests/builtins_import_test.py @@ -1,15 +1,20 @@ +import importlib.metadata +from unittest.mock import patch + import pytest import aikido_zen aikido_zen.protect() +import aikido_zen.sinks.builtins_import as builtins_import from aikido_zen.background_process.packages import PackagesStore @pytest.fixture(autouse=True) def run_around_tests(): PackagesStore.clear() + builtins_import.names_without_version.clear() def test_flask_import(): @@ -24,6 +29,17 @@ def test_django_import(): assert PackagesStore.get_package("django")["version"] == "4.0" +def test_name_without_a_distribution_is_looked_up_once(): + with patch( + "importlib.metadata.version", wraps=importlib.metadata.version + ) as version: + for _ in range(10): + import json + + assert [c.args for c in version.call_args_list] == [("json",)] + assert PackagesStore.get_package("json") is None + + def test_recursive_package_store(monkeypatch): """Test that recursive imports during package scanning don't cause max recursion depth errors.""" From 567ac77719bb5652a1b8a28f8d3b4fab16626ad7 Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Mon, 26 Oct 2026 16:56:39 +0100 Subject: [PATCH 2/5] Move the version lookup and miss cache into PackagesStore --- aikido_zen/background_process/packages.py | 17 +++++++++++++++++ aikido_zen/sinks/builtins_import.py | 18 +----------------- aikido_zen/sinks/tests/builtins_import_test.py | 4 ++-- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/aikido_zen/background_process/packages.py b/aikido_zen/background_process/packages.py index 6b744bb9d..ecf7d9f7b 100644 --- a/aikido_zen/background_process/packages.py +++ b/aikido_zen/background_process/packages.py @@ -56,9 +56,26 @@ def is_version_supported(version, required_version): # packages store, uses python's built in GlobalInterpreterLock (GIL) packages_store = {} +# version() scans all of sys.path, so names it cannot find (re, json) are looked up once. +names_without_version = set() class PackagesStore: + @staticmethod + def add_installed_package(name): + """Stores the package's installed version, if it has one.""" + if name in names_without_version or PackagesStore.get_package(name): + return + version = None + try: + version = importlib_metadata.version(name) + except importlib_metadata.PackageNotFoundError: + pass + if version: + PackagesStore.add_package(name, version) + else: + names_without_version.add(name) + @staticmethod def export(): result = [] diff --git a/aikido_zen/sinks/builtins_import.py b/aikido_zen/sinks/builtins_import.py index 914bcbbd9..2a0481624 100644 --- a/aikido_zen/sinks/builtins_import.py +++ b/aikido_zen/sinks/builtins_import.py @@ -1,14 +1,9 @@ from aikido_zen.sinks import on_import, patch_function, after import contextvars -import importlib.metadata -from importlib.metadata import PackageNotFoundError from aikido_zen.background_process.packages import PackagesStore running_import_scan = contextvars.ContextVar("running_import_scan", default=False) -# version() scans all of sys.path, so names it cannot find (re, json) are looked up once. -names_without_version = set() - @after def _import(func, instance, args, kwargs, return_value): @@ -33,18 +28,7 @@ def _import(func, instance, args, kwargs, return_value): # Avoid circular dependencies, this is a double safety-check for if contextvar check fails. return - if name in names_without_version or PackagesStore.get_package(name): - return - - version = None - try: - version = importlib.metadata.version(name) - except PackageNotFoundError: - pass - if version: - PackagesStore.add_package(name, version) - else: - names_without_version.add(name) + PackagesStore.add_installed_package(name) finally: running_import_scan.set(False) diff --git a/aikido_zen/sinks/tests/builtins_import_test.py b/aikido_zen/sinks/tests/builtins_import_test.py index a2261ee4d..93a1b3d6a 100644 --- a/aikido_zen/sinks/tests/builtins_import_test.py +++ b/aikido_zen/sinks/tests/builtins_import_test.py @@ -7,14 +7,14 @@ aikido_zen.protect() -import aikido_zen.sinks.builtins_import as builtins_import +from aikido_zen.background_process import packages from aikido_zen.background_process.packages import PackagesStore @pytest.fixture(autouse=True) def run_around_tests(): PackagesStore.clear() - builtins_import.names_without_version.clear() + packages.names_without_version.clear() def test_flask_import(): From 5d22f4dd9cbc23f8cc8b4a1aa6d3f40cf17bbd27 Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Mon, 26 Oct 2026 17:00:48 +0100 Subject: [PATCH 3/5] Remove the docstring that restates add_installed_package --- aikido_zen/background_process/packages.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aikido_zen/background_process/packages.py b/aikido_zen/background_process/packages.py index ecf7d9f7b..20a718bf6 100644 --- a/aikido_zen/background_process/packages.py +++ b/aikido_zen/background_process/packages.py @@ -63,7 +63,6 @@ def is_version_supported(version, required_version): class PackagesStore: @staticmethod def add_installed_package(name): - """Stores the package's installed version, if it has one.""" if name in names_without_version or PackagesStore.get_package(name): return version = None From 43fa657dde12f06944072604b64514fc26ff2026 Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Mon, 26 Oct 2026 18:27:49 +0100 Subject: [PATCH 4/5] Let add_package remember names without a version --- aikido_zen/background_process/packages.py | 12 ++++++++---- aikido_zen/sinks/tests/builtins_import_test.py | 3 +-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/aikido_zen/background_process/packages.py b/aikido_zen/background_process/packages.py index 20a718bf6..b49fe2a5a 100644 --- a/aikido_zen/background_process/packages.py +++ b/aikido_zen/background_process/packages.py @@ -70,10 +70,7 @@ def add_installed_package(name): version = importlib_metadata.version(name) except importlib_metadata.PackageNotFoundError: pass - if version: - PackagesStore.add_package(name, version) - else: - names_without_version.add(name) + PackagesStore.add_package(name, version) @staticmethod def export(): @@ -86,6 +83,9 @@ def export(): @staticmethod def add_package(package, version): + if not version: + names_without_version.add(package) + return packages_store[package] = { "name": package, "version": version, @@ -107,6 +107,10 @@ def clear(): for package in packages_store.values(): package["cleared"] = True + @staticmethod + def clear_names_without_version(): + names_without_version.clear() + @staticmethod def import_list(imported_packages): for package in imported_packages: diff --git a/aikido_zen/sinks/tests/builtins_import_test.py b/aikido_zen/sinks/tests/builtins_import_test.py index 93a1b3d6a..893403977 100644 --- a/aikido_zen/sinks/tests/builtins_import_test.py +++ b/aikido_zen/sinks/tests/builtins_import_test.py @@ -7,14 +7,13 @@ aikido_zen.protect() -from aikido_zen.background_process import packages from aikido_zen.background_process.packages import PackagesStore @pytest.fixture(autouse=True) def run_around_tests(): PackagesStore.clear() - packages.names_without_version.clear() + PackagesStore.clear_names_without_version() def test_flask_import(): From 3c45deebf16c8580298828f2cdd93c256df74aa2 Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Mon, 26 Oct 2026 18:58:38 +0100 Subject: [PATCH 5/5] Access the package store directly inside PackagesStore. --- aikido_zen/background_process/packages.py | 4 ++-- aikido_zen/sinks/tests/builtins_import_test.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/aikido_zen/background_process/packages.py b/aikido_zen/background_process/packages.py index b49fe2a5a..991ba7823 100644 --- a/aikido_zen/background_process/packages.py +++ b/aikido_zen/background_process/packages.py @@ -63,7 +63,7 @@ def is_version_supported(version, required_version): class PackagesStore: @staticmethod def add_installed_package(name): - if name in names_without_version or PackagesStore.get_package(name): + if name in names_without_version or name in packages_store: return version = None try: @@ -114,6 +114,6 @@ def clear_names_without_version(): @staticmethod def import_list(imported_packages): for package in imported_packages: - if PackagesStore.get_package(package["name"]): + if package["name"] in packages_store: continue packages_store[package["name"]] = package diff --git a/aikido_zen/sinks/tests/builtins_import_test.py b/aikido_zen/sinks/tests/builtins_import_test.py index 893403977..889c95198 100644 --- a/aikido_zen/sinks/tests/builtins_import_test.py +++ b/aikido_zen/sinks/tests/builtins_import_test.py @@ -42,17 +42,23 @@ def test_name_without_a_distribution_is_looked_up_once(): def test_recursive_package_store(monkeypatch): """Test that recursive imports during package scanning don't cause max recursion depth errors.""" - def recursive_get_package(name): + calls = [] + + def recursive_add_installed_package(name): """Recursively add package and its dependencies to PackagesStore.""" + calls.append(name) import flask PackagesStore.clear() - monkeypatch.setattr(PackagesStore, "get_package", recursive_get_package) + monkeypatch.setattr( + PackagesStore, "add_installed_package", recursive_add_installed_package + ) import flask # Restore the original method after the test monkeypatch.undo() + assert calls == ["flask"] def test_recursive_package_store_2(monkeypatch):