Skip to content

Commit 4f945a4

Browse files
committed
Add lazy import tests ported from internal test suite
Port missing test coverage from the internal lazy imports test suite, covering behaviors not previously tested in cpython: - Dict operations with lazy values (copy, |, update preserve proxies) - Submodule laziness (unaccessed imports stay lazy until touched) - Attribute side effects (submodule imports don't overwrite parent attrs) - Module/variable name collisions (submodule vs variable ordering) - Deleted module reimport (sys.modules deletion and reimport) - Circular import resolution (lazy imports breaking circular deadlocks) - Dict mutation during module loading (no crash on dict resize) Adds supporting test data modules: metasyntactic package hierarchy, versioned package, module_same_name_var_order packages, and circular_import_pkg.
1 parent 3a62c8f commit 4f945a4

22 files changed

Lines changed: 266 additions & 0 deletions

File tree

Lib/test/test_lazy_import/__init__.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,5 +2018,223 @@ def test_set_bad_filter(self):
20182018
self.assertRaises(ValueError, _testcapi.PyImport_SetLazyImportsFilter, 42)
20192019

20202020

2021+
2022+
class DictOperationsWithLazyTests(unittest.TestCase):
2023+
"""Tests for dict operations with lazy import values."""
2024+
2025+
def tearDown(self):
2026+
for key in list(sys.modules.keys()):
2027+
if key.startswith('test.test_lazy_import.data'):
2028+
del sys.modules[key]
2029+
sys.set_lazy_imports_filter(None)
2030+
sys.set_lazy_imports("normal")
2031+
2032+
def test_dict_copy_preserves_lazy(self):
2033+
"""dict.copy() should preserve lazy import proxy objects."""
2034+
sys.set_lazy_imports("all")
2035+
from test.test_lazy_import.data.metasyntactic import names
2036+
d = names.__dict__.copy()
2037+
self.assertIsInstance(d["Foo"], types.LazyImportType)
2038+
self.assertEqual(d["Metasyntactic"], "Metasyntactic")
2039+
2040+
def test_dict_or_preserves_lazy(self):
2041+
"""dict | should keep the winning side's lazy/resolved status."""
2042+
sys.set_lazy_imports("all")
2043+
from test.test_lazy_import.data.metasyntactic import names
2044+
lazy = names.__dict__.copy()
2045+
resolved = {"Foo": "resolved"}
2046+
self.assertEqual((lazy | resolved)["Foo"], "resolved")
2047+
self.assertIsInstance((resolved | lazy)["Foo"], types.LazyImportType)
2048+
2049+
def test_dict_update_preserves_lazy(self):
2050+
"""dict.update() should transfer lazy import proxy objects."""
2051+
sys.set_lazy_imports("all")
2052+
from test.test_lazy_import.data.metasyntactic import names
2053+
target = {}
2054+
target.update(names.__dict__)
2055+
self.assertIsInstance(target["Foo"], types.LazyImportType)
2056+
2057+
2058+
class SubmoduleLazinessTests(unittest.TestCase):
2059+
"""Tests that module-level lazy imports remain lazy until accessed."""
2060+
2061+
def tearDown(self):
2062+
for key in list(sys.modules.keys()):
2063+
if key.startswith('test.test_lazy_import.data'):
2064+
del sys.modules[key]
2065+
sys.set_lazy_imports_filter(None)
2066+
sys.set_lazy_imports("normal")
2067+
2068+
def test_unaccessed_imports_stay_lazy(self):
2069+
"""Imports in 'all' mode should stay lazy until accessed."""
2070+
sys.set_lazy_imports("all")
2071+
from test.test_lazy_import.data.metasyntactic import names
2072+
self.assertIsInstance(names.__dict__["Foo"], types.LazyImportType)
2073+
self.assertNotIn(
2074+
"test.test_lazy_import.data.metasyntactic.foo", sys.modules
2075+
)
2076+
_ = names.Foo
2077+
self.assertEqual(names.Foo, "Foo")
2078+
self.assertIn(
2079+
"test.test_lazy_import.data.metasyntactic.foo", sys.modules
2080+
)
2081+
self.assertIsInstance(names.__dict__["Ack"], types.LazyImportType)
2082+
self.assertNotIn(
2083+
"test.test_lazy_import.data.metasyntactic.foo.ack", sys.modules
2084+
)
2085+
2086+
2087+
class AttributeSideEffectTests(unittest.TestCase):
2088+
"""Tests that submodule imports don't overwrite parent attributes."""
2089+
2090+
def tearDown(self):
2091+
for key in list(sys.modules.keys()):
2092+
if key.startswith('test.test_lazy_import.data'):
2093+
del sys.modules[key]
2094+
sys.set_lazy_imports_filter(None)
2095+
sys.set_lazy_imports("normal")
2096+
2097+
def test_version_submodule_does_not_overwrite(self):
2098+
"""A __version__ submodule should not overwrite the parent's
2099+
__version__ attribute imported in __init__.py."""
2100+
import test.test_lazy_import.data.versioned as versioned
2101+
self.assertEqual(versioned.__version__, "1.0")
2102+
self.assertEqual(
2103+
versioned.__copyright__,
2104+
"Copyright (c) 2001-2022 Python Software Foundation.",
2105+
)
2106+
2107+
2108+
class ModuleVariableNameCollisionTests(unittest.TestCase):
2109+
"""Tests for name collision between a submodule and a variable."""
2110+
2111+
def tearDown(self):
2112+
for key in list(sys.modules.keys()):
2113+
if key.startswith('test.test_lazy_import.data'):
2114+
del sys.modules[key]
2115+
sys.set_lazy_imports_filter(None)
2116+
sys.set_lazy_imports("normal")
2117+
2118+
def test_variable_after_import_wins(self):
2119+
"""Variable assigned after import should overwrite the submodule."""
2120+
from test.test_lazy_import.data import module_same_name_var_order1
2121+
self.assertEqual(module_same_name_var_order1.bar, "Blah")
2122+
2123+
def test_import_after_variable_wins(self):
2124+
"""Import after variable assignment should overwrite the variable."""
2125+
from test.test_lazy_import.data import module_same_name_var_order2
2126+
bar_mod = sys.modules[
2127+
"test.test_lazy_import.data.module_same_name_var_order2.bar"
2128+
]
2129+
self.assertIs(module_same_name_var_order2.bar, bar_mod)
2130+
2131+
2132+
class DeletedModuleReimportTests(unittest.TestCase):
2133+
"""Tests for reimporting after module deletion from sys.modules."""
2134+
2135+
def tearDown(self):
2136+
for key in list(sys.modules.keys()):
2137+
if key.startswith('test.test_lazy_import.data'):
2138+
del sys.modules[key]
2139+
sys.set_lazy_imports_filter(None)
2140+
sys.set_lazy_imports("normal")
2141+
2142+
def test_reimport_creates_new_module(self):
2143+
"""Deleting and reimporting should create a new module object."""
2144+
import test.test_lazy_import.data.metasyntactic.foo
2145+
import test.test_lazy_import.data.metasyntactic.foo.bar.baz
2146+
2147+
first_bar = test.test_lazy_import.data.metasyntactic.foo.bar
2148+
2149+
del sys.modules[
2150+
"test.test_lazy_import.data.metasyntactic.foo.bar"
2151+
]
2152+
2153+
import test.test_lazy_import.data.metasyntactic.foo.bar.thud
2154+
2155+
second_bar = test.test_lazy_import.data.metasyntactic.foo.bar
2156+
2157+
self.assertIsNot(first_bar, second_bar)
2158+
self.assertIn("baz", dir(first_bar))
2159+
self.assertNotIn("thud", dir(first_bar))
2160+
self.assertIn("thud", dir(second_bar))
2161+
self.assertNotIn("baz", dir(second_bar))
2162+
2163+
2164+
@support.requires_subprocess()
2165+
class CircularImportLazyTests(unittest.TestCase):
2166+
"""Tests that lazy imports can break circular import patterns."""
2167+
2168+
def test_succeeds_with_lazy(self):
2169+
"""Same-level circular imports should succeed with lazy mode."""
2170+
proc = assert_python_ok(
2171+
"-X", "lazy_imports=all", "-c",
2172+
"import test.test_lazy_import.data.circular_import_pkg.main;"
2173+
"print('OK')",
2174+
)
2175+
self.assertIn(b"OK", proc.out)
2176+
2177+
def test_fails_without_lazy(self):
2178+
"""Same-level circular imports should fail without lazy mode."""
2179+
result = subprocess.run(
2180+
[sys.executable, "-X", "lazy_imports=none", "-c",
2181+
"import test.test_lazy_import.data.circular_import_pkg.main"],
2182+
capture_output=True, text=True,
2183+
)
2184+
self.assertNotEqual(result.returncode, 0)
2185+
self.assertIn("ImportError", result.stderr)
2186+
2187+
2188+
@support.requires_subprocess()
2189+
class DictMutationDuringLoadTests(unittest.TestCase):
2190+
"""Tests that module dict mutation during loading doesn't crash."""
2191+
2192+
def test_dict_mutation_during_import(self):
2193+
"""Iterating a module dict while lazy imports resolve should not
2194+
crash even if the dict is mutated during iteration."""
2195+
with tempfile.TemporaryDirectory() as tmpdir:
2196+
pkg = os.path.join(tmpdir, "dcwl_pkg")
2197+
os.makedirs(pkg)
2198+
2199+
with open(os.path.join(pkg, "__init__.py"), "w") as f:
2200+
f.write(textwrap.dedent("""\
2201+
from .elements import elements_function
2202+
2203+
def __go(lcls):
2204+
global __all__
2205+
__all__ = sorted(
2206+
name
2207+
for name, obj in lcls.items()
2208+
if not name.startswith("_")
2209+
)
2210+
2211+
__go(locals())
2212+
"""))
2213+
2214+
with open(os.path.join(pkg, "elements.py"), "w") as f:
2215+
f.write(textwrap.dedent("""\
2216+
from .elements_sub import elements_sub_function
2217+
2218+
def elements_function():
2219+
pass
2220+
"""))
2221+
2222+
with open(os.path.join(pkg, "elements_sub.py"), "w") as f:
2223+
f.write("def elements_sub_function(): pass\n")
2224+
2225+
env = os.environ.copy()
2226+
env["PYTHONPATH"] = tmpdir
2227+
result = subprocess.run(
2228+
[sys.executable, "-X", "lazy_imports=all",
2229+
"-c", "import dcwl_pkg; print('OK')"],
2230+
capture_output=True, text=True, env=env,
2231+
)
2232+
self.assertEqual(
2233+
result.returncode, 0,
2234+
f"stdout: {result.stdout}, stderr: {result.stderr}",
2235+
)
2236+
self.assertIn("OK", result.stdout)
2237+
2238+
20212239
if __name__ == '__main__':
20222240
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .x import X2
2+
X2()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def X1():
2+
return "X"
3+
4+
from .y import Y1
5+
6+
def X2():
7+
return Y1()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def Y1():
2+
return "Y"
3+
4+
from .x import X2
5+
6+
def Y2():
7+
return X2()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Foo = "Foo"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Ack = "Ack"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Bar = "Bar"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Baz = "Baz"

0 commit comments

Comments
 (0)