Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes no error thrown for Final fields declared with init=False and not assigned inside __init__ #14285

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
29 changes: 28 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +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 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"
Expand Down Expand Up @@ -600,6 +608,25 @@ 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
and sym.node.has_explicit_value
)
):
msg.final_field_not_set_in_init(name, ctx=ctx)


def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
"""Type check descriptor access.

Expand Down
3 changes: 3 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
36 changes: 36 additions & 0 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
CallExpr,
Context,
Expression,
FuncDef,
JsonDict,
MemberExpr,
NameExpr,
PlaceholderNode,
RefExpr,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down
74 changes: 74 additions & 0 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -2001,3 +2001,77 @@ 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) # E: Final name must be initialized with a value
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]

[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]
4 changes: 2 additions & 2 deletions test-data/unit/check-final.test
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ 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().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]
Expand Down