Skip to content
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

Adds support for __slots__ assignment, refs #10801 #10864

Merged
merged 15 commits into from
Sep 21, 2021
54 changes: 54 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2189,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 = {
Expand Down Expand Up @@ -2518,6 +2519,59 @@ 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 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

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

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):
# 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,
infer_lvalue_type: bool = True) -> None:
Expand Down
5 changes: 5 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2298,6 +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
# 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]]

# The attributes 'assuming' and 'assuming_proper' represent structural subtype matrices.
#
Expand Down Expand Up @@ -2401,6 +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 = None
self.assuming = []
self.assuming_proper = []
self.inferring = []
Expand Down
57 changes: 57 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -3353,6 +3354,62 @@ def process__deletable__(self, s: AssignmentStmt) -> None:
assert self.type
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):

# 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

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
sobolevn marked this conversation as resolved.
Show resolved Hide resolved

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 concrete_slots and key is not None:
rvalue.append(key)
else:
concrete_slots = False

slots = []
for item in rvalue:
# Special case for `'__dict__'` value:
# 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:
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)

#
# Misc statements
#
Expand Down
1 change: 1 addition & 0 deletions mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading