Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -1065,12 +1065,14 @@ def generate_getseter_declarations(cl: ClassIR, emitter: Emitter) -> None:
getter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
)
)
emitter.emit_line("static int")
emitter.emit_line(
"{}({} *self, PyObject *value, void *closure);".format(
setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
# Final attributes are read-only, so they have no setter.
if attr not in cl.final_attributes:
emitter.emit_line("static int")
emitter.emit_line(
"{}({} *self, PyObject *value, void *closure);".format(
setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
)
)
)

for prop, (getter, setter) in cl.properties.items():
if getter.decl.implicit:
Expand Down Expand Up @@ -1099,11 +1101,15 @@ def generate_getseters_table(cl: ClassIR, name: str, emitter: Emitter) -> None:
if not cl.is_trait:
for attr in cl.attributes:
emitter.emit_line(f'{{"{attr}",')
emitter.emit_line(
" (getter){}, (setter){},".format(
getter_name(cl, attr, emitter.names), setter_name(cl, attr, emitter.names)
if attr in cl.final_attributes:
# Final attributes are read-only, so emit a NULL setter.
emitter.emit_line(f" (getter){getter_name(cl, attr, emitter.names)}, NULL,")
else:
emitter.emit_line(
" (getter){}, (setter){},".format(
getter_name(cl, attr, emitter.names), setter_name(cl, attr, emitter.names)
)
)
)
emitter.emit_line(" NULL, NULL},")
for prop, (getter, setter) in cl.properties.items():
if getter.decl.implicit:
Expand All @@ -1129,8 +1135,10 @@ def generate_getseters(cl: ClassIR, emitter: Emitter) -> None:
if not cl.is_trait:
for i, (attr, rtype) in enumerate(cl.attributes.items()):
generate_getter(cl, attr, rtype, emitter)
emitter.emit_line("")
generate_setter(cl, attr, rtype, emitter)
# Final attributes are read-only, so they have no setter.
if attr not in cl.final_attributes:
emitter.emit_line("")
generate_setter(cl, attr, rtype, emitter)
if i < len(cl.attributes) - 1:
emitter.emit_line("")
for prop, (getter, setter) in cl.properties.items():
Expand Down
4 changes: 4 additions & 0 deletions mypyc/ir/class_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ def __init__(
)
# Attributes defined in the class (not inherited)
self.attributes: dict[str, RType] = {}
# Final attributes defined in the class (not inherited)
self.final_attributes: set[str] = set()
# Deletable attributes
self.deletable: list[str] = []
# We populate method_types with the signatures of every method before
Expand Down Expand Up @@ -396,6 +398,7 @@ def serialize(self) -> JsonDict:
"ctor": self.ctor.serialize(),
# We serialize dicts as lists to ensure order is preserved
"attributes": [(k, t.serialize()) for k, t in self.attributes.items()],
"final_attributes": sorted(self.final_attributes),
# We try to serialize a name reference, but if the decl isn't in methods
# then we can't be sure that will work so we serialize the whole decl.
"method_decls": [
Expand Down Expand Up @@ -456,6 +459,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR:
ir.builtin_base = data["builtin_base"]
ir.ctor = FuncDecl.deserialize(data["ctor"], ctx)
ir.attributes = {k: deserialize_type(t, ctx) for k, t in data["attributes"]}
ir.final_attributes = set(data["final_attributes"])
ir.method_decls = {
k: ctx.functions[v].decl if isinstance(v, str) else FuncDecl.deserialize(v, ctx)
for k, v in data["method_decls"]
Expand Down
2 changes: 2 additions & 0 deletions mypyc/irbuild/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,8 @@ def prepare_methods_and_attributes(
add_getter_declaration(ir, name, attr_rtype, module_name)
add_setter_declaration(ir, name, attr_rtype, module_name)
ir.attributes[name] = attr_rtype
if node.node.is_final:
ir.final_attributes.add(name)
elif isinstance(node.node, (FuncDef, Decorator)):
prepare_method_def(ir, module_name, cdef, mapper, node.node, options)
elif isinstance(node.node, OverloadedFuncDef):
Expand Down
40 changes: 40 additions & 0 deletions mypyc/test-data/run-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2825,6 +2825,46 @@ def test_final_attribute() -> None:
assert C.b['x'] == 'y'
assert C.a is C.b

[case testFinalInstanceAttributeCannotBeRebound]
from typing import Any, Final

from testutil import assertRaises

class C:
def __init__(self, x: int) -> None:
self.x: Final[int] = x

class D(C):
def __init__(self, x: int, y: int) -> None:
super().__init__(x)
self.y: Final[int] = y

def rebind_via_any(o: Any, value: int) -> None:
o.x = value

def test_rebind_via_any() -> None:
c = C(1)
with assertRaises(AttributeError):
rebind_via_any(c, 2)
assert c.x == 1

def test_rebind_via_setattr() -> None:
c = C(1)
with assertRaises(AttributeError):
setattr(c, "x", 3)
assert c.x == 1

def test_rebind_inherited_via_setattr() -> None:
d = D(1, 2)
# Inherited Final attribute can't be modified.
with assertRaises(AttributeError):
setattr(d, "x", 3)
# The subclass's own Final attribute can't be modified either.
with assertRaises(AttributeError):
setattr(d, "y", 4)
assert d.x == 1
assert d.y == 2

[case testClassDerivedFromIntEnum]
from enum import IntEnum, auto

Expand Down
Loading