Skip to content

Commit 7e9aa74

Browse files
authored
Enable Final instance attributes for attrs (#14232)
A quick patch to enable the following scenario: ```python @define class C: a: Final[int] # `a` is a final instance attribute ``` There are some edge cases I haven't covered here that would be complex to handle and not add much value IMO, so I think this will be useful like this.
1 parent d2ab2e7 commit 7e9aa74

File tree

2 files changed

+44
-3
lines changed

2 files changed

+44
-3
lines changed

mypy/plugins/attrs.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from typing import Iterable, List, cast
6-
from typing_extensions import Final
6+
from typing_extensions import Final, Literal
77

88
import mypy.plugin # To avoid circular imports.
99
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
@@ -756,20 +756,28 @@ def _add_init(
756756
ctx: mypy.plugin.ClassDefContext,
757757
attributes: list[Attribute],
758758
adder: MethodAdder,
759-
method_name: str,
759+
method_name: Literal["__init__", "__attrs_init__"],
760760
) -> None:
761761
"""Generate an __init__ method for the attributes and add it to the class."""
762-
# Convert attributes to arguments with kw_only arguments at the end of
762+
# Convert attributes to arguments with kw_only arguments at the end of
763763
# the argument list
764764
pos_args = []
765765
kw_only_args = []
766+
sym_table = ctx.cls.info.names
766767
for attribute in attributes:
767768
if not attribute.init:
768769
continue
769770
if attribute.kw_only:
770771
kw_only_args.append(attribute.argument(ctx))
771772
else:
772773
pos_args.append(attribute.argument(ctx))
774+
775+
# If the attribute is Final, present in `__init__` and has
776+
# no default, make sure it doesn't error later.
777+
if not attribute.has_default and attribute.name in sym_table:
778+
sym_node = sym_table[attribute.name].node
779+
if isinstance(sym_node, Var) and sym_node.is_final:
780+
sym_node.final_set_in_init = True
773781
args = pos_args + kw_only_args
774782
if all(
775783
# We use getattr rather than instance checks because the variable.type

test-data/unit/check-attr.test

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,3 +1834,36 @@ class Sub(Base):
18341834
# This matches runtime semantics
18351835
reveal_type(Sub) # N: Revealed type is "def (*, name: builtins.str, first_name: builtins.str, last_name: builtins.str) -> __main__.Sub"
18361836
[builtins fixtures/property.pyi]
1837+
1838+
[case testFinalInstanceAttribute]
1839+
from attrs import define
1840+
from typing import Final
1841+
1842+
@define
1843+
class C:
1844+
a: Final[int]
1845+
1846+
reveal_type(C) # N: Revealed type is "def (a: builtins.int) -> __main__.C"
1847+
1848+
C(1).a = 2 # E: Cannot assign to final attribute "a"
1849+
1850+
[builtins fixtures/property.pyi]
1851+
1852+
[case testFinalInstanceAttributeInheritance]
1853+
from attrs import define
1854+
from typing import Final
1855+
1856+
@define
1857+
class C:
1858+
a: Final[int]
1859+
1860+
@define
1861+
class D(C):
1862+
b: Final[str]
1863+
1864+
reveal_type(D) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> __main__.D"
1865+
1866+
D(1, "").a = 2 # E: Cannot assign to final attribute "a"
1867+
D(1, "").b = "2" # E: Cannot assign to final attribute "b"
1868+
1869+
[builtins fixtures/property.pyi]

0 commit comments

Comments
 (0)