Skip to content

Conversation

emmanuel-ferdman
Copy link
Contributor

@emmanuel-ferdman emmanuel-ferdman commented Oct 4, 2025

Type of Changes

Type
βœ“ πŸ› Bug fix

Description

This PR does:

This PR moves object dunder methods from FunctionModel to ObjectModel as Unknown placeholders, making them available to all object types.

Fixes #2742 #2741.

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Copy link

codecov bot commented Oct 4, 2025

Codecov Report

❌ Patch coverage is 93.02326% with 3 lines in your changes missing coverage. Please review.
βœ… Project coverage is 93.33%. Comparing base (2ce47a2) to head (c4f01a1).

Files with missing lines Patch % Lines
astroid/raw_building.py 25.00% 3 Missing ⚠️

❌ Your patch check has failed because the patch coverage (93.02%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2847      +/-   ##
==========================================
- Coverage   93.37%   93.33%   -0.04%     
==========================================
  Files          92       92              
  Lines       11150    11168      +18     
==========================================
+ Hits        10411    10424      +13     
- Misses        739      744       +5     
Flag Coverage Ξ”
linux 93.20% <93.02%> (-0.04%) ⬇️
pypy 93.33% <93.02%> (-0.04%) ⬇️
windows 93.32% <93.02%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Ξ”
astroid/bases.py 89.74% <100.00%> (+0.05%) ⬆️
astroid/interpreter/objectmodel.py 96.33% <100.00%> (+0.06%) ⬆️
astroid/nodes/scoped_nodes/scoped_nodes.py 93.61% <100.00%> (+0.02%) ⬆️
astroid/raw_building.py 93.98% <25.00%> (-0.60%) ⬇️

... and 1 file with indirect coverage changes

πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Pierre-Sassoulas Pierre-Sassoulas added the Enhancement ✨ Improvement to a component label Oct 5, 2025
@Pierre-Sassoulas Pierre-Sassoulas added this to the 4.1.0 milestone Oct 5, 2025
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As can be seen from the docs you link, most of these should live on ObjectModel instead as they come from object.

We tried to do so before in #1519 but failed. Perhaps you want to have another look at that PR and see if you can fix the issue we faced there?

@emmanuel-ferdman
Copy link
Contributor Author

@DanielNoord Thanks! I’ll take a look at this. From that thread, it seems you were close to a solution, except for one test case that didn’t pass. Do you remember which test it was?

@DanielNoord
Copy link
Collaborator

@emmanuel-ferdman I believe it was an issue in the numpy brain, which overwrites __eq__. That overwrite doesn't work well with the changes to ObjectModel.

emmanuel-ferdman and others added 3 commits October 6, 2025 02:48
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
@emmanuel-ferdman
Copy link
Contributor Author

@DanielNoord I've put together an initial solution for moving object dunders from FunctionModel to ObjectModel. I've tested it against both the astroid test suite and the pylint test suite (all passing). Since I'm still learning the codebase, I'd appreciate any guidance on whether this approach is correct.

The solution uses a placeholder pattern: ObjectModel provides Unknown placeholders for all 24 object dunders, and the lookup logic skips these placeholders to find actual implementations.
Key changes:

  • ObjectModel now has all object dunders as Unknown placeholders that act as fallbacks when no actual implementation exists
  • Lookup logic in BaseInstance.getattr() and ClassDef.getattr() skips Unknown placeholders and continues searching
  • Builtin dunders (from the builtins module) return Uninferable instead of raising InferenceError since we can't infer their result without executing C code
  • Special case for __hash__ = None to ensure unhashable types (list, dict, set) properly override object's __hash__
  • Enhanced type.__new__() validation to raise descriptive InferenceError instead of silently returning None

How it works:

  • When a class overrides a dunder: lookup finds the actual implementation and returns the bound method
  • When a class doesn't override: lookup finds the Unknown placeholder, skips it, continues searching, eventually returns the placeholder which yields Uninferable when called
  • For unhashable builtin types (list, dict, set): __hash__ is explicitly set to None in locals.

Thanks for any feedback πŸ™Œ

Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing, very well written PR and a nice set of tests.

Well done on getting this to pass all tests. I have left some comments, but would really like to help you push this over the line :)

Comment on lines 2360 to 2365
special_attr = self.special_attributes.lookup(name)
if not isinstance(
special_attr, (util.UninferableBase, node_classes.Unknown)
):
result = [special_attr]
return result
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the effect of this? What will we eventually return if the if is not True?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is a short-circuit used when there are no concrete locals/ancestor definitions: originally it always returned special_attributes.lookup(name) (even if that was Unknown or Uninferable). The new behavior only returns the special_attr when it is a concrete value (not node_classes.Unknown or util.UninferableBase), preventing a placeholder from being returned prematurely and masking an override in a metaclass/base class. If the if is not true we continue the normal lookup (metaclass lookup, collect/filter locals/ancestors) and ultimately return any real definitions found - otherwise an AttributeInferenceError is raised. Placeholders (Unknown/Uninferable) therefore mean β€œkeep looking,” not β€œreturn this as the final result.”

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unkown feels like a nice placeholder for "keep looking", Uninferable generally means "stop inferring, you won't be able to". Can we make this only check for Unknown? Or does that not work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea! ObjectModel only returns Unknown as placeholders, never Uninferable. I changed both checks (in ClassDef.getattr() and BaseInstance.getattr()) to only check for Unknown. If Uninferable somehow appeared from special_attributes, we should treat it as a final value and return it, not skip it. All tests pass with this change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should update the docstring of Unknown to reflect this?

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasoning as the previous hash question - hash = None is the only attribute where None has special semantic meaning in Python (marks unhashable types). Other None values don't need this override behavior.

)
inferred = next(eq_result.infer())
assert isinstance(inferred, nodes.Const)
assert inferred.value == "custom equality"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
@emmanuel-ferdman emmanuel-ferdman changed the title Add missing operators in FunctionModel Move object dunders from FunctionModel to ObjectModel Oct 7, 2025
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One final comment about a docstring.

Also, can you add a Changelog for this?

Nice work on this! @jacobtylerwalls will be happy to see we finally got this to work :)

Comment on lines 2360 to 2365
special_attr = self.special_attributes.lookup(name)
if not isinstance(
special_attr, (util.UninferableBase, node_classes.Unknown)
):
result = [special_attr]
return result
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should update the docstring of Unknown to reflect this?

@DanielNoord
Copy link
Collaborator

@emmanuel-ferdman Will this also close pylint-dev/pylint#6094? The old PR references it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement ✨ Improvement to a component
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add __le__, __ge__ and other missing attributes to FunctionModel and ObjectModel
3 participants