| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -87,6 +87,8 @@ extern void _PyImport_ClearModulesByIndex(PyInterpreterState *interp); | |||
| 87 | 87 | extern PyObject * _PyImport_InitLazyModules( | |
| 88 | 88 | PyInterpreterState *interp); | |
| 89 | 89 | extern void _PyImport_ClearLazyModules(PyInterpreterState *interp); | |
| 90 | + extern int _PyImport_DiscardLazyModule( | ||
| 91 | + PyInterpreterState *interp, PyObject *name); | ||
| 90 | 92 | ||
| 91 | 93 | extern int _PyImport_InitDefaultImportFunc(PyInterpreterState *interp); | |
| 92 | 94 | extern int _PyImport_IsDefaultImportFunc( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -53,7 +53,8 @@ def test_sys_lazy_modules(self): | |||
| 53 | 53 | self.fail('lazy import failed') | |
| 54 | 54 | ||
| 55 | 55 | 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) | ||
| 57 | 58 | self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules) | |
| 58 | 59 | test.test_lazy_import.data.basic_from_unused.basic2 | |
| 59 | 60 | self.assertNotIn("test.test_import.data", sys.lazy_modules) | |
@@ -1305,6 +1306,262 @@ def test_lazy_module_without_children_is_tracked(self): | |||
| 1305 | 1306 | """) | |
| 1306 | 1307 | assert_python_ok("-c", code) | |
| 1307 | 1308 | ||
| 1309 | + def test_already_loaded_module_is_not_tracked(self): | ||
| 1310 | + """A lazy import of an already loaded module should not be tracked.""" | ||
| 1311 | + code = textwrap.dedent(""" | ||
| 1312 | + import sys | ||
| 1313 | + | ||
| 1314 | + # Loaded by a regular import. | ||
| 1315 | + import json | ||
| 1316 | + lazy import json as lazy_json | ||
| 1317 | + assert "json" not in sys.lazy_modules, ( | ||
| 1318 | + f"expected 'json' not in sys.lazy_modules, got {sys.lazy_modules}" | ||
| 1319 | + ) | ||
| 1320 | + | ||
| 1321 | + # Loaded by reifying an earlier lazy import. | ||
| 1322 | + lazy import base64 | ||
| 1323 | + _ = base64.b64encode | ||
| 1324 | + lazy import base64 as lazy_base64 | ||
| 1325 | + assert "base64" not in sys.lazy_modules, ( | ||
| 1326 | + f"expected 'base64' not in sys.lazy_modules, got {sys.lazy_modules}" | ||
| 1327 | + ) | ||
| 1328 | + """) | ||
| 1329 | + assert_python_ok("-c", code) | ||
| 1330 | + | ||
| 1331 | + def test_already_loaded_submodule_is_not_tracked(self): | ||
| 1332 | + """`lazy from` a loaded submodule should not be tracked either.""" | ||
| 1333 | + code = textwrap.dedent(""" | ||
| 1334 | + import sys | ||
| 1335 | + import test.test_lazy_import.data.pkg.b | ||
| 1336 | + lazy from test.test_lazy_import.data.pkg import b | ||
| 1337 | + assert "test.test_lazy_import.data.pkg.b" not in sys.lazy_modules, ( | ||
| 1338 | + f"expected 'pkg.b' untracked, got {sys.lazy_modules}" | ||
| 1339 | + ) | ||
| 1340 | + """) | ||
| 1341 | + assert_python_ok("-c", code) | ||
| 1342 | + | ||
| 1343 | + def test_attribute_entry_removed_on_reification(self): | ||
| 1344 | + """`lazy from x import attr` should untrack "x.attr" once resolved.""" | ||
| 1345 | + code = textwrap.dedent(""" | ||
| 1346 | + import sys | ||
| 1347 | + lazy from test.test_lazy_import.data.basic2 import x | ||
| 1348 | + assert "test.test_lazy_import.data.basic2.x" in sys.lazy_modules, ( | ||
| 1349 | + f"expected 'basic2.x' tracked, got {sys.lazy_modules}" | ||
| 1350 | + ) | ||
| 1351 | + _ = x | ||
| 1352 | + assert "test.test_lazy_import.data.basic2.x" not in sys.lazy_modules, ( | ||
| 1353 | + f"expected 'basic2.x' untracked, got {sys.lazy_modules}" | ||
| 1354 | + ) | ||
| 1355 | + """) | ||
| 1356 | + assert_python_ok("-c", code) | ||
| 1357 | + | ||
| 1358 | + def test_already_loaded_attribute_is_not_tracked(self): | ||
| 1359 | + code = textwrap.dedent(""" | ||
| 1360 | + import sys | ||
| 1361 | + import math | ||
| 1362 | + | ||
| 1363 | + lazy from math import pi | ||
| 1364 | + assert "math.pi" not in sys.lazy_modules, sys.lazy_modules | ||
| 1365 | + assert pi == math.pi | ||
| 1366 | + """) | ||
| 1367 | + assert_python_ok("-c", code) | ||
| 1368 | + | ||
| 1369 | + def test_cached_lazy_attribute_tracks_its_target(self): | ||
| 1370 | + code = textwrap.dedent(""" | ||
| 1371 | + import sys | ||
| 1372 | + import test.test_lazy_import.data.basic_from_unused | ||
| 1373 | + | ||
| 1374 | + holder = "test.test_lazy_import.data.basic_from_unused" | ||
| 1375 | + target = "test.test_lazy_import.data.basic2" | ||
| 1376 | + | ||
| 1377 | + namespace = {"__name__": "cached_import_test"} | ||
| 1378 | + exec(f"lazy from {holder} import basic2", namespace) | ||
| 1379 | + assert holder + ".basic2" not in sys.lazy_modules, sys.lazy_modules | ||
| 1380 | + assert target in sys.lazy_modules, sys.lazy_modules | ||
| 1381 | + exec("assert basic2.x == 42", namespace) | ||
| 1382 | + assert target not in sys.lazy_modules, sys.lazy_modules | ||
| 1383 | + """) | ||
| 1384 | + assert_python_ok("-c", code) | ||
| 1385 | + | ||
| 1386 | + def test_cached_attribute_keeps_pending_module_tracked(self): | ||
| 1387 | + code = textwrap.dedent(""" | ||
| 1388 | + import sys | ||
| 1389 | + import test.test_lazy_import.data.pkg as pkg | ||
| 1390 | + pkg.b = 42 | ||
| 1391 | + | ||
| 1392 | + lazy import test.test_lazy_import.data.pkg.b as pending | ||
| 1393 | + lazy from test.test_lazy_import.data.pkg import b | ||
| 1394 | + name = "test.test_lazy_import.data.pkg.b" | ||
| 1395 | + assert b == 42, b | ||
| 1396 | + assert name not in sys.modules, sys.modules | ||
| 1397 | + assert name in sys.lazy_modules, sys.lazy_modules | ||
| 1398 | + assert pending.foo() == "foo" | ||
| 1399 | + assert name not in sys.lazy_modules, sys.lazy_modules | ||
| 1400 | + """) | ||
| 1401 | + assert_python_ok("-c", code) | ||
| 1402 | + | ||
| 1403 | + def test_failed_reification_stays_tracked(self): | ||
| 1404 | + """A lazy import that fails to resolve must stay tracked.""" | ||
| 1405 | + code = textwrap.dedent(""" | ||
| 1406 | + import sys | ||
| 1407 | + lazy import test.test_lazy_import.data.broken_module | ||
| 1408 | + try: | ||
| 1409 | + _ = test.test_lazy_import.data.broken_module | ||
| 1410 | + except ValueError: | ||
| 1411 | + pass | ||
| 1412 | + else: | ||
| 1413 | + raise AssertionError("ValueError was not raised") | ||
| 1414 | + assert "test.test_lazy_import.data.broken_module" in sys.lazy_modules, ( | ||
| 1415 | + f"failed reification must stay tracked, got {sys.lazy_modules}" | ||
| 1416 | + ) | ||
| 1417 | + """) | ||
| 1418 | + assert_python_ok("-c", code) | ||
| 1419 | + | ||
| 1420 | + def test_blocked_module_is_still_tracked(self): | ||
| 1421 | + """A ``None`` entry in sys.modules must not count as loaded.""" | ||
| 1422 | + code = textwrap.dedent(""" | ||
| 1423 | + import sys | ||
| 1424 | + sys.modules['test.test_lazy_import.data.basic2'] = None | ||
| 1425 | + lazy import test.test_lazy_import.data.basic2 | ||
| 1426 | + assert "test.test_lazy_import.data.basic2" in sys.lazy_modules, ( | ||
| 1427 | + f"blocked module must stay tracked, got {sys.lazy_modules}" | ||
| 1428 | + ) | ||
| 1429 | + """) | ||
| 1430 | + assert_python_ok("-c", code) | ||
| 1431 | + | ||
| 1432 | + def test_initializing_module_is_still_tracked(self): | ||
| 1433 | + """A module that is still executing must not count as loaded.""" | ||
| 1434 | + code = textwrap.dedent(""" | ||
| 1435 | + import sys | ||
| 1436 | + name = "test.test_lazy_import.data.init_fails" | ||
| 1437 | + try: | ||
| 1438 | + import test.test_lazy_import.data.init_fails | ||
| 1439 | + except ValueError: | ||
| 1440 | + pass | ||
| 1441 | + else: | ||
| 1442 | + raise AssertionError("ValueError was not raised") | ||
| 1443 | + assert name not in sys.modules, "failed import left a module behind" | ||
| 1444 | + assert name in sys.lazy_modules, ( | ||
| 1445 | + f"expected {name!r} tracked, got {sys.lazy_modules}" | ||
| 1446 | + ) | ||
| 1447 | + """) | ||
| 1448 | + assert_python_ok("-c", code) | ||
| 1449 | + | ||
| 1450 | + def test_module_spec_descriptor_is_not_run(self): | ||
| 1451 | + """Checking whether a module is loaded must not run its descriptors.""" | ||
| 1452 | + code = textwrap.dedent(""" | ||
| 1453 | + import sys | ||
| 1454 | + import types | ||
| 1455 | + | ||
| 1456 | + class RaisingSpec(types.ModuleType): | ||
| 1457 | + @property | ||
| 1458 | + def __spec__(self): | ||
| 1459 | + raise RuntimeError("__spec__ descriptor was run") | ||
| 1460 | + | ||
| 1461 | + sys.modules["raising_spec"] = RaisingSpec("raising_spec") | ||
| 1462 | + lazy import raising_spec | ||
| 1463 | + assert "raising_spec" not in sys.lazy_modules, ( | ||
| 1464 | + f"expected 'raising_spec' untracked, got {sys.lazy_modules}" | ||
| 1465 | + ) | ||
| 1466 | + """) | ||
| 1467 | + assert_python_ok("-c", code) | ||
| 1468 | + | ||
| 1469 | + def test_spec_initializing_descriptor_is_not_run(self): | ||
| 1470 | + code = textwrap.dedent(""" | ||
| 1471 | + import sys | ||
| 1472 | + import types | ||
| 1473 | + | ||
| 1474 | + class Spec: | ||
| 1475 | + @property | ||
| 1476 | + def _initializing(self): | ||
| 1477 | + raise RuntimeError("_initializing descriptor was run") | ||
| 1478 | + | ||
| 1479 | + @property | ||
| 1480 | + def __dict__(self): | ||
| 1481 | + raise RuntimeError("__dict__ descriptor was run") | ||
| 1482 | + | ||
| 1483 | + class SlottedSpec: | ||
| 1484 | + __slots__ = () | ||
| 1485 | + | ||
| 1486 | + @property | ||
| 1487 | + def _initializing(self): | ||
| 1488 | + raise RuntimeError("_initializing descriptor was run") | ||
| 1489 | + | ||
| 1490 | + for spec in (Spec(), SlottedSpec()): | ||
| 1491 | + module = types.ModuleType("custom_spec") | ||
| 1492 | + module.__spec__ = spec | ||
| 1493 | + sys.modules["custom_spec"] = module | ||
| 1494 | + lazy import custom_spec | ||
| 1495 | + """) | ||
| 1496 | + assert_python_ok("-c", code) | ||
| 1497 | + | ||
| 1498 | + def test_cached_attribute_does_not_check_spec_twice(self): | ||
| 1499 | + code = textwrap.dedent(""" | ||
| 1500 | + import sys | ||
| 1501 | + import types | ||
| 1502 | + | ||
| 1503 | + class Spec: | ||
| 1504 | + def __init__(self): | ||
| 1505 | + self.calls = 0 | ||
| 1506 | + | ||
| 1507 | + @property | ||
| 1508 | + def _initializing(self): | ||
| 1509 | + self.calls += 1 | ||
| 1510 | + if self.calls == 2: | ||
| 1511 | + raise RuntimeError("_initializing was read twice") | ||
| 1512 | + return False | ||
| 1513 | + | ||
| 1514 | + module = types.ModuleType("cached_spec") | ||
| 1515 | + module.__spec__ = Spec() | ||
| 1516 | + module.attr = 1 | ||
| 1517 | + sys.modules["cached_spec"] = module | ||
| 1518 | + lazy from cached_spec import attr | ||
| 1519 | + assert attr == 1, attr | ||
| 1520 | + """) | ||
| 1521 | + assert_python_ok("-c", code) | ||
| 1522 | + | ||
| 1523 | + def test_spec_initializing_truth_conversion_is_not_run(self): | ||
| 1524 | + code = textwrap.dedent(""" | ||
| 1525 | + import sys | ||
| 1526 | + import types | ||
| 1527 | + from importlib.machinery import ModuleSpec | ||
| 1528 | + | ||
| 1529 | + class Flag: | ||
| 1530 | + def __bool__(self): | ||
| 1531 | + raise RuntimeError("_initializing truth conversion was run") | ||
| 1532 | + | ||
| 1533 | + released = memoryview(b"") | ||
| 1534 | + released.release() | ||
| 1535 | + for flag in (Flag(), released): | ||
| 1536 | + sys.lazy_modules.discard("custom_spec") | ||
| 1537 | + module = types.ModuleType("custom_spec") | ||
| 1538 | + module.__spec__ = ModuleSpec("custom_spec", None) | ||
| 1539 | + module.__spec__._initializing = flag | ||
| 1540 | + sys.modules["custom_spec"] = module | ||
| 1541 | + lazy import custom_spec | ||
| 1542 | + assert "custom_spec" in sys.lazy_modules, sys.lazy_modules | ||
| 1543 | + """) | ||
| 1544 | + assert_python_ok("-c", code) | ||
| 1545 | + | ||
| 1546 | + def test_pending_submodule_is_still_tracked(self): | ||
| 1547 | + """`lazy from` a submodule that is not loaded must stay tracked.""" | ||
| 1548 | + code = textwrap.dedent(""" | ||
| 1549 | + import sys | ||
| 1550 | + lazy from test.test_lazy_import.data.pkg import b | ||
| 1551 | + assert "test.test_lazy_import.data.pkg.b" in sys.lazy_modules, ( | ||
| 1552 | + f"expected 'pkg.b' tracked, got {sys.lazy_modules}" | ||
| 1553 | + ) | ||
| 1554 | + import test.test_lazy_import.data.pkg | ||
| 1555 | + assert "test.test_lazy_import.data.pkg.b" not in sys.modules, ( | ||
| 1556 | + "loading the package must not load the submodule" | ||
| 1557 | + ) | ||
| 1558 | + assert "test.test_lazy_import.data.pkg.b" in sys.lazy_modules, ( | ||
| 1559 | + f"loading the package must not untrack the submodule, " | ||
| 1560 | + f"got {sys.lazy_modules}" | ||
| 1561 | + ) | ||
| 1562 | + """) | ||
| 1563 | + assert_python_ok("-c", code) | ||
| 1564 | + | ||
| 1308 | 1565 | ||
| 1309 | 1566 | @support.requires_subprocess() | |
| 1310 | 1567 | class CommandLineAndEnvVarTests(unittest.TestCase): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,4 @@ | |||
| 1 | + # Imported by test_initializing_module_is_still_tracked. The module it imports | ||
| 2 | + # lazily imports this one back while this one is still initializing. | ||
| 3 | + import test.test_lazy_import.data.lazy_on_init_fails | ||
| 4 | + raise ValueError("initialization failed") | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1 @@ | |||
| 1 | + lazy import test.test_lazy_import.data.init_fails | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + Resolved names are now removed from :data:`sys.lazy_modules` more | ||
| 2 | + consistently: a lazy import of an already loaded module is no longer recorded, | ||
| 3 | + and reifying ``lazy from pkg import attr`` now discards the ``pkg.attr`` entry. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -452,6 +452,14 @@ lazy_import_resolve_impl(PyThreadState *tstate, PyObject *lazy_import, | |||
| 452 | 452 | lazy_import_add_exception_cause(tstate, lz); | |
| 453 | 453 | } | |
| 454 | 454 | assert(obj == NULL || !PyLazyImport_CheckExact(obj)); | |
| 455 | + if (obj != NULL) { | ||
| 456 | + PyObject *name = lazy_import_name(lz); | ||
| 457 | + if (name == NULL || | ||
| 458 | + _PyImport_DiscardLazyModule(tstate->interp, name) < 0) { | ||
| 459 | + Py_CLEAR(obj); | ||
| 460 | + } | ||
| 461 | + Py_XDECREF(name); | ||
| 462 | + } | ||
| 455 | 463 | if (resolving != NULL) { | |
| 456 | 464 | // A failed set resize can leave the placeholder inserted. Removing by | |
| 457 | 465 | // identity also permits greenlets to finish in a different order. | |
| Back | FazBrowse Home | New Git URL |
0 commit comments