| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -267,6 +267,7 @@ struct _Py_global_strings { | |||
| 267 | 267 | STRUCT_FOR_ID(_filters) | |
| 268 | 268 | STRUCT_FOR_ID(_finalizing) | |
| 269 | 269 | STRUCT_FOR_ID(_find_and_load) | |
| 270 | + STRUCT_FOR_ID(_find_and_load_lazy_submodule) | ||
| 270 | 271 | STRUCT_FOR_ID(_fix_up_module) | |
| 271 | 272 | STRUCT_FOR_ID(_flags_) | |
| 272 | 273 | STRUCT_FOR_ID(_get_sourcefile) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -39,8 +39,13 @@ extern PyObject * _PyImport_GetAbsName( | |||
| 39 | 39 | // Symbol is exported for the JIT on Windows builds. | |
| 40 | 40 | PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate( | |
| 41 | 41 | PyThreadState *tstate, PyObject *lazy_import); | |
| 42 | - extern PyObject * _PyImport_TryLoadLazySubmodule( | ||
| 43 | - PyObject *mod_name, PyObject *attr_name); | ||
| 42 | + typedef enum { | ||
| 43 | + _Py_LAZY_SUBMODULE_ERROR = -1, | ||
| 44 | + _Py_LAZY_SUBMODULE_NOT_FOUND = 0, | ||
| 45 | + _Py_LAZY_SUBMODULE_LOADED = 1, | ||
| 46 | + } _PyLazySubmoduleImportResult; | ||
| 47 | + extern _PyLazySubmoduleImportResult _PyImport_TryLoadLazySubmodule( | ||
| 48 | + PyObject *mod_name, PyObject *attr_name, PyObject **result); | ||
| 44 | 49 | extern PyObject * _PyImport_LazyImportModuleLevelObject( | |
| 45 | 50 | PyThreadState *tstate, PyObject *name, PyObject *builtins, | |
| 46 | 51 | PyObject *globals, PyObject *locals, PyObject *fromlist, int level); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1260,7 +1260,7 @@ def _sanity_check(name, package, level): | |||
| 1260 | 1260 | ||
| 1261 | 1261 | _ERR_MSG_PREFIX = 'No module named ' | |
| 1262 | 1262 | ||
| 1263 | - def _find_and_load_unlocked(name, import_): | ||
| 1263 | + def _find_and_load_unlocked(name, import_, *, lazy_submodule=False): | ||
| 1264 | 1264 | path = None | |
| 1265 | 1265 | sys.audit( | |
| 1266 | 1266 | "import", | |
@@ -1283,6 +1283,8 @@ def _find_and_load_unlocked(name, import_): | |||
| 1283 | 1283 | try: | |
| 1284 | 1284 | path = parent_module.__path__ | |
| 1285 | 1285 | except AttributeError: | |
| 1286 | + if lazy_submodule: | ||
| 1287 | + return None | ||
| 1286 | 1288 | msg = f'{_ERR_MSG_PREFIX}{name!r}; {parent!r} is not a package' | |
| 1287 | 1289 | raise ModuleNotFoundError(msg, name=name) from None | |
| 1288 | 1290 | parent_spec = parent_module.__spec__ | |
@@ -1295,6 +1297,8 @@ def _find_and_load_unlocked(name, import_): | |||
| 1295 | 1297 | child = name.rpartition('.')[2] | |
| 1296 | 1298 | spec = _find_spec(name, path) | |
| 1297 | 1299 | if spec is None: | |
| 1300 | + if lazy_submodule: | ||
| 1301 | + return None | ||
| 1298 | 1302 | raise ModuleNotFoundError(f'{_ERR_MSG_PREFIX}{name!r}', name=name) | |
| 1299 | 1303 | else: | |
| 1300 | 1304 | if parent_spec: | |
@@ -1326,7 +1330,7 @@ def _find_and_load_unlocked(name, import_): | |||
| 1326 | 1330 | _NEEDS_LOADING = object() | |
| 1327 | 1331 | ||
| 1328 | 1332 | ||
| 1329 | - def _find_and_load(name, import_): | ||
| 1333 | + def _find_and_load(name, import_, *, lazy_submodule=False): | ||
| 1330 | 1334 | """Find and load the module.""" | |
| 1331 | 1335 | ||
| 1332 | 1336 | # Optimization: we avoid unneeded module locking if the module | |
@@ -1343,7 +1347,8 @@ def _find_and_load(name, import_): | |||
| 1343 | 1347 | with lock_manager: | |
| 1344 | 1348 | module = sys.modules.get(name, _NEEDS_LOADING) | |
| 1345 | 1349 | if module is _NEEDS_LOADING: | |
| 1346 | - return _find_and_load_unlocked(name, import_) | ||
| 1350 | + return _find_and_load_unlocked( | ||
| 1351 | + name, import_, lazy_submodule=lazy_submodule) | ||
| 1347 | 1352 | ||
| 1348 | 1353 | # Optimization: only call _bootstrap._lock_unlock_module() if | |
| 1349 | 1354 | # module.__spec__._initializing is True. | |
@@ -1357,7 +1362,7 @@ def _find_and_load(name, import_): | |||
| 1357 | 1362 | # to preserve normal semantics: the caller gets the exception from | |
| 1358 | 1363 | # the actual import failure rather than a synthetic error. | |
| 1359 | 1364 | if sys.modules.get(name) is not module: | |
| 1360 | - return _find_and_load(name, import_) | ||
| 1365 | + return _find_and_load(name, import_, lazy_submodule=lazy_submodule) | ||
| 1361 | 1366 | ||
| 1362 | 1367 | if module is None: | |
| 1363 | 1368 | message = f'import of {name} halted; None in sys.modules' | |
@@ -1366,6 +1371,10 @@ def _find_and_load(name, import_): | |||
| 1366 | 1371 | return module | |
| 1367 | 1372 | ||
| 1368 | 1373 | ||
| 1374 | + def _find_and_load_lazy_submodule(name, import_): | ||
| 1375 | + return _find_and_load(name, import_, lazy_submodule=True) | ||
| 1376 | + | ||
| 1377 | + | ||
| 1369 | 1378 | def _gcd_import(name, package=None, level=0): | |
| 1370 | 1379 | """Import and return the module based on its name, the package the call is | |
| 1371 | 1380 | being made from, and the level adjustment. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -684,52 +684,35 @@ def test_lazy_modules_tracks_lazy_imports(self): | |||
| 684 | 684 | ||
| 685 | 685 | @support.requires_subprocess() | |
| 686 | 686 | class ErrorHandlingTests(LazyImportTestCase): | |
| 687 | - """Tests for error handling during lazy import reification. | ||
| 687 | + """Tests for error handling during lazy import reification.""" | ||
| 688 | 688 | ||
| 689 | - PEP 810: Errors during reification should show exception chaining with | ||
| 690 | - both the lazy import definition location and the access location. | ||
| 691 | - """ | ||
| 692 | - | ||
| 693 | - def test_import_error_shows_chained_traceback(self): | ||
| 689 | + def test_missing_lazy_submodule_raises_attribute_error(self): | ||
| 694 | 690 | """Accessing a nonexistent lazy submodule via parent attr raises AttributeError.""" | |
| 695 | 691 | code = textwrap.dedent(""" | |
| 696 | - import sys | ||
| 697 | 692 | lazy import test.test_lazy_import.data.nonexistent_module | |
| 698 | 693 | ||
| 699 | 694 | try: | |
| 700 | - x = test.test_lazy_import.data.nonexistent_module | ||
| 701 | - except AttributeError as e: | ||
| 702 | - print("OK") | ||
| 695 | + _ = test.test_lazy_import.data.nonexistent_module | ||
| 696 | + except AttributeError: | ||
| 697 | + pass | ||
| 698 | + else: | ||
| 699 | + raise AssertionError("AttributeError was not raised") | ||
| 703 | 700 | """) | |
| 704 | - result = subprocess.run( | ||
| 705 | - [sys.executable, "-c", code], | ||
| 706 | - capture_output=True, | ||
| 707 | - text=True | ||
| 708 | - ) | ||
| 709 | - self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") | ||
| 710 | - self.assertIn("OK", result.stdout) | ||
| 701 | + assert_python_ok("-c", code) | ||
| 711 | 702 | ||
| 712 | - def test_attribute_error_on_from_import_shows_chained_traceback(self): | ||
| 703 | + def test_missing_lazy_from_import_shows_chained_traceback(self): | ||
| 713 | 704 | """Accessing missing attribute from lazy from-import should chain errors.""" | |
| 714 | - # Tests 'lazy from module import nonexistent' behavior | ||
| 715 | 705 | code = textwrap.dedent(""" | |
| 716 | - import sys | ||
| 717 | 706 | lazy from test.test_lazy_import.data.basic2 import nonexistent_name | |
| 718 | 707 | ||
| 719 | 708 | try: | |
| 720 | - x = nonexistent_name | ||
| 709 | + _ = nonexistent_name | ||
| 721 | 710 | except ImportError as e: | |
| 722 | - # PEP 810: Enhanced error reporting through exception chaining | ||
| 723 | 711 | assert e.__cause__ is not None, "Expected chained exception" | |
| 724 | - print("OK") | ||
| 712 | + else: | ||
| 713 | + raise AssertionError("ImportError was not raised") | ||
| 725 | 714 | """) | |
| 726 | - result = subprocess.run( | ||
| 727 | - [sys.executable, "-c", code], | ||
| 728 | - capture_output=True, | ||
| 729 | - text=True | ||
| 730 | - ) | ||
| 731 | - self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") | ||
| 732 | - self.assertIn("OK", result.stdout) | ||
| 715 | + assert_python_ok("-c", code) | ||
| 733 | 716 | ||
| 734 | 717 | def test_reification_retries_on_failure(self): | |
| 735 | 718 | """Failed reification should allow retry on subsequent access. | |
@@ -739,53 +722,92 @@ def test_reification_retries_on_failure(self): | |||
| 739 | 722 | """ | |
| 740 | 723 | code = textwrap.dedent(""" | |
| 741 | 724 | import sys | |
| 742 | - import types | ||
| 743 | 725 | ||
| 744 | 726 | lazy import test.test_lazy_import.data.broken_module | |
| 745 | 727 | ||
| 746 | - # First access - should fail | ||
| 747 | 728 | try: | |
| 748 | - x = test.test_lazy_import.data.broken_module | ||
| 749 | - except AttributeError: | ||
| 750 | - pass | ||
| 729 | + _ = test.test_lazy_import.data.broken_module | ||
| 730 | + except ValueError as exc: | ||
| 731 | + assert str(exc) == "This module always fails to import", exc | ||
| 732 | + else: | ||
| 733 | + raise AssertionError("ValueError was not raised") | ||
| 734 | + | ||
| 735 | + assert "test.test_lazy_import.data.broken_module" not in sys.modules | ||
| 751 | 736 | ||
| 752 | - # The lazy object should still be a lazy proxy (not reified) | ||
| 753 | - g = globals() | ||
| 754 | - lazy_obj = g['test'] | ||
| 755 | - # The root 'test' binding should still allow retry | ||
| 756 | - # Second access - should also fail (retry the import) | ||
| 757 | 737 | try: | |
| 758 | - x = test.test_lazy_import.data.broken_module | ||
| 759 | - except AttributeError: | ||
| 760 | - print("OK - retry worked") | ||
| 738 | + _ = test.test_lazy_import.data.broken_module | ||
| 739 | + except ValueError as exc: | ||
| 740 | + assert str(exc) == "This module always fails to import", exc | ||
| 741 | + else: | ||
| 742 | + raise AssertionError("ValueError was not raised") | ||
| 761 | 743 | """) | |
| 762 | - result = subprocess.run( | ||
| 763 | - [sys.executable, "-c", code], | ||
| 764 | - capture_output=True, | ||
| 765 | - text=True | ||
| 766 | - ) | ||
| 767 | - self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") | ||
| 768 | - self.assertIn("OK", result.stdout) | ||
| 744 | + assert_python_ok("-c", code) | ||
| 769 | 745 | ||
| 770 | - def test_error_during_module_execution_propagates(self): | ||
| 771 | - """Errors in module code during reification should propagate correctly.""" | ||
| 746 | + def test_lazy_submodule_traceback_hides_importlib_frames(self): | ||
| 772 | 747 | code = textwrap.dedent(""" | |
| 773 | - import sys | ||
| 748 | + import traceback | ||
| 749 | + | ||
| 774 | 750 | lazy import test.test_lazy_import.data.broken_module | |
| 775 | 751 | ||
| 776 | 752 | try: | |
| 777 | 753 | _ = test.test_lazy_import.data.broken_module | |
| 778 | - print("FAIL - should have raised") | ||
| 779 | - except AttributeError: | ||
| 780 | - print("OK") | ||
| 754 | + except ValueError as exc: | ||
| 755 | + frames = traceback.extract_tb(exc.__traceback__) | ||
| 756 | + assert [frame.name for frame in frames] == ["<module>", "<module>"], frames | ||
| 757 | + assert frames[0].filename == "<string>", frames | ||
| 758 | + assert frames[1].filename.endswith("broken_module.py"), frames | ||
| 759 | + else: | ||
| 760 | + raise AssertionError("ValueError was not raised") | ||
| 781 | 761 | """) | |
| 782 | - result = subprocess.run( | ||
| 783 | - [sys.executable, "-c", code], | ||
| 784 | - capture_output=True, | ||
| 785 | - text=True | ||
| 786 | - ) | ||
| 787 | - self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") | ||
| 788 | - self.assertIn("OK", result.stdout) | ||
| 762 | + assert_python_ok("-c", code) | ||
| 763 | + | ||
| 764 | + def test_module_not_found_during_module_execution_propagates(self): | ||
| 765 | + code = textwrap.dedent(""" | ||
| 766 | + lazy import test.test_lazy_import.data.missing_dependency | ||
| 767 | + | ||
| 768 | + try: | ||
| 769 | + _ = test.test_lazy_import.data.missing_dependency | ||
| 770 | + except ModuleNotFoundError as exc: | ||
| 771 | + assert exc.name == "missing_dependency_for_lazy_import_test", exc.name | ||
| 772 | + assert str(exc) == "No module named 'missing_dependency_for_lazy_import_test'", exc | ||
| 773 | + else: | ||
| 774 | + raise AssertionError("ModuleNotFoundError was not raised") | ||
| 775 | + """) | ||
| 776 | + assert_python_ok("-c", code) | ||
| 777 | + | ||
| 778 | + def test_self_named_module_not_found_during_module_execution_propagates(self): | ||
| 779 | + code = textwrap.dedent(""" | ||
| 780 | + lazy import test.test_lazy_import.data.self_named_module_not_found | ||
| 781 | + | ||
| 782 | + try: | ||
| 783 | + _ = test.test_lazy_import.data.self_named_module_not_found | ||
| 784 | + except ModuleNotFoundError as exc: | ||
| 785 | + assert exc.name == "test.test_lazy_import.data.self_named_module_not_found", exc.name | ||
| 786 | + assert str(exc) == "boom", exc | ||
| 787 | + else: | ||
| 788 | + raise AssertionError("ModuleNotFoundError was not raised") | ||
| 789 | + """) | ||
| 790 | + assert_python_ok("-c", code) | ||
| 791 | + | ||
| 792 | + def test_none_in_sys_modules_during_submodule_resolution_propagates(self): | ||
| 793 | + code = textwrap.dedent(""" | ||
| 794 | + import sys | ||
| 795 | + | ||
| 796 | + sys.modules["test.test_lazy_import.data.blocked_module"] = None | ||
| 797 | + lazy import test.test_lazy_import.data.blocked_module | ||
| 798 | + | ||
| 799 | + try: | ||
| 800 | + _ = test.test_lazy_import.data.blocked_module | ||
| 801 | + except ModuleNotFoundError as exc: | ||
| 802 | + assert exc.name == "test.test_lazy_import.data.blocked_module", exc.name | ||
| 803 | + assert str(exc) == ( | ||
| 804 | + "import of test.test_lazy_import.data.blocked_module " | ||
| 805 | + "halted; None in sys.modules" | ||
| 806 | + ), exc | ||
| 807 | + else: | ||
| 808 | + raise AssertionError("ModuleNotFoundError was not raised") | ||
| 809 | + """) | ||
| 810 | + assert_python_ok("-c", code) | ||
| 789 | 811 | ||
| 790 | 812 | def test_circular_lazy_import_does_not_crash_for_gh_144727(self): | |
| 791 | 813 | with tempfile.TemporaryDirectory() as tmpdir: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1 @@ | |||
| 1 | + import missing_dependency_for_lazy_import_test | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1 @@ | |||
| 1 | + raise ModuleNotFoundError("boom", name=__name__) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,2 @@ | |||
| 1 | + Propagate exceptions raised while importing lazy submodules instead of | ||
| 2 | + reporting them as missing attributes. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments