Skip to content

Commit b562cc2

Browse files
authored
[mypyc] Only allow deleting attributes declared using __deletable__ (#10524)
Native classes now must declare attributes that can be deleted. This improves type safety and will make it possible to support always defined attributes in the future. Example: ``` class C: x: int y: int z: int __deletable__ = ['x', 'y'] ``` Now `x` and `y` can be deleted, but `z` can't be: ``` def f(c: C) -> None: del c.x # Ok del c.y # Ok del c.z # Error ``` `__deletable__` has no effect in non-native classes and their behavior is same as before. Closes mypyc/mypyc#853.
1 parent 4642a31 commit b562cc2

17 files changed

+330
-16
lines changed

mypy/checker.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,7 @@ def visit_class_def(self, defn: ClassDef) -> None:
17311731
if not defn.has_incompatible_baseclass:
17321732
# Otherwise we've already found errors; more errors are not useful
17331733
self.check_multiple_inheritance(typ)
1734+
self.check_final_deletable(typ)
17341735

17351736
if defn.decorators:
17361737
sig = type_object_type(defn.info, self.named_type) # type: Type
@@ -1757,6 +1758,14 @@ def visit_class_def(self, defn: ClassDef) -> None:
17571758
if typ.is_protocol and typ.defn.type_vars:
17581759
self.check_protocol_variance(defn)
17591760

1761+
def check_final_deletable(self, typ: TypeInfo) -> None:
1762+
# These checks are only for mypyc. Only perform some checks that are easier
1763+
# to implement here than in mypyc.
1764+
for attr in typ.deletable_attributes:
1765+
node = typ.names.get(attr)
1766+
if node and isinstance(node.node, Var) and node.node.is_final:
1767+
self.fail(message_registry.CANNOT_MAKE_DELETABLE_FINAL, node.node)
1768+
17601769
def check_init_subclass(self, defn: ClassDef) -> None:
17611770
"""Check that keywords in a class definition are valid arguments for __init_subclass__().
17621771
@@ -1927,8 +1936,8 @@ class C(B, A[int]): ... # this is unsafe because...
19271936
self.msg.cant_override_final(name, base2.name, ctx)
19281937
if is_final_node(first.node):
19291938
self.check_if_final_var_override_writable(name, second.node, ctx)
1930-
# __slots__ is special and the type can vary across class hierarchy.
1931-
if name == '__slots__':
1939+
# __slots__ and __deletable__ are special and the type can vary across class hierarchy.
1940+
if name in ('__slots__', '__deletable__'):
19321941
ok = True
19331942
if not ok:
19341943
self.msg.base_class_definitions_incompatible(name, base1, base2,
@@ -2236,6 +2245,9 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[
22362245
# redefine it.
22372246
if lvalue_node.name == "__slots__" and base.fullname != "builtins.object":
22382247
continue
2248+
# We don't care about the type of "__deletable__".
2249+
if lvalue_node.name == "__deletable__":
2250+
continue
22392251

22402252
if is_private(lvalue_node.name):
22412253
continue

mypy/message_registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@
129129
"Final name declared in class body cannot depend on type variables" # type: Final
130130
CANNOT_ACCESS_FINAL_INSTANCE_ATTR = \
131131
'Cannot access final instance attribute "{}" on class object' # type: Final
132+
CANNOT_MAKE_DELETABLE_FINAL = \
133+
"Deletable attribute cannot be final" # type: Final
132134

133135
# ClassVar
134136
CANNOT_OVERRIDE_INSTANCE_VAR = \

mypy/nodes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,8 @@ class FuncItem(FuncBase):
608608
'expanded', # Variants of function with type variables with values expanded
609609
)
610610

611+
__deletable__ = ('arguments', 'max_pos', 'min_args')
612+
611613
def __init__(self,
612614
arguments: List[Argument],
613615
body: 'Block',
@@ -2348,6 +2350,7 @@ class is generic then it will be a type constructor of higher kind.
23482350
is_protocol = False # Is this a protocol class?
23492351
runtime_protocol = False # Does this protocol support isinstance checks?
23502352
abstract_attributes = None # type: List[str]
2353+
deletable_attributes = None # type: List[str] # Used by mypyc only
23512354

23522355
# The attributes 'assuming' and 'assuming_proper' represent structural subtype matrices.
23532356
#
@@ -2450,6 +2453,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No
24502453
self._fullname = defn.fullname
24512454
self.is_abstract = False
24522455
self.abstract_attributes = []
2456+
self.deletable_attributes = []
24532457
self.assuming = []
24542458
self.assuming_proper = []
24552459
self.inferring = []

mypy/semanal.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ class SemanticAnalyzer(NodeVisitor[None],
156156
AST. Note that type checking is performed as a separate pass.
157157
"""
158158

159+
__deletable__ = ['patches', 'options', 'cur_mod_node']
160+
159161
# Module name space
160162
modules = None # type: Dict[str, MypyFile]
161163
# Global name space for current module
@@ -2025,6 +2027,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
20252027
if not s.type:
20262028
self.process_module_assignment(s.lvalues, s.rvalue, s)
20272029
self.process__all__(s)
2030+
self.process__deletable__(s)
20282031

20292032
def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool:
20302033
"""Special case 'X = X' in global scope.
@@ -3314,6 +3317,25 @@ def process__all__(self, s: AssignmentStmt) -> None:
33143317
isinstance(s.rvalue, (ListExpr, TupleExpr))):
33153318
self.add_exports(s.rvalue.items)
33163319

3320+
def process__deletable__(self, s: AssignmentStmt) -> None:
3321+
if not self.options.mypyc:
3322+
return
3323+
if (len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and
3324+
s.lvalues[0].name == '__deletable__' and s.lvalues[0].kind == MDEF):
3325+
rvalue = s.rvalue
3326+
if not isinstance(rvalue, (ListExpr, TupleExpr)):
3327+
self.fail('"__deletable__" must be initialized with a list or tuple expression', s)
3328+
return
3329+
items = rvalue.items
3330+
attrs = []
3331+
for item in items:
3332+
if not isinstance(item, StrExpr):
3333+
self.fail('Invalid "__deletable__" item; string literal expected', item)
3334+
else:
3335+
attrs.append(item.value)
3336+
assert self.type
3337+
self.type.deletable_attributes = attrs
3338+
33173339
#
33183340
# Misc statements
33193341
#

mypyc/codegen/emitclass.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -818,12 +818,24 @@ def generate_setter(cl: ClassIR,
818818
setter_name(cl, attr, emitter.names),
819819
cl.struct_name(emitter.names)))
820820
emitter.emit_line('{')
821+
822+
deletable = cl.is_deletable(attr)
823+
if not deletable:
824+
emitter.emit_line('if (value == NULL) {')
825+
emitter.emit_line('PyErr_SetString(PyExc_AttributeError,')
826+
emitter.emit_line(' "{} object attribute {} cannot be deleted");'.format(repr(cl.name),
827+
repr(attr)))
828+
emitter.emit_line('return -1;')
829+
emitter.emit_line('}')
830+
821831
if rtype.is_refcounted:
822832
attr_expr = 'self->{}'.format(attr_field)
823833
emitter.emit_undefined_attr_check(rtype, attr_expr, '!=')
824834
emitter.emit_dec_ref('self->{}'.format(attr_field), rtype)
825835
emitter.emit_line('}')
826-
emitter.emit_line('if (value != NULL) {')
836+
837+
if deletable:
838+
emitter.emit_line('if (value != NULL) {')
827839
if rtype.is_unboxed:
828840
emitter.emit_unbox('value', 'tmp', rtype, custom_failure='return -1;', declare_dest=True)
829841
elif is_same_type(rtype, object_rprimitive):
@@ -834,8 +846,10 @@ def generate_setter(cl: ClassIR,
834846
' return -1;')
835847
emitter.emit_inc_ref('tmp', rtype)
836848
emitter.emit_line('self->{} = tmp;'.format(attr_field))
837-
emitter.emit_line('} else')
838-
emitter.emit_line(' self->{} = {};'.format(attr_field, emitter.c_undefined_value(rtype)))
849+
if deletable:
850+
emitter.emit_line('} else')
851+
emitter.emit_line(' self->{} = {};'.format(attr_field,
852+
emitter.c_undefined_value(rtype)))
839853
emitter.emit_line('return 0;')
840854
emitter.emit_line('}')
841855

mypyc/doc/native_classes.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,39 @@ as pure native classes.
159159
If a class definition uses an unsupported class decorator, *mypyc
160160
compiles the class into a regular Python class*.
161161

162+
Deleting attributes
163+
-------------------
164+
165+
By default, attributes defined in native classes can't be deleted. You
166+
can explicitly allow certain attributes to be deleted by using
167+
``__deletable__``::
168+
169+
class Cls:
170+
x: int = 0
171+
y: int = 0
172+
other: int = 0
173+
174+
__deletable__ = ['x', 'y'] # 'x' and 'y' can be deleted
175+
176+
o = Cls()
177+
del o.x # OK
178+
del o.y # OK
179+
del o.other # Error
180+
181+
You must initialize the ``__deletable__`` attribute in the class body,
182+
using a list or a tuple expression with only string literal items that
183+
refer to attributes. These are not valid::
184+
185+
a = ['x', 'y']
186+
187+
class Cls:
188+
x: int
189+
y: int
190+
191+
__deletable__ = a # Error: cannot use variable 'a'
192+
193+
__deletable__ = ('a',) # Error: not in a class body
194+
162195
Other properties
163196
----------------
164197

mypyc/errors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ def error(self, msg: str, path: str, line: int) -> None:
1313
self._errors.report(line, None, msg, severity='error', file=path)
1414
self.num_errors += 1
1515

16+
def note(self, msg: str, path: str, line: int) -> None:
17+
self._errors.report(line, None, msg, severity='note', file=path)
18+
1619
def warning(self, msg: str, path: str, line: int) -> None:
1720
self._errors.report(line, None, msg, severity='warning', file=path)
1821
self.num_warnings += 1

mypyc/ir/class_ir.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ def __init__(self, name: str, module_name: str, is_trait: bool = False,
110110
self.ctor = FuncDecl(name, None, module_name, FuncSignature([], RInstance(self)))
111111

112112
self.attributes = OrderedDict() # type: OrderedDict[str, RType]
113+
# Deletable attributes
114+
self.deletable = [] # type: List[str]
113115
# We populate method_types with the signatures of every method before
114116
# we generate methods, and we rely on this information being present.
115117
self.method_decls = OrderedDict() # type: OrderedDict[str, FuncDecl]
@@ -211,6 +213,12 @@ def has_attr(self, name: str) -> bool:
211213
return False
212214
return True
213215

216+
def is_deletable(self, name: str) -> bool:
217+
for ir in self.mro:
218+
if name in ir.deletable:
219+
return True
220+
return False
221+
214222
def name_prefix(self, names: NameGenerator) -> str:
215223
return names.private_name(self.module_name, self.name)
216224

mypyc/irbuild/builder.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,9 @@ def warning(self, msg: str, line: int) -> None:
11191119
def error(self, msg: str, line: int) -> None:
11201120
self.errors.error(msg, self.module_path, line)
11211121

1122+
def note(self, msg: str, line: int) -> None:
1123+
self.errors.note(msg, self.module_path, line)
1124+
11221125

11231126
def gen_arg_defaults(builder: IRBuilder) -> None:
11241127
"""Generate blocks for arguments that have default values.

mypyc/irbuild/classdef.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,12 @@ def generate_attr_defaults(builder: IRBuilder, cdef: ClassDef) -> None:
362362
and isinstance(stmt.lvalues[0], NameExpr)
363363
and not is_class_var(stmt.lvalues[0])
364364
and not isinstance(stmt.rvalue, TempNode)):
365-
if stmt.lvalues[0].name == '__slots__':
365+
name = stmt.lvalues[0].name
366+
if name == '__slots__':
367+
continue
368+
369+
if name == '__deletable__':
370+
check_deletable_declaration(builder, cls, stmt.line)
366371
continue
367372

368373
# Skip type annotated assignments in dataclasses
@@ -398,6 +403,22 @@ def generate_attr_defaults(builder: IRBuilder, cdef: ClassDef) -> None:
398403
builder.leave_method()
399404

400405

406+
def check_deletable_declaration(builder: IRBuilder, cl: ClassIR, line: int) -> None:
407+
for attr in cl.deletable:
408+
if attr not in cl.attributes:
409+
if not cl.has_attr(attr):
410+
builder.error('Attribute "{}" not defined'.format(attr), line)
411+
continue
412+
for base in cl.mro:
413+
if attr in base.property_types:
414+
builder.error('Cannot make property "{}" deletable'.format(attr), line)
415+
break
416+
else:
417+
_, base = cl.attr_details(attr)
418+
builder.error(('Attribute "{}" not defined in "{}" ' +
419+
'(defined in "{}")').format(attr, cl.name, base.name), line)
420+
421+
401422
def create_ne_from_eq(builder: IRBuilder, cdef: ClassDef) -> None:
402423
"""Create a "__ne__" method from a "__eq__" method (if only latter exists)."""
403424
cls = builder.mapper.type_to_ir[cdef.info]

mypyc/irbuild/prepare.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ def build_type_map(mapper: Mapper,
5454
class_ir = ClassIR(cdef.name, module.fullname, is_trait(cdef),
5555
is_abstract=cdef.info.is_abstract)
5656
class_ir.is_ext_class = is_extension_class(cdef)
57+
if class_ir.is_ext_class:
58+
class_ir.deletable = cdef.info.deletable_attributes[:]
5759
# If global optimizations are disabled, turn of tracking of class children
5860
if not options.global_opts:
5961
class_ir.children = None
@@ -179,7 +181,7 @@ def prepare_class_def(path: str, module_name: str, cdef: ClassDef,
179181

180182
if isinstance(node.node, Var):
181183
assert node.node.type, "Class member %s missing type" % name
182-
if not node.node.is_classvar and name != '__slots__':
184+
if not node.node.is_classvar and name not in ('__slots__', '__deletable__'):
183185
ir.attributes[name] = mapper.type_to_rtype(node.node.type)
184186
elif isinstance(node.node, (FuncDef, Decorator)):
185187
prepare_method_def(ir, module_name, cdef, mapper, node.node)

mypyc/irbuild/statement.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
Assign, Unreachable, RaiseStandardError, LoadErrorValue, BasicBlock, TupleGet, Value, Register,
2121
Branch, NO_TRACEBACK_LINE_NO
2222
)
23-
from mypyc.ir.rtypes import exc_rtuple
23+
from mypyc.ir.rtypes import RInstance, exc_rtuple
2424
from mypyc.primitives.generic_ops import py_delattr_op
2525
from mypyc.primitives.misc_ops import type_op
2626
from mypyc.primitives.exc_ops import (
@@ -658,6 +658,13 @@ def transform_del_item(builder: IRBuilder, target: AssignmentTarget, line: int)
658658
line=line
659659
)
660660
elif isinstance(target, AssignmentTargetAttr):
661+
if isinstance(target.obj_type, RInstance):
662+
cl = target.obj_type.class_ir
663+
if not cl.is_deletable(target.attr):
664+
builder.error('"{}" cannot be deleted'.format(target.attr), line)
665+
builder.note(
666+
'Using "__deletable__ = ' +
667+
'[\'<attr>\']" in the class body enables "del obj.<attr>"', line)
661668
key = builder.load_str(target.attr)
662669
builder.call_c(py_delattr_op, [target.obj, key], line)
663670
elif isinstance(target, AssignmentTargetRegister):

mypyc/test-data/irbuild-classes.test

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,3 +1069,68 @@ class A(B): pass
10691069
class B(C): pass
10701070
class C: pass
10711071
[out]
1072+
1073+
[case testDeletableSemanticAnalysis]
1074+
class Err1:
1075+
__deletable__ = 'x' # E: "__deletable__" must be initialized with a list or tuple expression
1076+
class Err2:
1077+
__deletable__ = [
1078+
1 # E: Invalid "__deletable__" item; string literal expected
1079+
]
1080+
class Err3:
1081+
__deletable__ = ['x', ['y'], 'z'] # E: Invalid "__deletable__" item; string literal expected
1082+
class Err4:
1083+
__deletable__ = (1,) # E: Invalid "__deletable__" item; string literal expected
1084+
a = ['x']
1085+
class Err5:
1086+
__deletable__ = a # E: "__deletable__" must be initialized with a list or tuple expression
1087+
1088+
class Ok1:
1089+
__deletable__ = ('x',)
1090+
x: int
1091+
class Ok2:
1092+
__deletable__ = ['x']
1093+
x: int
1094+
1095+
[case testInvalidDeletableAttribute]
1096+
class NotDeletable:
1097+
__deletable__ = ['x']
1098+
x: int
1099+
y: int
1100+
1101+
def g(o: NotDeletable) -> None:
1102+
del o.x
1103+
del o.y # E: "y" cannot be deleted \
1104+
# N: Using "__deletable__ = ['<attr>']" in the class body enables "del obj.<attr>"
1105+
1106+
class Base:
1107+
x: int
1108+
1109+
class Deriv(Base):
1110+
__deletable__ = ['x'] # E: Attribute "x" not defined in "Deriv" (defined in "Base")
1111+
1112+
class UndefinedDeletable:
1113+
__deletable__ = ['x'] # E: Attribute "x" not defined
1114+
1115+
class DeletableProperty:
1116+
__deletable__ = ['prop'] # E: Cannot make property "prop" deletable
1117+
1118+
@property
1119+
def prop(self) -> int:
1120+
return 5
1121+
1122+
[case testFinalDeletable]
1123+
from typing import Final
1124+
1125+
class DeletableFinal1:
1126+
x: Final[int] # E: Deletable attribute cannot be final
1127+
1128+
__deletable__ = ['x']
1129+
1130+
def __init__(self, x: int) -> None:
1131+
self.x = x
1132+
1133+
class DeletableFinal2:
1134+
X: Final = 0 # E: Deletable attribute cannot be final
1135+
1136+
__deletable__ = ['X']

mypyc/test-data/irbuild-statements.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,7 @@ L0:
880880

881881
[case testDelAttribute]
882882
class Dummy():
883+
__deletable__ = ('x', 'y')
883884
def __init__(self, x: int, y: int) -> None:
884885
self.x = x
885886
self.y = y

0 commit comments

Comments
 (0)