Skip to content

Commit a6eadee

Browse files
authored
[mypyc] Make instance attribute read-only at runtime if Final (#21666)
Previously this was only enforced statically. I plan to add more optimizations that take advantage of Final in the future.
1 parent d67a138 commit a6eadee

4 files changed

Lines changed: 65 additions & 11 deletions

File tree

mypyc/codegen/emitclass.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,12 +1065,14 @@ def generate_getseter_declarations(cl: ClassIR, emitter: Emitter) -> None:
10651065
getter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
10661066
)
10671067
)
1068-
emitter.emit_line("static int")
1069-
emitter.emit_line(
1070-
"{}({} *self, PyObject *value, void *closure);".format(
1071-
setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
1068+
# Final attributes are read-only, so they have no setter.
1069+
if attr not in cl.final_attributes:
1070+
emitter.emit_line("static int")
1071+
emitter.emit_line(
1072+
"{}({} *self, PyObject *value, void *closure);".format(
1073+
setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
1074+
)
10721075
)
1073-
)
10741076

10751077
for prop, (getter, setter) in cl.properties.items():
10761078
if getter.decl.implicit:
@@ -1099,11 +1101,15 @@ def generate_getseters_table(cl: ClassIR, name: str, emitter: Emitter) -> None:
10991101
if not cl.is_trait:
11001102
for attr in cl.attributes:
11011103
emitter.emit_line(f'{{"{attr}",')
1102-
emitter.emit_line(
1103-
" (getter){}, (setter){},".format(
1104-
getter_name(cl, attr, emitter.names), setter_name(cl, attr, emitter.names)
1104+
if attr in cl.final_attributes:
1105+
# Final attributes are read-only, so emit a NULL setter.
1106+
emitter.emit_line(f" (getter){getter_name(cl, attr, emitter.names)}, NULL,")
1107+
else:
1108+
emitter.emit_line(
1109+
" (getter){}, (setter){},".format(
1110+
getter_name(cl, attr, emitter.names), setter_name(cl, attr, emitter.names)
1111+
)
11051112
)
1106-
)
11071113
emitter.emit_line(" NULL, NULL},")
11081114
for prop, (getter, setter) in cl.properties.items():
11091115
if getter.decl.implicit:
@@ -1129,8 +1135,10 @@ def generate_getseters(cl: ClassIR, emitter: Emitter) -> None:
11291135
if not cl.is_trait:
11301136
for i, (attr, rtype) in enumerate(cl.attributes.items()):
11311137
generate_getter(cl, attr, rtype, emitter)
1132-
emitter.emit_line("")
1133-
generate_setter(cl, attr, rtype, emitter)
1138+
# Final attributes are read-only, so they have no setter.
1139+
if attr not in cl.final_attributes:
1140+
emitter.emit_line("")
1141+
generate_setter(cl, attr, rtype, emitter)
11341142
if i < len(cl.attributes) - 1:
11351143
emitter.emit_line("")
11361144
for prop, (getter, setter) in cl.properties.items():

mypyc/ir/class_ir.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ def __init__(
145145
)
146146
# Attributes defined in the class (not inherited)
147147
self.attributes: dict[str, RType] = {}
148+
# Final attributes defined in the class (not inherited)
149+
self.final_attributes: set[str] = set()
148150
# Deletable attributes
149151
self.deletable: list[str] = []
150152
# We populate method_types with the signatures of every method before
@@ -396,6 +398,7 @@ def serialize(self) -> JsonDict:
396398
"ctor": self.ctor.serialize(),
397399
# We serialize dicts as lists to ensure order is preserved
398400
"attributes": [(k, t.serialize()) for k, t in self.attributes.items()],
401+
"final_attributes": sorted(self.final_attributes),
399402
# We try to serialize a name reference, but if the decl isn't in methods
400403
# then we can't be sure that will work so we serialize the whole decl.
401404
"method_decls": [
@@ -456,6 +459,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR:
456459
ir.builtin_base = data["builtin_base"]
457460
ir.ctor = FuncDecl.deserialize(data["ctor"], ctx)
458461
ir.attributes = {k: deserialize_type(t, ctx) for k, t in data["attributes"]}
462+
ir.final_attributes = set(data["final_attributes"])
459463
ir.method_decls = {
460464
k: ctx.functions[v].decl if isinstance(v, str) else FuncDecl.deserialize(v, ctx)
461465
for k, v in data["method_decls"]

mypyc/irbuild/prepare.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,8 @@ def prepare_methods_and_attributes(
645645
add_getter_declaration(ir, name, attr_rtype, module_name)
646646
add_setter_declaration(ir, name, attr_rtype, module_name)
647647
ir.attributes[name] = attr_rtype
648+
if node.node.is_final:
649+
ir.final_attributes.add(name)
648650
elif isinstance(node.node, (FuncDef, Decorator)):
649651
prepare_method_def(ir, module_name, cdef, mapper, node.node, options)
650652
elif isinstance(node.node, OverloadedFuncDef):

mypyc/test-data/run-classes.test

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2825,6 +2825,46 @@ def test_final_attribute() -> None:
28252825
assert C.b['x'] == 'y'
28262826
assert C.a is C.b
28272827

2828+
[case testFinalInstanceAttributeCannotBeRebound]
2829+
from typing import Any, Final
2830+
2831+
from testutil import assertRaises
2832+
2833+
class C:
2834+
def __init__(self, x: int) -> None:
2835+
self.x: Final[int] = x
2836+
2837+
class D(C):
2838+
def __init__(self, x: int, y: int) -> None:
2839+
super().__init__(x)
2840+
self.y: Final[int] = y
2841+
2842+
def rebind_via_any(o: Any, value: int) -> None:
2843+
o.x = value
2844+
2845+
def test_rebind_via_any() -> None:
2846+
c = C(1)
2847+
with assertRaises(AttributeError):
2848+
rebind_via_any(c, 2)
2849+
assert c.x == 1
2850+
2851+
def test_rebind_via_setattr() -> None:
2852+
c = C(1)
2853+
with assertRaises(AttributeError):
2854+
setattr(c, "x", 3)
2855+
assert c.x == 1
2856+
2857+
def test_rebind_inherited_via_setattr() -> None:
2858+
d = D(1, 2)
2859+
# Inherited Final attribute can't be modified.
2860+
with assertRaises(AttributeError):
2861+
setattr(d, "x", 3)
2862+
# The subclass's own Final attribute can't be modified either.
2863+
with assertRaises(AttributeError):
2864+
setattr(d, "y", 4)
2865+
assert d.x == 1
2866+
assert d.y == 2
2867+
28282868
[case testClassDerivedFromIntEnum]
28292869
from enum import IntEnum, auto
28302870

0 commit comments

Comments
 (0)