Merge branch 'maintenance/4.0.x' into merge-maintenance-into-main
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 4d31f71..f798cf5 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -122,7 +122,7 @@
. venv/bin/activate
pytest --cov
- name: Upload coverage artifact
- uses: &actions-upload-artifact actions/upload-artifact@v4.6.2
+ uses: &actions-upload-artifact actions/upload-artifact@v5.0.0
with:
name: coverage-linux-${{ matrix.python-version }}
path: .coverage
@@ -226,7 +226,7 @@
- name: Install dependencies
run: pip install -U -r requirements_minimal.txt
- name: Download all coverage artifacts
- uses: actions/download-artifact@v5.0.0
+ uses: actions/download-artifact@v6.0.0
- name: Combine Linux coverage results
run: |
coverage combine coverage-linux*/.coverage
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 9b30ac2..44b2080 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -50,7 +50,7 @@
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -61,7 +61,7 @@
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v3
+ uses: github/codeql-action/autobuild@v4
# âšī¸ Command-line programs to run using the OS shell.
# đ https://git.io/JvXDl
@@ -75,4 +75,4 @@
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2c6cbdb..8e06a63 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -34,7 +34,7 @@
run: |
python -m build
- name: Upload release assets
- uses: actions/upload-artifact@v4.6.2
+ uses: actions/upload-artifact@v5.0.0
with:
name: release-assets
path: dist/
@@ -50,7 +50,7 @@
id-token: write
steps:
- name: Download release assets
- uses: actions/download-artifact@v5.0.0
+ uses: actions/download-artifact@v6.0.0
with:
name: release-assets
path: dist/
@@ -67,13 +67,13 @@
id-token: write
steps:
- name: Download release assets
- uses: actions/download-artifact@v5.0.0
+ uses: actions/download-artifact@v6.0.0
with:
name: release-assets
path: dist/
- name: Sign the dists with Sigstore and upload assets to Github release
if: github.event_name == 'release'
- uses: sigstore/gh-action-sigstore-python@v3.0.1
+ uses: sigstore/gh-action-sigstore-python@v3.1.0
with:
inputs: |
./dist/*.tar.gz
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 70095b1..1f6f967 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,7 +10,7 @@
- id: end-of-file-fixer
exclude: tests/testdata
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: "v0.13.2"
+ rev: "v0.14.3"
hooks:
- id: ruff-check
args: ["--fix"]
@@ -22,7 +22,7 @@
exclude: tests/testdata|setup.py
types: [python]
- repo: https://github.com/asottile/pyupgrade
- rev: v3.20.0
+ rev: v3.21.0
hooks:
- id: pyupgrade
exclude: tests/testdata
@@ -32,7 +32,7 @@
hooks:
- id: black-disable-checker
exclude: tests/test_nodes_lineno.py
- - repo: https://github.com/psf/black
+ - repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.9.0
hooks:
- id: black
@@ -81,6 +81,6 @@
- id: prettier
args: [--prose-wrap=always, --print-width=88]
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: "v2.6.0"
+ rev: "v2.11.0"
hooks:
- id: pyproject-fmt
diff --git a/ChangeLog b/ChangeLog
index 60bca91..812dea3 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,22 @@
============================
Release date: TBA
+* Make `type.__new__()` raise clear errors instead of returning `None`
+
+* Move object dunder methods from ``FunctionModel`` to ``ObjectModel`` to make them
+ available on all object types, not just functions.
+
+ Closes #2742
+ Closes #2741
+ Closes pylint-dev/pylint#6094
+
+* ``lineno`` and ``end_lineno`` are now available on ``Arguments``.
+
+* Add helper to iterate over all annotations nodes of function arguments,
+ ``Arguments.get_annotations()``.
+
+ Refs #2860
+
What's New in astroid 4.0.3?
@@ -119,7 +135,7 @@
Closes #2672
-* Add basic support for ``ast.TemplateStr`` and ``ast.Interpolation``added in Python 3.14.
+* Add basic support for ``ast.TemplateStr`` and ``ast.Interpolation`` added in Python 3.14.
Refs #2789
diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py
index 8ef2395..dfe71c3 100644
--- a/astroid/__pkginfo__.py
+++ b/astroid/__pkginfo__.py
@@ -2,5 +2,5 @@
# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
-__version__ = "4.0.2"
+__version__ = "4.1.0-dev0"
version = __version__
diff --git a/astroid/bases.py b/astroid/bases.py
index a029da6..7fb3da2 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -250,7 +250,9 @@
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
@@ -571,10 +573,14 @@
raise InferenceError(context=context) from e
if not isinstance(mcs, nodes.ClassDef):
# Not a valid first argument.
- return None
+ raise InferenceError(
+ "type.__new__() requires a class for metaclass", context=context
+ )
if not mcs.is_subtype_of("builtins.type"):
# Not a valid metaclass.
- return None
+ raise InferenceError(
+ "type.__new__() metaclass must be a subclass of type", context=context
+ )
# Verify the name
try:
@@ -583,10 +589,14 @@
raise InferenceError(context=context) from e
if not isinstance(name, nodes.Const):
# Not a valid name, needs to be a const.
- return None
+ raise InferenceError(
+ "type.__new__() requires a constant for name", context=context
+ )
if not isinstance(name.value, str):
# Needs to be a string.
- return None
+ raise InferenceError(
+ "type.__new__() requires a string for name", context=context
+ )
# Verify the bases
try:
@@ -595,14 +605,18 @@
raise InferenceError(context=context) from e
if not isinstance(bases, nodes.Tuple):
# Needs to be a tuple.
- return None
+ raise InferenceError(
+ "type.__new__() requires a tuple for bases", context=context
+ )
try:
inferred_bases = [next(elt.infer(context=context)) for elt in bases.elts]
except StopIteration as e:
raise InferenceError(context=context) from e
if any(not isinstance(base, nodes.ClassDef) for base in inferred_bases):
# All the bases needs to be Classes
- return None
+ raise InferenceError(
+ "type.__new__() requires classes for bases", context=context
+ )
# Verify the attributes.
try:
@@ -611,7 +625,9 @@
raise InferenceError(context=context) from e
if not isinstance(attrs, nodes.Dict):
# Needs to be a dictionary.
- return None
+ raise InferenceError(
+ "type.__new__() requires a dict for attrs", context=context
+ )
cls_locals: dict[str, list[InferenceResult]] = collections.defaultdict(list)
for key, value in attrs.items:
try:
@@ -664,9 +680,13 @@
and self.bound.name == "type"
and self.name == "__new__"
and isinstance(caller, nodes.Call)
- and len(caller.args) == 4
):
# Check if we have a ``type.__new__(mcs, name, bases, attrs)`` call.
+ if len(caller.args) != 4:
+ raise InferenceError(
+ f"type.__new__() requires 4 arguments, got {len(caller.args)}",
+ context=context,
+ )
new_cls = self._infer_type_new_call(caller, context)
if new_cls:
return iter((new_cls,))
diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py
index eac9e43..3745107 100644
--- a/astroid/interpreter/objectmodel.py
+++ b/astroid/interpreter/objectmodel.py
@@ -163,6 +163,33 @@
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 @@
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/node_classes.py b/astroid/nodes/node_classes.py
index 0d3a425..1d16b74 100644
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -1020,6 +1020,23 @@
if elt is not None:
yield elt
+ def get_annotations(self) -> Iterator[nodes.NodeNG]:
+ """Iterate over all annotations nodes."""
+ for elt in self.posonlyargs_annotations:
+ if elt is not None:
+ yield elt
+ for elt in self.annotations:
+ if elt is not None:
+ yield elt
+ if self.varargannotation is not None:
+ yield self.varargannotation
+
+ for elt in self.kwonlyargs_annotations:
+ if elt is not None:
+ yield elt
+ if self.kwargannotation is not None:
+ yield self.kwargannotation
+
@decorators.raise_if_nothing_inferred
def _infer(
self: nodes.Arguments, context: InferenceContext | None = None, **kwargs: Any
@@ -4966,9 +4983,10 @@
class Unknown(_base_nodes.AssignTypeNode):
"""This node represents a node in a constructed AST where
- introspection is not possible. At the moment, it's only used in
- the args attribute of FunctionDef nodes where function signature
- introspection failed.
+ introspection is not possible.
+
+ Used in the args attribute of FunctionDef nodes where function signature
+ introspection failed, and as a placeholder in ObjectModel.
"""
name = "Unknown"
diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py
index f232db3..d769b07 100644
--- a/astroid/nodes/scoped_nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes/scoped_nodes.py
@@ -1620,6 +1620,16 @@
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):
@@ -2348,8 +2358,10 @@
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 d1bbbd5..e7c5562 100644
--- a/astroid/raw_building.py
+++ b/astroid/raw_building.py
@@ -78,7 +78,11 @@
"""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 @@
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/astroid/rebuilder.py b/astroid/rebuilder.py
index 97f3a39..f7d38a9 100644
--- a/astroid/rebuilder.py
+++ b/astroid/rebuilder.py
@@ -9,6 +9,7 @@
from __future__ import annotations
import ast
+import itertools
import sys
import token
from collections.abc import Callable, Collection, Generator
@@ -589,6 +590,16 @@
type_comment_kwonlyargs=type_comment_kwonlyargs,
type_comment_posonlyargs=type_comment_posonlyargs,
)
+ if start_end_lineno_pairs := [
+ (arg.lineno, arg.end_lineno)
+ for arg in itertools.chain(
+ node.args, node.posonlyargs, node.kwonlyargs, [node.vararg, node.kwarg]
+ )
+ if arg
+ ]:
+ newnode.lineno = min(startend[0] for startend in start_end_lineno_pairs)
+ newnode.end_lineno = max(startend[1] for startend in start_end_lineno_pairs)
+
# save argument names in locals:
assert newnode.parent
if vararg:
diff --git a/requirements_minimal.txt b/requirements_minimal.txt
index 9506151..6877e4b 100644
--- a/requirements_minimal.txt
+++ b/requirements_minimal.txt
@@ -3,7 +3,7 @@
tbump~=6.11
# Tools used to run tests
-coverage~=7.10
+coverage~=7.11
pytest
pytest-cov~=7.0
mypy
diff --git a/tbump.toml b/tbump.toml
index 251afc5..42dcf88 100644
--- a/tbump.toml
+++ b/tbump.toml
@@ -1,7 +1,7 @@
github_url = "https://github.com/pylint-dev/astroid"
[version]
-current = "4.0.2"
+current = "4.1.0-dev0"
regex = '''
^(?P<major>0|[1-9]\d*)
\.
diff --git a/tests/test_inference.py b/tests/test_inference.py
index bf41e8c..dd17b60 100644
--- a/tests/test_inference.py
+++ b/tests/test_inference.py
@@ -1503,7 +1503,7 @@
def test_python25_no_relative_import(self) -> None:
ast = resources.build_file("data/package/absimport.py")
- self.assertTrue(ast.absolute_import_activated(), True)
+ self.assertTrue(ast.absolute_import_activated())
inferred = next(
test_utils.get_name_node(ast, "import_package_subpackage_module").infer()
)
@@ -1512,7 +1512,7 @@
def test_nonregr_absolute_import(self) -> None:
ast = resources.build_file("data/absimp/string.py", "data.absimp.string")
- self.assertTrue(ast.absolute_import_activated(), True)
+ self.assertTrue(ast.absolute_import_activated())
inferred = next(test_utils.get_name_node(ast, "string").infer())
self.assertIsInstance(inferred, nodes.Module)
self.assertEqual(inferred.name, "string")
@@ -5618,8 +5618,9 @@
)
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_nodes.py b/tests/test_nodes.py
index 8d2f4d5..939c581 100644
--- a/tests/test_nodes.py
+++ b/tests/test_nodes.py
@@ -2314,6 +2314,19 @@
assert node.args.default_value("flavor").value == "good"
+def test_arguments_annotations():
+ node = extract_node(
+ "def fruit(eat: str, /, peel: bool, *args: int, trim: float, **kwargs: bytes): ..."
+ )
+ assert isinstance(node.args, nodes.Arguments)
+ annotation_names = [
+ ann.name for ann in node.args.get_annotations() if isinstance(ann, nodes.Name)
+ ]
+ assert all(
+ name in annotation_names for name in ("str", "bool", "int", "float", "bytes")
+ )
+
+
def test_deprecated_nodes_import_from_toplevel():
# pylint: disable=import-outside-toplevel,no-name-in-module
with pytest.raises(
diff --git a/tests/test_nodes_lineno.py b/tests/test_nodes_lineno.py
index f8c6f91..25fb339 100644
--- a/tests/test_nodes_lineno.py
+++ b/tests/test_nodes_lineno.py
@@ -589,6 +589,8 @@
assert (f1.body[0].lineno, f1.body[0].col_offset) == (6, 4)
assert (f1.body[0].end_lineno, f1.body[0].end_col_offset) == (6, 8)
+ assert (f1.args.lineno, f1.args.end_lineno) == (2, 4)
+
# pos only arguments
# TODO fix column offset: arg -> arg (AssignName)
assert isinstance(f1.args.posonlyargs[0], nodes.AssignName)
diff --git a/tests/test_object_model.py b/tests/test_object_model.py
index f3015db..f36a0d5 100644
--- a/tests/test_object_model.py
+++ b/tests/test_object_model.py
@@ -265,10 +265,13 @@
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 @@
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 @@
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 @@
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 @@
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