10000 [dataclass_transform] support default parameters by wesleywright · Pull Request #14580 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

[dataclass_transform] support default parameters #14580

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

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
consolidate functions for finding dataclass transform specs
  • Loading branch information
wesleywright committed Feb 6, 2023
commit 12c8c74b81389f48cd5f6cff429614ff159e30ec
35 changes: 14 additions & 21 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@
ARG_STAR,
ARG_STAR2,
MDEF,
SYMBOL_FUNCBASE_TYPES,
Argument,
AssignmentStmt,
CallExpr,
Context,
DataclassTransformSpec,
Decorator,
Expression,
JsonDict,
NameExpr,
Expand All @@ -41,6 +39,7 @@
add_method,
deserialize_and_fixup_type,
)
from mypy.semanal_shared import find_dataclass_transform_spec
from mypy.server.trigger import make_wildcard_trigger
from mypy.state import state
from mypy.typeops import map_type_from_supertype
Expand Down Expand Up @@ -679,26 +678,20 @@ def _get_transform_spec(reason: Expression) -> DataclassTransformSpec:
In those cases, we return a default spec rather than one based on a call to
`typing.dataclass_transform`.
"""
node: Node | None = reason
if _is_dataclasses_decorator(reason):
return _TRANSFORM_SPEC_FOR_DATACLASSES

# The spec only lives on the function/class definition itself, so we need to unwrap down to that
# point
if isinstance(node, CallExpr):
# Decorators may take the form of either @decorator or @decorator(...); unwrap the latter
node = node.callee
if isinstance(node, RefExpr):
# If we see dataclasses.dataclass here, we know that we're not going to find a transform
# spec, so return early.
if node.fullname in dataclass_makers:
return _TRANSFORM_SPEC_FOR_DATACLASSES
node = node.node
if isinstance(node, Decorator):
node = node.func

if isinstance(node, SYMBOL_FUNCBASE_TYPES) and node.dataclass_transform_spec is not None:
return node.dataclass_transform_spec

raise AssertionError(
spec = find_dataclass_transform_spec(reason)
assert spec is not None, (
"trying to find dataclass transform spec, but reason is neither dataclasses.dataclass nor "
"decorated with typing.dataclass_transform"
)
return spec


def _is_dataclasses_decorator(node: Node) -> bool:
if isinstance(node, CallExpr):
node = node.callee
if isinstance(node, RefExpr):
return node.fullname in dataclass_makers
return False
24 changes: 2 additions & 22 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@
PRIORITY_FALLBACKS,
SemanticAnalyzerInterface,
calculate_tuple_fallback,
find_dataclass_transform_spec,
has_placeholder,
set_callable_name as set_callable_name,
)
Expand Down Expand Up @@ -1737,7 +1738,7 @@ def apply_class_plugin_hooks(self, defn: ClassDef) -> None:
# Special case: if the decorator is itself decorated with
# typing.dataclass_transform, apply the hook for the dataclasses plugin
# TODO: remove special casing here
if hook is None and is_dataclass_transform_decorator(decorator):
if hook is None and find_dataclass_transform_spec(decorator):
hook = dataclasses_plugin.dataclass_tag_callback
if hook:
hook(ClassDefContext(defn, decorator, self))
Expand Down Expand Up @@ -6687,24 +6688,3 @@ def halt(self, reason: str = ...) -> NoReturn:
return isinstance(stmt, PassStmt) or (
isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr)
)


def is_dataclass_transform_decorator(node: Node | None) -> bool:
if isinstance(node, RefExpr):
return is_dataclass_transform_decorator(node.node)
if isinstance(node, CallExpr):
# Like dataclasses.dataclass, transform-based decorators can be applied either with or
# without parameters; ie, both of these forms are accepted:
#
# @typing.dataclass_transform
# class Foo: ...
# @typing.dataclass_transform(eq=True, order=True, ...)
# class Bar: ...
#
# We need to unwrap the call for the second variant.
return is_dataclass_transform_decorator(node.callee)

if isinstance(node, Decorator) and node.func.dataclass_transform_spec is not None:
return True

return False
4 changes: 2 additions & 2 deletions mypy/semanal_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
from mypy.semanal import (
SemanticAnalyzer,
apply_semantic_analyzer_patches,
is_dataclass_transform_decorator,
remove_imported_names_from_symtable,
)
from mypy.semanal_classprop import (
Expand All @@ -51,6 +50,7 @@
check_protocol_status,
)
from mypy.semanal_infer import infer_decorator_signature_if_simple
from mypy.semanal_shared import find_dataclass_transform_spec
from mypy.semanal_typeargs import TypeArgumentAnalyzer
from mypy.server.aststrip import SavedAttributes
from mypy.util import is_typeshed_file
Expand Down Expand Up @@ -467,7 +467,7 @@ def apply_hooks_to_class(
# Special case: if the decorator is itself decorated with
# typing.dataclass_transform, apply the hook for the dataclasses plugin
# TODO: remove special casing here
if hook is None and is_dataclass_transform_decorator(decorator):
if hook is None and find_dataclass_transform_spec(decorator):
hook = dataclasses_plugin.dataclass_class_maker_callback

if hook:
Expand Down
42 changes: 42 additions & 0 deletions mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@
from mypy import join
from mypy.errorcodes import ErrorCode
from mypy.nodes import (
SYMBOL_FUNCBASE_TYPES,
CallExpr,
Context,
DataclassTransformSpec,
Decorator,
Expression,
FuncDef,
Node,
RefExpr,
SymbolNode,
SymbolTable,
SymbolTableNode,
Expand Down Expand Up @@ -341,3 +346,40 @@ def visit_placeholder_type(self, t: PlaceholderType) -> bool:
def has_placeholder(typ: Type) -> bool:
"""Check if a type contains any placeholder types (recursively)."""
return typ.accept(HasPlaceholders())


def find_dataclass_transform_spec(node: Node | None) -> DataclassTransformSpec | None:
"""
Find the dataclass transform spec for the given node, if any exists.

Per PEP 681 (https://peps.python.org/pep-0681/#the-dataclass-transform-decorator), dataclass
transforms can be specified in multiple ways, including decorator functions and
metaclasses/base classes. This function resolves the spec from any of these variants.
"""

# The spec only lives on the function/class definition itself, so we need to unwrap down to that 628C
# point
if isinstance(node, CallExpr):
# Like dataclasses.dataclass, transform-based decorators can be applied either with or
# without parameters; ie, both of these forms are accepted:
#
# @typing.dataclass_transform
# class Foo: ...
# @typing.dataclass_transform(eq=True, order=True, ...)
# class Bar: ...
#
# We need to unwrap the call for the second variant.
node = node.callee

if isinstance(node, RefExpr):
node = node.node

if isinstance(node, Decorator):
# typing.dataclass_transform usage must always result in a Decorator; it always uses the
# `@dataclass_transform(...)` syntax and never `@dataclass_transform`
node = node.func

if isinstance(node, SYMBOL_FUNCBASE_TYPES):
return node.dataclass_transform_spec

return None
0