diff --git a/astroid/bases.py b/astroid/bases.py index 625da31aa..7fb3da2af 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -250,7 +250,9 @@ def getattr( values = self._proxied.instance_attr(name, context) except AttributeInferenceError as exc: if self.special_attributes and name in self.special_attributes: - return [self.special_attributes.lookup(name)] + special_attr = self.special_attributes.lookup(name) + if not isinstance(special_attr, nodes.Unknown): + return [special_attr] if lookupclass: # Class attributes not available through the instance diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py index eac9e4308..3745107fb 100644 --- a/astroid/interpreter/objectmodel.py +++ b/astroid/interpreter/objectmodel.py @@ -163,6 +163,33 @@ def attr___init__(self) -> bases.BoundMethod: return bases.BoundMethod(proxy=node, bound=_get_bound_node(self)) + # Base object attributes that return Unknown as fallback placeholders. + @property + def attr___ne__(self): + return node_classes.Unknown(parent=self._instance) + + attr___class__ = attr___ne__ + attr___delattr__ = attr___ne__ + attr___dir__ = attr___ne__ + attr___doc__ = attr___ne__ + attr___eq__ = attr___ne__ + attr___format__ = attr___ne__ + attr___ge__ = attr___ne__ + attr___getattribute__ = attr___ne__ + attr___getstate__ = attr___ne__ + attr___gt__ = attr___ne__ + attr___hash__ = attr___ne__ + attr___init_subclass__ = attr___ne__ + attr___le__ = attr___ne__ + attr___lt__ = attr___ne__ + attr___reduce__ = attr___ne__ + attr___reduce_ex__ = attr___ne__ + attr___repr__ = attr___ne__ + attr___setattr__ = attr___ne__ + attr___sizeof__ = attr___ne__ + attr___str__ = attr___ne__ + attr___subclasshook__ = attr___ne__ + class ModuleModel(ObjectModel): def _builtins(self): @@ -459,30 +486,15 @@ def test(self): return DescriptorBoundMethod(proxy=self._instance, bound=self._instance) - # These are here just for completion. + # Function-specific attributes. @property - def attr___ne__(self): + def attr___call__(self): return node_classes.Unknown(parent=self._instance) - attr___subclasshook__ = attr___ne__ - attr___str__ = attr___ne__ - attr___sizeof__ = attr___ne__ - attr___setattr___ = attr___ne__ - attr___repr__ = attr___ne__ - attr___reduce__ = attr___ne__ - attr___reduce_ex__ = attr___ne__ - attr___lt__ = attr___ne__ - attr___eq__ = attr___ne__ - attr___gt__ = attr___ne__ - attr___format__ = attr___ne__ - attr___delattr___ = attr___ne__ - attr___getattribute__ = attr___ne__ - attr___hash__ = attr___ne__ - attr___dir__ = attr___ne__ - attr___call__ = attr___ne__ - attr___class__ = attr___ne__ - attr___closure__ = attr___ne__ - attr___code__ = attr___ne__ + attr___builtins__ = attr___call__ + attr___closure__ = attr___call__ + attr___code__ = attr___call__ + attr___type_params__ = attr___call__ class ClassModel(ObjectModel): diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 885562393..8035008fd 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -1618,6 +1618,16 @@ def infer_call_result( yield node_classes.Const(None) return + # Builtin dunder methods have empty bodies, return Uninferable. + if ( + len(self.body) == 0 + and self.name.startswith("__") + and self.name.endswith("__") + and self.root().qname() == "builtins" + ): + yield util.Uninferable + return + raise InferenceError("The function does not have any return statements") for returnnode in itertools.chain((first_return,), returns): @@ -2346,8 +2356,10 @@ def getattr( values += classnode.locals.get(name, []) if name in self.special_attributes and class_context and not values: - result = [self.special_attributes.lookup(name)] - return result + special_attr = self.special_attributes.lookup(name) + if not isinstance(special_attr, node_classes.Unknown): + result = [special_attr] + return result if class_context: values += self._metaclass_lookup_attribute(name, context) diff --git a/astroid/raw_building.py b/astroid/raw_building.py index d1bbbd556..e7c556263 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -78,7 +78,11 @@ def attach_const_node(node, name: str, value) -> None: """create a Const node and register it in the locals of the given node with the specified name """ - if name not in node.special_attributes: + # Special case: __hash__ = None overrides ObjectModel for unhashable types. + # See https://docs.python.org/3/reference/datamodel.html#object.__hash__ + if name == "__hash__" and value is None: + _attach_local_node(node, nodes.const_factory(value), name) + elif name not in node.special_attributes: _attach_local_node(node, nodes.const_factory(value), name) @@ -507,7 +511,11 @@ def object_build( elif inspect.isdatadescriptor(member): child = object_build_datadescriptor(node, member) elif isinstance(member, tuple(node_classes.CONST_CLS)): - if alias in node.special_attributes: + # Special case: __hash__ = None overrides ObjectModel for unhashable types. + # See https://docs.python.org/3/reference/datamodel.html#object.__hash__ + if alias in node.special_attributes and not ( + alias == "__hash__" and member is None + ): continue child = nodes.const_factory(member) elif inspect.isroutine(member): diff --git a/tests/test_inference.py b/tests/test_inference.py index bf41e8cf5..47c82a98f 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -5618,8 +5618,9 @@ def test_cannot_infer_call_result_for_builtin_methods() -> None: ) inferred = next(node.infer()) lenmeth = next(inferred.igetattr("__len__")) - with pytest.raises(InferenceError): - next(lenmeth.infer_call_result(None, None)) + # Builtin dunder methods now return Uninferable instead of raising InferenceError + result = next(lenmeth.infer_call_result(None, None)) + assert result is util.Uninferable def test_unpack_dicts_in_assignment() -> None: diff --git a/tests/test_object_model.py b/tests/test_object_model.py index f3015db9c..f36a0d53d 100644 --- a/tests/test_object_model.py +++ b/tests/test_object_model.py @@ -265,10 +265,13 @@ def test_module_model(self) -> None: xml.__setattr__ #@ xml.__reduce_ex__ #@ xml.__lt__ #@ + xml.__le__ #@ xml.__eq__ #@ + xml.__ne__ #@ + xml.__ge__ #@ xml.__gt__ #@ xml.__format__ #@ - xml.__delattr___ #@ + xml.__delattr__ #@ xml.__getattribute__ #@ xml.__hash__ #@ xml.__dir__ #@ @@ -324,9 +327,13 @@ def test_module_model(self) -> None: new_ = next(ast_nodes[10].infer()) assert isinstance(new_, bases.BoundMethod) - # The following nodes are just here for theoretical completeness, - # and they either return Uninferable or raise InferenceError. - for ast_node in ast_nodes[11:28]: + # Inherited attributes return Uninferable. + for ast_node in ast_nodes[11:29]: + inferred = next(ast_node.infer()) + self.assertIs(inferred, astroid.Uninferable) + + # Attributes that don't exist on modules raise InferenceError. + for ast_node in ast_nodes[29:31]: with pytest.raises(InferenceError): next(ast_node.infer()) @@ -449,16 +456,23 @@ def func(a=1, b=2): func.__reduce_ex__ #@ func.__lt__ #@ + func.__le__ #@ func.__eq__ #@ + func.__ne__ #@ + func.__ge__ #@ func.__gt__ #@ func.__format__ #@ - func.__delattr___ #@ + func.__delattr__ #@ func.__getattribute__ #@ func.__hash__ #@ func.__dir__ #@ func.__class__ #@ func.__setattr__ #@ + func.__builtins__ #@ + func.__getstate__ #@ + func.__init_subclass__ #@ + func.__type_params__ #@ ''', module_name="fake_module", ) @@ -511,16 +525,11 @@ def func(a=1, b=2): new_ = next(ast_nodes[10].infer()) assert isinstance(new_, bases.BoundMethod) - # The following nodes are just here for theoretical completeness, - # and they either return Uninferable or raise InferenceError. - for ast_node in ast_nodes[11:26]: + # Remaining attributes return Uninferable. + for ast_node in ast_nodes[11:34]: inferred = next(ast_node.infer()) assert inferred is util.Uninferable - for ast_node in ast_nodes[26:27]: - with pytest.raises(InferenceError): - inferred = next(ast_node.infer()) - def test_empty_return_annotation(self) -> None: ast_node = builder.extract_node( """ @@ -897,3 +906,112 @@ class Apple(TypedDict): assert next(apple.infer()) is astroid.Uninferable assert isinstance(pear, nodes.Attribute) assert next(pear.infer()) is astroid.Uninferable + + +def test_object_dunder_methods_can_be_overridden() -> None: + """Test that ObjectModel dunders don't block class overrides.""" + # Test instance method override + eq_result = builder.extract_node( + """ + class MyClass: + def __eq__(self, other): + return "custom equality" + + MyClass().__eq__(None) #@ + """ + ) + inferred = next(eq_result.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value == "custom equality" + + # Test that __eq__ on instance returns a bound method + eq_method = builder.extract_node( + """ + class MyClass: + def __eq__(self, other): + return True + + MyClass().__eq__ #@ + """ + ) + inferred = next(eq_method.infer()) + assert isinstance(inferred, astroid.BoundMethod) + + # Test other commonly overridden dunders + for dunder, return_val in ( + ("__ne__", "not equal"), + ("__lt__", "less than"), + ("__le__", "less or equal"), + ("__gt__", "greater than"), + ("__ge__", "greater or equal"), + ("__str__", "string repr"), + ("__repr__", "repr"), + ("__hash__", 42), + ): + node = builder.extract_node( + f""" + class MyClass: + def {dunder}(self, *args): + return {return_val!r} + + MyClass().{dunder}() #@ + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.Const), f"{dunder} failed to infer correctly" + assert inferred.value == return_val, f"{dunder} returned wrong value" + + +def test_unoverridden_object_dunders_return_uninferable() -> None: + """Test that un-overridden object dunders return Uninferable when called.""" + for dunder in ( + "__eq__", + "__hash__", + "__lt__", + "__le__", + "__gt__", + "__ge__", + "__ne__", + ): + node = builder.extract_node( + f""" + class MyClass: + pass + + MyClass().{dunder}(None) if "{dunder}" != "__hash__" else MyClass().{dunder}() #@ + """ + ) + result = next(node.infer()) + assert result is util.Uninferable + + +def test_all_object_dunders_accessible() -> None: + """Test that object dunders are accessible on classes and instances.""" + # Use actual dunders from object in the current Python version + object_dunders = [attr for attr in dir(object) if attr.startswith("__")] + + cls, instance = builder.extract_node( + """ + class MyClass: + pass + + MyClass #@ + MyClass() #@ + """ + ) + cls = next(cls.infer()) + instance = next(instance.infer()) + + for dunder in object_dunders: + assert cls.getattr(dunder) + assert instance.getattr(dunder) + + +def test_hash_none_for_unhashable_builtins() -> None: + """Test that unhashable builtin types have __hash__ = None.""" + for type_name in ("list", "dict", "set"): + node = builder.extract_node(f"{type_name} #@") + cls = next(node.infer()) + hash_attr = cls.getattr("__hash__")[0] + assert isinstance(hash_attr, nodes.Const) + assert hash_attr.value is None