@@ -53,7 +53,8 @@ def test_sys_lazy_modules(self):
5353 self .fail ('lazy import failed' )
5454
5555 self .assertFalse ("test.test_lazy_import.data.basic2" in sys .modules )
56- self .assertIn ("test.test_lazy_import.data" , sys .lazy_modules )
56+ # The package is already loaded, so it is not a pending import.
57+ self .assertNotIn ("test.test_lazy_import.data" , sys .lazy_modules )
5758 self .assertIn ("test.test_lazy_import.data.basic2" , sys .lazy_modules )
5859 test .test_lazy_import .data .basic_from_unused .basic2
5960 self .assertNotIn ("test.test_import.data" , sys .lazy_modules )
@@ -1313,6 +1314,262 @@ def test_lazy_module_without_children_is_tracked(self):
13131314 """ )
13141315 assert_python_ok ("-c" , code )
13151316
1317+ def test_already_loaded_module_is_not_tracked (self ):
1318+ """A lazy import of an already loaded module should not be tracked."""
1319+ code = textwrap .dedent ("""
1320+ import sys
1321+
1322+ # Loaded by a regular import.
1323+ import json
1324+ lazy import json as lazy_json
1325+ assert "json" not in sys.lazy_modules, (
1326+ f"expected 'json' not in sys.lazy_modules, got {sys.lazy_modules}"
1327+ )
1328+
1329+ # Loaded by reifying an earlier lazy import.
1330+ lazy import base64
1331+ _ = base64.b64encode
1332+ lazy import base64 as lazy_base64
1333+ assert "base64" not in sys.lazy_modules, (
1334+ f"expected 'base64' not in sys.lazy_modules, got {sys.lazy_modules}"
1335+ )
1336+ """ )
1337+ assert_python_ok ("-c" , code )
1338+
1339+ def test_already_loaded_submodule_is_not_tracked (self ):
1340+ """`lazy from` a loaded submodule should not be tracked either."""
1341+ code = textwrap .dedent ("""
1342+ import sys
1343+ import test.test_lazy_import.data.pkg.b
1344+ lazy from test.test_lazy_import.data.pkg import b
1345+ assert "test.test_lazy_import.data.pkg.b" not in sys.lazy_modules, (
1346+ f"expected 'pkg.b' untracked, got {sys.lazy_modules}"
1347+ )
1348+ """ )
1349+ assert_python_ok ("-c" , code )
1350+
1351+ def test_attribute_entry_removed_on_reification (self ):
1352+ """`lazy from x import attr` should untrack "x.attr" once resolved."""
1353+ code = textwrap .dedent ("""
1354+ import sys
1355+ lazy from test.test_lazy_import.data.basic2 import x
1356+ assert "test.test_lazy_import.data.basic2.x" in sys.lazy_modules, (
1357+ f"expected 'basic2.x' tracked, got {sys.lazy_modules}"
1358+ )
1359+ _ = x
1360+ assert "test.test_lazy_import.data.basic2.x" not in sys.lazy_modules, (
1361+ f"expected 'basic2.x' untracked, got {sys.lazy_modules}"
1362+ )
1363+ """ )
1364+ assert_python_ok ("-c" , code )
1365+
1366+ def test_already_loaded_attribute_is_not_tracked (self ):
1367+ code = textwrap .dedent ("""
1368+ import sys
1369+ import math
1370+
1371+ lazy from math import pi
1372+ assert "math.pi" not in sys.lazy_modules, sys.lazy_modules
1373+ assert pi == math.pi
1374+ """ )
1375+ assert_python_ok ("-c" , code )
1376+
1377+ def test_cached_lazy_attribute_tracks_its_target (self ):
1378+ code = textwrap .dedent ("""
1379+ import sys
1380+ import test.test_lazy_import.data.basic_from_unused
1381+
1382+ holder = "test.test_lazy_import.data.basic_from_unused"
1383+ target = "test.test_lazy_import.data.basic2"
1384+
1385+ namespace = {"__name__": "cached_import_test"}
1386+ exec(f"lazy from {holder} import basic2", namespace)
1387+ assert holder + ".basic2" not in sys.lazy_modules, sys.lazy_modules
1388+ assert target in sys.lazy_modules, sys.lazy_modules
1389+ exec("assert basic2.x == 42", namespace)
1390+ assert target not in sys.lazy_modules, sys.lazy_modules
1391+ """ )
1392+ assert_python_ok ("-c" , code )
1393+
1394+ def test_cached_attribute_keeps_pending_module_tracked (self ):
1395+ code = textwrap .dedent ("""
1396+ import sys
1397+ import test.test_lazy_import.data.pkg as pkg
1398+ pkg.b = 42
1399+
1400+ lazy import test.test_lazy_import.data.pkg.b as pending
1401+ lazy from test.test_lazy_import.data.pkg import b
1402+ name = "test.test_lazy_import.data.pkg.b"
1403+ assert b == 42, b
1404+ assert name not in sys.modules, sys.modules
1405+ assert name in sys.lazy_modules, sys.lazy_modules
1406+ assert pending.foo() == "foo"
1407+ assert name not in sys.lazy_modules, sys.lazy_modules
1408+ """ )
1409+ assert_python_ok ("-c" , code )
1410+
1411+ def test_failed_reification_stays_tracked (self ):
1412+ """A lazy import that fails to resolve must stay tracked."""
1413+ code = textwrap .dedent ("""
1414+ import sys
1415+ lazy import test.test_lazy_import.data.broken_module
1416+ try:
1417+ _ = test.test_lazy_import.data.broken_module
1418+ except ValueError:
1419+ pass
1420+ else:
1421+ raise AssertionError("ValueError was not raised")
1422+ assert "test.test_lazy_import.data.broken_module" in sys.lazy_modules, (
1423+ f"failed reification must stay tracked, got {sys.lazy_modules}"
1424+ )
1425+ """ )
1426+ assert_python_ok ("-c" , code )
1427+
1428+ def test_blocked_module_is_still_tracked (self ):
1429+ """A ``None`` entry in sys.modules must not count as loaded."""
1430+ code = textwrap .dedent ("""
1431+ import sys
1432+ sys.modules['test.test_lazy_import.data.basic2'] = None
1433+ lazy import test.test_lazy_import.data.basic2
1434+ assert "test.test_lazy_import.data.basic2" in sys.lazy_modules, (
1435+ f"blocked module must stay tracked, got {sys.lazy_modules}"
1436+ )
1437+ """ )
1438+ assert_python_ok ("-c" , code )
1439+
1440+ def test_initializing_module_is_still_tracked (self ):
1441+ """A module that is still executing must not count as loaded."""
1442+ code = textwrap .dedent ("""
1443+ import sys
1444+ name = "test.test_lazy_import.data.init_fails"
1445+ try:
1446+ import test.test_lazy_import.data.init_fails
1447+ except ValueError:
1448+ pass
1449+ else:
1450+ raise AssertionError("ValueError was not raised")
1451+ assert name not in sys.modules, "failed import left a module behind"
1452+ assert name in sys.lazy_modules, (
1453+ f"expected {name!r} tracked, got {sys.lazy_modules}"
1454+ )
1455+ """ )
1456+ assert_python_ok ("-c" , code )
1457+
1458+ def test_module_spec_descriptor_is_not_run (self ):
1459+ """Checking whether a module is loaded must not run its descriptors."""
1460+ code = textwrap .dedent ("""
1461+ import sys
1462+ import types
1463+
1464+ class RaisingSpec(types.ModuleType):
1465+ @property
1466+ def __spec__(self):
1467+ raise RuntimeError("__spec__ descriptor was run")
1468+
1469+ sys.modules["raising_spec"] = RaisingSpec("raising_spec")
1470+ lazy import raising_spec
1471+ assert "raising_spec" not in sys.lazy_modules, (
1472+ f"expected 'raising_spec' untracked, got {sys.lazy_modules}"
1473+ )
1474+ """ )
1475+ assert_python_ok ("-c" , code )
1476+
1477+ def test_spec_initializing_descriptor_is_not_run (self ):
1478+ code = textwrap .dedent ("""
1479+ import sys
1480+ import types
1481+
1482+ class Spec:
1483+ @property
1484+ def _initializing(self):
1485+ raise RuntimeError("_initializing descriptor was run")
1486+
1487+ @property
1488+ def __dict__(self):
1489+ raise RuntimeError("__dict__ descriptor was run")
1490+
1491+ class SlottedSpec:
1492+ __slots__ = ()
1493+
1494+ @property
1495+ def _initializing(self):
1496+ raise RuntimeError("_initializing descriptor was run")
1497+
1498+ for spec in (Spec(), SlottedSpec()):
1499+ module = types.ModuleType("custom_spec")
1500+ module.__spec__ = spec
1501+ sys.modules["custom_spec"] = module
1502+ lazy import custom_spec
1503+ """ )
1504+ assert_python_ok ("-c" , code )
1505+
1506+ def test_cached_attribute_does_not_check_spec_twice (self ):
1507+ code = textwrap .dedent ("""
1508+ import sys
1509+ import types
1510+
1511+ class Spec:
1512+ def __init__(self):
1513+ self.calls = 0
1514+
1515+ @property
1516+ def _initializing(self):
1517+ self.calls += 1
1518+ if self.calls == 2:
1519+ raise RuntimeError("_initializing was read twice")
1520+ return False
1521+
1522+ module = types.ModuleType("cached_spec")
1523+ module.__spec__ = Spec()
1524+ module.attr = 1
1525+ sys.modules["cached_spec"] = module
1526+ lazy from cached_spec import attr
1527+ assert attr == 1, attr
1528+ """ )
1529+ assert_python_ok ("-c" , code )
1530+
1531+ def test_spec_initializing_truth_conversion_is_not_run (self ):
1532+ code = textwrap .dedent ("""
1533+ import sys
1534+ import types
1535+ from importlib.machinery import ModuleSpec
1536+
1537+ class Flag:
1538+ def __bool__(self):
1539+ raise RuntimeError("_initializing truth conversion was run")
1540+
1541+ released = memoryview(b"")
1542+ released.release()
1543+ for flag in (Flag(), released):
1544+ sys.lazy_modules.discard("custom_spec")
1545+ module = types.ModuleType("custom_spec")
1546+ module.__spec__ = ModuleSpec("custom_spec", None)
1547+ module.__spec__._initializing = flag
1548+ sys.modules["custom_spec"] = module
1549+ lazy import custom_spec
1550+ assert "custom_spec" in sys.lazy_modules, sys.lazy_modules
1551+ """ )
1552+ assert_python_ok ("-c" , code )
1553+
1554+ def test_pending_submodule_is_still_tracked (self ):
1555+ """`lazy from` a submodule that is not loaded must stay tracked."""
1556+ code = textwrap .dedent ("""
1557+ import sys
1558+ lazy from test.test_lazy_import.data.pkg import b
1559+ assert "test.test_lazy_import.data.pkg.b" in sys.lazy_modules, (
1560+ f"expected 'pkg.b' tracked, got {sys.lazy_modules}"
1561+ )
1562+ import test.test_lazy_import.data.pkg
1563+ assert "test.test_lazy_import.data.pkg.b" not in sys.modules, (
1564+ "loading the package must not load the submodule"
1565+ )
1566+ assert "test.test_lazy_import.data.pkg.b" in sys.lazy_modules, (
1567+ f"loading the package must not untrack the submodule, "
1568+ f"got {sys.lazy_modules}"
1569+ )
1570+ """ )
1571+ assert_python_ok ("-c" , code )
1572+
13161573
13171574@support .requires_subprocess ()
13181575class CommandLineAndEnvVarTests (unittest .TestCase ):
0 commit comments