@@ -167,6 +167,103 @@ def test_from_import_with_imported_module_getattr(self):
167167 """ )
168168 assert_python_ok ("-c" , code )
169169
170+ @support .requires_subprocess ()
171+ def test_dotted_import_calls_full_name_import_hook (self ):
172+ """Dotted lazy imports should call __import__ with the full name."""
173+ code = textwrap .dedent (r"""
174+ import atexit
175+ import builtins
176+ import shutil
177+ import sys
178+ import tempfile
179+ import types
180+ from pathlib import Path
181+
182+ PKG = "hookpkg_gh_152297"
183+ SUBMOD = f"{PKG}.sub"
184+
185+ real_import = builtins.__import__
186+ full_name_calls = []
187+ calls = []
188+
189+ tmpdir = tempfile.mkdtemp()
190+ atexit.register(shutil.rmtree, tmpdir, ignore_errors=True)
191+
192+ package_dir = Path(tmpdir, PKG)
193+ package_dir.mkdir()
194+ (package_dir / "__init__.py").touch()
195+ (package_dir / "sub.py").write_text(
196+ "VALUE = 'real-submodule'\n", encoding="utf-8")
197+
198+ sys.path.insert(0, tmpdir)
199+ atexit.register(setattr, builtins, "__import__", real_import)
200+
201+ def custom_import(name, globals=None, locals=None, fromlist=(),
202+ level=0):
203+ caller = (
204+ globals.get("__name__")
205+ if isinstance(globals, dict) else None
206+ )
207+ call = (name, caller, tuple(fromlist or ()))
208+ calls.append(call)
209+
210+ if name == SUBMOD:
211+ full_name_calls.append(call)
212+ package = real_import(PKG, globals, locals, (), level)
213+ module = types.ModuleType(SUBMOD)
214+ module.VALUE = "hooked-submodule"
215+ package.sub = module
216+ sys.modules[SUBMOD] = module
217+ return package
218+
219+ return real_import(name, globals, locals, fromlist, level)
220+
221+ builtins.__import__ = custom_import
222+
223+ lazy import hookpkg_gh_152297.sub
224+
225+ assert not full_name_calls, calls
226+ assert hookpkg_gh_152297.sub.VALUE == "hooked-submodule", calls
227+ assert full_name_calls == [(SUBMOD, "__main__", ())], calls
228+ """ )
229+ assert_python_ok ("-c" , code )
230+
231+ @support .requires_subprocess ()
232+ def test_dotted_import_first_use_loads_full_target (self ):
233+ """First use of a dotted lazy import should load the submodule."""
234+ code = textwrap .dedent (r"""
235+ import atexit
236+ import shutil
237+ import sys
238+ import tempfile
239+ from pathlib import Path
240+
241+ PKG = "hookpkg_gh_152297_default"
242+ SUBMOD = f"{PKG}.sub"
243+
244+ tmpdir = tempfile.mkdtemp()
245+ atexit.register(shutil.rmtree, tmpdir, ignore_errors=True)
246+
247+ package_dir = Path(tmpdir, PKG)
248+ package_dir.mkdir()
249+ (package_dir / "__init__.py").touch()
250+ (package_dir / "sub.py").write_text(
251+ "VALUE = 42\n", encoding="utf-8")
252+
253+ sys.path.insert(0, tmpdir)
254+
255+ lazy import hookpkg_gh_152297_default.sub
256+
257+ assert SUBMOD not in sys.modules
258+
259+ _ = hookpkg_gh_152297_default
260+
261+ assert PKG in sys.modules
262+ assert SUBMOD in sys.modules
263+ assert hookpkg_gh_152297_default.sub.VALUE == 42
264+ """ )
265+ assert_python_ok ("-c" , code )
266+
170267
171268class GlobalLazyImportModeTests (LazyImportTestCase ):
172269 """Tests for sys.set_lazy_imports() global mode control."""
@@ -658,14 +755,15 @@ class ErrorHandlingTests(LazyImportTestCase):
658755 """
659756
660757 def test_import_error_shows_chained_traceback (self ):
661- """Accessing a nonexistent lazy submodule via parent attr raises AttributeError ."""
758+ """A failed dotted lazy import should preserve the import failure ."""
662759 code = textwrap .dedent ("""
663760 import sys
664761 lazy import test.test_lazy_import.data.nonexistent_module
665762
666763 try:
667764 x = test.test_lazy_import.data.nonexistent_module
668- except AttributeError as e:
765+ except ModuleNotFoundError as e:
766+ assert e.__cause__ is not None, "Expected chained exception"
669767 print("OK")
670768 """ )
671769 result = subprocess .run (
@@ -710,20 +808,22 @@ def test_reification_retries_on_failure(self):
710808
711809 lazy import test.test_lazy_import.data.broken_module
712810
811+ def lazy_proxy():
812+ return globals()['test']
813+
713814 # First access - should fail
714815 try:
715816 x = test.test_lazy_import.data.broken_module
716- except AttributeError :
817+ except ValueError :
717818 pass
718819
719- # The lazy object should still be a lazy proxy (not reified)
720- g = globals()
721- lazy_obj = g['test']
722- # The root 'test' binding should still allow retry
820+ assert type(lazy_proxy()) is types.LazyImportType
821+
723822 # Second access - should also fail (retry the import)
724823 try:
725824 x = test.test_lazy_import.data.broken_module
726- except AttributeError:
825+ except ValueError:
826+ assert type(lazy_proxy()) is types.LazyImportType
727827 print("OK - retry worked")
728828 """ )
729829 result = subprocess .run (
@@ -743,7 +843,8 @@ def test_error_during_module_execution_propagates(self):
743843 try:
744844 _ = test.test_lazy_import.data.broken_module
745845 print("FAIL - should have raised")
746- except AttributeError:
846+ except ValueError as e:
847+ assert str(e) == "This module always fails to import"
747848 print("OK")
748849 """ )
749850 result = subprocess .run (
0 commit comments