Skip to content

Commit 9715124

Browse files
committed
Do not reify in tracebacks
1 parent 9bcbe0a commit 9715124

File tree

3 files changed

+69
-0
lines changed

3 files changed

+69
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
print("BAR_MODULE_LOADED")
12
def f(): pass

Lib/test/test_traceback.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5273,5 +5273,47 @@ def expected(t, m, fn, l, f, E, e, z):
52735273
]
52745274
self.assertEqual(actual, expected(**colors))
52755275

5276+
5277+
class TestLazyImportSuggestions(unittest.TestCase):
5278+
"""Test that lazy imports are not reified when computing AttributeError suggestions."""
5279+
5280+
def test_attribute_error_does_not_reify_lazy_imports(self):
5281+
"""Printing an AttributeError should not trigger lazy import reification."""
5282+
# pkg.bar prints "BAR_MODULE_LOADED" when imported.
5283+
# If lazy import is reified during suggestion computation, we'll see it.
5284+
code = textwrap.dedent("""
5285+
lazy import test.test_import.data.lazy_imports.pkg.bar
5286+
test.test_import.data.lazy_imports.pkg.nonexistent
5287+
""")
5288+
rc, stdout, stderr = assert_python_failure('-c', code)
5289+
self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
5290+
5291+
def test_traceback_formatting_does_not_reify_lazy_imports(self):
5292+
"""Formatting a traceback should not trigger lazy import reification."""
5293+
code = textwrap.dedent("""
5294+
import traceback
5295+
lazy import test.test_import.data.lazy_imports.pkg.bar
5296+
try:
5297+
test.test_import.data.lazy_imports.pkg.nonexistent
5298+
except AttributeError:
5299+
traceback.format_exc()
5300+
print("OK")
5301+
""")
5302+
rc, stdout, stderr = assert_python_ok('-c', code)
5303+
self.assertIn(b"OK", stdout)
5304+
self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
5305+
5306+
def test_suggestion_still_works_for_non_lazy_attributes(self):
5307+
"""Suggestions should still work for non-lazy module attributes."""
5308+
code = textwrap.dedent("""
5309+
lazy import test.test_import.data.lazy_imports.pkg.bar
5310+
# Typo for __name__
5311+
test.test_import.data.lazy_imports.pkg.__nme__
5312+
""")
5313+
rc, stdout, stderr = assert_python_failure('-c', code)
5314+
self.assertIn(b"__name__", stderr)
5315+
self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
5316+
5317+
52765318
if __name__ == "__main__":
52775319
unittest.main()

Lib/traceback.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import linecache
66
import sys
77
import textwrap
8+
import types
89
import warnings
910
import codeop
1011
import keyword
@@ -1623,12 +1624,29 @@ def _substitution_cost(ch_a, ch_b):
16231624
return _MOVE_COST
16241625

16251626

1627+
def _is_lazy_import(obj, attr_name):
1628+
"""Check if attr_name in obj's __dict__ is a lazy import.
1629+
1630+
Returns True if obj is a module and the attribute is a LazyImportType,
1631+
False otherwise. This avoids triggering module loading when computing
1632+
suggestions for AttributeError.
1633+
"""
1634+
if not isinstance(obj, types.ModuleType):
1635+
return False
1636+
obj_dict = getattr(obj, '__dict__', None)
1637+
if obj_dict is None:
1638+
return False
1639+
attr_value = obj_dict.get(attr_name)
1640+
return isinstance(attr_value, types.LazyImportType)
1641+
1642+
16261643
def _check_for_nested_attribute(obj, wrong_name, attrs):
16271644
"""Check if any attribute of obj has the wrong_name as a nested attribute.
16281645
16291646
Returns the first nested attribute suggestion found, or None.
16301647
Limited to checking 20 attributes.
16311648
Only considers non-descriptor attributes to avoid executing arbitrary code.
1649+
Skips lazy imports to avoid triggering module loading.
16321650
"""
16331651
# Check for nested attributes (only one level deep)
16341652
attrs_to_check = [x for x in attrs if not x.startswith('_')][:20] # Limit number of attributes to check
@@ -1639,6 +1657,10 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
16391657
if attr_from_class is not None and hasattr(attr_from_class, '__get__'):
16401658
continue # Skip descriptors to avoid executing arbitrary code
16411659

1660+
# Skip lazy imports to avoid triggering module loading
1661+
if _is_lazy_import(obj, attr_name):
1662+
continue
1663+
16421664
# Safe to get the attribute since it's not a descriptor
16431665
attr_obj = getattr(obj, attr_name)
16441666

@@ -1662,6 +1684,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
16621684
except TypeError: # Attributes are unsortable, e.g. int and str
16631685
d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys())
16641686
d = sorted([x for x in d if isinstance(x, str)])
1687+
# Filter out lazy imports to avoid triggering module loading
1688+
d = [x for x in d if not _is_lazy_import(obj, x)]
16651689
hide_underscored = (wrong_name[:1] != '_')
16661690
if hide_underscored and tb is not None:
16671691
while tb.tb_next is not None:
@@ -1681,6 +1705,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
16811705
except TypeError: # Attributes are unsortable, e.g. int and str
16821706
d = list(mod.__dict__.keys())
16831707
d = sorted([x for x in d if isinstance(x, str)])
1708+
# Filter out lazy imports to avoid triggering module loading
1709+
d = [x for x in d if not _is_lazy_import(mod, x)]
16841710
if wrong_name[:1] != '_':
16851711
d = [x for x in d if x[:1] != '_']
16861712
except Exception:

0 commit comments

Comments
 (0)