-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Add __setattr__ support #3451
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
Add __setattr__ support #3451
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.
Thanks!
I have few comments.
test-data/unit/check-classes.test
Outdated
b.at = '3' | ||
integer = b.at | ||
[out] | ||
main:13: error: Incompatible types in assignment (expression has type "str", variable has type "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.
Could you please use inline errors with # E:
syntax?
docs/source/cheat_sheet.rst
Outdated
def __setattr__(self, name, value): | ||
# type: (str, int) -> None | ||
... | ||
a.foo = bar() # works if bar() returns int, fails otherwise |
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.
Maybe just use an integer like 42
instead of bar()
here and in Python 3 version?
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.
Fair point, and perhaps a str
with a comment about it failing.
mypy/checkmember.py
Outdated
typ = map_instance_to_supertype(itype, setattr_func.info) | ||
setattr_type = expand_type_by_instance(bound_type, typ) | ||
if isinstance(setattr_type, CallableType): | ||
return setattr_type.arg_types[-1] |
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.
Should we have some requirement that __setattr__
and __getattr__
types are somehow compatible? In either case this decision should be explicitly documented with some motivation.
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 something I considered, but since we don't know what __setattr__
does in some edge cases, I think it is safer to not assume that the types are the same. Consider a class which takes str attributes, and in __setattr__
converts it to an int before binding to itself.
But I don't know what the best decision here is.
[case testSetAttr] | ||
from typing import Union | ||
class A: | ||
def __setattr__(self, name: str, value: Any) -> 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.
Two more ideas for tests:
- presence of both
__setattr__
and normal attributes __setattr__
in superclass
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.
A few others:
__setattr__
is defined, but not a callable__setattr__
is defined but takes the wrong arguments
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.
@ilevkivskyi could you explain 'presence of both setattr and normal attributes'
You mean something like:
class Ex:
def __setattr__(self, name: str, value: int) -> None:...
test = '42' # type: str
e = Ex()
e.test = 'hello'
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, something like this (plus e.whatever = 5
)
docs/source/cheat_sheet.rst
Outdated
@@ -149,6 +149,15 @@ When you're puzzled or when things are complicated | |||
reveal_type(c) # -> error: Revealed type is 'builtins.list[builtins.str]' | |||
print(c) # -> [4] the object is not cast | |||
|
|||
# if you want dynamic attributes on your class, have it override __setattr__ in a stub |
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.
And also __getattr__
, depending on our decision on that matter.
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.
Right, perhaps I should make that more clear and which one is for which case.
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 like you put it in the py3 but not the py2 cheat sheet. It should be the same in both.
mypy/checkmember.py
Outdated
typ = map_instance_to_supertype(itype, setattr_func.info) | ||
setattr_type = expand_type_by_instance(bound_type, typ) | ||
if isinstance(setattr_type, CallableType): | ||
return setattr_type.arg_types[-1] |
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.
Is this going to crash if __setattr__
is defined to take no arguments?
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.
Is this going to crash if
__setattr__
is defined to take no arguments?
Good catch!
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.
Ah, yes. I fixed this by checking len of the args.
mypy/checkmember.py
Outdated
else: | ||
setattr_func = info.get_method('__setattr__') | ||
if setattr_func and setattr_func.info.fullname() != 'builtins.object': | ||
setattr_f = function_type(setattr_func, builtin_type('builtins.function')) |
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's confusing to have separate variables called setattr_func
and setattr_f
. Perhaps the first one should be setattr_method
.
I believe I resolved all of the points reviewed. If I missed something or there is something else needed, lmk. |
test-data/unit/check-classes.test
Outdated
class B: | ||
def __setattr__(self, name: str, value: int) -> None: ... | ||
def __getattr__(self, name: str) -> str: ... | ||
integer = None # type: 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.
why not just integer = 0
? that will work with strict-optional too
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.
Done.
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.
Thanks! Just few very minor comments. Also please update PY2 cheat-sheet as proposed by @JelleZijlstra.
docs/source/cheat_sheet_py3.rst
Outdated
a.foo = 42 # works | ||
a.bar = 'Ex-parrot' # fails type checking | ||
f = None # type: int | ||
b = None # type: str |
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.
Why do you need these two lines with None
assignments?
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 believe I was going to add more examples, but I don't think its needed. Removed.
test-data/unit/check-classes.test
Outdated
s.success = 4 | ||
s.fail = 'fail' # E: Incompatible types in assignment (expression has type "str", variable has type "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.
One empty line will be enough here.
test-data/unit/check-classes.test
Outdated
class B: | ||
def __setattr__(self, name, value: int): ... | ||
b = B() | ||
b.fail = 5 |
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 think the attribute name fail
in a test that passes is misleading.
test-data/unit/check-classes.test
Outdated
c = C() | ||
c.check = 13 | ||
|
||
[case test testAttributes] |
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 probably too generic test case name. Maybe testGetAttrAndSetattr
? Also there is an extra test
word.
test-data/unit/check-classes.test
Outdated
b.at = '3' # E: Incompatible types in assignment (expression has type "str", variable has type "int") | ||
integer = b.at # E: Incompatible types in assignment (expression has type "str", variable has type "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.
Again, remove the second added empty line.
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.
Thanks! This (finally :-) looks good to me!
Thank you! Sorry it took so long :^) next time I'll know more about what is needed! |
* master: (23 commits) Make return type of open() more precise (python#3477) Add test cases that delete a file during incremental checking (python#3461) Parse each format-string component separately (python#3390) Don't warn about returning Any if it is a proper subtype of the return type (python#3473) Add __setattr__ support (python#3451) Remove bundled lib-typing (python#3337) Move version of extensions to post-release (python#3348) Fix None slice bounds with strict-optional (python#3445) Allow NewType subclassing NewType. (python#3465) Add console scripts (python#3074) Fix 'variance' label. Change label for variance section to just 'variance' (python#3429) Better error message for invalid package names passed to mypy (python#3447) Fix last character cut in html-report if file does not end with newline (python#3466) Print pytest output as it happens (python#3463) Add mypy roadmap (python#3460) Add flag to avoid interpreting arguments with a default of None as Optional (python#3248) Add type checking plugin support for functions (python#3299) Mismatch of inferred type and return type note (python#3428) Sync typeshed (python#3449) ...
This is my attempt at solving #521, which allows the setting of arbitrary attributes, restricted by the typing of
values
in__setattr__
. I did not do the same signature checking as__getattr__
, as @JelleZijlstra pointed out, the check would be done based onobject
's method.I want to add some more in the docs about this, as I think it deserves to be well documented, I just wasn't sure where to put it.
Also the wiki should be updated if this is merged.