From 1f09e3e25cb2a0772cd47e8b512f5f4f0df194b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 2 Dec 2022 00:14:55 +0100 Subject: [PATCH 1/2] Enable Final instance attributes for attrs --- mypy/plugins/attrs.py | 16 +++++++++++++--- test-data/unit/check-attr.test | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index ce0f45967152c..67d8001d8421a 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Iterable, List, cast -from typing_extensions import Final +from typing_extensions import Final, Literal import mypy.plugin # To avoid circular imports. from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type @@ -756,10 +756,10 @@ def _add_init( ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute], adder: MethodAdder, - method_name: str, + method_name: Literal["__init__", "__attrs_init__"], ) -> None: """Generate an __init__ method for the attributes and add it to the class.""" - # Convert attributes to arguments with kw_only arguments at the end of + # Convert attributes to arguments with kw_only arguments at the end of # the argument list pos_args = [] kw_only_args = [] @@ -770,6 +770,16 @@ def _add_init( kw_only_args.append(attribute.argument(ctx)) else: pos_args.append(attribute.argument(ctx)) + + # If the attribute is Final, present in `__init__` and has + # no default, make sure it doesn't error later. + if not attribute.has_default: + for ti in ctx.cls.info.mro: + if attribute.name in ti.names: + sym_node = ti.names[attribute.name].node + if isinstance(sym_node, Var) and sym_node.is_final: + sym_node.final_set_in_init = True + break args = pos_args + kw_only_args if all( # We use getattr rather than instance checks because the variable.type diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 4d27d5f39d1e7..096b6815b55bd 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1834,3 +1834,17 @@ class Sub(Base): # This matches runtime semantics reveal_type(Sub) # N: Revealed type is "def (*, name: builtins.str, first_name: builtins.str, last_name: builtins.str) -> __main__.Sub" [builtins fixtures/property.pyi] + +[case testFinalInstanceAttribute] +from attrs import define +from typing import Final + +@define +class C: + a: Final[int] + +reveal_type(C) # N: Revealed type is "def (a: builtins.int) -> __main__.C" + +C(1).a = 2 # E: Cannot assign to final attribute "a" + +[builtins fixtures/property.pyi] From 9acbca0dfc6ba68ffdd6ffb1745f35a62c45fbb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Tue, 6 Dec 2022 00:56:32 +0100 Subject: [PATCH 2/2] Don't search the MRO --- mypy/plugins/attrs.py | 12 +++++------- test-data/unit/check-attr.test | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 67d8001d8421a..16e8891e5f575 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -763,6 +763,7 @@ def _add_init( # the argument list pos_args = [] kw_only_args = [] + sym_table = ctx.cls.info.names for attribute in attributes: if not attribute.init: continue @@ -773,13 +774,10 @@ def _add_init( # If the attribute is Final, present in `__init__` and has # no default, make sure it doesn't error later. - if not attribute.has_default: - for ti in ctx.cls.info.mro: - if attribute.name in ti.names: - sym_node = ti.names[attribute.name].node - if isinstance(sym_node, Var) and sym_node.is_final: - sym_node.final_set_in_init = True - break + if not attribute.has_default and attribute.name in sym_table: + sym_node = sym_table[attribute.name].node + if isinstance(sym_node, Var) and sym_node.is_final: + sym_node.final_set_in_init = True args = pos_args + kw_only_args if all( # We use getattr rather than instance checks because the variable.type diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 096b6815b55bd..f555f2ea70111 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1848,3 +1848,22 @@ reveal_type(C) # N: Revealed type is "def (a: builtins.int) -> __main__.C" C(1).a = 2 # E: Cannot assign to final attribute "a" [builtins fixtures/property.pyi] + +[case testFinalInstanceAttributeInheritance] +from attrs import define +from typing import Final + +@define +class C: + a: Final[int] + +@define +class D(C): + b: Final[str] + +reveal_type(D) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> __main__.D" + +D(1, "").a = 2 # E: Cannot assign to final attribute "a" +D(1, "").b = "2" # E: Cannot assign to final attribute "b" + +[builtins fixtures/property.pyi] \ No newline at end of file