Skip to content

Commit 3ea62ba

Browse files
[Backport maintenance/4.1.x] Prevent Unknown from leaking via special attribute lookup (#2973)
Prevent Unknown from leaking via special attribute lookup (#2963) (cherry picked from commit ffbdccf) Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com> Co-authored-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
1 parent e064505 commit 3ea62ba

File tree

4 files changed

+63
-4
lines changed

4 files changed

+63
-4
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Release date: TBA
1616
* Let `UnboundMethodModel` inherit from `FunctionModel` to improve inference of
1717
dunder methods for unbound methods.
1818

19+
* Filter ``Unknown`` from ``UnboundMethod`` and ``Super`` special attribute
20+
lookup to prevent placeholder nodes from leaking during inference.
21+
1922
What's New in astroid 4.1.0?
2023
============================
2124
Release date: 2026-02-08

astroid/bases.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,14 +462,18 @@ def is_bound(self) -> bool:
462462

463463
def getattr(self, name: str, context: InferenceContext | None = None):
464464
if name in self.special_attributes:
465-
return [self.special_attributes.lookup(name)]
465+
special_attr = self.special_attributes.lookup(name)
466+
if not isinstance(special_attr, nodes.Unknown):
467+
return [special_attr]
466468
return self._proxied.getattr(name, context)
467469

468470
def igetattr(
469471
self, name: str, context: InferenceContext | None = None
470472
) -> Iterator[InferenceResult]:
471473
if name in self.special_attributes:
472-
return iter((self.special_attributes.lookup(name),))
474+
special_attr = self.special_attributes.lookup(name)
475+
if not isinstance(special_attr, nodes.Unknown):
476+
return iter((special_attr,))
473477
return self._proxied.igetattr(name, context)
474478

475479
def infer_call_result(

astroid/objects.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,10 @@ def igetattr( # noqa: C901
222222
# Only if we haven't found any explicit overwrites for the
223223
# attribute we look it up in the special attributes
224224
if not found and name in self.special_attributes:
225-
yield self.special_attributes.lookup(name)
226-
return
225+
special_attr = self.special_attributes.lookup(name)
226+
if not isinstance(special_attr, node_classes.Unknown):
227+
yield special_attr
228+
return
227229

228230
if not found:
229231
raise AttributeInferenceError(target=self, attribute=name, context=context)

tests/test_object_model.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,3 +948,53 @@ def test_hash_none_for_unhashable_builtins() -> None:
948948
hash_attr = cls.getattr("__hash__")[0]
949949
assert isinstance(hash_attr, nodes.Const)
950950
assert hash_attr.value is None
951+
952+
953+
def test_unbound_method_object_dunders_return_uninferable() -> None:
954+
"""Test that ObjectModel-only dunders on unbound methods return Uninferable."""
955+
node = builder.extract_node("""
956+
class A:
957+
def test(self): pass
958+
A.test #@
959+
""")
960+
unbound = next(node.infer())
961+
assert isinstance(unbound, bases.UnboundMethod)
962+
963+
for dunder in (
964+
"__eq__",
965+
"__ne__",
966+
"__lt__",
967+
"__le__",
968+
"__gt__",
969+
"__ge__",
970+
"__repr__",
971+
"__str__",
972+
"__hash__",
973+
):
974+
inferred = next(unbound.igetattr(dunder))
975+
assert inferred is util.Uninferable, f"{dunder} should be Uninferable"
976+
977+
attrs = unbound.getattr(dunder)
978+
assert attrs, f"{dunder} getattr should return results"
979+
980+
981+
def test_super_special_attributes_fallback() -> None:
982+
"""Test that super() special attributes use the fallback path correctly."""
983+
doc_node = builder.extract_node("""
984+
class Base:
985+
def method(self):
986+
return super().__doc__
987+
Base().method() #@
988+
""")
989+
doc_inferred = next(doc_node.infer())
990+
assert doc_inferred is util.Uninferable
991+
992+
thisclass_node = builder.extract_node("""
993+
class Base:
994+
def method(self):
995+
return super().__thisclass__
996+
Base().method() #@
997+
""")
998+
thisclass_inferred = next(thisclass_node.infer())
999+
assert isinstance(thisclass_inferred, nodes.ClassDef)
1000+
assert thisclass_inferred.name == "Base"

0 commit comments

Comments
 (0)