Skip to content

Commit a2d5480

Browse files
authored
Merge branch 'main' into hy/close_issue_137942
2 parents 3a847bc + bb8791c commit a2d5480

27 files changed

+2480
-577
lines changed

Doc/reference/simple_stmts.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -971,10 +971,17 @@ as globals. It would be impossible to assign to a global variable without
971971
:keyword:`!global`, although free variables may refer to globals without being
972972
declared global.
973973

974-
The :keyword:`global` statement applies to the entire scope of a function or
975-
class body. A :exc:`SyntaxError` is raised if a variable is used or
974+
The :keyword:`!global` statement applies to the entire current scope
975+
(module, function body or class definition).
976+
A :exc:`SyntaxError` is raised if a variable is used or
976977
assigned to prior to its global declaration in the scope.
977978

979+
At the module level, all variables are global, so a :keyword:`!global`
980+
statement has no effect.
981+
However, variables must still not be used or
982+
assigned to prior to their :keyword:`!global` declaration.
983+
This requirement is relaxed in the interactive prompt (:term:`REPL`).
984+
978985
.. index::
979986
pair: built-in function; exec
980987
pair: built-in function; eval

Doc/tutorial/classes.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ Usually, a method is called right after it is bound::
359359

360360
x.f()
361361

362-
In the :class:`!MyClass` example, this will return the string ``'hello world'``.
362+
If ``x = MyClass()``, as above, this will return the string ``'hello world'``.
363363
However, it is not necessary to call a method right away: ``x.f`` is a method
364364
object, and can be stored away and called at a later time. For example::
365365

Doc/whatsnew/3.15.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,45 @@ production systems where traditional profiling approaches would be too intrusive
169169
(Contributed by Pablo Galindo and László Kiss Kollár in :gh:`135953`.)
170170

171171

172+
Improved error messages
173+
-----------------------
174+
175+
* The interpreter now provides more helpful suggestions in :exc:`AttributeError`
176+
exceptions when accessing an attribute on an object that does not exist, but
177+
a similar attribute is available through one of its members.
178+
179+
For example, if the object has an attribute that itself exposes the requested
180+
name, the error message will suggest accessing it via that inner attribute:
181+
182+
.. code-block:: python
183+
184+
@dataclass
185+
class Circle:
186+
radius: float
187+
188+
@property
189+
def area(self) -> float:
190+
return pi * self.radius**2
191+
192+
class Container:
193+
def __init__(self, inner: Circle) -> None:
194+
self.inner = inner
195+
196+
circle = Circle(radius=4.0)
197+
container = Container(circle)
198+
print(container.area)
199+
200+
Running this code now produces a clearer suggestion:
201+
202+
.. code-block:: pycon
203+
204+
Traceback (most recent call last):
205+
File "/home/pablogsal/github/python/main/lel.py", line 42, in <module>
206+
print(container.area)
207+
^^^^^^^^^^^^^^
208+
AttributeError: 'Container' object has no attribute 'area'. Did you mean: 'inner.area'?
209+
210+
172211
Other language changes
173212
======================
174213

Include/internal/pycore_ceval.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ _PyEval_EvalFrame(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwfl
123123
return tstate->interp->eval_frame(tstate, frame, throwflag);
124124
}
125125

126+
#ifdef _Py_TIER2
127+
#ifdef _Py_JIT
128+
_Py_CODEUNIT *_Py_LazyJitTrampoline(
129+
struct _PyExecutorObject *current_executor, _PyInterpreterFrame *frame,
130+
_PyStackRef *stack_pointer, PyThreadState *tstate
131+
);
132+
#else
133+
_Py_CODEUNIT *_PyTier2Interpreter(
134+
struct _PyExecutorObject *current_executor, _PyInterpreterFrame *frame,
135+
_PyStackRef *stack_pointer, PyThreadState *tstate
136+
);
137+
#endif
138+
#endif
139+
140+
extern _PyJitEntryFuncPtr _Py_jit_entry;
141+
126142
extern PyObject*
127143
_PyEval_Vector(PyThreadState *tstate,
128144
PyFunctionObject *func, PyObject *locals,

Include/internal/pycore_interp_structs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ struct _Py_unique_id_pool {
765765

766766
#endif
767767

768+
typedef _Py_CODEUNIT *(*_PyJitEntryFuncPtr)(struct _PyExecutorObject *exec, _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate);
768769

769770
/* PyInterpreterState holds the global state for one of the runtime's
770771
interpreters. Typically the initial (main) interpreter is the only one.

Include/internal/pycore_optimizer.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ typedef struct _PyExecutorObject {
8282
uint32_t code_size;
8383
size_t jit_size;
8484
void *jit_code;
85-
void *jit_side_entry;
8685
_PyExitData exits[1];
8786
} _PyExecutorObject;
8887

Lib/test/test_traceback.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4262,6 +4262,184 @@ def __getattribute__(self, attr):
42624262
self.assertIn("Did you mean", actual)
42634263
self.assertIn("bluch", actual)
42644264

4265+
def test_getattr_nested_attribute_suggestions(self):
4266+
# Test that nested attributes are suggested when no direct match
4267+
class Inner:
4268+
def __init__(self):
4269+
self.value = 42
4270+
self.data = "test"
4271+
4272+
class Outer:
4273+
def __init__(self):
4274+
self.inner = Inner()
4275+
4276+
# Should suggest 'inner.value'
4277+
actual = self.get_suggestion(Outer(), 'value')
4278+
self.assertIn("Did you mean: 'inner.value'", actual)
4279+
4280+
# Should suggest 'inner.data'
4281+
actual = self.get_suggestion(Outer(), 'data')
4282+
self.assertIn("Did you mean: 'inner.data'", actual)
4283+
4284+
def test_getattr_nested_prioritizes_direct_matches(self):
4285+
# Test that direct attribute matches are prioritized over nested ones
4286+
class Inner:
4287+
def __init__(self):
4288+
self.foo = 42
4289+
4290+
class Outer:
4291+
def __init__(self):
4292+
self.inner = Inner()
4293+
self.fooo = 100 # Similar to 'foo'
4294+
4295+
# Should suggest 'fooo' (direct) not 'inner.foo' (nested)
4296+
actual = self.get_suggestion(Outer(), 'foo')
4297+
self.assertIn("Did you mean: 'fooo'", actual)
4298+
self.assertNotIn("inner.foo", actual)
4299+
4300+
def test_getattr_nested_with_property(self):
4301+
# Test that descriptors (including properties) are suggested in nested attributes
4302+
class Inner:
4303+
@property
4304+
def computed(self):
4305+
return 42
4306+
4307+
class Outer:
4308+
def __init__(self):
4309+
self.inner = Inner()
4310+
4311+
actual = self.get_suggestion(Outer(), 'computed')
4312+
# Descriptors should not be suggested to avoid executing arbitrary code
4313+
self.assertIn("inner.computed", actual)
4314+
4315+
def test_getattr_nested_no_suggestion_for_deep_nesting(self):
4316+
# Test that deeply nested attributes (2+ levels) are not suggested
4317+
class Deep:
4318+
def __init__(self):
4319+
self.value = 42
4320+
4321+
class Middle:
4322+
def __init__(self):
4323+
self.deep = Deep()
4324+
4325+
class Outer:
4326+
def __init__(self):
4327+
self.middle = Middle()
4328+
4329+
# Should not suggest 'middle.deep.value' (too deep)
4330+
actual = self.get_suggestion(Outer(), 'value')
4331+
self.assertNotIn("Did you mean", actual)
4332+
4333+
def test_getattr_nested_ignores_private_attributes(self):
4334+
# Test that nested suggestions ignore private attributes
4335+
class Inner:
4336+
def __init__(self):
4337+
self.public_value = 42
4338+
4339+
class Outer:
4340+
def __init__(self):
4341+
self._private_inner = Inner()
4342+
4343+
# Should not suggest '_private_inner.public_value'
4344+
actual = self.get_suggestion(Outer(), 'public_value')
4345+
self.assertNotIn("Did you mean", actual)
4346+
4347+
def test_getattr_nested_limits_attribute_checks(self):
4348+
# Test that nested suggestions are limited to checking first 20 non-private attributes
4349+
class Inner:
4350+
def __init__(self):
4351+
self.target_value = 42
4352+
4353+
class Outer:
4354+
def __init__(self):
4355+
# Add many attributes before 'inner'
4356+
for i in range(25):
4357+
setattr(self, f'attr_{i:02d}', i)
4358+
# Add the inner object after 20+ attributes
4359+
self.inner = Inner()
4360+
4361+
obj = Outer()
4362+
# Verify that 'inner' is indeed present but after position 20
4363+
attrs = [x for x in sorted(dir(obj)) if not x.startswith('_')]
4364+
inner_position = attrs.index('inner')
4365+
self.assertGreater(inner_position, 19, "inner should be after position 20 in sorted attributes")
4366+
4367+
# Should not suggest 'inner.target_value' because inner is beyond the first 20 attributes checked
4368+
actual = self.get_suggestion(obj, 'target_value')
4369+
self.assertNotIn("inner.target_value", actual)
4370+
4371+
def test_getattr_nested_returns_first_match_only(self):
4372+
# Test that only the first nested match is returned (not multiple)
4373+
class Inner1:
4374+
def __init__(self):
4375+
self.value = 1
4376+
4377+
class Inner2:
4378+
def __init__(self):
4379+
self.value = 2
4380+
4381+
class Inner3:
4382+
def __init__(self):
4383+
self.value = 3
4384+
4385+
class Outer:
4386+
def __init__(self):
4387+
# Multiple inner objects with same attribute
4388+
self.a_inner = Inner1()
4389+
self.b_inner = Inner2()
4390+
self.c_inner = Inner3()
4391+
4392+
# Should suggest only the first match (alphabetically)
4393+
actual = self.get_suggestion(Outer(), 'value')
4394+
self.assertIn("'a_inner.value'", actual)
4395+
# Verify it's a single suggestion, not multiple
4396+
self.assertEqual(actual.count("Did you mean"), 1)
4397+
4398+
def test_getattr_nested_handles_attribute_access_exceptions(self):
4399+
# Test that exceptions raised when accessing attributes don't crash the suggestion system
4400+
class ExplodingProperty:
4401+
@property
4402+
def exploding_attr(self):
4403+
raise RuntimeError("BOOM! This property always explodes")
4404+
4405+
def __repr__(self):
4406+
raise RuntimeError("repr also explodes")
4407+
4408+
class SafeInner:
4409+
def __init__(self):
4410+
self.target = 42
4411+
4412+
class Outer:
4413+
def __init__(self):
4414+
self.exploder = ExplodingProperty() # Accessing attributes will raise
4415+
self.safe_inner = SafeInner()
4416+
4417+
# Should still suggest 'safe_inner.target' without crashing
4418+
# even though accessing exploder.target would raise an exception
4419+
actual = self.get_suggestion(Outer(), 'target')
4420+
self.assertIn("'safe_inner.target'", actual)
4421+
4422+
def test_getattr_nested_handles_hasattr_exceptions(self):
4423+
# Test that exceptions in hasattr don't crash the system
4424+
class WeirdObject:
4425+
def __getattr__(self, name):
4426+
if name == 'target':
4427+
raise RuntimeError("Can't check for target attribute")
4428+
raise AttributeError(f"No attribute {name}")
4429+
4430+
class NormalInner:
4431+
def __init__(self):
4432+
self.target = 100
4433+
4434+
class Outer:
4435+
def __init__(self):
4436+
self.weird = WeirdObject() # hasattr will raise for 'target'
4437+
self.normal = NormalInner()
4438+
4439+
# Should still find 'normal.target' even though weird.target check fails
4440+
actual = self.get_suggestion(Outer(), 'target')
4441+
self.assertIn("'normal.target'", actual)
4442+
42654443
def make_module(self, code):
42664444
tmpdir = Path(tempfile.mkdtemp())
42674445
self.addCleanup(shutil.rmtree, tmpdir)

Lib/traceback.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,34 @@ def _substitution_cost(ch_a, ch_b):
16011601
return _MOVE_COST
16021602

16031603

1604+
def _check_for_nested_attribute(obj, wrong_name, attrs):
1605+
"""Check if any attribute of obj has the wrong_name as a nested attribute.
1606+
1607+
Returns the first nested attribute suggestion found, or None.
1608+
Limited to checking 20 attributes.
1609+
Only considers non-descriptor attributes to avoid executing arbitrary code.
1610+
"""
1611+
# Check for nested attributes (only one level deep)
1612+
attrs_to_check = [x for x in attrs if not x.startswith('_')][:20] # Limit number of attributes to check
1613+
for attr_name in attrs_to_check:
1614+
with suppress(Exception):
1615+
# Check if attr_name is a descriptor - if so, skip it
1616+
attr_from_class = getattr(type(obj), attr_name, None)
1617+
if attr_from_class is not None and hasattr(attr_from_class, '__get__'):
1618+
continue # Skip descriptors to avoid executing arbitrary code
1619+
1620+
# Safe to get the attribute since it's not a descriptor
1621+
attr_obj = getattr(obj, attr_name)
1622+
1623+
# Check if the nested attribute exists and is not a descriptor
1624+
nested_attr_from_class = getattr(type(attr_obj), wrong_name, None)
1625+
1626+
if hasattr(attr_obj, wrong_name):
1627+
return f"{attr_name}.{wrong_name}"
1628+
1629+
return None
1630+
1631+
16041632
def _compute_suggestion_error(exc_value, tb, wrong_name):
16051633
if wrong_name is None or not isinstance(wrong_name, str):
16061634
return None
@@ -1666,7 +1694,9 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
16661694
except ImportError:
16671695
pass
16681696
else:
1669-
return _suggestions._generate_suggestions(d, wrong_name)
1697+
suggestion = _suggestions._generate_suggestions(d, wrong_name)
1698+
if suggestion:
1699+
return suggestion
16701700

16711701
# Compute closest match
16721702

@@ -1691,6 +1721,14 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
16911721
if not suggestion or current_distance < best_distance:
16921722
suggestion = possible_name
16931723
best_distance = current_distance
1724+
1725+
# If no direct attribute match found, check for nested attributes
1726+
if not suggestion and isinstance(exc_value, AttributeError):
1727+
with suppress(Exception):
1728+
nested_suggestion = _check_for_nested_attribute(exc_value.obj, wrong_name, d)
1729+
if nested_suggestion:
1730+
return nested_suggestion
1731+
16941732
return suggestion
16951733

16961734

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Replace the shim code added to every piece of jitted code with a single
2+
trampoline function.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show error suggestions on nested attribute access. Patch by Pablo Galindo

0 commit comments

Comments
 (0)