Skip to content

Commit 8ac0dc2

Browse files
authored
Adds support for __slots__ assignment (#10864)
### Description Fixes #10801 We can now detect assignment that are not matching defined `__slots__`. Example: ```python class A: __slots__ = ('a',) class B(A): __slots__ = ('b',) def __init__(self) -> None: self.a = 1 # ok self.b = 2 # ok self.c = 3 # error b: B reveal_type(b.c) ```
1 parent b3ff2a6 commit 8ac0dc2

File tree

7 files changed

+640
-3
lines changed

7 files changed

+640
-3
lines changed

mypy/checker.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2228,6 +2228,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
22282228
if not inferred.is_final:
22292229
rvalue_type = remove_instance_last_known_values(rvalue_type)
22302230
self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue)
2231+
self.check_assignment_to_slots(lvalue)
22312232

22322233
# (type, operator) tuples for augmented assignments supported with partial types
22332234
partial_type_augmented_ops: Final = {
@@ -2557,6 +2558,59 @@ def check_final(self,
25572558
if lv.node.is_final and not is_final_decl:
25582559
self.msg.cant_assign_to_final(name, lv.node.info is None, s)
25592560

2561+
def check_assignment_to_slots(self, lvalue: Lvalue) -> None:
2562+
if not isinstance(lvalue, MemberExpr):
2563+
return
2564+
2565+
inst = get_proper_type(self.expr_checker.accept(lvalue.expr))
2566+
if not isinstance(inst, Instance):
2567+
return
2568+
if inst.type.slots is None:
2569+
return # Slots do not exist, we can allow any assignment
2570+
if lvalue.name in inst.type.slots:
2571+
return # We are assigning to an existing slot
2572+
for base_info in inst.type.mro[:-1]:
2573+
if base_info.names.get('__setattr__') is not None:
2574+
# When type has `__setattr__` defined,
2575+
# we can assign any dynamic value.
2576+
# We exclude object, because it always has `__setattr__`.
2577+
return
2578+
2579+
definition = inst.type.get(lvalue.name)
2580+
if definition is None:
2581+
# We don't want to duplicate
2582+
# `"SomeType" has no attribute "some_attr"`
2583+
# error twice.
2584+
return
2585+
if self.is_assignable_slot(lvalue, definition.type):
2586+
return
2587+
2588+
self.fail(
2589+
'Trying to assign name "{}" that is not in "__slots__" of type "{}"'.format(
2590+
lvalue.name, inst.type.fullname,
2591+
),
2592+
lvalue,
2593+
)
2594+
2595+
def is_assignable_slot(self, lvalue: Lvalue, typ: Optional[Type]) -> bool:
2596+
if getattr(lvalue, 'node', None):
2597+
return False # This is a definition
2598+
2599+
typ = get_proper_type(typ)
2600+
if typ is None or isinstance(typ, AnyType):
2601+
return True # Any can be literally anything, like `@propery`
2602+
if isinstance(typ, Instance):
2603+
# When working with instances, we need to know if they contain
2604+
# `__set__` special method. Like `@property` does.
2605+
# This makes assigning to properties possible,
2606+
# even without extra slot spec.
2607+
return typ.type.get('__set__') is not None
2608+
if isinstance(typ, FunctionLike):
2609+
return True # Can be a property, or some other magic
2610+
if isinstance(typ, UnionType):
2611+
return all(self.is_assignable_slot(lvalue, u) for u in typ.items)
2612+
return False
2613+
25602614
def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression,
25612615
context: Context,
25622616
infer_lvalue_type: bool = True) -> None:

mypy/nodes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2298,6 +2298,10 @@ class is generic then it will be a type constructor of higher kind.
22982298
runtime_protocol = False # Does this protocol support isinstance checks?
22992299
abstract_attributes: List[str]
23002300
deletable_attributes: List[str] # Used by mypyc only
2301+
# Does this type have concrete `__slots__` defined?
2302+
# If class does not have `__slots__` defined then it is `None`,
2303+
# if it has empty `__slots__` then it is an empty set.
2304+
slots: Optional[Set[str]]
23012305

23022306
# The attributes 'assuming' and 'assuming_proper' represent structural subtype matrices.
23032307
#
@@ -2401,6 +2405,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No
24012405
self.is_abstract = False
24022406
self.abstract_attributes = []
24032407
self.deletable_attributes = []
2408+
self.slots = None
24042409
self.assuming = []
24052410
self.assuming_proper = []
24062411
self.inferring = []

mypy/semanal.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,6 +2048,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
20482048
self.process_module_assignment(s.lvalues, s.rvalue, s)
20492049
self.process__all__(s)
20502050
self.process__deletable__(s)
2051+
self.process__slots__(s)
20512052

20522053
def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool:
20532054
"""Special case 'X = X' in global scope.
@@ -3365,6 +3366,62 @@ def process__deletable__(self, s: AssignmentStmt) -> None:
33653366
assert self.type
33663367
self.type.deletable_attributes = attrs
33673368

3369+
def process__slots__(self, s: AssignmentStmt) -> None:
3370+
"""
3371+
Processing ``__slots__`` if defined in type.
3372+
3373+
See: https://docs.python.org/3/reference/datamodel.html#slots
3374+
"""
3375+
# Later we can support `__slots__` defined as `__slots__ = other = ('a', 'b')`
3376+
if (isinstance(self.type, TypeInfo) and
3377+
len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and
3378+
s.lvalues[0].name == '__slots__' and s.lvalues[0].kind == MDEF):
3379+
3380+
# We understand `__slots__` defined as string, tuple, list, set, and dict:
3381+
if not isinstance(s.rvalue, (StrExpr, ListExpr, TupleExpr, SetExpr, DictExpr)):
3382+
# For example, `__slots__` can be defined as a variable,
3383+
# we don't support it for now.
3384+
return
3385+
3386+
if any(p.slots is None for p in self.type.mro[1:-1]):
3387+
# At least one type in mro (excluding `self` and `object`)
3388+
# does not have concrete `__slots__` defined. Ignoring.
3389+
return
3390+
3391+
concrete_slots = True
3392+
rvalue: List[Expression] = []
3393+
if isinstance(s.rvalue, StrExpr):
3394+
rvalue.append(s.rvalue)
3395+
elif isinstance(s.rvalue, (ListExpr, TupleExpr, SetExpr)):
3396+
rvalue.extend(s.rvalue.items)
3397+
else:
3398+
# We have a special treatment of `dict` with possible `{**kwargs}` usage.
3399+
# In this case we consider all `__slots__` to be non-concrete.
3400+
for key, _ in s.rvalue.items:
3401+
if concrete_slots and key is not None:
3402+
rvalue.append(key)
3403+
else:
3404+
concrete_slots = False
3405+
3406+
slots = []
3407+
for item in rvalue:
3408+
# Special case for `'__dict__'` value:
3409+
# when specified it will still allow any attribute assignment.
3410+
if isinstance(item, StrExpr) and item.value != '__dict__':
3411+
slots.append(item.value)
3412+
else:
3413+
concrete_slots = False
3414+
if not concrete_slots:
3415+
# Some slot items are dynamic, we don't want any false positives,
3416+
# so, we just pretend that this type does not have any slots at all.
3417+
return
3418+
3419+
# We need to copy all slots from super types:
3420+
for super_type in self.type.mro[1:-1]:
3421+
assert super_type.slots is not None
3422+
slots.extend(super_type.slots)
3423+
self.type.slots = set(slots)
3424+
33683425
#
33693426
# Misc statements
33703427
#

mypy/test/testcheck.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
'check-typeguard.test',
9696
'check-functools.test',
9797
'check-singledispatch.test',
98+
'check-slots.test',
9899
]
99100

100101
# Tests that use Python 3.8-only AST features (like expression-scoped ignores):

0 commit comments

Comments
 (0)