Skip to content

Commit 46e41d9

Browse files
emmatypingilevkivskyi
authored andcommitted
Add __setattr__ support (#3451)
* add __setattr__ support * fix edge case of null signature, better tests for __setattr__ * modify test to be strict-optional friendly * correct docs and test data
1 parent d238ab5 commit 46e41d9

File tree

5 files changed

+124
-2
lines changed

5 files changed

+124
-2
lines changed

docs/source/cheat_sheet.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ When you're puzzled or when things are complicated
149149
reveal_type(c) # -> error: Revealed type is 'builtins.list[builtins.str]'
150150
print(c) # -> [4] the object is not cast
151151
152+
# if you want dynamic attributes on your class, have it override __setattr__ or __getattr__
153+
# in a stub or in your source code.
154+
# __setattr__ allows for dynamic assignment to names
155+
# __getattr__ allows for dynamic access to names
156+
class A:
157+
# this will allow assignment to any A.x, if x is the same type as `value`
158+
def __setattr__(self, name, value):
159+
# type: (str, int) -> None
160+
...
161+
a.foo = 42 # works
162+
a.bar = 'Ex-parrot' # fails type checking
163+
152164
# TODO: explain "Need type annotation for variable" when
153165
# initializing with None or an empty container
154166

docs/source/cheat_sheet_py3.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,19 @@ When you're puzzled or when things are complicated
142142
reveal_type(c) # -> error: Revealed type is 'builtins.list[builtins.str]'
143143
print(c) # -> [4] the object is not cast
144144
145+
# if you want dynamic attributes on your class, have it override __setattr__ or __getattr__
146+
# in a stub or in your source code.
147+
# __setattr__ allows for dynamic assignment to names
148+
# __getattr__ allows for dynamic access to names
149+
class A:
150+
# this will allow assignment to any A.x, if x is the same type as `value`
151+
def __setattr__(self, name: str, value: int) -> None: ...
152+
# this will allow access to any A.x, if x is compatible with the return type
153+
def __getattr__(self, name: str) -> int: ...
154+
a.foo = 42 # works
155+
a.bar = 'Ex-parrot' # fails type checking
156+
157+
145158
# TODO: explain "Need type annotation for variable" when
146159
# initializing with None or an empty container
147160

mypy/checker.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,8 @@ def is_implicit_any(t: Type) -> bool:
616616
self.check_reverse_op_method(item, typ, name)
617617
elif name in ('__getattr__', '__getattribute__'):
618618
self.check_getattr_method(typ, defn)
619-
619+
elif name == '__setattr__':
620+
self.check_setattr_method(typ, defn)
620621
# Refuse contravariant return type variable
621622
if isinstance(typ.ret_type, TypeVarType):
622623
if typ.ret_type.variance == CONTRAVARIANT:
@@ -916,6 +917,15 @@ def check_getattr_method(self, typ: CallableType, context: Context) -> None:
916917
if not is_subtype(typ, method_type):
917918
self.msg.invalid_signature(typ, context)
918919

920+
def check_setattr_method(self, typ: CallableType, context: Context) -> None:
921+
method_type = CallableType([AnyType(), self.named_type('builtins.str'), AnyType()],
922+
[nodes.ARG_POS, nodes.ARG_POS, nodes.ARG_POS],
923+
[None, None, None],
924+
NoneTyp(),
925+
self.named_type('builtins.function'))
926+
if not is_subtype(typ, method_type):
927+
self.msg.invalid_signature(typ, context)
928+
919929
def expand_typevars(self, defn: FuncItem,
920930
typ: CallableType) -> List[Tuple[FuncItem, CallableType]]:
921931
# TODO use generator

mypy/checkmember.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,15 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
245245
getattr_type = expand_type_by_instance(bound_method, typ)
246246
if isinstance(getattr_type, CallableType):
247247
return getattr_type.ret_type
248+
else:
249+
setattr_meth = info.get_method('__setattr__')
250+
if setattr_meth and setattr_meth.info.fullname() != 'builtins.object':
251+
setattr_func = function_type(setattr_meth, builtin_type('builtins.function'))
252+
bound_type = bind_self(setattr_func, original_type)
253+
typ = map_instance_to_supertype(itype, setattr_meth.info)
254+
setattr_type = expand_type_by_instance(bound_type, typ)
255+
if isinstance(setattr_type, CallableType) and len(setattr_type.arg_types) > 0:
256+
return setattr_type.arg_types[-1]
248257

249258
if itype.type.fallback_to_any:
250259
return AnyType()

test-data/unit/check-classes.test

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1651,7 +1651,6 @@ b = a.bar
16511651
[out]
16521652
main:9: error: Incompatible types in assignment (expression has type "A", variable has type "B")
16531653

1654-
16551654
[case testGetAttrSignature]
16561655
class A:
16571656
def __getattr__(self, x: str) -> A: pass
@@ -1665,6 +1664,85 @@ class D:
16651664
main:4: error: Invalid signature "def (__main__.B, __main__.A) -> __main__.B"
16661665
main:6: error: Invalid signature "def (__main__.C, builtins.str, builtins.str) -> __main__.C"
16671666

1667+
[case testSetAttr]
1668+
from typing import Union
1669+
class A:
1670+
def __setattr__(self, name: str, value: Any) -> None: ...
1671+
1672+
a = A()
1673+
a.test = 'hello'
1674+
1675+
class B:
1676+
def __setattr__(self, name: str, value: Union[int, str]) -> None: ...
1677+
1678+
b = B()
1679+
b.both = 1
1680+
b.work = '2'
1681+
1682+
class C:
1683+
def __setattr__(self, name: str, value: str) -> None: ...
1684+
1685+
c = C()
1686+
c.fail = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "str")
1687+
1688+
class D:
1689+
__setattr__ = 'hello'
1690+
1691+
d = D()
1692+
d.crash = 4 # E: "D" has no attribute "crash"
1693+
1694+
class Ex:
1695+
def __setattr__(self, name: str, value: int) -> None:...
1696+
test = '42' # type: str
1697+
e = Ex()
1698+
e.test = 'hello'
1699+
e.t = 4
1700+
1701+
class Super:
1702+
def __setattr__(self, name: str, value: int) -> None: ...
1703+
1704+
class Sub(Super):
1705+
...
1706+
s = Sub()
1707+
s.success = 4
1708+
s.fail = 'fail' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
1709+
1710+
[case testSetAttrSignature]
1711+
class Test:
1712+
def __setattr__() -> None: ... # E: Method must have at least one argument # E: Invalid signature "def ()"
1713+
t = Test()
1714+
t.crash = 'test' # E: "Test" has no attribute "crash"
1715+
1716+
class A:
1717+
def __setattr__(self): ... # E: Invalid signature "def (self: Any) -> Any"
1718+
a = A()
1719+
a.test = 4 # E: "A" has no attribute "test"
1720+
1721+
class B:
1722+
def __setattr__(self, name, value: int): ...
1723+
b = B()
1724+
b.integer = 5
1725+
1726+
class C:
1727+
def __setattr__(self, name: int, value: int) -> None: ... # E: Invalid signature "def (__main__.C, builtins.int, builtins.int)"
1728+
c = C()
1729+
c.check = 13
1730+
1731+
[case testGetAttrAndSetattr]
1732+
class A:
1733+
def __setattr__(self, name: str, value: Any) -> None: ...
1734+
def __getattr__(self, name: str) -> Any: ...
1735+
a = A()
1736+
a.test = 4
1737+
t = a.test
1738+
1739+
class B:
1740+
def __setattr__(self, name: str, value: int) -> None: ...
1741+
def __getattr__(self, name: str) -> str: ...
1742+
integer = 0
1743+
b = B()
1744+
b.at = '3' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
1745+
integer = b.at # E: Incompatible types in assignment (expression has type "str", variable has type "int")
16681746

16691747
-- CallableType objects
16701748
-- ----------------

0 commit comments

Comments
 (0)