From e36a5f6d5859f86671067d19abdd4940a572bc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Fern=C3=A1ndez=20Iglesias?= Date: Mon, 6 May 2024 20:33:46 +0200 Subject: [PATCH 1/2] Fix error on instance property and init-only variable with the same name in a dataclass --- mypy/nodes.py | 18 ++++++++----- mypy/semanal.py | 13 ++++++++- test-data/unit/check-dataclasses.test | 39 +++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index bb278d92392d..cb61d34add4e 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3165,12 +3165,18 @@ def get_method(self, name: str) -> FuncBase | Decorator | None: for cls in self.mro: if name in cls.names: node = cls.names[name].node - if isinstance(node, FuncBase): - return node - elif isinstance(node, Decorator): # Two `if`s make `mypyc` happy - return node - else: - return None + elif possible_redefinitions := sorted( + [n for n in cls.names.keys() if n.startswith(f"{name}-redefinition")] + ): + node = cls.names[possible_redefinitions[-1]].node + else: + continue + if isinstance(node, FuncBase): + return node + elif isinstance(node, Decorator): # Two `if`s make `mypyc` happy + return node + else: + return None return None def calculate_metaclass_type(self) -> mypy.types.Instance | None: diff --git a/mypy/semanal.py b/mypy/semanal.py index 91a6b1808987..19b411181f9f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6211,7 +6211,10 @@ def add_symbol_table_node( if not is_same_symbol(old, new): if isinstance(new, (FuncDef, Decorator, OverloadedFuncDef, TypeInfo)): self.add_redefinition(names, name, symbol) - if not (isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new)): + if not ( + is_init_only(old) + or (isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new)) + ): self.name_already_defined(name, context, existing) elif name not in self.missing_names[-1] and "*" not in self.missing_names[-1]: names[name] = symbol @@ -7195,3 +7198,11 @@ def halt(self, reason: str = ...) -> NoReturn: return isinstance(stmt, PassStmt) or ( isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr) ) + + +def is_init_only(node: SymbolNode | None) -> bool: + return ( + isinstance(node, Var) + and isinstance(type := get_proper_type(node.type), Instance) + and type.type.fullname == "dataclasses.InitVar" + ) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index a055507cdd78..1aef417b9d04 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2475,3 +2475,42 @@ class Base: class Child(Base): y: int [builtins fixtures/dataclasses.pyi] + +[case testDataclassesInitVarsWithProperty] +from dataclasses import InitVar, dataclass, field + +@dataclass +class Test: + foo: InitVar[str] + _foo: str = field(init=False) + + def __post_init__(self, foo: str) -> None: + self._foo = foo + + @property + def foo(self) -> str: + return self._foo + + @foo.setter + def foo(self, value: str) -> None: + self._foo = value + +reveal_type(Test) # N: Revealed type is "def (foo: builtins.str) -> __main__.Test" +test = Test(42) # E: Argument 1 to "Test" has incompatible type "int"; expected "str" +test = Test("foo") +test.foo +[builtins fixtures/dataclasses.pyi] + +[case testDataclassesWithProperty] +from dataclasses import dataclass + +@dataclass +class Test: + @property + def foo(self) -> str: + return "a" + + @foo.setter + def foo(self, value: str) -> None: + pass +[builtins fixtures/dataclasses.pyi] From 504089cb7ed98c9524af5fc7a8ed35b60d8b01d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Fern=C3=A1ndez=20Iglesias?= Date: Sun, 26 May 2024 10:34:51 +0200 Subject: [PATCH 2/2] Add warning if redefined dataclass InitVar has a default value --- mypy/semanal.py | 13 +++++++------ test-data/unit/check-dataclasses.test | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 19b411181f9f..263b44c269b7 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6211,9 +6211,11 @@ def add_symbol_table_node( if not is_same_symbol(old, new): if isinstance(new, (FuncDef, Decorator, OverloadedFuncDef, TypeInfo)): self.add_redefinition(names, name, symbol) - if not ( - is_init_only(old) - or (isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new)) + if isinstance(old, Var) and is_init_only(old): + if old.has_explicit_value: + self.fail("InitVar with default value cannot be redefined", context) + elif not ( + isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new) ): self.name_already_defined(name, context, existing) elif name not in self.missing_names[-1] and "*" not in self.missing_names[-1]: @@ -7200,9 +7202,8 @@ def halt(self, reason: str = ...) -> NoReturn: ) -def is_init_only(node: SymbolNode | None) -> bool: +def is_init_only(node: Var) -> bool: return ( - isinstance(node, Var) - and isinstance(type := get_proper_type(node.type), Instance) + isinstance(type := get_proper_type(node.type), Instance) and type.type.fullname == "dataclasses.InitVar" ) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 1aef417b9d04..2c9dd4600315 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2501,6 +2501,23 @@ test = Test("foo") test.foo [builtins fixtures/dataclasses.pyi] +[case testDataclassesDefaultValueInitVarWithProperty] +from dataclasses import InitVar, dataclass, field + +@dataclass +class Test: + foo: InitVar[str] = "foo" + _foo: str = field(init=False) + + def __post_init__(self, foo: str) -> None: + self._foo = foo + + @property # E: InitVar with default value cannot be redefined + def foo(self) -> str: + return self._foo + +[builtins fixtures/dataclasses.pyi] + [case testDataclassesWithProperty] from dataclasses import dataclass