From 391e1de9e16f988be5e16a0561eb150563fad4dd Mon Sep 17 00:00:00 2001 From: Lisa Carrier Date: Sun, 31 Aug 2025 15:02:50 -0700 Subject: [PATCH 1/8] Adds sys.audit event for import_module. --- Lib/importlib/__init__.py | 13 ++++++++++++- Lib/test/audit-tests.py | 16 ++++++++++++++++ Lib/test/test_audit.py | 3 +++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Lib/importlib/__init__.py b/Lib/importlib/__init__.py index a7d57561ead046..d2a2a98b503c1e 100644 --- a/Lib/importlib/__init__.py +++ b/Lib/importlib/__init__.py @@ -85,7 +85,18 @@ def import_module(name, package=None): if character != '.': break level += 1 - return _bootstrap._gcd_import(name[level:], package, level) + module = _bootstrap._gcd_import(name[level:], package, level) + if module: + sys.audit( + "import", + name, + # We could try to grab __file__ here but it breaks LazyLoader + None, + sys.path, + sys.meta_path, + sys.path_hooks + ) + return module _RELOADING = {} diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index 6884ac0dbe6ff0..4abf1a9a79848e 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -672,6 +672,22 @@ def hook(event, args): assertEqual(event_script_path, tmp_file.name) assertEqual(remote_event_script_path, tmp_file.name) +def test_import_module(): + import importlib + + with TestHook() as hook: + importlib.import_module("os") # random stdlib + importlib.import_module("pythoninfo") # random module + + actual = [a[0] for e, a in hook.seen if e == "import"] + assertSequenceEqual( + [ + "os", + "pythoninfo", + ], + actual, + ) + if __name__ == "__main__": from test.support import suppress_msvcrt_asserts diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index 077765fcda210a..56fa87c3a5e5c8 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -331,5 +331,8 @@ def test_sys_remote_exec(self): if returncode: self.fail(stderr) + def test_import_module(self): + self.do_test("test_import_module") + if __name__ == "__main__": unittest.main() From 441de32f5caccc091f0cdd98f8728b869b681154 Mon Sep 17 00:00:00 2001 From: Lisa Carrier Date: Tue, 2 Sep 2025 19:27:53 -0700 Subject: [PATCH 2/8] Moves audit to _gcd_import. --- Lib/importlib/__init__.py | 10 ---------- Lib/importlib/_bootstrap.py | 4 ++++ Lib/test/audit-tests.py | 8 ++++++-- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Lib/importlib/__init__.py b/Lib/importlib/__init__.py index d2a2a98b503c1e..71166c6a6450b8 100644 --- a/Lib/importlib/__init__.py +++ b/Lib/importlib/__init__.py @@ -86,16 +86,6 @@ def import_module(name, package=None): break level += 1 module = _bootstrap._gcd_import(name[level:], package, level) - if module: - sys.audit( - "import", - name, - # We could try to grab __file__ here but it breaks LazyLoader - None, - sys.path, - sys.meta_path, - sys.path_hooks - ) return module diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py index 499da1e04efea8..d5974378d7974f 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -1395,6 +1395,10 @@ def _gcd_import(name, package=None, level=0): _sanity_check(name, package, level) if level > 0: name = _resolve_name(name, package, level) + module = sys.modules.get(name, _NEEDS_LOADING) + if (module is _NEEDS_LOADING or + getattr(getattr(module, "__spec__", None), "_initializing", False)): + sys.audit("import", name, None, sys.path, sys.meta_path, sys.path_hooks) return _find_and_load(name, _gcd_import) diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index 4abf1a9a79848e..7d173b8158133a 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -676,14 +676,18 @@ def test_import_module(): import importlib with TestHook() as hook: - importlib.import_module("os") # random stdlib + importlib.import_module("importlib") # already imported, won't get logged + importlib.import_module("email") # standard library module importlib.import_module("pythoninfo") # random module + importlib.import_module(".test_importlib.abc", "test") # relative import actual = [a[0] for e, a in hook.seen if e == "import"] assertSequenceEqual( [ - "os", + "email", "pythoninfo", + "test.test_importlib.abc", + "test.test_importlib" ], actual, ) From e6436c40b4745f401d8ded4be49bc4d524bfa192 Mon Sep 17 00:00:00 2001 From: Lisa Carrier Date: Wed, 3 Sep 2025 19:51:56 -0700 Subject: [PATCH 3/8] Removes audit from import.c, moves to _find_and_load_unlocked. --- Lib/importlib/_bootstrap.py | 5 +---- Python/import.c | 9 --------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py index d5974378d7974f..115b8a809868fc 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -1306,6 +1306,7 @@ def _sanity_check(name, package, level): _ERR_MSG_PREFIX = 'No module named ' def _find_and_load_unlocked(name, import_): + sys.audit("import", name, None, sys.path, sys.meta_path, sys.path_hooks) path = None parent = name.rpartition('.')[0] parent_spec = None @@ -1395,10 +1396,6 @@ def _gcd_import(name, package=None, level=0): _sanity_check(name, package, level) if level > 0: name = _resolve_name(name, package, level) - module = sys.modules.get(name, _NEEDS_LOADING) - if (module is _NEEDS_LOADING or - getattr(getattr(module, "__spec__", None), "_initializing", False)): - sys.audit("import", name, None, sys.path, sys.meta_path, sys.path_hooks) return _find_and_load(name, _gcd_import) diff --git a/Python/import.c b/Python/import.c index 9dee20ecb63c91..10f61d9d1d56dc 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3694,15 +3694,6 @@ import_find_and_load(PyThreadState *tstate, PyObject *abs_name) Py_XDECREF(sys_path); return NULL; } - if (_PySys_Audit(tstate, "import", "OOOOO", - abs_name, Py_None, sys_path ? sys_path : Py_None, - sys_meta_path ? sys_meta_path : Py_None, - sys_path_hooks ? sys_path_hooks : Py_None) < 0) { - Py_XDECREF(sys_path_hooks); - Py_XDECREF(sys_meta_path); - Py_XDECREF(sys_path); - return NULL; - } Py_XDECREF(sys_path_hooks); Py_XDECREF(sys_meta_path); Py_XDECREF(sys_path); From ac56ef09ff9f4642e3476ddeef614063247887b4 Mon Sep 17 00:00:00 2001 From: Lisa Carrier Date: Wed, 3 Sep 2025 19:54:37 -0700 Subject: [PATCH 4/8] Removes unneeded change. --- Lib/importlib/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/importlib/__init__.py b/Lib/importlib/__init__.py index 71166c6a6450b8..a7d57561ead046 100644 --- a/Lib/importlib/__init__.py +++ b/Lib/importlib/__init__.py @@ -85,8 +85,7 @@ def import_module(name, package=None): if character != '.': break level += 1 - module = _bootstrap._gcd_import(name[level:], package, level) - return module + return _bootstrap._gcd_import(name[level:], package, level) _RELOADING = {} From 5207807206f0e04acdff748f099dec83d306c478 Mon Sep 17 00:00:00 2001 From: Lisa Carrier Date: Thu, 4 Sep 2025 19:20:45 -0700 Subject: [PATCH 5/8] Moves audit and uses path argument. --- Lib/importlib/_bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py index 115b8a809868fc..0c7133d40c19e0 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -1306,8 +1306,8 @@ def _sanity_check(name, package, level): _ERR_MSG_PREFIX = 'No module named ' def _find_and_load_unlocked(name, import_): - sys.audit("import", name, None, sys.path, sys.meta_path, sys.path_hooks) path = None + sys.audit("import", name, path, sys.path, sys.meta_path, sys.path_hooks) parent = name.rpartition('.')[0] parent_spec = None if parent: From 9bc440739bb669275ea7aba653aa8985b54d9f4c Mon Sep 17 00:00:00 2001 From: Lisa Carrier Date: Sun, 14 Sep 2025 13:13:56 -0700 Subject: [PATCH 6/8] Adds more tests and cleans up import.c. --- Lib/importlib/_bootstrap.py | 9 +++- Lib/test/audit-tests.py | 63 ++++++++++++++++++++++++-- Lib/test/audit_test_data/__init__.py | 0 Lib/test/audit_test_data/submodule.py | 0 Lib/test/audit_test_data/submodule2.py | 0 Lib/test/test_audit.py | 6 +++ Python/import.c | 18 -------- 7 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 Lib/test/audit_test_data/__init__.py create mode 100644 Lib/test/audit_test_data/submodule.py create mode 100644 Lib/test/audit_test_data/submodule2.py diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py index 0c7133d40c19e0..13d4bd46756a3e 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -1306,8 +1306,15 @@ def _sanity_check(name, package, level): _ERR_MSG_PREFIX = 'No module named ' def _find_and_load_unlocked(name, import_): + sys.audit( + "import", + name, + None, + getattr(sys, "path"), + getattr(sys, "meta_path"), + getattr(sys, "path_hooks") + ) path = None - sys.audit("import", name, path, sys.path, sys.meta_path, sys.path_hooks) parent = name.rpartition('.')[0] parent_spec = None if parent: diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index 7d173b8158133a..b17cef453f9500 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -679,15 +679,72 @@ def test_import_module(): importlib.import_module("importlib") # already imported, won't get logged importlib.import_module("email") # standard library module importlib.import_module("pythoninfo") # random module - importlib.import_module(".test_importlib.abc", "test") # relative import + importlib.import_module(".audit_test_data.submodule", "test") # relative import + importlib.import_module("test.audit_test_data.submodule2") # absolute import + importlib.import_module("_testcapi") # extension module actual = [a[0] for e, a in hook.seen if e == "import"] assertSequenceEqual( [ "email", "pythoninfo", - "test.test_importlib.abc", - "test.test_importlib" + "test.audit_test_data.submodule", + "test.audit_test_data", + "test.audit_test_data.submodule2", + "_testcapi", + "_testcapi", + ], + actual, + ) + +def test_builtin__import__(): + import importlib # noqa: F401 + + with TestHook() as hook: + __import__("importlib") + __import__("email") + __import__("pythoninfo") + __import__("test.audit_test_data.submodule", fromlist=["audit_test_data"]) + __import__("test.audit_test_data.submodule2") + __import__("_testcapi") + + actual = [a[0] for e, a in hook.seen if e == "import"] + assertSequenceEqual( + [ + "email", + "pythoninfo", + "test.audit_test_data.submodule", + "test.audit_test_data", + "test.audit_test_data.submodule2", + "_testcapi", + "_testcapi", + ], + actual, + ) + +def test_import_statement(): + import importlib # noqa: F401 + + with TestHook() as hook: + import importlib # noqa: F401 + import email # noqa: F401 + import pythoninfo # noqa: F401 + from test.audit_test_data import submodule # noqa: F401 + import test.audit_test_data.submodule2 # noqa: F401 + import _testcapi # noqa: F401 + + actual = [a[0] for e, a in hook.seen if e == "import"] + # Import statement ordering is different because the package is + # loaded first and then the submodule + assertSequenceEqual( + [ + "email", + "pythoninfo", + "test.audit_test_data", + "test.audit_test_data.submodule", + "test.audit_test_data.submodule2", + "_testcapi", + "_testcapi", ], actual, ) diff --git a/Lib/test/audit_test_data/__init__.py b/Lib/test/audit_test_data/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/Lib/test/audit_test_data/submodule.py b/Lib/test/audit_test_data/submodule.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/Lib/test/audit_test_data/submodule2.py b/Lib/test/audit_test_data/submodule2.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index 56fa87c3a5e5c8..db4e1eb9999c1f 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -334,5 +334,11 @@ def test_sys_remote_exec(self): def test_import_module(self): self.do_test("test_import_module") + def test_builtin__import__(self): + self.do_test("test_builtin__import__") + + def test_import_statement(self): + self.do_test("test_import_statement") + if __name__ == "__main__": unittest.main() diff --git a/Python/import.c b/Python/import.c index 10f61d9d1d56dc..d01c4d478283ff 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3681,24 +3681,6 @@ import_find_and_load(PyThreadState *tstate, PyObject *abs_name) PyTime_t t1 = 0, accumulated_copy = accumulated; - PyObject *sys_path, *sys_meta_path, *sys_path_hooks; - if (PySys_GetOptionalAttrString("path", &sys_path) < 0) { - return NULL; - } - if (PySys_GetOptionalAttrString("meta_path", &sys_meta_path) < 0) { - Py_XDECREF(sys_path); - return NULL; - } - if (PySys_GetOptionalAttrString("path_hooks", &sys_path_hooks) < 0) { - Py_XDECREF(sys_meta_path); - Py_XDECREF(sys_path); - return NULL; - } - Py_XDECREF(sys_path_hooks); - Py_XDECREF(sys_meta_path); - Py_XDECREF(sys_path); - - /* XOptions is initialized after first some imports. * So we can't have negative cache before completed initialization. * Anyway, importlib._find_and_load is much slower than From 135539fdc3fc9164ba8be9c24988c2e315c4e143 Mon Sep 17 00:00:00 2001 From: Lisa Carrier Date: Thu, 18 Sep 2025 06:42:59 -0700 Subject: [PATCH 7/8] Fixes rel imports, test whole tuple, fixes args. --- Lib/importlib/_bootstrap.py | 10 +++---- Lib/test/audit-tests.py | 58 ++++++++++++++++++++----------------- Makefile.pre.in | 1 + 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py index 13d4bd46756a3e..43c66765dd9779 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -1306,15 +1306,15 @@ def _sanity_check(name, package, level): _ERR_MSG_PREFIX = 'No module named ' def _find_and_load_unlocked(name, import_): + path = None sys.audit( "import", name, - None, - getattr(sys, "path"), - getattr(sys, "meta_path"), - getattr(sys, "path_hooks") + path, + getattr(sys, "path", None), + getattr(sys, "meta_path", None), + getattr(sys, "path_hooks", None) ) - path = None parent = name.rpartition('.')[0] parent_spec = None if parent: diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index b17cef453f9500..834cc6706ef7c9 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -8,6 +8,7 @@ import contextlib import os import sys +import unittest.mock class TestHook: @@ -683,16 +684,16 @@ def test_import_module(): importlib.import_module("test.audit_test_data.submodule2") # absolute import importlib.import_module("_testcapi") # extension module - actual = [a[0] for e, a in hook.seen if e == "import"] + actual = [a for e, a in hook.seen if e == "import"] assertSequenceEqual( [ - "email", - "pythoninfo", - "test.audit_test_data.submodule", - "test.audit_test_data", - "test.audit_test_data.submodule2", - "_testcapi", - "_testcapi", + ("email", None, sys.path, sys.meta_path, sys.path_hooks), + ("pythoninfo", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data.submodule", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data.submodule2", None, sys.path, sys.meta_path, sys.path_hooks), + ("_testcapi", None, sys.path, sys.meta_path, sys.path_hooks), + ("_testcapi", unittest.mock.ANY, None, None, None) ], actual, ) @@ -704,47 +705,52 @@ def test_builtin__import__(): __import__("importlib") __import__("email") __import__("pythoninfo") - __import__("test.audit_test_data.submodule", fromlist=["audit_test_data"]) + __import__("audit_test_data.submodule", level=1, globals={"__package__": "test"}) __import__("test.audit_test_data.submodule2") __import__("_testcapi") - actual = [a[0] for e, a in hook.seen if e == "import"] + actual = [a for e, a in hook.seen if e == "import"] assertSequenceEqual( [ - "email", - "pythoninfo", - "test.audit_test_data.submodule", - "test.audit_test_data", - "test.audit_test_data.submodule2", - "_testcapi", - "_testcapi", + ("email", None, sys.path, sys.meta_path, sys.path_hooks), + ("pythoninfo", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data.submodule", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data.submodule2", None, sys.path, sys.meta_path, sys.path_hooks), + ("_testcapi", None, sys.path, sys.meta_path, sys.path_hooks), + ("_testcapi", unittest.mock.ANY, None, None, None) ], actual, ) def test_import_statement(): import importlib # noqa: F401 + # Set __package__ so relative imports work + old_package = globals().get("__package__", None) + globals()["__package__"] = "test" with TestHook() as hook: import importlib # noqa: F401 import email # noqa: F401 import pythoninfo # noqa: F401 - from test.audit_test_data import submodule # noqa: F401 + from .audit_test_data import submodule # noqa: F401 import test.audit_test_data.submodule2 # noqa: F401 import _testcapi # noqa: F401 - actual = [a[0] for e, a in hook.seen if e == "import"] + globals()["__package__"] = old_package + + actual = [a for e, a in hook.seen if e == "import"] # Import statement ordering is different because the package is # loaded first and then the submodule assertSequenceEqual( [ - "email", - "pythoninfo", - "test.audit_test_data", - "test.audit_test_data.submodule", - "test.audit_test_data.submodule2", - "_testcapi", - "_testcapi", + ("email", None, sys.path, sys.meta_path, sys.path_hooks), + ("pythoninfo", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data.submodule", None, sys.path, sys.meta_path, sys.path_hooks), + ("test.audit_test_data.submodule2", None, sys.path, sys.meta_path, sys.path_hooks), + ("_testcapi", None, sys.path, sys.meta_path, sys.path_hooks), + ("_testcapi", unittest.mock.ANY, None, None, None) ], actual, ) diff --git a/Makefile.pre.in b/Makefile.pre.in index 9ce6ec65f142d8..de5f59ea73d951 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -2590,6 +2590,7 @@ TESTSUBDIRS= idlelib/idle_test \ test/test_ast \ test/test_ast/data \ test/archivetestdata \ + test/audit_test_data \ test/audiodata \ test/certdata \ test/certdata/capath \ From 2387863cc5cf664bcd99efa3aa89acfaa6d0fc70 Mon Sep 17 00:00:00 2001 From: Lisa Carrier Date: Thu, 18 Sep 2025 07:38:13 -0700 Subject: [PATCH 8/8] Uses swap_item for globals(). --- Lib/test/audit-tests.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index 834cc6706ef7c9..a893932169a089 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -9,6 +9,7 @@ import os import sys import unittest.mock +from test.support import swap_item class TestHook: @@ -726,18 +727,14 @@ def test_builtin__import__(): def test_import_statement(): import importlib # noqa: F401 # Set __package__ so relative imports work - old_package = globals().get("__package__", None) - globals()["__package__"] = "test" - - with TestHook() as hook: - import importlib # noqa: F401 - import email # noqa: F401 - import pythoninfo # noqa: F401 - from .audit_test_data import submodule # noqa: F401 - import test.audit_test_data.submodule2 # noqa: F401 - import _testcapi # noqa: F401 - - globals()["__package__"] = old_package + with swap_item(globals(), "__package__", "test"): + with TestHook() as hook: + import importlib # noqa: F401 + import email # noqa: F401 + import pythoninfo # noqa: F401 + from .audit_test_data import submodule # noqa: F401 + import test.audit_test_data.submodule2 # noqa: F401 + import _testcapi # noqa: F401 actual = [a for e, a in hook.seen if e == "import"] # Import statement ordering is different because the package is