-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Make descriptor assignments more like normal assignments #5853
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
Conversation
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.
I'll leave this to @JukkaL's judgment.
mypy/checker.py
Outdated
set_type = inferred_dunder_set_type.arg_types[1] | ||
# Special case: if the rvalue_type is a subtype of both '__get__' and '__set__' types, | ||
# then we invoke the binder to narrow type by this assignment. Technically, this is not | ||
# safe, but in practice this is what a user expects. |
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.
I'm still worried about edge cases here. E.g. what about this pair:
def __get__(self, obj, typ) -> float: return self._val
def __set__(self, obj, value) -> None: self._val = float(value)
and then the usage
if not isinstance(a.foo, int):
a.foo = int(a.foo)
blah[a.foo] # Suppose blah is a List, so the index must be int
Perhaps I would worry less if we only went after real unions here, not subtypes. (Though in most cases subtypes will also work -- it's really mostly float/int, where casting int to float changes the representation.)
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.
Yes, this is clearly a trade off. @JukkaL what do you think?
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.
What about only doing the binder magic when the relevant types in __get__
and __set__
are the same? We might need some logic to deal with overloads. Now if __set__
does some type conversion, it will likely accept a more general type. This would still be unsafe, but I that's unavoidable.
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.
This is a bit complicated because of the overload case. I propose to instead to apply binder only if "get" type is a subtype of "set" type, and the rvalue type is a subtype of "get" type. This covers the situation when the "get" and "set" types are the same. And in general looks reasonably safe. I added this in latest commit.
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.
Looks reasonable. Left an idea about a potential way to partially deal with unsafety.
mypy/checker.py
Outdated
set_type = inferred_dunder_set_type.arg_types[1] | ||
# Special case: if the rvalue_type is a subtype of both '__get__' and '__set__' types, | ||
# then we invoke the binder to narrow type by this assignment. Technically, this is not | ||
# safe, but in practice this is what a user expects. |
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.
What about only doing the binder magic when the relevant types in __get__
and __set__
are the same? We might need some logic to deal with overloads. Now if __set__
does some type conversion, it will likely accept a more general type. This would still be unsafe, but I that's unavoidable.
test-data/unit/check-classes.test
Outdated
T = TypeVar('T') | ||
|
||
class IntDescr: | ||
def __get__(self, obj: T, typ: Type[T]) -> Optional[int]: ... |
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.
Style nit: use 4-space indent (here and elsewhere).
test-data/unit/check-classes.test
Outdated
|
||
def meth_spec(self) -> B: | ||
self.spec = B() | ||
return self.spec |
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.
Use reveal_type(self.spec)
since plausibly the inferred type could be Any
, which would be wrong.
test-data/unit/check-classes.test
Outdated
|
||
def meth_spec(self) -> A: | ||
self.spec = A() | ||
return self.spec |
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.
Use reveal_type()
to make sure self.spec
is not Any
, for example.
test-data/unit/check-classes.test
Outdated
def meth_spec(self) -> int: | ||
if self.spec is None: | ||
self.spec = 0 | ||
return self.spec |
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.
Use reveal_type(self.spec)
to get more certainty about the inferred type.
"""Type member assignment. | ||
|
||
This defers to check_simple_assignment, unless the member expression | ||
is a descriptor, in which case this checks descriptor semantics as well. | ||
|
||
Return the inferred rvalue_type and whether to infer anything about the attribute type. | ||
Note: this method exists here and not in checkmember.py, because we need to take |
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.
Add an empty line before "Note: ..." to make it more prominent.
mypy/checker.py
Outdated
@@ -2491,59 +2491,67 @@ def check_simple_assignment(self, lvalue_type: Optional[Type], rvalue: Expressio | |||
return rvalue_type | |||
|
|||
def check_member_assignment(self, instance_type: Type, attribute_type: Type, | |||
rvalue: Expression, context: Context) -> Tuple[Type, bool]: | |||
rvalue: Expression, context: Context) -> Tuple[Type, Type, bool]: | |||
"""Type member assignment. | |||
|
|||
This defers to check_simple_assignment, unless the member expression | |||
is a descriptor, in which case this checks descriptor semantics as well. | |||
|
|||
Return the inferred rvalue_type and whether to infer anything about the attribute type. |
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.
Document the new return type. Could you also rewrite "whether to infer anything about the attribute type", as it wasn't immediately clear to me what it means. Maybe explicitly mention binder here, as "infer" usually implies inferring the real type of an attribute. Or is it also used for inferring the type of an attribute on initial assignment?
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.
It is only for binder, I updated this.
|
||
function = function_type(dunder_set, self.named_type('builtins.function')) | ||
bound_method = bind_self(function, attribute_type) | ||
typ = map_instance_to_supertype(attribute_type, dunder_set.info) | ||
dunder_set_type = expand_type_by_instance(bound_method, typ) | ||
|
||
_, inferred_dunder_set_type = self.expr_checker.check_call( | ||
dunder_set_type, [TempNode(instance_type), rvalue], | ||
dunder_set_type, [TempNode(instance_type), TempNode(AnyType(TypeOfAny.special_form))], |
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.
Do we lose some type context from using Any
here?
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.
Initially I thought this is is probably OK, but now I am not so sure. Anyway, to be on the safe side, we can check the call twice. Once we use the real expression but turn off errors, just to infer the type (if it is invalid, we will later show a nice "Invalid type in assignment..." error later). Second time we check with the Any
type to show the remaining ___set__
specific errors (like wrong type of first argument).
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.
(The above is what I already did in the latest commit.)
@JukkaL Thanks for review! I addressed your comments. |
@JukkaL Are you happy with the updates I made? |
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.
Looks good to me now!
Fixes #5851
This makes descriptor assignments behave more like normal assignments. This includes:
I was not able to refactor all the logic to
checkmember.py
, but this is probably OK. Also this PR can introduce potential unsafety as discussed in the issue, but this is probably not much different from using binder in some other situations.