From c09f47e361eb831ffd96ca74900c6c5eb0a53a52 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Wed, 24 May 2017 22:20:46 -0700 Subject: [PATCH 1/4] add __setattr__ support --- docs/source/cheat_sheet.rst | 9 +++++++ docs/source/cheat_sheet_py3.rst | 7 +++++ mypy/checkmember.py | 9 +++++++ test-data/unit/check-classes.test | 43 +++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/docs/source/cheat_sheet.rst b/docs/source/cheat_sheet.rst index 49919a56831c..dcca72de7368 100644 --- a/docs/source/cheat_sheet.rst +++ b/docs/source/cheat_sheet.rst @@ -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 + # or in your source code. + class A: + # this will allow assignment to any A.x, if x is the same type as `value` + def __setattr__(self, name, value): + # type: (str, int) -> None + ... + a.foo = bar() # works if bar() returns int, fails otherwise + # TODO: explain "Need type annotation for variable" when # initializing with None or an empty container diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index adeab7d734d4..5846e8804afc 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -142,6 +142,13 @@ 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 + # or in your source code. + class A: + # this will allow assignment to any A.x, if x is the same type as `value` + def __setattr__(self, name: str, value: int) -> None: ... + a.foo = bar() # works if bar returns int, fails otherwise + # TODO: explain "Need type annotation for variable" when # initializing with None or an empty container diff --git a/mypy/checkmember.py b/mypy/checkmember.py index d4dca6b6441d..463152e72cc3 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -245,6 +245,15 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, getattr_type = expand_type_by_instance(bound_method, typ) if isinstance(getattr_type, CallableType): return getattr_type.ret_type + 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')) + bound_type = bind_self(setattr_f, original_type) + 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] if itype.type.fallback_to_any: return AnyType() diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 0b6bb6873d62..e6a6c184b79e 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1665,6 +1665,49 @@ class D: main:4: error: Invalid signature "def (__main__.B, __main__.A) -> __main__.B" main:6: error: Invalid signature "def (__main__.C, builtins.str, builtins.str) -> __main__.C" +[case testSetAttr] +from typing import Union +class A: + def __setattr__(self, name: str, value: Any) -> None: ... + +a = A() +a.test = 'hello' + +class B: + def __setattr__(self, name: str, value: Union[int, str]) -> None: ... + +b = B() +b.both = 1 +b.work = '2' + +class C: + def __setattr__(self, name: str, value: str) -> None: ... + +c = C() +c.fail = 4 +[out] +main:19: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case test testAttributes] +class A: + def __setattr__(self, name: str, value: Any) -> None: ... + def __getattr__(self, name: str) -> Any: ... +a = A() +a.test = 4 +t = a.test + +class B: + def __setattr__(self, name: str, value: int) -> None: ... + def __getattr__(self, name: str) -> str: ... +integer = None # type: int +b = B() +b.at = '3' +integer = b.at +[out] +main:13: error: Incompatible types in assignment (expression has type "str", variable has type "int") +main:14: error: Incompatible types in assignment (expression has type "str", variable has type "int") + + -- CallableType objects -- ---------------- From a3e1cf0ea203925e6136ae86a5462d3457a92f33 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Thu, 25 May 2017 14:42:43 -0700 Subject: [PATCH 2/4] fix edge case of null signature, better tests for __setattr__ --- docs/source/cheat_sheet.rst | 3 +- docs/source/cheat_sheet_py3.rst | 14 ++++++-- mypy/checker.py | 12 ++++++- mypy/checkmember.py | 12 +++---- test-data/unit/check-classes.test | 55 ++++++++++++++++++++++++++----- 5 files changed, 77 insertions(+), 19 deletions(-) diff --git a/docs/source/cheat_sheet.rst b/docs/source/cheat_sheet.rst index dcca72de7368..621fa701e214 100644 --- a/docs/source/cheat_sheet.rst +++ b/docs/source/cheat_sheet.rst @@ -156,7 +156,8 @@ When you're puzzled or when things are complicated def __setattr__(self, name, value): # type: (str, int) -> None ... - a.foo = bar() # works if bar() returns int, fails otherwise + a.foo = 42 # works + a.bar = 'Ex-parrot' # fails type checking # TODO: explain "Need type annotation for variable" when # initializing with None or an empty container diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index 5846e8804afc..26fbe792e369 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -142,12 +142,20 @@ 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 - # or in your source code. + # if you want dynamic attributes on your class, have it override __setattr__ or __getattr__ + # in a stub or in your source code. + # __setattr__ allows for dynamic assignment to names + # __getattr__ allows for dynamic access to names class A: # this will allow assignment to any A.x, if x is the same type as `value` def __setattr__(self, name: str, value: int) -> None: ... - a.foo = bar() # works if bar returns int, fails otherwise + # this will allow access to any A.x, if x is compatible with the return type + def __getattr__(self, name: str) -> int: ... + a.foo = 42 # works + a.bar = 'Ex-parrot' # fails type checking + f = None # type: int + b = None # type: str + # TODO: explain "Need type annotation for variable" when # initializing with None or an empty container diff --git a/mypy/checker.py b/mypy/checker.py index 870c561852b6..ba5d6811092e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -616,7 +616,8 @@ def is_implicit_any(t: Type) -> bool: self.check_reverse_op_method(item, typ, name) elif name in ('__getattr__', '__getattribute__'): self.check_getattr_method(typ, defn) - + elif name == '__setattr__': + self.check_setattr_method(typ, defn) # Refuse contravariant return type variable if isinstance(typ.ret_type, TypeVarType): if typ.ret_type.variance == CONTRAVARIANT: @@ -916,6 +917,15 @@ def check_getattr_method(self, typ: CallableType, context: Context) -> None: if not is_subtype(typ, method_type): self.msg.invalid_signature(typ, context) + def check_setattr_method(self, typ: CallableType, context: Context) -> None: + method_type = CallableType([AnyType(), self.named_type('builtins.str'), AnyType()], + [nodes.ARG_POS, nodes.ARG_POS, nodes.ARG_POS], + [None, None, None], + NoneTyp(), + self.named_type('builtins.function')) + if not is_subtype(typ, method_type): + self.msg.invalid_signature(typ, context) + def expand_typevars(self, defn: FuncItem, typ: CallableType) -> List[Tuple[FuncItem, CallableType]]: # TODO use generator diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 463152e72cc3..4525b1446bda 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -246,13 +246,13 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, if isinstance(getattr_type, CallableType): return getattr_type.ret_type 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')) - bound_type = bind_self(setattr_f, original_type) - typ = map_instance_to_supertype(itype, setattr_func.info) + setattr_meth = info.get_method('__setattr__') + if setattr_meth and setattr_meth.info.fullname() != 'builtins.object': + setattr_func = function_type(setattr_meth, builtin_type('builtins.function')) + bound_type = bind_self(setattr_func, original_type) + typ = map_instance_to_supertype(itype, setattr_meth.info) setattr_type = expand_type_by_instance(bound_type, typ) - if isinstance(setattr_type, CallableType): + if isinstance(setattr_type, CallableType) and len(setattr_type.arg_types) > 0: return setattr_type.arg_types[-1] if itype.type.fallback_to_any: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index e6a6c184b79e..aab6903c316b 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1684,9 +1684,51 @@ class C: def __setattr__(self, name: str, value: str) -> None: ... c = C() -c.fail = 4 -[out] -main:19: error: Incompatible types in assignment (expression has type "int", variable has type "str") +c.fail = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "str") + +class D: + __setattr__ = 'hello' + +d = D() +d.crash = 4 # E: "D" has no attribute "crash" + +class Ex: + def __setattr__(self, name: str, value: int) -> None:... + test = '42' # type: str +e = Ex() +e.test = 'hello' +e.t = 4 + +class Super: + def __setattr__(self, name: str, value: int) -> None: ... + +class Sub(Super): + ... +s = Sub() +s.success = 4 +s.fail = 'fail' # E: Incompatible types in assignment (expression has type "str", variable has type "int") + + +[case testSetAttrSignature] +class Test: + def __setattr__() -> None: ... # E: Method must have at least one argument # E: Invalid signature "def ()" +t = Test() +t.crash = 'test' # E: "Test" has no attribute "crash" + +class A: + def __setattr__(self): ... # E: Invalid signature "def (self: Any) -> Any" +a = A() +a.test = 4 # E: "A" has no attribute "test" + +class B: + def __setattr__(self, name, value: int): ... +b = B() +b.fail = 5 + +class C: + def __setattr__(self, name: int, value: int) -> None: ... # E: Invalid signature "def (__main__.C, builtins.int, builtins.int)" +c = C() +c.check = 13 [case test testAttributes] class A: @@ -1701,11 +1743,8 @@ class B: def __getattr__(self, name: str) -> str: ... integer = None # type: int b = B() -b.at = '3' -integer = b.at -[out] -main:13: error: Incompatible types in assignment (expression has type "str", variable has type "int") -main:14: error: Incompatible types in assignment (expression has type "str", variable has type "int") +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") From a2afab60230bcf82c5da6016d4afd245a7e616e6 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Thu, 25 May 2017 16:18:16 -0700 Subject: [PATCH 3/4] modify test to be strict-optional friendly --- test-data/unit/check-classes.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index aab6903c316b..7b88da5f1af4 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1741,7 +1741,7 @@ t = a.test class B: def __setattr__(self, name: str, value: int) -> None: ... def __getattr__(self, name: str) -> str: ... -integer = None # type: int +integer = 0 b = B() 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") From 73def65ccf16d36f15f429d5979f76c4fa5356b3 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Sun, 28 May 2017 16:28:21 -0700 Subject: [PATCH 4/4] correct docs and test data --- docs/source/cheat_sheet.rst | 6 ++++-- docs/source/cheat_sheet_py3.rst | 2 -- test-data/unit/check-classes.test | 8 ++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/source/cheat_sheet.rst b/docs/source/cheat_sheet.rst index 621fa701e214..f8e7146c65f4 100644 --- a/docs/source/cheat_sheet.rst +++ b/docs/source/cheat_sheet.rst @@ -149,8 +149,10 @@ 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 - # or in your source code. + # if you want dynamic attributes on your class, have it override __setattr__ or __getattr__ + # in a stub or in your source code. + # __setattr__ allows for dynamic assignment to names + # __getattr__ allows for dynamic access to names class A: # this will allow assignment to any A.x, if x is the same type as `value` def __setattr__(self, name, value): diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index 26fbe792e369..5ef62b28134f 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -153,8 +153,6 @@ When you're puzzled or when things are complicated def __getattr__(self, name: str) -> int: ... a.foo = 42 # works a.bar = 'Ex-parrot' # fails type checking - f = None # type: int - b = None # type: str # TODO: explain "Need type annotation for variable" when diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 7b88da5f1af4..f5b6703f5bbf 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1651,7 +1651,6 @@ b = a.bar [out] main:9: error: Incompatible types in assignment (expression has type "A", variable has type "B") - [case testGetAttrSignature] class A: def __getattr__(self, x: str) -> A: pass @@ -1708,7 +1707,6 @@ s = Sub() s.success = 4 s.fail = 'fail' # E: Incompatible types in assignment (expression has type "str", variable has type "int") - [case testSetAttrSignature] class Test: def __setattr__() -> None: ... # E: Method must have at least one argument # E: Invalid signature "def ()" @@ -1723,14 +1721,14 @@ a.test = 4 # E: "A" has no attribute "test" class B: def __setattr__(self, name, value: int): ... b = B() -b.fail = 5 +b.integer = 5 class C: def __setattr__(self, name: int, value: int) -> None: ... # E: Invalid signature "def (__main__.C, builtins.int, builtins.int)" c = C() c.check = 13 -[case test testAttributes] +[case testGetAttrAndSetattr] class A: def __setattr__(self, name: str, value: Any) -> None: ... def __getattr__(self, name: str) -> Any: ... @@ -1746,8 +1744,6 @@ b = B() 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") - - -- CallableType objects -- ----------------