Add __setattr__ support#3451
Conversation
ilevkivskyi
left a comment
There was a problem hiding this comment.
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.
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.
Maybe just use an integer like 42 instead of bar() here and in Python 3 version?
There was a problem hiding this comment.
Fair point, and perhaps a str with a comment about it failing.
| 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.
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.
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.
Two more ideas for tests:
- presence of both
__setattr__and normal attributes __setattr__in superclass
There was a problem hiding this comment.
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.
@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.
Yes, something like this (plus e.whatever = 5)
docs/source/cheat_sheet.rst
Outdated
| 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.
And also __getattr__, depending on our decision on that matter.
There was a problem hiding this comment.
Right, perhaps I should make that more clear and which one is for which case.
There was a problem hiding this comment.
Looks like you put it in the py3 but not the py2 cheat sheet. It should be the same in both.
| 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.
Is this going to crash if __setattr__ is defined to take no arguments?
There was a problem hiding this comment.
Is this going to crash if
__setattr__is defined to take no arguments?
Good catch!
There was a problem hiding this comment.
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.
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.
why not just integer = 0? that will work with strict-optional too
ilevkivskyi
left a comment
There was a problem hiding this comment.
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.
Why do you need these two lines with None assignments?
There was a problem hiding this comment.
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.
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.
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.
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.
Again, remove the second added empty line.
ilevkivskyi
left a comment
There was a problem hiding this comment.
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
valuesin__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.