Skip to content

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

Merged
merged 6 commits into from
May 29, 2017
Merged

Add __setattr__ support #3451

merged 6 commits into from
May 29, 2017

Conversation

emmatyping
Copy link
Member

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 on object'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.

Copy link
Member

@ilevkivskyi ilevkivskyi left a 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.

b.at = '3'
integer = b.at
[out]
main:13: error: Incompatible types in assignment (expression has type "str", variable has type "int")
Copy link
Member

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?

def __setattr__(self, name, value):
# type: (str, int) -> None
...
a.foo = bar() # works if bar() returns int, fails otherwise
Copy link
Member

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?

Copy link
Member Author

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.

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]
Copy link
Member

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.

Copy link
Member Author

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: ...
Copy link
Member

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

Copy link
Member

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

Copy link
Member Author

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'

Copy link
Member

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)

@@ -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
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

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]
Copy link
Member

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?

Copy link
Member

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!

Copy link
Member Author

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.

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'))
Copy link
Member

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.

@emmatyping
Copy link
Member Author

I believe I resolved all of the points reviewed. If I missed something or there is something else needed, lmk.

class B:
def __setattr__(self, name: str, value: int) -> None: ...
def __getattr__(self, name: str) -> str: ...
integer = None # type: int
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

Copy link
Member

@ilevkivskyi ilevkivskyi left a 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.

a.foo = 42 # works
a.bar = 'Ex-parrot' # fails type checking
f = None # type: int
b = None # type: str
Copy link
Member

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?

Copy link
Member Author

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.

s.success = 4
s.fail = 'fail' # E: Incompatible types in assignment (expression has type "str", variable has type "int")


Copy link
Member

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.

class B:
def __setattr__(self, name, value: int): ...
b = B()
b.fail = 5
Copy link
Member

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.

c = C()
c.check = 13

[case test testAttributes]
Copy link
Member

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.

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")


Copy link
Member

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.

Copy link
Member

@ilevkivskyi ilevkivskyi left a 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!

@ilevkivskyi ilevkivskyi merged commit 46e41d9 into python:master May 29, 2017
@emmatyping
Copy link
Member Author

Thank you! Sorry it took so long :^) next time I'll know more about what is needed!

carljm added a commit to carljm/mypy that referenced this pull request May 30, 2017
* 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)
  ...
@ilevkivskyi ilevkivskyi mentioned this pull request Jun 10, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants