From a0977b2cc87eefbb5dd93f38fe6490c538d488c1 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 24 Jul 2021 19:06:45 +0300 Subject: [PATCH 01/15] WIP: adds support for `__slots__` assignment, refs #10801 --- mypy/checker.py | 13 +++++++++++++ mypy/nodes.py | 2 ++ mypy/semanal.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index ae93b8558add..222ca87c1c66 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2086,6 +2086,19 @@ def accept_items(e: Expression) -> None: def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True, new_syntax: bool = False) -> None: """Type check a single assignment: lvalue = rvalue.""" + if isinstance(lvalue, MemberExpr) and lvalue.node: + name = lvalue.node.name + inst = self.expr_checker.accept(lvalue.expr) + if (isinstance(inst, Instance) and + inst.type.slots and + name not in inst.type.slots): + self.fail( + 'Trying to assing name "{}" that is not in "__slots__" of type "{}"'.format( + name, inst.type.fullname, + ), + lvalue, + ) + if isinstance(lvalue, TupleExpr) or isinstance(lvalue, ListExpr): self.check_assignment_to_multiple_lvalues(lvalue.items, rvalue, rvalue, infer_lvalue_type) diff --git a/mypy/nodes.py b/mypy/nodes.py index 2efe814a94ed..b393f8e3c02b 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2298,6 +2298,7 @@ class is generic then it will be a type constructor of higher kind. runtime_protocol = False # Does this protocol support isinstance checks? abstract_attributes: List[str] deletable_attributes: List[str] # Used by mypyc only + slots: Set[str] # The attributes 'assuming' and 'assuming_proper' represent structural subtype matrices. # @@ -2401,6 +2402,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No self.is_abstract = False self.abstract_attributes = [] self.deletable_attributes = [] + self.slots = set() self.assuming = [] self.assuming_proper = [] self.inferring = [] diff --git a/mypy/semanal.py b/mypy/semanal.py index 15036e065265..7de9ecde23fe 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2038,6 +2038,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.process_module_assignment(s.lvalues, s.rvalue, s) self.process__all__(s) self.process__deletable__(s) + self.process__slots__(s) def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool: """Special case 'X = X' in global scope. @@ -3353,6 +3354,33 @@ def process__deletable__(self, s: AssignmentStmt) -> None: assert self.type self.type.deletable_attributes = attrs + def process__slots__(self, s: AssignmentStmt) -> None: + if (isinstance(self.type, TypeInfo) and + len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and + s.lvalues[0].name == '__slots__' and s.lvalues[0].kind == MDEF): + + if not isinstance(s.rvalue, (StrExpr, ListExpr, TupleExpr)): + self.fail( + '"__slots__" must be initialized with a list, string, or tuple expression', + s, + ) + return + + rvalue: List[Expression] = [rvalue] if isinstance( + rvalue, StrExpr, + ) else rvalue.items # type: ignore + slots = [] + for item in rvalue: + if isinstance(item, StrExpr): + slots.append(item.value) + else: + self.fail('Invalid "__slots__" item; string literal expected', item) + + # We need to copy all slots for super types: + for super_type in self.type.mro[1:]: + slots.extend(super_type.slots) + self.type.slots = set(slots) + # # Misc statements # From 070a41c595ae7f1c8de3007242cf4ee0d310a646 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 24 Jul 2021 21:33:44 +0300 Subject: [PATCH 02/15] Fixes CI --- mypy/checker.py | 2 +- mypy/semanal.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 222ca87c1c66..ab38514390bb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2093,7 +2093,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type inst.type.slots and name not in inst.type.slots): self.fail( - 'Trying to assing name "{}" that is not in "__slots__" of type "{}"'.format( + 'Trying to assign name "{}" that is not in "__slots__" of type "{}"'.format( name, inst.type.fullname, ), lvalue, diff --git a/mypy/semanal.py b/mypy/semanal.py index 7de9ecde23fe..b977599aec73 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3366,9 +3366,9 @@ def process__slots__(self, s: AssignmentStmt) -> None: ) return - rvalue: List[Expression] = [rvalue] if isinstance( - rvalue, StrExpr, - ) else rvalue.items # type: ignore + rvalue: List[Expression] = [s.rvalue] if isinstance( + s.rvalue, StrExpr, + ) else s.rvalue.items slots = [] for item in rvalue: if isinstance(item, StrExpr): @@ -3376,7 +3376,7 @@ def process__slots__(self, s: AssignmentStmt) -> None: else: self.fail('Invalid "__slots__" item; string literal expected', item) - # We need to copy all slots for super types: + # We need to copy all slots from super types: for super_type in self.type.mro[1:]: slots.extend(super_type.slots) self.type.slots = set(slots) From 89f68597272e16a92afb44e44242b13f9280250d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 25 Jul 2021 13:12:45 +0300 Subject: [PATCH 03/15] Adds tests for `__slots__` --- mypy/checker.py | 2 +- mypy/nodes.py | 7 +- mypy/semanal.py | 49 ++++++-- mypy/test/testcheck.py | 1 + test-data/unit/check-slots.test | 211 ++++++++++++++++++++++++++++++++ 5 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 test-data/unit/check-slots.test diff --git a/mypy/checker.py b/mypy/checker.py index ab38514390bb..16f388e8dda3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2090,7 +2090,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type name = lvalue.node.name inst = self.expr_checker.accept(lvalue.expr) if (isinstance(inst, Instance) and - inst.type.slots and + inst.type.slots is not None and name not in inst.type.slots): self.fail( 'Trying to assign name "{}" that is not in "__slots__" of type "{}"'.format( diff --git a/mypy/nodes.py b/mypy/nodes.py index b393f8e3c02b..ae8c9691b7d9 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2298,7 +2298,10 @@ class is generic then it will be a type constructor of higher kind. runtime_protocol = False # Does this protocol support isinstance checks? abstract_attributes: List[str] deletable_attributes: List[str] # Used by mypyc only - slots: Set[str] + # Does this type has concrete `__slots__` defined? + # If class does not have `__slots__` defined then it is `None`, + # if it has empty `__slots__` then it is an empty set. + slots: Optional[Set[str]] # The attributes 'assuming' and 'assuming_proper' represent structural subtype matrices. # @@ -2402,7 +2405,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No self.is_abstract = False self.abstract_attributes = [] self.deletable_attributes = [] - self.slots = set() + self.slots = None self.assuming = [] self.assuming_proper = [] self.inferring = [] diff --git a/mypy/semanal.py b/mypy/semanal.py index b977599aec73..746dac5a0740 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3355,29 +3355,56 @@ def process__deletable__(self, s: AssignmentStmt) -> None: self.type.deletable_attributes = attrs def process__slots__(self, s: AssignmentStmt) -> None: + """ + Processing ``__slots__`` if defined in type. + + See: https://docs.python.org/3/reference/datamodel.html#slots + """ + # Later we can support `__slots__` defined as `__slots__ = other = ('a', 'b')` if (isinstance(self.type, TypeInfo) and len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and s.lvalues[0].name == '__slots__' and s.lvalues[0].kind == MDEF): - if not isinstance(s.rvalue, (StrExpr, ListExpr, TupleExpr)): - self.fail( - '"__slots__" must be initialized with a list, string, or tuple expression', - s, - ) + # We understand `__slots__` defined as string, tuple, list, set, and dict: + if not isinstance(s.rvalue, (StrExpr, ListExpr, TupleExpr, SetExpr, DictExpr)): + # For example, `__slots__` can be defined as a variable, + # we don't support it for now. return - rvalue: List[Expression] = [s.rvalue] if isinstance( - s.rvalue, StrExpr, - ) else s.rvalue.items + if any(p.slots is None for p in self.type.mro[1:-1]): + # At least one type in mro (excluding `self` and `object`) + # does not have concrete `__slots__` defined. Ignoring. + return + + concrete_slots = True + rvalue: List[Expression] = [] + if isinstance(s.rvalue, StrExpr): + rvalue.append(s.rvalue) + elif isinstance(s.rvalue, (ListExpr, TupleExpr, SetExpr)): + rvalue.extend(s.rvalue.items) + else: + # We have a special treatment of `dict` with possible `{**kwargs}` usage. + # In this case we consider all `__slots__` to be non-concrete. + for key, _ in s.rvalue.items: + if key is not None: + rvalue.append(key) + else: + concrete_slots = False + slots = [] for item in rvalue: - if isinstance(item, StrExpr): + # Special case for `'__dict__'` value: + # when specified it will still allows any attribute assignment + if isinstance(item, StrExpr) and item.value != '__dict__': slots.append(item.value) else: - self.fail('Invalid "__slots__" item; string literal expected', item) + concrete_slots = False + if not concrete_slots: + return # We need to copy all slots from super types: - for super_type in self.type.mro[1:]: + for super_type in self.type.mro[1:-1]: + assert super_type.slots is not None slots.extend(super_type.slots) self.type.slots = set(slots) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 5e14a03d58cb..aaf1bd73fc2a 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -96,6 +96,7 @@ 'check-typeguard.test', 'check-functools.test', 'check-singledispatch.test', + 'check-slots.test', ] # Tests that use Python 3.8-only AST features (like expression-scoped ignores): diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test new file mode 100644 index 000000000000..9a1bf0ec9f81 --- /dev/null +++ b/test-data/unit/check-slots.test @@ -0,0 +1,211 @@ +[case testSlotsDefinitionWithStrAndListAndTuple] +class A: + __slots__ = "a" +class B: + __slots__ = ("a", "b") +class C: + __slots__ = ['c'] + +class WithVariable: + __fields__ = ['a', 'b'] + __slots__ = __fields__ +[builtins fixtures/tuple.pyi] +[builtins fixtures/list.pyi] + + +[case testSlotsDefinitionWithDict] +class D: + __slots__ = {'key': 'docs'} + def __init__(self) -> None: + self.key = 1 + self.missing = 2 # E: Trying to assign name "missing" that is not in "__slots__" of type "__main__.D" +[builtins fixtures/dict.pyi] + + +[case testSlotsDefinitionWithDynamicDict] +slot_kwargs = {'b': 'docs'} +class WithDictKwargs: + __slots__ = {'a': 'docs', **slot_kwargs} + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.c = 3 +[builtins fixtures/dict.pyi] + + +[case testSlotsDefinitionWithSet] +class E: + __slots__ = {'e'} + def __init__(self) -> None: + self.e = 1 + self.missing = 2 # E: Trying to assign name "missing" that is not in "__slots__" of type "__main__.E" +[builtins fixtures/set.pyi] + + +[case testSlotsDefinitionOutsideOfClass] +__slots__ = ("a", "b") +class A: + def __init__(self) -> None: + self.x = 1 + self.y = 2 +[builtins fixtures/tuple.pyi] + + +[case testSlotsDefinitionMutlipleVars1] +class A: + __slots__ = __fields__ = ("a", "b") + def __init__(self) -> None: + self.x = 1 + self.y = 2 +[builtins fixtures/tuple.pyi] + + + +[case testSlotsDefinitionMutlipleVars2] +class A: + __fields__ = __slots__ = ("a", "b") + def __init__(self) -> None: + self.x = 1 + self.y = 2 +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentEmptySlots] +class A: + __slots__ = () + def __init__(self) -> None: + self.a = 1 # E: Trying to assign name "a" that is not in "__slots__" of type "__main__.A" + self.b = 2 # E: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" + +a = A() +a.a = 1 +a.b = 2 +a.missing = 2 # E: "A" has no attribute "missing" +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithSuper] +class A: + __slots__ = ("a",) +class B(A): + __slots__ = ("b",) + + def __init__(self) -> None: + self.a = 1 + self._one = 1 # E: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" + +b = B() +b.a = 1 +b.b = 2 # E: "B" has no attribute "b" +b._two = 2 # E: "B" has no attribute "_two" +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithMixing] +class A: + __slots__ = ("a",) +class Mixin: + __slots__ = () +class B(A, Mixin): + __slots__ = ("b",) + + def __init__(self) -> None: + self.a = 1 + self._one = 1 # E: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" + +b = B() +b.a = 1 +b.b = 2 # E: "B" has no attribute "b" +b._two = 2 # E: "B" has no attribute "_two" +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithoutSuperSlots] +class A: + pass # no slots +class B(A): + __slots__ = ("a", "b") + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.missing = 3 + +b = B() +b.a = 1 +b.b = 2 +b.missing = 3 +b.extra = 4 # E: "B" has no attribute "extra" +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithoutSuperMixingSlots] +class A: + __slots__ = () +class Mixin: + pass # no slots +class B(A, Mixin): + __slots__ = ("a", "b") + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.missing = 3 + +b = B() +b.a = 1 +b.b = 2 +b.missing = 3 +b.extra = 4 # E: "B" has no attribute "extra" +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithExplicitDict] +class A: + __slots__ = ("a",) +class B(A): + __slots__ = ("__dict__",) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + +b = B() +b.a = 1 +b.b = 2 +b.c = 3 # E: "B" has no attribute "c" +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithExplicitSuperDict] +class A: + __slots__ = ("__dict__",) +class B(A): + __slots__ = ("a",) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + +b = B() +b.a = 1 +b.b = 2 +b.c = 3 # E: "B" has no attribute "c" +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithVariable] +slot_name = "b" +class A: + __slots__ = ("a", slot_name) + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.c = 3 + +a = A() +a.a = 1 +a.b = 2 +a.c = 3 +a.d = 4 # E: "A" has no attribute "d" +[builtins fixtures/tuple.pyi] From a392d8a8c7e7fc75a7ef736edb226403ac66f31a Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 25 Jul 2021 13:49:42 +0300 Subject: [PATCH 04/15] Ready for a review --- mypy/checker.py | 2 +- test-data/unit/check-slots.test | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 16f388e8dda3..17384d15dc63 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2088,7 +2088,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type """Type check a single assignment: lvalue = rvalue.""" if isinstance(lvalue, MemberExpr) and lvalue.node: name = lvalue.node.name - inst = self.expr_checker.accept(lvalue.expr) + inst = get_proper_type(self.expr_checker.accept(lvalue.expr)) if (isinstance(inst, Instance) and inst.type.slots is not None and name not in inst.type.slots): diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 9a1bf0ec9f81..9fabf9e31463 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -209,3 +209,11 @@ a.b = 2 a.c = 3 a.d = 4 # E: "A" has no attribute "d" [builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentMultipleLeftValues] +class A: + __slots__ = ("a", "b") + def __init__(self) -> None: + self.a, self.b, self.c = (1, 2, 3) # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.A" +[builtins fixtures/tuple.pyi] From 0195f733a0530dffd609834012be5df1f6c620e3 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 25 Jul 2021 13:53:41 +0300 Subject: [PATCH 05/15] Adds the last test --- test-data/unit/check-slots.test | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 9fabf9e31463..8c5a48078480 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -120,6 +120,20 @@ b._two = 2 # E: "B" has no attribute "_two" [builtins fixtures/tuple.pyi] +[case testSlotsAssignmentWithSlottedSuperButNoChildSlots] +class A: + __slots__ = ("a",) +class B(A): + def __init__(self) -> None: + self.a = 1 + self.b = 1 + +b = B() +b.a = 1 +b.b = 2 +[builtins fixtures/tuple.pyi] + + [case testSlotsAssignmentWithoutSuperSlots] class A: pass # no slots From 7c4aa9f3f93fc3832d7df839a2f4e98580ef3d5e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 25 Jul 2021 14:25:57 +0300 Subject: [PATCH 06/15] More tests --- test-data/unit/check-slots.test | 36 +++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 8c5a48078480..5b66a8f171a3 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -1,14 +1,31 @@ [case testSlotsDefinitionWithStrAndListAndTuple] class A: __slots__ = "a" + def __init__(self) -> None: + self.a = 1 + self.b = 2 # E: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" + class B: __slots__ = ("a", "b") + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.c = 3 # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.B" + class C: __slots__ = ['c'] + def __init__(self) -> None: + self.a = 1 # E: Trying to assign name "a" that is not in "__slots__" of type "__main__.C" + self.c = 3 class WithVariable: __fields__ = ['a', 'b'] __slots__ = __fields__ + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.c = 3 [builtins fixtures/tuple.pyi] [builtins fixtures/list.pyi] @@ -88,19 +105,34 @@ a.missing = 2 # E: "A" has no attribute "missing" class A: __slots__ = ("a",) class B(A): - __slots__ = ("b",) + __slots__ = ("b", "c") def __init__(self) -> None: self.a = 1 + self.b = 2 self._one = 1 # E: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" b = B() b.a = 1 -b.b = 2 # E: "B" has no attribute "b" +b.b = 2 +b.c = 3 # E: "B" has no attribute "c" b._two = 2 # E: "B" has no attribute "_two" [builtins fixtures/tuple.pyi] +[case testSlotsAssignmentWithSuperDuplicateSlots] +class A: + __slots__ = ("a",) +class B(A): + __slots__ = ("a", "b",) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self._one = 1 # E: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" +[builtins fixtures/tuple.pyi] + + [case testSlotsAssignmentWithMixing] class A: __slots__ = ("a",) From fce051afbdf90efe1a008b9442039b0dda62c239 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 25 Jul 2021 15:02:29 +0300 Subject: [PATCH 07/15] Typos --- mypy/semanal.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 746dac5a0740..fa9a16cf2f36 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3386,7 +3386,7 @@ def process__slots__(self, s: AssignmentStmt) -> None: # We have a special treatment of `dict` with possible `{**kwargs}` usage. # In this case we consider all `__slots__` to be non-concrete. for key, _ in s.rvalue.items: - if key is not None: + if concrete_slots and key is not None: rvalue.append(key) else: concrete_slots = False @@ -3394,12 +3394,14 @@ def process__slots__(self, s: AssignmentStmt) -> None: slots = [] for item in rvalue: # Special case for `'__dict__'` value: - # when specified it will still allows any attribute assignment + # when specified it will still allow any attribute assignment. if isinstance(item, StrExpr) and item.value != '__dict__': slots.append(item.value) else: concrete_slots = False if not concrete_slots: + # Some slot items are dynamic, we don't want any false positives, + # so, we just pretend that this type does not have any slots at all. return # We need to copy all slots from super types: From 4edc4bf53a60b91ca7154c38011526de9cdefeb1 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 25 Jul 2021 15:57:55 +0300 Subject: [PATCH 08/15] Typos --- mypy/nodes.py | 2 +- test-data/unit/check-slots.test | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index ae8c9691b7d9..73b9c9331a54 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2298,7 +2298,7 @@ class is generic then it will be a type constructor of higher kind. runtime_protocol = False # Does this protocol support isinstance checks? abstract_attributes: List[str] deletable_attributes: List[str] # Used by mypyc only - # Does this type has concrete `__slots__` defined? + # Does this type have concrete `__slots__` defined? # If class does not have `__slots__` defined then it is `None`, # if it has empty `__slots__` then it is an empty set. slots: Optional[Set[str]] diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 5b66a8f171a3..54918de696b1 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -137,16 +137,18 @@ class B(A): class A: __slots__ = ("a",) class Mixin: - __slots__ = () + __slots__ = ("m",) class B(A, Mixin): __slots__ = ("b",) def __init__(self) -> None: self.a = 1 + self.m = 2 self._one = 1 # E: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" b = B() b.a = 1 +b.m = 2 b.b = 2 # E: "B" has no attribute "b" b._two = 2 # E: "B" has no attribute "_two" [builtins fixtures/tuple.pyi] From 4c112aea82a50ae5c9501309c801cd6f8aac4c13 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 9 Aug 2021 12:14:13 +0300 Subject: [PATCH 09/15] Adds test for multiple `=` parts --- test-data/unit/check-slots.test | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 54918de696b1..bb6568b135d3 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -265,3 +265,14 @@ class A: def __init__(self) -> None: self.a, self.b, self.c = (1, 2, 3) # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.A" [builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentMultipleAssignments] +class A: + __slots__ = ("a",) + def __init__(self) -> None: + self.a = self.b = self.c = 1 +[out] +main:4: error: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" +main:4: error: Trying to assign name "c" that is not in "__slots__" of type "__main__.A" +[builtins fixtures/tuple.pyi] From 7ecfa7f8ebdf4fb6a3b86655735271cac4477aa4 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 14 Aug 2021 20:37:04 +0300 Subject: [PATCH 10/15] Typos, `Mutliple` -> `Multiple` --- test-data/unit/check-slots.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index bb6568b135d3..f874ad30ffb9 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -68,7 +68,7 @@ class A: [builtins fixtures/tuple.pyi] -[case testSlotsDefinitionMutlipleVars1] +[case testSlotsDefinitionMultipleVars1] class A: __slots__ = __fields__ = ("a", "b") def __init__(self) -> None: @@ -78,7 +78,7 @@ class A: -[case testSlotsDefinitionMutlipleVars2] +[case testSlotsDefinitionMultipleVars2] class A: __fields__ = __slots__ = ("a", "b") def __init__(self) -> None: From a69dd230c908b8fec235064eefb5824c1eaa2d78 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 14 Sep 2021 18:11:47 +0300 Subject: [PATCH 11/15] Adds an extra test for class-level attributes --- mypy/checker.py | 7 +++---- test-data/unit/check-slots.test | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 17384d15dc63..017714f06143 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2086,15 +2086,14 @@ def accept_items(e: Expression) -> None: def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True, new_syntax: bool = False) -> None: """Type check a single assignment: lvalue = rvalue.""" - if isinstance(lvalue, MemberExpr) and lvalue.node: - name = lvalue.node.name + if isinstance(lvalue, MemberExpr): inst = get_proper_type(self.expr_checker.accept(lvalue.expr)) if (isinstance(inst, Instance) and inst.type.slots is not None and - name not in inst.type.slots): + lvalue.name not in inst.type.slots): self.fail( 'Trying to assign name "{}" that is not in "__slots__" of type "{}"'.format( - name, inst.type.fullname, + lvalue.name, inst.type.fullname, ), lvalue, ) diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index f874ad30ffb9..e55dfa710375 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -68,6 +68,22 @@ class A: [builtins fixtures/tuple.pyi] +[case testSlotsDefinitionWithClassVar] +class A: + __slots__ = ('a',) + b = 4 + + def __init__(self) -> None: + self.a = 1 + + # You cannot override class-level variables, but you can use them: + b = self.b + self.b = 2 # E: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" + + self.c = 3 # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.A" +[builtins fixtures/tuple.pyi] + + [case testSlotsDefinitionMultipleVars1] class A: __slots__ = __fields__ = ("a", "b") @@ -77,7 +93,6 @@ class A: [builtins fixtures/tuple.pyi] - [case testSlotsDefinitionMultipleVars2] class A: __fields__ = __slots__ = ("a", "b") From 7b4b1e1aab23fe765110c69e6e2bad9cf0005d9a Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 14 Sep 2021 18:35:58 +0300 Subject: [PATCH 12/15] Adds extra tests requested by @ethanhs --- test-data/unit/check-slots.test | 104 +++++++++++++++++++++++++++--- test-data/unit/fixtures/set.pyi | 1 + test-data/unit/fixtures/tuple.pyi | 6 +- 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index e55dfa710375..baac3444c831 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -106,13 +106,20 @@ class A: class A: __slots__ = () def __init__(self) -> None: - self.a = 1 # E: Trying to assign name "a" that is not in "__slots__" of type "__main__.A" - self.b = 2 # E: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" + self.a = 1 + self.b = 2 a = A() a.a = 1 a.b = 2 -a.missing = 2 # E: "A" has no attribute "missing" +a.missing = 2 +[out] +main:4: error: Trying to assign name "a" that is not in "__slots__" of type "__main__.A" +main:5: error: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" +main:8: error: Trying to assign name "a" that is not in "__slots__" of type "__main__.A" +main:9: error: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" +main:10: error: Trying to assign name "missing" that is not in "__slots__" of type "__main__.A" +main:10: error: "A" has no attribute "missing" [builtins fixtures/tuple.pyi] @@ -125,13 +132,18 @@ class B(A): def __init__(self) -> None: self.a = 1 self.b = 2 - self._one = 1 # E: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" + self._one = 1 b = B() b.a = 1 b.b = 2 -b.c = 3 # E: "B" has no attribute "c" -b._two = 2 # E: "B" has no attribute "_two" +b.c = 3 +b._two = 2 +[out] +main:9: error: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" +main:14: error: "B" has no attribute "c" +main:15: error: Trying to assign name "_two" that is not in "__slots__" of type "__main__.B" +main:15: error: "B" has no attribute "_two" [builtins fixtures/tuple.pyi] @@ -148,7 +160,7 @@ class B(A): [builtins fixtures/tuple.pyi] -[case testSlotsAssignmentWithMixing] +[case testSlotsAssignmentWithMixin] class A: __slots__ = ("a",) class Mixin: @@ -159,13 +171,18 @@ class B(A, Mixin): def __init__(self) -> None: self.a = 1 self.m = 2 - self._one = 1 # E: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" + self._one = 1 b = B() b.a = 1 b.m = 2 -b.b = 2 # E: "B" has no attribute "b" -b._two = 2 # E: "B" has no attribute "_two" +b.b = 2 +b._two = 2 +[out] +main:11: error: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" +main:16: error: "B" has no attribute "b" +main:17: error: Trying to assign name "_two" that is not in "__slots__" of type "__main__.B" +main:17: error: "B" has no attribute "_two" [builtins fixtures/tuple.pyi] @@ -291,3 +308,70 @@ class A: main:4: error: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" main:4: error: Trying to assign name "c" that is not in "__slots__" of type "__main__.A" [builtins fixtures/tuple.pyi] + + +[case testSlotsWithTupleCall] +class A: + # TODO: for now this way of writing tuples are not recognised + __slots__ = tuple(("a", "b")) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.missing = 3 +[builtins fixtures/tuple.pyi] + + +[case testSlotsWithListCall] +class A: + # TODO: for now this way of writing lists are not recognised + __slots__ = list(("a", "b")) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.missing = 3 +[builtins fixtures/tuple.pyi] +[builtins fixtures/list.pyi] + + +[case testSlotsWithSetCall] +class A: + # TODO: for now this way of writing sets are not recognised + __slots__ = set(("a", "b")) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.missing = 3 +[builtins fixtures/tuple.pyi] +[builtins fixtures/set.pyi] + + +[case testSlotsWithDictCall] +class A: + # TODO: for now this way of writing dicts are not recognised + __slots__ = dict((("a", "docs"), ("b", "docs"))) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.missing = 3 +[builtins fixtures/tuple.pyi] +[builtins fixtures/dict.pyi] + + +[case testSlotsWithAny] +from typing import Any + +some_obj: Any + +class A: + # You can do anything with `Any`: + __slots__ = some_obj + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + self.missing = 3 +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/fixtures/set.pyi b/test-data/unit/fixtures/set.pyi index c2e1f6f75237..9852bbc9fcc6 100644 --- a/test-data/unit/fixtures/set.pyi +++ b/test-data/unit/fixtures/set.pyi @@ -17,6 +17,7 @@ class bool: pass class ellipsis: pass class set(Iterable[T], Generic[T]): + def __init__(self, iterable: Iterable[T] = ...) -> None: ... def __iter__(self) -> Iterator[T]: pass def __contains__(self, item: object) -> bool: pass def __ior__(self, x: Set[T]) -> None: pass diff --git a/test-data/unit/fixtures/tuple.pyi b/test-data/unit/fixtures/tuple.pyi index a101595c6f30..323373f1ff43 100644 --- a/test-data/unit/fixtures/tuple.pyi +++ b/test-data/unit/fixtures/tuple.pyi @@ -1,7 +1,8 @@ # Builtins stub used in tuple-related test cases. -from typing import Iterable, Iterator, TypeVar, Generic, Sequence, Any, overload, Tuple +from typing import Iterable, Iterator, TypeVar, Generic, Sequence, Any, overload, Tuple, Type +T = TypeVar("T") Tco = TypeVar('Tco', covariant=True) class object: @@ -11,6 +12,7 @@ class type: def __init__(self, *a: object) -> None: pass def __call__(self, *a: object) -> object: pass class tuple(Sequence[Tco], Generic[Tco]): + def __new__(cls: Type[T], iterable: Iterable[Tco] = ...) -> T: ... def __iter__(self) -> Iterator[Tco]: pass def __contains__(self, item: object) -> bool: pass def __getitem__(self, x: int) -> Tco: pass @@ -30,8 +32,6 @@ class str: pass # For convenience class bytes: pass class unicode: pass -T = TypeVar('T') - class list(Sequence[T], Generic[T]): @overload def __getitem__(self, i: int) -> T: ... From af1335193f505ddf538bdaa99100d84970279bcc Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 15 Sep 2021 01:59:26 +0300 Subject: [PATCH 13/15] Adds more tests --- mypy/checker.py | 38 ++++++++----- test-data/unit/check-slots.test | 96 +++++++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 017714f06143..8b80f0ca8eae 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2086,18 +2086,7 @@ def accept_items(e: Expression) -> None: def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True, new_syntax: bool = False) -> None: """Type check a single assignment: lvalue = rvalue.""" - if isinstance(lvalue, MemberExpr): - inst = get_proper_type(self.expr_checker.accept(lvalue.expr)) - if (isinstance(inst, Instance) and - inst.type.slots is not None and - lvalue.name not in inst.type.slots): - self.fail( - 'Trying to assign name "{}" that is not in "__slots__" of type "{}"'.format( - lvalue.name, inst.type.fullname, - ), - lvalue, - ) - + self.check_assignment_to_slots(lvalue) if isinstance(lvalue, TupleExpr) or isinstance(lvalue, ListExpr): self.check_assignment_to_multiple_lvalues(lvalue.items, rvalue, rvalue, infer_lvalue_type) @@ -2530,6 +2519,31 @@ def check_final(self, if lv.node.is_final and not is_final_decl: self.msg.cant_assign_to_final(name, lv.node.info is None, s) + def check_assignment_to_slots(self, lvalue: Lvalue) -> None: + if isinstance(lvalue, MemberExpr): + inst = get_proper_type(self.expr_checker.accept(lvalue.expr)) + if not isinstance(inst, Instance): + return + if inst.type.slots is None: + return # Slots do not exist, we can allow any assignment + if lvalue.name in inst.type.slots: + return # We are assigning to an existing slot + for base_info in inst.type.mro[:-1]: + if base_info.names.get('__setattr__') is not None: + # When type has `__setattr__` defined, + # we can assign any dynamic value. + # We exclude object, because it always has `__setattr__`. + return + + definition = inst.type.get(lvalue.name) + if lvalue.node or (definition and isinstance(definition.node, Var)): + self.fail( + 'Trying to assign name "{}" that is not in "__slots__" of type "{}"'.format( + lvalue.name, inst.type.fullname, + ), + lvalue, + ) + def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression, context: Context, infer_lvalue_type: bool = True) -> None: diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index baac3444c831..13c8530794a7 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -81,6 +81,8 @@ class A: self.b = 2 # E: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" self.c = 3 # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.A" + +A.b = 1 [builtins fixtures/tuple.pyi] @@ -118,7 +120,6 @@ main:4: error: Trying to assign name "a" that is not in "__slots__" of type "__m main:5: error: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" main:8: error: Trying to assign name "a" that is not in "__slots__" of type "__main__.A" main:9: error: Trying to assign name "b" that is not in "__slots__" of type "__main__.A" -main:10: error: Trying to assign name "missing" that is not in "__slots__" of type "__main__.A" main:10: error: "A" has no attribute "missing" [builtins fixtures/tuple.pyi] @@ -142,7 +143,6 @@ b._two = 2 [out] main:9: error: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" main:14: error: "B" has no attribute "c" -main:15: error: Trying to assign name "_two" that is not in "__slots__" of type "__main__.B" main:15: error: "B" has no attribute "_two" [builtins fixtures/tuple.pyi] @@ -181,7 +181,6 @@ b._two = 2 [out] main:11: error: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" main:16: error: "B" has no attribute "b" -main:17: error: Trying to assign name "_two" that is not in "__slots__" of type "__main__.B" main:17: error: "B" has no attribute "_two" [builtins fixtures/tuple.pyi] @@ -240,6 +239,97 @@ b.extra = 4 # E: "B" has no attribute "extra" [builtins fixtures/tuple.pyi] +[case testSlotsAssignmentWithExplicitSetattr] +class A: + __slots__ = ("a",) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + + def __setattr__(self, k, v) -> None: + ... + +a = A() +a.a = 1 +a.b = 2 +a.c = 3 +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithParentSetattr] +class Parent: + __slots__ = () + + def __setattr__(self, k, v) -> None: + ... + +class A(Parent): + __slots__ = ("a",) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + +a = A() +a.a = 1 +a.b = 2 +a.c = 3 +[builtins fixtures/tuple.pyi] + + +[case testSlotsAssignmentWithProps] +class A: + __slots__ = ("a",) + + @property + def first(self) -> int: + ... + + @first.setter + def first(self, arg: int) -> None: + ... + +class B(A): + __slots__ = ("b",) + + def __init__(self) -> None: + self.a = 1 + self.b = 2 + + @property + def second(self) -> int: + ... + + @second.setter + def second(self, arg: int) -> None: + ... + +b = B() +b.a = 1 +b.b = 2 +b.first = 1 +b.second = 2 +b.extra = 3 # E: "B" has no attribute "extra" +[builtins fixtures/tuple.pyi] +[builtins fixtures/property.pyi] + + +[case testSlotsAssignmentWithMethodReassign] +class A: + __slots__ = () + + def __init__(self) -> None: + self.method = lambda: None # E: Cannot assign to a method + + def method(self) -> None: + ... + +a = A() +a.method = lambda: None # E: Cannot assign to a method +[builtins fixtures/tuple.pyi] + + [case testSlotsAssignmentWithExplicitDict] class A: __slots__ = ("a",) From 7051ddea232c8d0adb7d2fd15429a57e5b5d905b Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 15 Sep 2021 14:39:20 +0300 Subject: [PATCH 14/15] More tests --- mypy/checker.py | 71 ++++++++++----- mypy/test/testcheck.py | 150 ++++++++++++++++---------------- test-data/unit/check-slots.test | 56 +++++++++++- 3 files changed, 178 insertions(+), 99 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8b80f0ca8eae..cab0c77c0a1e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2086,7 +2086,6 @@ def accept_items(e: Expression) -> None: def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True, new_syntax: bool = False) -> None: """Type check a single assignment: lvalue = rvalue.""" - self.check_assignment_to_slots(lvalue) if isinstance(lvalue, TupleExpr) or isinstance(lvalue, ListExpr): self.check_assignment_to_multiple_lvalues(lvalue.items, rvalue, rvalue, infer_lvalue_type) @@ -2190,6 +2189,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type if not inferred.is_final: rvalue_type = remove_instance_last_known_values(rvalue_type) self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue) + self.check_assignment_to_slots(lvalue) # (type, operator) tuples for augmented assignments supported with partial types partial_type_augmented_ops: Final = { @@ -2520,29 +2520,56 @@ def check_final(self, self.msg.cant_assign_to_final(name, lv.node.info is None, s) def check_assignment_to_slots(self, lvalue: Lvalue) -> None: - if isinstance(lvalue, MemberExpr): - inst = get_proper_type(self.expr_checker.accept(lvalue.expr)) - if not isinstance(inst, Instance): + if not isinstance(lvalue, MemberExpr): + return + + inst = get_proper_type(self.expr_checker.accept(lvalue.expr)) + if not isinstance(inst, Instance): + return + if inst.type.slots is None: + return # Slots do not exist, we can allow any assignment + if lvalue.name in inst.type.slots: + return # We are assigning to an existing slot + for base_info in inst.type.mro[:-1]: + if base_info.names.get('__setattr__') is not None: + # When type has `__setattr__` defined, + # we can assign any dynamic value. + # We exclude object, because it always has `__setattr__`. return - if inst.type.slots is None: - return # Slots do not exist, we can allow any assignment - if lvalue.name in inst.type.slots: - return # We are assigning to an existing slot - for base_info in inst.type.mro[:-1]: - if base_info.names.get('__setattr__') is not None: - # When type has `__setattr__` defined, - # we can assign any dynamic value. - # We exclude object, because it always has `__setattr__`. - return - definition = inst.type.get(lvalue.name) - if lvalue.node or (definition and isinstance(definition.node, Var)): - self.fail( - 'Trying to assign name "{}" that is not in "__slots__" of type "{}"'.format( - lvalue.name, inst.type.fullname, - ), - lvalue, - ) + definition = inst.type.get(lvalue.name) + if definition is None: + # We don't want to duplicate + # `"SomeType" has no attribute "some_attr"` + # error twice. + return + if self.is_assignable_slot(lvalue, definition.type): + return + + self.fail( + 'Trying to assign name "{}" that is not in "__slots__" of type "{}"'.format( + lvalue.name, inst.type.fullname, + ), + lvalue, + ) + + def is_assignable_slot(self, lvalue: Lvalue, typ: Optional[Type]) -> bool: + if getattr(lvalue, 'node', None): + return False # This is a definition + + if typ is None or isinstance(typ, AnyType): + return True # Any can be literally anything, like `@propery` + if isinstance(typ, Instance): + # When working with instances, we need to know if they contain + # `__set__` special method. Like `@property` does. + # This makes assigning to properties possible, + # even without extra slot spec. + return typ.type.get('__set__') is not None + if isinstance(typ, FunctionLike): + return True # Can be a property, or some other magic + if isinstance(typ, UnionType): + return all(self.is_assignable_slot(lvalue, u) for u in typ.items) + return False def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression, context: Context, diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index aaf1bd73fc2a..df55268085d7 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -25,85 +25,85 @@ # List of files that contain test case descriptions. typecheck_files = [ - 'check-basic.test', - 'check-union-or-syntax.test', - 'check-callable.test', - 'check-classes.test', - 'check-statements.test', - 'check-generics.test', - 'check-dynamic-typing.test', - 'check-inference.test', - 'check-inference-context.test', - 'check-kwargs.test', - 'check-overloading.test', - 'check-type-checks.test', - 'check-abstract.test', - 'check-multiple-inheritance.test', - 'check-super.test', - 'check-modules.test', - 'check-typevar-values.test', - 'check-unsupported.test', - 'check-unreachable-code.test', - 'check-unions.test', - 'check-isinstance.test', - 'check-lists.test', - 'check-namedtuple.test', - 'check-narrowing.test', - 'check-typeddict.test', - 'check-type-aliases.test', - 'check-ignore.test', - 'check-type-promotion.test', - 'check-semanal-error.test', - 'check-flags.test', - 'check-incremental.test', - 'check-serialize.test', - 'check-bound.test', - 'check-optional.test', - 'check-fastparse.test', - 'check-warnings.test', - 'check-async-await.test', - 'check-newtype.test', - 'check-class-namedtuple.test', - 'check-selftype.test', - 'check-python2.test', - 'check-columns.test', - 'check-functions.test', - 'check-tuples.test', - 'check-expressions.test', - 'check-generic-subtyping.test', - 'check-varargs.test', - 'check-newsyntax.test', - 'check-protocols.test', - 'check-underscores.test', - 'check-classvar.test', - 'check-enum.test', - 'check-incomplete-fixture.test', - 'check-custom-plugin.test', - 'check-default-plugin.test', - 'check-attr.test', - 'check-ctypes.test', - 'check-dataclasses.test', - 'check-final.test', - 'check-redefine.test', - 'check-literal.test', - 'check-newsemanal.test', - 'check-inline-config.test', - 'check-reports.test', - 'check-errorcodes.test', - 'check-annotated.test', - 'check-parameter-specification.test', - 'check-generic-alias.test', - 'check-typeguard.test', - 'check-functools.test', - 'check-singledispatch.test', + # 'check-basic.test', + # 'check-union-or-syntax.test', + # 'check-callable.test', + # 'check-classes.test', + # 'check-statements.test', + # 'check-generics.test', + # 'check-dynamic-typing.test', + # 'check-inference.test', + # 'check-inference-context.test', + # 'check-kwargs.test', + # 'check-overloading.test', + # 'check-type-checks.test', + # 'check-abstract.test', + # 'check-multiple-inheritance.test', + # 'check-super.test', + # 'check-modules.test', + # 'check-typevar-values.test', + # 'check-unsupported.test', + # 'check-unreachable-code.test', + # 'check-unions.test', + # 'check-isinstance.test', + # 'check-lists.test', + # 'check-namedtuple.test', + # 'check-narrowing.test', + # 'check-typeddict.test', + # 'check-type-aliases.test', + # 'check-ignore.test', + # 'check-type-promotion.test', + # 'check-semanal-error.test', + # 'check-flags.test', + # 'check-incremental.test', + # 'check-serialize.test', + # 'check-bound.test', + # 'check-optional.test', + # 'check-fastparse.test', + # 'check-warnings.test', + # 'check-async-await.test', + # 'check-newtype.test', + # 'check-class-namedtuple.test', + # 'check-selftype.test', + # 'check-python2.test', + # 'check-columns.test', + # 'check-functions.test', + # 'check-tuples.test', + # 'check-expressions.test', + # 'check-generic-subtyping.test', + # 'check-varargs.test', + # 'check-newsyntax.test', + # 'check-protocols.test', + # 'check-underscores.test', + # 'check-classvar.test', + # 'check-enum.test', + # 'check-incomplete-fixture.test', + # 'check-custom-plugin.test', + # 'check-default-plugin.test', + # 'check-attr.test', + # 'check-ctypes.test', + # 'check-dataclasses.test', + # 'check-final.test', + # 'check-redefine.test', + # 'check-literal.test', + # 'check-newsemanal.test', + # 'check-inline-config.test', + # 'check-reports.test', + # 'check-errorcodes.test', + # 'check-annotated.test', + # 'check-parameter-specification.test', + # 'check-generic-alias.test', + # 'check-typeguard.test', + # 'check-functools.test', + # 'check-singledispatch.test', 'check-slots.test', ] # Tests that use Python 3.8-only AST features (like expression-scoped ignores): -if sys.version_info >= (3, 8): - typecheck_files.append('check-python38.test') -if sys.version_info >= (3, 9): - typecheck_files.append('check-python39.test') +# if sys.version_info >= (3, 8): +# typecheck_files.append('check-python38.test') +# if sys.version_info >= (3, 9): +# typecheck_files.append('check-python39.test') # Special tests for platforms with case-insensitive filesystems. if sys.platform in ('darwin', 'win32'): diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 13c8530794a7..96e4eba3c966 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -139,11 +139,13 @@ b = B() b.a = 1 b.b = 2 b.c = 3 +b._one = 1 b._two = 2 [out] main:9: error: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" main:14: error: "B" has no attribute "c" -main:15: error: "B" has no attribute "_two" +main:15: error: Trying to assign name "_one" that is not in "__slots__" of type "__main__.B" +main:16: error: "B" has no attribute "_two" [builtins fixtures/tuple.pyi] @@ -279,6 +281,10 @@ a.c = 3 [case testSlotsAssignmentWithProps] +from typing import Any + +custom_prop: Any + class A: __slots__ = ("a",) @@ -296,6 +302,7 @@ class B(A): def __init__(self) -> None: self.a = 1 self.b = 2 + self.c = 3 @property def second(self) -> int: @@ -305,16 +312,61 @@ class B(A): def second(self, arg: int) -> None: ... + def get_third(self) -> int: + ... + + def set_third(self, arg: int) -> None: + ... + + third = custom_prop(get_third, set_third) + b = B() b.a = 1 b.b = 2 +b.c = 3 b.first = 1 b.second = 2 -b.extra = 3 # E: "B" has no attribute "extra" +b.third = 3 +b.extra = 'extra' +[out] +main:22: error: Trying to assign name "c" that is not in "__slots__" of type "__main__.B" +main:43: error: Trying to assign name "c" that is not in "__slots__" of type "__main__.B" +main:47: error: "B" has no attribute "extra" [builtins fixtures/tuple.pyi] [builtins fixtures/property.pyi] +[case testSlotsAssignmentWithUnionProps] +from typing import Any, Callable, Union + +custom_obj: Any + +class custom_property(object): + def __set__(self, *args, **kwargs): + ... + +class A: + __slots__ = ("a",) + + def __init__(self) -> None: + self.a = 1 + + b: custom_property + c: Union[Any, custom_property] + d: Union[Callable, custom_property] + e: Callable + +a = A() +a.a = 1 +a.b = custom_obj +a.c = custom_obj +a.d = custom_obj +a.e = custom_obj +[out] +[builtins fixtures/tuple.pyi] +[builtins fixtures/dict.pyi] + + [case testSlotsAssignmentWithMethodReassign] class A: __slots__ = () From e26afe8cd95a9e73b83e0aab1a8c8ce647e8256f Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 15 Sep 2021 14:52:04 +0300 Subject: [PATCH 15/15] Fixes self-check --- mypy/checker.py | 1 + mypy/test/testcheck.py | 150 ++++++++++++++++++++--------------------- 2 files changed, 76 insertions(+), 75 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index cab0c77c0a1e..f05a31e47ec6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2557,6 +2557,7 @@ def is_assignable_slot(self, lvalue: Lvalue, typ: Optional[Type]) -> bool: if getattr(lvalue, 'node', None): return False # This is a definition + typ = get_proper_type(typ) if typ is None or isinstance(typ, AnyType): return True # Any can be literally anything, like `@propery` if isinstance(typ, Instance): diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index df55268085d7..aaf1bd73fc2a 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -25,85 +25,85 @@ # List of files that contain test case descriptions. typecheck_files = [ - # 'check-basic.test', - # 'check-union-or-syntax.test', - # 'check-callable.test', - # 'check-classes.test', - # 'check-statements.test', - # 'check-generics.test', - # 'check-dynamic-typing.test', - # 'check-inference.test', - # 'check-inference-context.test', - # 'check-kwargs.test', - # 'check-overloading.test', - # 'check-type-checks.test', - # 'check-abstract.test', - # 'check-multiple-inheritance.test', - # 'check-super.test', - # 'check-modules.test', - # 'check-typevar-values.test', - # 'check-unsupported.test', - # 'check-unreachable-code.test', - # 'check-unions.test', - # 'check-isinstance.test', - # 'check-lists.test', - # 'check-namedtuple.test', - # 'check-narrowing.test', - # 'check-typeddict.test', - # 'check-type-aliases.test', - # 'check-ignore.test', - # 'check-type-promotion.test', - # 'check-semanal-error.test', - # 'check-flags.test', - # 'check-incremental.test', - # 'check-serialize.test', - # 'check-bound.test', - # 'check-optional.test', - # 'check-fastparse.test', - # 'check-warnings.test', - # 'check-async-await.test', - # 'check-newtype.test', - # 'check-class-namedtuple.test', - # 'check-selftype.test', - # 'check-python2.test', - # 'check-columns.test', - # 'check-functions.test', - # 'check-tuples.test', - # 'check-expressions.test', - # 'check-generic-subtyping.test', - # 'check-varargs.test', - # 'check-newsyntax.test', - # 'check-protocols.test', - # 'check-underscores.test', - # 'check-classvar.test', - # 'check-enum.test', - # 'check-incomplete-fixture.test', - # 'check-custom-plugin.test', - # 'check-default-plugin.test', - # 'check-attr.test', - # 'check-ctypes.test', - # 'check-dataclasses.test', - # 'check-final.test', - # 'check-redefine.test', - # 'check-literal.test', - # 'check-newsemanal.test', - # 'check-inline-config.test', - # 'check-reports.test', - # 'check-errorcodes.test', - # 'check-annotated.test', - # 'check-parameter-specification.test', - # 'check-generic-alias.test', - # 'check-typeguard.test', - # 'check-functools.test', - # 'check-singledispatch.test', + 'check-basic.test', + 'check-union-or-syntax.test', + 'check-callable.test', + 'check-classes.test', + 'check-statements.test', + 'check-generics.test', + 'check-dynamic-typing.test', + 'check-inference.test', + 'check-inference-context.test', + 'check-kwargs.test', + 'check-overloading.test', + 'check-type-checks.test', + 'check-abstract.test', + 'check-multiple-inheritance.test', + 'check-super.test', + 'check-modules.test', + 'check-typevar-values.test', + 'check-unsupported.test', + 'check-unreachable-code.test', + 'check-unions.test', + 'check-isinstance.test', + 'check-lists.test', + 'check-namedtuple.test', + 'check-narrowing.test', + 'check-typeddict.test', + 'check-type-aliases.test', + 'check-ignore.test', + 'check-type-promotion.test', + 'check-semanal-error.test', + 'check-flags.test', + 'check-incremental.test', + 'check-serialize.test', + 'check-bound.test', + 'check-optional.test', + 'check-fastparse.test', + 'check-warnings.test', + 'check-async-await.test', + 'check-newtype.test', + 'check-class-namedtuple.test', + 'check-selftype.test', + 'check-python2.test', + 'check-columns.test', + 'check-functions.test', + 'check-tuples.test', + 'check-expressions.test', + 'check-generic-subtyping.test', + 'check-varargs.test', + 'check-newsyntax.test', + 'check-protocols.test', + 'check-underscores.test', + 'check-classvar.test', + 'check-enum.test', + 'check-incomplete-fixture.test', + 'check-custom-plugin.test', + 'check-default-plugin.test', + 'check-attr.test', + 'check-ctypes.test', + 'check-dataclasses.test', + 'check-final.test', + 'check-redefine.test', + 'check-literal.test', + 'check-newsemanal.test', + 'check-inline-config.test', + 'check-reports.test', + 'check-errorcodes.test', + 'check-annotated.test', + 'check-parameter-specification.test', + 'check-generic-alias.test', + 'check-typeguard.test', + 'check-functools.test', + 'check-singledispatch.test', 'check-slots.test', ] # Tests that use Python 3.8-only AST features (like expression-scoped ignores): -# if sys.version_info >= (3, 8): -# typecheck_files.append('check-python38.test') -# if sys.version_info >= (3, 9): -# typecheck_files.append('check-python39.test') +if sys.version_info >= (3, 8): + typecheck_files.append('check-python38.test') +if sys.version_info >= (3, 9): + typecheck_files.append('check-python39.test') # Special tests for platforms with case-insensitive filesystems. if sys.platform in ('darwin', 'win32'):