From dd28694a63967920ce7da3f15d08fa65564608b7 Mon Sep 17 00:00:00 2001 From: UncIeRick <yhe3@andrew.cmu.edu> Date: Fri, 9 Dec 2022 14:44:03 -0500 Subject: [PATCH 1/5] added 2 testcases --- test-data/unit/check-dataclasses.test | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index c248f8db8585..a1a426ec9bd5 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2001,3 +2001,29 @@ class Bar(Foo): ... e: Element[Bar] reveal_type(e.elements) # N: Revealed type is "typing.Sequence[__main__.Element[__main__.Bar]]" [builtins fixtures/dataclasses.pyi] + + + +[case testErrorFinalFieldNoInitNoArgumentPassed] +from typing import Final +from dataclasses import dataclass, field +@dataclass +class Foo: + a: Final[int] = field(init=False) +Foo().a # E: Final field "a" not set +[builtins fixtures/dataclasses.pyi] + +[case testNoErrorFinalFieldDelayedInit] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Bar: + a: Final[int] = field() + b: Final[int] + + def __init__(self) -> None: + self.a = 1 + self.b = 1 + +[builtins fixtures/dataclasses.pyi] From 13fbb451ede370d815beed106ab5e6049c09eb6a Mon Sep 17 00:00:00 2001 From: Jake Zych <zych.jake@gmail.com> Date: Sun, 11 Dec 2022 19:32:45 -0500 Subject: [PATCH 2/5] add unit tests ensuring correct behavior is preserved --- test-data/unit/check-dataclasses.test | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index a1a426ec9bd5..a32764e06999 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2027,3 +2027,53 @@ class Bar: self.b = 1 [builtins fixtures/dataclasses.pyi] + +[case testErrorFinalFieldInitNoArgumentPassed] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Foo: + a: Final[int] = field() + +Foo().a # E: Missing positional argument "a" in call to "Foo" +[builtins fixtures/dataclasses.pyi] + +[case testFinalFieldGeneratedInitArgumentPassed] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Foo: + a: Final[int] = field() + +Foo(1).a +[builtins fixtures/dataclasses.pyi] + +[case testFinalFieldInit] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Foo: + a: Final[int] = field(init=False) + + def __init__(self): + self.a = 1 + +Foo().a +[builtins fixtures/dataclasses.pyi] + +[case testFinalFieldPostInit] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Foo: + a: Final[int] = field(init=False) + + def __post_init__(self): + self.a = 1 + +Foo().a +[builtins fixtures/dataclasses.pyi] \ No newline at end of file From da944e8d97308856039052a9bb5feccff392af60 Mon Sep 17 00:00:00 2001 From: Jake Zych <zych.jake@gmail.com> Date: Sun, 11 Dec 2022 21:30:28 -0500 Subject: [PATCH 3/5] fix init=False dataclass field errors --- mypy/checkmember.py | 21 ++++++++++++++++ mypy/messages.py | 3 +++ mypy/plugins/dataclasses.py | 36 +++++++++++++++++++++++++++ test-data/unit/check-dataclasses.test | 6 ++--- test-data/unit/check-final.test | 6 ++--- 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 554b49d3eda2..f937258f7ac8 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -512,6 +512,9 @@ def analyze_member_var_access( if mx.is_lvalue and not mx.chk.get_final_context(): check_final_member(name, info, mx.msg, mx.context) + if not mx.is_lvalue and not mx.chk.get_final_context(): + check_final_assigned_in_init(name, info, mx.msg, mx.context) + return analyze_var(name, v, itype, info, mx, implicit=implicit) elif isinstance(v, FuncDef): assert False, "Did not expect a function" @@ -600,6 +603,24 @@ def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Cont msg.cant_assign_to_final(name, attr_assign=True, ctx=ctx) +def check_final_assigned_in_init( + name: str, info: TypeInfo, msg: MessageBuilder, ctx: Context +) -> None: + """Give an error if the final being accessed was never assigned in init (or the class).""" + for base in info.mro: + sym = base.names.get(name) + if ( + sym + and is_final_node(sym.node) + and ( + isinstance(sym.node, Var) + and not sym.node.final_set_in_init + and sym.node.final_unset_in_class + ) + ): + msg.final_field_not_set_in_init(name, ctx=ctx) + + def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type: """Type check descriptor access. diff --git a/mypy/messages.py b/mypy/messages.py index 85fa30512534..9bef6afade7c 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1402,6 +1402,9 @@ def cant_assign_to_final(self, name: str, attr_assign: bool, ctx: Context) -> No kind = "attribute" if attr_assign else "name" self.fail(f'Cannot assign to final {kind} "{unmangle(name)}"', ctx) + def final_field_not_set_in_init(self, name: str, ctx: Context) -> None: + self.fail(f'Final field "{name}" not set', ctx) + def protocol_members_cant_be_final(self, ctx: Context) -> None: self.fail("Protocol member cannot be final", ctx) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 75496d5e56f9..ab17a0ed61d4 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -19,7 +19,9 @@ CallExpr, Context, Expression, + FuncDef, JsonDict, + MemberExpr, NameExpr, PlaceholderNode, RefExpr, @@ -412,7 +414,36 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: # Second, collect attributes belonging to the current class. current_attr_names: set[str] = set() kw_only = _get_decorator_bool_argument(ctx, "kw_only", False) + final_unset_fields = set() for stmt in cls.defs.body: + # Check statements in __init__ and __post_init__ to see if any + # Final class variables = field() are being properly set + if isinstance(stmt, FuncDef) and ( + stmt.name == "__init__" or stmt.name == "__post_init__" + ): + for init_stmt in stmt.body.body: + if not isinstance(init_stmt, AssignmentStmt): + continue + if not ( + isinstance(init_stmt.lvalues[0], MemberExpr) + or isinstance(init_stmt.lvalues[0], NameExpr) + ): + continue + final_lhs = init_stmt.lvalues[0] + sym = cls.info.names.get(final_lhs.name) + if sym is None: + # There was probably a semantic analysis error. + continue + + node = sym.node + assert not isinstance(node, PlaceholderNode) + assert isinstance(node, Var) + if final_lhs.name in final_unset_fields: + node.final_unset_in_class = False + node.final_set_in_init = True + init_stmt.is_final_def = True + continue + # Any assignment that doesn't use the new type declaration # syntax can be ignored out of hand. if not (isinstance(stmt, AssignmentStmt) and stmt.new_syntax): @@ -469,6 +500,11 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: else: is_in_init = bool(ctx.api.parse_bool(is_in_init_param)) + if has_field_call and node.is_final: + if not is_in_init: + node.final_unset_in_class = True + final_unset_fields.add(lhs.name) + has_default = False # Ensure that something like x: int = field() is rejected # after an attribute with a default. diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index a32764e06999..e8a64686486e 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2002,14 +2002,12 @@ e: Element[Bar] reveal_type(e.elements) # N: Revealed type is "typing.Sequence[__main__.Element[__main__.Bar]]" [builtins fixtures/dataclasses.pyi] - - [case testErrorFinalFieldNoInitNoArgumentPassed] from typing import Final from dataclasses import dataclass, field @dataclass class Foo: - a: Final[int] = field(init=False) + a: Final[int] = field(init=False) # E: Final name must be initialized with a value Foo().a # E: Final field "a" not set [builtins fixtures/dataclasses.pyi] @@ -2023,7 +2021,7 @@ class Bar: b: Final[int] def __init__(self) -> None: - self.a = 1 + self.a = 1 self.b = 1 [builtins fixtures/dataclasses.pyi] diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index da034caced76..8e0a8ad45f80 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -210,10 +210,10 @@ class C: y: Final[int] # E: Final name must be initialized with a value def __init__(self) -> None: self.z: Final # E: Type in Final[...] can only be omitted if there is an initializer -reveal_type(x) # N: Revealed type is "Any" +reveal_type(x) # N: Revealed type is "Any" reveal_type(y) # N: Revealed type is "builtins.int" -reveal_type(C().x) # N: Revealed type is "Any" -reveal_type(C().y) # N: Revealed type is "builtins.int" +reveal_type(C().x) # E: Final field "x" not set # N: Revealed type is "Any" +reveal_type(C().y) # E: Final field "y" not set # N: Revealed type is "builtins.int" reveal_type(C().z) # N: Revealed type is "Any" [out] From b699ecae17f19d36a523a1e6e8eb7147be1030bd Mon Sep 17 00:00:00 2001 From: Jake Zych <zych.jake@gmail.com> Date: Tue, 13 Dec 2022 14:00:33 -0500 Subject: [PATCH 4/5] ignore stubs in final unset field access check --- mypy/checkmember.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index f937258f7ac8..4b2e47b1ceca 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -512,7 +512,7 @@ def analyze_member_var_access( if mx.is_lvalue and not mx.chk.get_final_context(): check_final_member(name, info, mx.msg, mx.context) - if not mx.is_lvalue and not mx.chk.get_final_context(): + if not mx.is_lvalue and not mx.chk.get_final_context() and not mx.chk.is_stub: check_final_assigned_in_init(name, info, mx.msg, mx.context) return analyze_var(name, v, itype, info, mx, implicit=implicit) From 1d5e1207a3c3f1acf53444569fc2eedc9fa24c88 Mon Sep 17 00:00:00 2001 From: Jake Zych <zych.jake@gmail.com> Date: Mon, 19 Dec 2022 19:54:40 -0600 Subject: [PATCH 5/5] add check for explicit value --- mypy/checkmember.py | 12 +++++++++--- test-data/unit/check-final.test | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 4b2e47b1ceca..bbda7adac7f0 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -511,10 +511,15 @@ def analyze_member_var_access( # independently of types. if mx.is_lvalue and not mx.chk.get_final_context(): check_final_member(name, info, mx.msg, mx.context) - - if not mx.is_lvalue and not mx.chk.get_final_context() and not mx.chk.is_stub: + # If accessing a final attribute, check if it was properly assigned + # in init (for dataclass field() specifically) + if ( + not mx.is_lvalue + and not mx.chk.get_final_context() + and not mx.chk.is_stub + and not mx.chk.is_typeshed_stub + ): check_final_assigned_in_init(name, info, mx.msg, mx.context) - return analyze_var(name, v, itype, info, mx, implicit=implicit) elif isinstance(v, FuncDef): assert False, "Did not expect a function" @@ -616,6 +621,7 @@ def check_final_assigned_in_init( isinstance(sym.node, Var) and not sym.node.final_set_in_init and sym.node.final_unset_in_class + and sym.node.has_explicit_value ) ): msg.final_field_not_set_in_init(name, ctx=ctx) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 8e0a8ad45f80..9422301b88d0 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -212,8 +212,8 @@ class C: self.z: Final # E: Type in Final[...] can only be omitted if there is an initializer reveal_type(x) # N: Revealed type is "Any" reveal_type(y) # N: Revealed type is "builtins.int" -reveal_type(C().x) # E: Final field "x" not set # N: Revealed type is "Any" -reveal_type(C().y) # E: Final field "y" not set # N: Revealed type is "builtins.int" +reveal_type(C().x) # N: Revealed type is "Any" +reveal_type(C().y) # N: Revealed type is "builtins.int" reveal_type(C().z) # N: Revealed type is "Any" [out]