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

[dataclass_transform] support implicit default for "init" parameter in field specifiers #14870

Closed
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
29 changes: 27 additions & 2 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface
from mypy.plugins.common import (
_get_callee_type,
_get_decorator_bool_argument,
add_attribute_to_class,
add_method_to_class,
Expand All @@ -47,7 +48,7 @@
from mypy.semanal_shared import find_dataclass_transform_spec, require_bool_literal_argument
from mypy.server.trigger import make_wildcard_trigger
from mypy.state import state
from mypy.typeops import map_type_from_supertype
from mypy.typeops import map_type_from_supertype, try_getting_literals_from_type
from mypy.types import (
AnyType,
CallableType,
Expand Down Expand Up @@ -509,7 +510,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:

is_in_init_param = field_args.get("init")
if is_in_init_param is None:
is_in_init = True
is_in_init = self._get_default_init_value_for_field_specifier(stmt.rvalue)
else:
is_in_init = bool(self._api.parse_bool(is_in_init_param))

Expand Down Expand Up @@ -738,6 +739,30 @@ def _get_bool_arg(self, name: str, default: bool) -> bool:
return require_bool_literal_argument(self._api, expression, name, default)
return default

def _get_default_init_value_for_field_specifier(self, call: Expression) -> bool:
"""
Find a default value for the `init` parameter of the specifier being called. If the
specifier's type signature includes an `init` parameter with a type of `Literal[True]` or
`Literal[False]`, return the appropriate boolean value from the literal. Otherwise,
fall back to the standard default of `True`.
"""
if not isinstance(call, CallExpr):
return True

specifier_type = _get_callee_type(call)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm reusing this existing helper from mypy.plugins.common, but unfortunately it doesn't truly support overloads:

    if isinstance(callee_node, (Var, SYMBOL_FUNCBASE_TYPES)) and callee_node.type:
        callee_node_type = get_proper_type(callee_node.type)
        if isinstance(callee_node_type, Overloaded):
            # We take the last overload.
            return callee_node_type.items[-1]
        elif isinstance(callee_node_type, CallableType):
            return callee_node_type

I'm not sure if there's an existing method that can resolve the appropriate overload or if some new plumbing will need to be added to MyPy.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't generally resolve overloads in a dataclass plugin, since semantic analysis is not complete yet, and certain type operations aren't strictly speaking available yet. We could perhaps do a best-effort resolution, but I'm not sure if it's worth the effort. Do you know of any use case where this is needed? Perhaps we can come up with a simplified implementation that is good enough for common use cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JukkaL I'm not sure if anything is using it in the wild, thought he PEP calls it out with an explicit example: https://peps.python.org/pep-0681/#field-specifier-parameters We've discussed this some in #14293 (comment), so I can try asking there if there any concrete use cases to be aware of.

if specifier_type is None:
return True

parameter = specifier_type.argument_by_name("init")
if parameter is None:
return True

literals = try_getting_literals_from_type(parameter.typ, bool, "builtins.bool")
if literals is None or len(literals) != 1:
return True

return literals[0]


def add_dataclass_tag(info: TypeInfo) -> None:
# The value is ignored, only the existence matters.
Expand Down
32 changes: 32 additions & 0 deletions test-data/unit/check-dataclass-transform.test
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,38 @@ Foo(a=1, b='bye')
[typing fixtures/typing-full.pyi]
[builtins fixtures/dataclasses.pyi]

[case testDataclassTransformFieldSpecifierImplicitInit]
# flags: --python-version 3.11
from typing import dataclass_transform, Literal, overload

def init(*, init: Literal[True] = True): ...
def no_init(*, init: Literal[False] = False): ...

@overload
def field_overload(*, custom: None, init: Literal[True] = True): ...
@overload
def field_overload(*, custom: str, init: Literal[False] = False): ...
def field_overload(*, custom, init): ...

@dataclass_transform(field_specifiers=(init, no_init, field_overload))
def my_dataclass(cls): return cls

@my_dataclass
class Foo:
a: int = init()
b: int = field_overload(custom=None)

bad1: int = no_init()
bad2: int = field_overload(custom="bad2")

reveal_type(Foo) # N: Revealed type is "def (a: builtins.int, b: builtins.int) -> __main__.Foo"
Foo(a=1, b=2)
Foo(a=1, b=2, bad1=0) # E: Unexpected keyword argument "bad1" for "Foo"
Foo(a=1, b=2, bad2=0) # E: Unexpected keyword argument "bad2" for "Foo"

[typing fixtures/typing-full.pyi]
[builtins fixtures/dataclasses.pyi]

[case testDataclassTransformOverloadsDecoratorOnOverload]
# flags: --python-version 3.11
from typing import dataclass_transform, overload, Any, Callable, Type, Literal
Expand Down