-
-
Notifications
You must be signed in to change notification settings - Fork 305
Move object dunders from FunctionModel
to ObjectModel
#2847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
aeedd50
50a56f8
b691e28
1bc57c5
2762ac9
6c4d29f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, (UninferableBase, nodes.Unknown)): | ||
return [special_attr] | ||
|
||
if lookupclass: | ||
# Class attributes not available through the instance | ||
|
@@ -571,10 +573,14 @@ def _infer_type_new_call( | |
raise InferenceError(context=context) from e | ||
if not isinstance(mcs, nodes.ClassDef): | ||
# Not a valid first argument. | ||
return None | ||
raise InferenceError( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels like it could be merged on its own, is that True? In that case I'd probably split this out to ensure we don't do too much in a single PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is independent of the object dunder work. It's about improving There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please split it! I'll make sure to review it asap, but generally I and other maintainers need quite a lot of mental energy to review |
||
"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 @@ def _infer_type_new_call( | |
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 @@ def _infer_type_new_call( | |
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 @@ def _infer_type_new_call( | |
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 @@ def infer_call_result( | |
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,)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1618,6 +1618,17 @@ def infer_call_result( | |
yield node_classes.Const(None) | ||
return | ||
|
||
# Builtin dunder methods have empty bodies, return Uninferable. | ||
if ( | ||
self.root().qname() == "builtins" | ||
and self.name.startswith("__") | ||
and self.name.endswith("__") | ||
and self.parent | ||
and self.parent.__class__.__name__ == "ClassDef" | ||
): | ||
yield util.Uninferable | ||
return | ||
Comment on lines
1621
to
1629
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I sort of understand this change, but don't understand why we need to special case it like this. Can we replace the checks with a look up in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need this case because
But on second thought, I simplified the case by explicitly checking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be okay if we always did this for |
||
|
||
raise InferenceError("The function does not have any return statements") | ||
|
||
for returnnode in itertools.chain((first_return,), returns): | ||
|
@@ -2346,8 +2357,12 @@ 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, (util.UninferableBase, node_classes.Unknown) | ||
): | ||
result = [special_attr] | ||
return result | ||
Comment on lines
+2359
to
+2364
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
if class_context: | ||
values += self._metaclass_lookup_attribute(name, context) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -78,7 +78,10 @@ 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. | ||
if name == "__hash__" and value is None: | ||
_attach_local_node(node, nodes.const_factory(value), name) | ||
Comment on lines
+81
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be the case for all attributes where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe only for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we can link that documentation here? It would prevent me from wondering why this special case is here the next time. And thanks for the link, TIL about something I knew implicitly but now know where to find the documentation for that feature π Appreciated! |
||
elif name not in node.special_attributes: | ||
_attach_local_node(node, nodes.const_factory(value), name) | ||
|
||
|
||
|
@@ -507,7 +510,10 @@ 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question as above There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same reasoning as the previous hash question - |
||
if alias in node.special_attributes and not ( | ||
alias == "__hash__" and member is None | ||
): | ||
continue | ||
child = nodes.const_factory(member) | ||
elif inspect.isroutine(member): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
Comment on lines
+5621
to
+5623
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I think the changes above now make more sense to me.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes - |
||
|
||
|
||
def test_unpack_dicts_in_assignment() -> None: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar question as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same pattern as the
ClassDef.getattr()
check - whenspecial_attr
isUnknown
orUninferable
, we skip the early return and continue to the if lookupclass: block which searches the class for the attribute. This ensures we find actual implementations in classes rather than stopping at theUnknown
placeholder.