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

Fix @dataclass(frozen=True) fail on name shadowing #12088

Closed
wants to merge 2 commits into from
Closed
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
24 changes: 18 additions & 6 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

from mypy.nodes import (
ARG_OPT, ARG_NAMED, ARG_NAMED_OPT, ARG_POS, ARG_STAR, ARG_STAR2, MDEF,
Argument, AssignmentStmt, CallExpr, Context, Expression, JsonDict,
Argument, AssignmentStmt, CallExpr, Context, Expression, JsonDict,
NameExpr, RefExpr, SymbolTableNode, TempNode, TypeInfo, Var, TypeVarExpr,
PlaceholderNode
PlaceholderNode, FuncBase,
)
from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface
from mypy.plugins.common import (
Expand Down Expand Up @@ -298,13 +298,16 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
# First, collect attributes belonging to the current class.
ctx = self._ctx
cls = self._ctx.cls
attrs: List[DataclassAttribute] = []
first_attrs: List[DataclassAttribute] = []
known_attrs: Set[str] = set()
kw_only = _get_decorator_bool_argument(ctx, 'kw_only', False)
shadowing: Set[str] = set()
for stmt in cls.defs.body:
# 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):
if isinstance(stmt, FuncBase) and stmt.name in known_attrs:
shadowing.add(stmt.name)
continue

# a: int, b: str = 1, 'foo' is not supported syntax so we
Expand Down Expand Up @@ -371,7 +374,7 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
is_kw_only = bool(ctx.api.parse_bool(field_kw_only_param))

known_attrs.add(lhs.name)
attrs.append(DataclassAttribute(
first_attrs.append(DataclassAttribute(
name=lhs.name,
is_in_init=is_in_init,
is_init_var=is_init_var,
Expand All @@ -383,6 +386,14 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
kw_only=is_kw_only,
))

# Now, we need to drop all items that are shadowed
# by methods in dataclass body. See #12084
attrs: List[DataclassAttribute] = [
attr
for attr in first_attrs
if attr.name not in shadowing
]

# Next, collect attributes belonging to any class in the MRO
# as long as those attributes weren't already collected. This
# makes it possible to overwrite attributes in subclasses.
Expand Down Expand Up @@ -457,8 +468,9 @@ def _freeze(self, attributes: List[DataclassAttribute]) -> None:
sym_node = info.names.get(attr.name)
if sym_node is not None:
var = sym_node.node
assert isinstance(var, Var)
var.is_property = True
if isinstance(var, Var):
# This can also be shadowed by a class / import. See #12084
var.is_property = True
else:
var = attr.to_var()
var.info = info
Expand Down
40 changes: 40 additions & 0 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -1536,3 +1536,43 @@ A(a=1, b=2)
A(1)
A(a="foo") # E: Argument "a" to "A" has incompatible type "str"; expected "int"
[builtins fixtures/dataclasses.pyi]

[case testFrozenInheritanceWithNameShadowing]
# flags: --python-version 3.7
# See: https://github.com/python/mypy/issues/12084
from dataclasses import dataclass
from typing import Callable

@dataclass(frozen=True)
class Parent:
__call__: Callable[..., None]

# We need to shadow `Var` with `FuncDef`
def __call__(self) -> None: # type: ignore
...

@dataclass(frozen=True)
class Child(Parent):
def __call__(self) -> None: # should not crash here
...
[builtins fixtures/dataclasses.pyi]

[case testFrozenInheritanceWithNameShadowing_ClassDef]
# flags: --python-version 3.7
# See: https://github.com/python/mypy/issues/12084
from dataclasses import dataclass
from typing import Callable

@dataclass(frozen=True)
class Parent:
__call__: Callable[..., None]

# We need to shadow `Var` with `ClassDef`
class __call__: # type: ignore
...

@dataclass(frozen=True)
class Child(Parent):
def __call__(self) -> None: # should not crash here
...
[builtins fixtures/dataclasses.pyi]