mirror of
https://github.com/davidhalter/jedi.git
synced 2025-12-06 14:04:26 +08:00
Merge pull request #2049 from Morikko/support-dataclass-transform
Some checks failed
ci / tests (3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.9) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.10) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.11) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.12) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.13) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.8) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.9) (push) Has been cancelled
ci / code-quality (push) Has been cancelled
ci / coverage (push) Has been cancelled
Some checks failed
ci / tests (3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.10, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.10, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.11, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.11, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.12, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.12, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.13, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.13, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.8, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.8, windows-2022, 3.9) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (3.9, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.10) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.11) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.12) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.13) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.8) (push) Has been cancelled
ci / tests (3.9, windows-2022, 3.9) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.10) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.11) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.12) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.13) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.8) (push) Has been cancelled
ci / tests (interpreter, ubuntu-24.04, 3.9) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.10) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.11) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.12) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.13) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.8) (push) Has been cancelled
ci / tests (interpreter, windows-2022, 3.9) (push) Has been cancelled
ci / code-quality (push) Has been cancelled
ci / coverage (push) Has been cancelled
Support dataclass transform
This commit is contained in:
@@ -156,6 +156,14 @@ def jedi_path():
|
|||||||
return os.path.dirname(__file__)
|
return os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def skip_pre_python311(environment):
|
||||||
|
if environment.version_info < (3, 11):
|
||||||
|
# This if is just needed to avoid that tests ever skip way more than
|
||||||
|
# they should for all Python versions.
|
||||||
|
pytest.skip()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def skip_pre_python38(environment):
|
def skip_pre_python38(environment):
|
||||||
if environment.version_info < (3, 8):
|
if environment.version_info < (3, 8):
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ py__doc__() Returns the docstring for a value.
|
|||||||
====================================== ========================================
|
====================================== ========================================
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from jedi import debug
|
from jedi import debug
|
||||||
from jedi.parser_utils import get_cached_parent_scope, expr_is_dotted, \
|
from jedi.parser_utils import get_cached_parent_scope, expr_is_dotted, \
|
||||||
function_is_property
|
function_is_property
|
||||||
@@ -47,11 +51,15 @@ from jedi.inference.filters import ParserTreeFilter
|
|||||||
from jedi.inference.names import TreeNameDefinition, ValueName
|
from jedi.inference.names import TreeNameDefinition, ValueName
|
||||||
from jedi.inference.arguments import unpack_arglist, ValuesArguments
|
from jedi.inference.arguments import unpack_arglist, ValuesArguments
|
||||||
from jedi.inference.base_value import ValueSet, iterator_to_value_set, \
|
from jedi.inference.base_value import ValueSet, iterator_to_value_set, \
|
||||||
NO_VALUES
|
NO_VALUES, ValueWrapper
|
||||||
from jedi.inference.context import ClassContext
|
from jedi.inference.context import ClassContext
|
||||||
from jedi.inference.value.function import FunctionAndClassBase
|
from jedi.inference.value.function import FunctionAndClassBase, FunctionMixin
|
||||||
|
from jedi.inference.value.decorator import Decoratee
|
||||||
from jedi.inference.gradual.generics import LazyGenericManager, TupleGenericManager
|
from jedi.inference.gradual.generics import LazyGenericManager, TupleGenericManager
|
||||||
from jedi.plugins import plugin_manager
|
from jedi.plugins import plugin_manager
|
||||||
|
from inspect import Parameter
|
||||||
|
from jedi.inference.names import BaseTreeParamName
|
||||||
|
from jedi.inference.signature import AbstractSignature
|
||||||
|
|
||||||
|
|
||||||
class ClassName(TreeNameDefinition):
|
class ClassName(TreeNameDefinition):
|
||||||
@@ -129,6 +137,65 @@ class ClassFilter(ParserTreeFilter):
|
|||||||
return [name for name in names if self._access_possible(name)]
|
return [name for name in names if self._access_possible(name)]
|
||||||
|
|
||||||
|
|
||||||
|
def init_param_value(arg_nodes) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
- ``True`` if ``@dataclass(init=True)``
|
||||||
|
- ``False`` if ``@dataclass(init=False)``
|
||||||
|
- ``None`` if not specified ``@dataclass()``
|
||||||
|
"""
|
||||||
|
for arg_node in arg_nodes:
|
||||||
|
if (
|
||||||
|
arg_node.type == "argument"
|
||||||
|
and arg_node.children[0].value == "init"
|
||||||
|
):
|
||||||
|
if arg_node.children[2].value == "False":
|
||||||
|
return False
|
||||||
|
elif arg_node.children[2].value == "True":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_dataclass_param_names(cls) -> List[DataclassParamName]:
|
||||||
|
"""
|
||||||
|
``cls`` is a :class:`ClassMixin`. The type is only documented as mypy would
|
||||||
|
complain that some fields are missing.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class A:
|
||||||
|
a: int
|
||||||
|
b: str = "toto"
|
||||||
|
|
||||||
|
For the previous example, the param names would be ``a`` and ``b``.
|
||||||
|
"""
|
||||||
|
param_names = []
|
||||||
|
filter_ = cls.as_context().get_global_filter()
|
||||||
|
for name in sorted(filter_.values(), key=lambda name: name.start_pos):
|
||||||
|
d = name.tree_name.get_definition()
|
||||||
|
annassign = d.children[1]
|
||||||
|
if d.type == 'expr_stmt' and annassign.type == 'annassign':
|
||||||
|
node = annassign.children[1]
|
||||||
|
if node.type == "atom_expr" and node.children[0].value == "ClassVar":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(annassign.children) < 4:
|
||||||
|
default = None
|
||||||
|
else:
|
||||||
|
default = annassign.children[3]
|
||||||
|
|
||||||
|
param_names.append(DataclassParamName(
|
||||||
|
parent_context=cls.parent_context,
|
||||||
|
tree_name=name.tree_name,
|
||||||
|
annotation_node=annassign.children[1],
|
||||||
|
default_node=default,
|
||||||
|
))
|
||||||
|
return param_names
|
||||||
|
|
||||||
|
|
||||||
class ClassMixin:
|
class ClassMixin:
|
||||||
def is_class(self):
|
def is_class(self):
|
||||||
return True
|
return True
|
||||||
@@ -221,6 +288,73 @@ class ClassMixin:
|
|||||||
assert x is not None
|
assert x is not None
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
|
def _has_dataclass_transform_metaclasses(self) -> Tuple[bool, Optional[bool]]:
|
||||||
|
for meta in self.get_metaclasses(): # type: ignore[attr-defined]
|
||||||
|
if (
|
||||||
|
isinstance(meta, Decoratee)
|
||||||
|
# Internal leakage :|
|
||||||
|
and isinstance(meta._wrapped_value, DataclassTransformer)
|
||||||
|
):
|
||||||
|
return True, meta._wrapped_value.init_mode_from_new()
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
def _get_dataclass_transform_signatures(self) -> List[DataclassSignature]:
|
||||||
|
"""
|
||||||
|
Returns: A non-empty list if the class has dataclass semantics else an
|
||||||
|
empty list.
|
||||||
|
|
||||||
|
The dataclass-like semantics will be assumed for any class that directly
|
||||||
|
or indirectly derives from the decorated class or uses the decorated
|
||||||
|
class as a metaclass.
|
||||||
|
"""
|
||||||
|
param_names = []
|
||||||
|
is_dataclass_transform = False
|
||||||
|
default_init_mode: Optional[bool] = None
|
||||||
|
for cls in reversed(list(self.py__mro__())):
|
||||||
|
if not is_dataclass_transform:
|
||||||
|
|
||||||
|
# If dataclass_transform is applied to a class, dataclass-like semantics
|
||||||
|
# will be assumed for any class that directly or indirectly derives from
|
||||||
|
# the decorated class or uses the decorated class as a metaclass.
|
||||||
|
if (
|
||||||
|
isinstance(cls, DataclassTransformer)
|
||||||
|
and cls.init_mode_from_init_subclass
|
||||||
|
):
|
||||||
|
is_dataclass_transform = True
|
||||||
|
default_init_mode = cls.init_mode_from_init_subclass
|
||||||
|
|
||||||
|
elif (
|
||||||
|
# Some object like CompiledValues would not be compatible
|
||||||
|
isinstance(cls, ClassMixin)
|
||||||
|
):
|
||||||
|
is_dataclass_transform, default_init_mode = (
|
||||||
|
cls._has_dataclass_transform_metaclasses()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attributes on the decorated class and its base classes are not
|
||||||
|
# considered to be fields.
|
||||||
|
if is_dataclass_transform:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# All inherited classes behave like dataclass semantics
|
||||||
|
if (
|
||||||
|
is_dataclass_transform
|
||||||
|
and isinstance(cls, ClassValue)
|
||||||
|
and (
|
||||||
|
cls.init_param_mode()
|
||||||
|
or (cls.init_param_mode() is None and default_init_mode)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
param_names.extend(
|
||||||
|
get_dataclass_param_names(cls)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_dataclass_transform:
|
||||||
|
return [DataclassSignature(cls, param_names)]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
def get_signatures(self):
|
def get_signatures(self):
|
||||||
# Since calling staticmethod without a function is illegal, the Jedi
|
# Since calling staticmethod without a function is illegal, the Jedi
|
||||||
# plugin doesn't return anything. Therefore call directly and get what
|
# plugin doesn't return anything. Therefore call directly and get what
|
||||||
@@ -232,6 +366,11 @@ class ClassMixin:
|
|||||||
return sigs
|
return sigs
|
||||||
args = ValuesArguments([])
|
args = ValuesArguments([])
|
||||||
init_funcs = self.py__call__(args).py__getattribute__('__init__')
|
init_funcs = self.py__call__(args).py__getattribute__('__init__')
|
||||||
|
|
||||||
|
dataclass_sigs = self._get_dataclass_transform_signatures()
|
||||||
|
if dataclass_sigs:
|
||||||
|
return dataclass_sigs
|
||||||
|
else:
|
||||||
return [sig.bind(self) for sig in init_funcs.get_signatures()]
|
return [sig.bind(self) for sig in init_funcs.get_signatures()]
|
||||||
|
|
||||||
def _as_context(self):
|
def _as_context(self):
|
||||||
@@ -319,6 +458,158 @@ class ClassMixin:
|
|||||||
return ValueSet({self})
|
return ValueSet({self})
|
||||||
|
|
||||||
|
|
||||||
|
class DataclassParamName(BaseTreeParamName):
|
||||||
|
"""
|
||||||
|
Represent a field declaration on a class with dataclass semantics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent_context, tree_name, annotation_node, default_node):
|
||||||
|
super().__init__(parent_context, tree_name)
|
||||||
|
self.annotation_node = annotation_node
|
||||||
|
self.default_node = default_node
|
||||||
|
|
||||||
|
def get_kind(self):
|
||||||
|
return Parameter.POSITIONAL_OR_KEYWORD
|
||||||
|
|
||||||
|
def infer(self):
|
||||||
|
if self.annotation_node is None:
|
||||||
|
return NO_VALUES
|
||||||
|
else:
|
||||||
|
return self.parent_context.infer_node(self.annotation_node)
|
||||||
|
|
||||||
|
|
||||||
|
class DataclassSignature(AbstractSignature):
|
||||||
|
"""
|
||||||
|
It represents the ``__init__`` signature of a class with dataclass semantics.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, value, param_names):
|
||||||
|
super().__init__(value)
|
||||||
|
self._param_names = param_names
|
||||||
|
|
||||||
|
def get_param_names(self, resolve_stars=False):
|
||||||
|
return self._param_names
|
||||||
|
|
||||||
|
|
||||||
|
class DataclassDecorator(ValueWrapper, FunctionMixin):
|
||||||
|
"""
|
||||||
|
A dataclass(-like) decorator with custom parameters.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
@dataclass(init=True) # this
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model(*, init=False): pass
|
||||||
|
|
||||||
|
@create_model(init=False) # or this
|
||||||
|
class B: ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, function, arguments, default_init: bool = True):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
function: Decoratee | function
|
||||||
|
arguments: The parameters to the dataclass function decorator
|
||||||
|
default_init: Boolean to indicate the default init value
|
||||||
|
"""
|
||||||
|
super().__init__(function)
|
||||||
|
argument_init = self._init_param_value(arguments)
|
||||||
|
self.init_param_mode = (
|
||||||
|
argument_init if argument_init is not None else default_init
|
||||||
|
)
|
||||||
|
|
||||||
|
def _init_param_value(self, arguments) -> Optional[bool]:
|
||||||
|
if not arguments.argument_node:
|
||||||
|
return None
|
||||||
|
|
||||||
|
arg_nodes = (
|
||||||
|
arguments.argument_node.children
|
||||||
|
if arguments.argument_node.type == "arglist"
|
||||||
|
else [arguments.argument_node]
|
||||||
|
)
|
||||||
|
|
||||||
|
return init_param_value(arg_nodes)
|
||||||
|
|
||||||
|
|
||||||
|
class DataclassTransformer(ValueWrapper, ClassMixin):
|
||||||
|
"""
|
||||||
|
A class decorated with the ``dataclass_transform`` decorator. dataclass-like
|
||||||
|
semantics will be assumed for any class that directly or indirectly derives
|
||||||
|
from the decorated class or uses the decorated class as a metaclass.
|
||||||
|
Attributes on the decorated class and its base classes are not considered to
|
||||||
|
be fields.
|
||||||
|
"""
|
||||||
|
def __init__(self, wrapped_value):
|
||||||
|
super().__init__(wrapped_value)
|
||||||
|
|
||||||
|
def init_mode_from_new(self) -> bool:
|
||||||
|
"""Default value if missing is ``True``"""
|
||||||
|
new_methods = self._wrapped_value.py__getattribute__("__new__")
|
||||||
|
|
||||||
|
if not new_methods:
|
||||||
|
return True
|
||||||
|
|
||||||
|
new_method = list(new_methods)[0]
|
||||||
|
|
||||||
|
for param in new_method.get_param_names():
|
||||||
|
if (
|
||||||
|
param.string_name == "init"
|
||||||
|
and param.default_node
|
||||||
|
and param.default_node.type == "keyword"
|
||||||
|
):
|
||||||
|
if param.default_node.value == "False":
|
||||||
|
return False
|
||||||
|
elif param.default_node.value == "True":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def init_mode_from_init_subclass(self) -> Optional[bool]:
|
||||||
|
# def __init_subclass__(cls) -> None: ... is hardcoded in the typeshed
|
||||||
|
# so the extra parameters can not be inferred.
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DataclassWrapper(ValueWrapper, ClassMixin):
|
||||||
|
"""
|
||||||
|
A class with dataclass semantics from a decorator. The init parameters are
|
||||||
|
only from the current class and parent classes decorated where the ``init``
|
||||||
|
parameter was ``True``.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class A: ... # this
|
||||||
|
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model(): pass
|
||||||
|
|
||||||
|
@create_model()
|
||||||
|
class B: ... # or this
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, wrapped_value, should_generate_init: bool
|
||||||
|
):
|
||||||
|
super().__init__(wrapped_value)
|
||||||
|
self.should_generate_init = should_generate_init
|
||||||
|
|
||||||
|
def get_signatures(self):
|
||||||
|
param_names = []
|
||||||
|
for cls in reversed(list(self.py__mro__())):
|
||||||
|
if (
|
||||||
|
isinstance(cls, DataclassWrapper)
|
||||||
|
and cls.should_generate_init
|
||||||
|
):
|
||||||
|
param_names.extend(get_dataclass_param_names(cls))
|
||||||
|
return [DataclassSignature(cls, param_names)]
|
||||||
|
|
||||||
|
|
||||||
class ClassValue(ClassMixin, FunctionAndClassBase, metaclass=CachedMetaClass):
|
class ClassValue(ClassMixin, FunctionAndClassBase, metaclass=CachedMetaClass):
|
||||||
api_type = 'class'
|
api_type = 'class'
|
||||||
|
|
||||||
@@ -385,6 +676,19 @@ class ClassValue(ClassMixin, FunctionAndClassBase, metaclass=CachedMetaClass):
|
|||||||
return values
|
return values
|
||||||
return NO_VALUES
|
return NO_VALUES
|
||||||
|
|
||||||
|
def init_param_mode(self) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
It returns ``True`` if ``class X(init=False):`` else ``False``.
|
||||||
|
"""
|
||||||
|
bases_arguments = self._get_bases_arguments()
|
||||||
|
|
||||||
|
if bases_arguments.argument_node.type != "arglist":
|
||||||
|
# If it is not inheriting from the base model and having
|
||||||
|
# extra parameters, then init behavior is not changed.
|
||||||
|
return None
|
||||||
|
|
||||||
|
return init_param_value(bases_arguments.argument_node.children)
|
||||||
|
|
||||||
@plugin_manager.decorate()
|
@plugin_manager.decorate()
|
||||||
def get_metaclass_signatures(self, metaclasses):
|
def get_metaclass_signatures(self, metaclasses):
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ compiled module that returns the types for C-builtins.
|
|||||||
"""
|
"""
|
||||||
import parso
|
import parso
|
||||||
import os
|
import os
|
||||||
from inspect import Parameter
|
|
||||||
|
|
||||||
from jedi import debug
|
from jedi import debug
|
||||||
from jedi.inference.utils import safe_property
|
from jedi.inference.utils import safe_property
|
||||||
@@ -25,15 +24,20 @@ from jedi.inference.value.instance import \
|
|||||||
from jedi.inference.base_value import ContextualizedNode, \
|
from jedi.inference.base_value import ContextualizedNode, \
|
||||||
NO_VALUES, ValueSet, ValueWrapper, LazyValueWrapper
|
NO_VALUES, ValueSet, ValueWrapper, LazyValueWrapper
|
||||||
from jedi.inference.value import ClassValue, ModuleValue
|
from jedi.inference.value import ClassValue, ModuleValue
|
||||||
from jedi.inference.value.klass import ClassMixin
|
from jedi.inference.value.decorator import Decoratee
|
||||||
|
from jedi.inference.value.klass import (
|
||||||
|
DataclassWrapper,
|
||||||
|
DataclassDecorator,
|
||||||
|
DataclassTransformer,
|
||||||
|
)
|
||||||
from jedi.inference.value.function import FunctionMixin
|
from jedi.inference.value.function import FunctionMixin
|
||||||
from jedi.inference.value import iterable
|
from jedi.inference.value import iterable
|
||||||
from jedi.inference.lazy_value import LazyTreeValue, LazyKnownValue, \
|
from jedi.inference.lazy_value import LazyTreeValue, LazyKnownValue, \
|
||||||
LazyKnownValues
|
LazyKnownValues
|
||||||
from jedi.inference.names import ValueName, BaseTreeParamName
|
from jedi.inference.names import ValueName
|
||||||
from jedi.inference.filters import AttributeOverwrite, publish_method, \
|
from jedi.inference.filters import AttributeOverwrite, publish_method, \
|
||||||
ParserTreeFilter, DictFilter
|
ParserTreeFilter, DictFilter
|
||||||
from jedi.inference.signature import AbstractSignature, SignatureWrapper
|
from jedi.inference.signature import SignatureWrapper
|
||||||
|
|
||||||
|
|
||||||
# Copied from Python 3.6's stdlib.
|
# Copied from Python 3.6's stdlib.
|
||||||
@@ -591,65 +595,103 @@ def _random_choice(sequences):
|
|||||||
|
|
||||||
|
|
||||||
def _dataclass(value, arguments, callback):
|
def _dataclass(value, arguments, callback):
|
||||||
|
"""
|
||||||
|
Decorator entry points for dataclass.
|
||||||
|
|
||||||
|
1. dataclass decorator declaration with parameters
|
||||||
|
2. dataclass semantics on a class from a dataclass(-like) decorator
|
||||||
|
"""
|
||||||
for c in _follow_param(value.inference_state, arguments, 0):
|
for c in _follow_param(value.inference_state, arguments, 0):
|
||||||
if c.is_class():
|
if c.is_class():
|
||||||
return ValueSet([DataclassWrapper(c)])
|
# Declare dataclass semantics on a class from a dataclass decorator
|
||||||
|
should_generate_init = (
|
||||||
|
# Customized decorator, init may be disabled
|
||||||
|
value.init_param_mode
|
||||||
|
if isinstance(value, DataclassDecorator)
|
||||||
|
# Bare dataclass decorator, always with init mode
|
||||||
|
else True
|
||||||
|
)
|
||||||
|
return ValueSet([DataclassWrapper(c, should_generate_init)])
|
||||||
else:
|
else:
|
||||||
|
# @dataclass(init=False)
|
||||||
|
# dataclass decorator customization
|
||||||
|
return ValueSet(
|
||||||
|
[
|
||||||
|
DataclassDecorator(
|
||||||
|
value,
|
||||||
|
arguments=arguments,
|
||||||
|
default_init=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return NO_VALUES
|
||||||
|
|
||||||
|
|
||||||
|
def _dataclass_transform(value, arguments, callback):
|
||||||
|
"""
|
||||||
|
Decorator entry points for dataclass_transform.
|
||||||
|
|
||||||
|
1. dataclass-like decorator instantiation from a dataclass_transform decorator
|
||||||
|
2. dataclass_transform decorator declaration with parameters
|
||||||
|
3. dataclass-like decorator declaration with parameters
|
||||||
|
4. dataclass-like semantics on a class from a dataclass-like decorator
|
||||||
|
"""
|
||||||
|
for c in _follow_param(value.inference_state, arguments, 0):
|
||||||
|
if c.is_class():
|
||||||
|
is_dataclass_transform = (
|
||||||
|
value.name.string_name == "dataclass_transform"
|
||||||
|
# The decorator function from dataclass_transform acting as the
|
||||||
|
# dataclass decorator.
|
||||||
|
and not isinstance(value, Decoratee)
|
||||||
|
# The decorator function from dataclass_transform acting as the
|
||||||
|
# dataclass decorator with customized parameters
|
||||||
|
and not isinstance(value, DataclassDecorator)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_dataclass_transform:
|
||||||
|
# Declare base class
|
||||||
|
return ValueSet([DataclassTransformer(c)])
|
||||||
|
else:
|
||||||
|
# Declare dataclass-like semantics on a class from a
|
||||||
|
# dataclass-like decorator
|
||||||
|
should_generate_init = value.init_param_mode
|
||||||
|
return ValueSet([DataclassWrapper(c, should_generate_init)])
|
||||||
|
elif c.is_function():
|
||||||
|
# dataclass-like decorator instantiation:
|
||||||
|
# @dataclass_transform
|
||||||
|
# def create_model()
|
||||||
|
return ValueSet(
|
||||||
|
[
|
||||||
|
DataclassDecorator(
|
||||||
|
value,
|
||||||
|
arguments=arguments,
|
||||||
|
default_init=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif (
|
||||||
|
# @dataclass_transform
|
||||||
|
# def create_model(): pass
|
||||||
|
# @create_model(init=...)
|
||||||
|
isinstance(value, Decoratee)
|
||||||
|
):
|
||||||
|
# dataclass (or like) decorator customization
|
||||||
|
return ValueSet(
|
||||||
|
[
|
||||||
|
DataclassDecorator(
|
||||||
|
value,
|
||||||
|
arguments=arguments,
|
||||||
|
default_init=value._wrapped_value.init_param_mode,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# dataclass_transform decorator with parameters; nothing impactful
|
||||||
return ValueSet([value])
|
return ValueSet([value])
|
||||||
return NO_VALUES
|
return NO_VALUES
|
||||||
|
|
||||||
|
|
||||||
class DataclassWrapper(ValueWrapper, ClassMixin):
|
|
||||||
def get_signatures(self):
|
|
||||||
param_names = []
|
|
||||||
for cls in reversed(list(self.py__mro__())):
|
|
||||||
if isinstance(cls, DataclassWrapper):
|
|
||||||
filter_ = cls.as_context().get_global_filter()
|
|
||||||
# .values ordering is not guaranteed, at least not in
|
|
||||||
# Python < 3.6, when dicts where not ordered, which is an
|
|
||||||
# implementation detail anyway.
|
|
||||||
for name in sorted(filter_.values(), key=lambda name: name.start_pos):
|
|
||||||
d = name.tree_name.get_definition()
|
|
||||||
annassign = d.children[1]
|
|
||||||
if d.type == 'expr_stmt' and annassign.type == 'annassign':
|
|
||||||
if len(annassign.children) < 4:
|
|
||||||
default = None
|
|
||||||
else:
|
|
||||||
default = annassign.children[3]
|
|
||||||
param_names.append(DataclassParamName(
|
|
||||||
parent_context=cls.parent_context,
|
|
||||||
tree_name=name.tree_name,
|
|
||||||
annotation_node=annassign.children[1],
|
|
||||||
default_node=default,
|
|
||||||
))
|
|
||||||
return [DataclassSignature(cls, param_names)]
|
|
||||||
|
|
||||||
|
|
||||||
class DataclassSignature(AbstractSignature):
|
|
||||||
def __init__(self, value, param_names):
|
|
||||||
super().__init__(value)
|
|
||||||
self._param_names = param_names
|
|
||||||
|
|
||||||
def get_param_names(self, resolve_stars=False):
|
|
||||||
return self._param_names
|
|
||||||
|
|
||||||
|
|
||||||
class DataclassParamName(BaseTreeParamName):
|
|
||||||
def __init__(self, parent_context, tree_name, annotation_node, default_node):
|
|
||||||
super().__init__(parent_context, tree_name)
|
|
||||||
self.annotation_node = annotation_node
|
|
||||||
self.default_node = default_node
|
|
||||||
|
|
||||||
def get_kind(self):
|
|
||||||
return Parameter.POSITIONAL_OR_KEYWORD
|
|
||||||
|
|
||||||
def infer(self):
|
|
||||||
if self.annotation_node is None:
|
|
||||||
return NO_VALUES
|
|
||||||
else:
|
|
||||||
return self.parent_context.infer_node(self.annotation_node)
|
|
||||||
|
|
||||||
|
|
||||||
class ItemGetterCallable(ValueWrapper):
|
class ItemGetterCallable(ValueWrapper):
|
||||||
def __init__(self, instance, args_value_set):
|
def __init__(self, instance, args_value_set):
|
||||||
super().__init__(instance)
|
super().__init__(instance)
|
||||||
@@ -798,22 +840,17 @@ _implemented = {
|
|||||||
# runtime_checkable doesn't really change anything and is just
|
# runtime_checkable doesn't really change anything and is just
|
||||||
# adding logs for infering stuff, so we can safely ignore it.
|
# adding logs for infering stuff, so we can safely ignore it.
|
||||||
'runtime_checkable': lambda value, arguments, callback: NO_VALUES,
|
'runtime_checkable': lambda value, arguments, callback: NO_VALUES,
|
||||||
|
# Python 3.11+
|
||||||
|
'dataclass_transform': _dataclass_transform,
|
||||||
|
},
|
||||||
|
'typing_extensions': {
|
||||||
|
# Python <3.11
|
||||||
|
'dataclass_transform': _dataclass_transform,
|
||||||
},
|
},
|
||||||
'dataclasses': {
|
'dataclasses': {
|
||||||
# For now this works at least better than Jedi trying to understand it.
|
# For now this works at least better than Jedi trying to understand it.
|
||||||
'dataclass': _dataclass
|
'dataclass': _dataclass
|
||||||
},
|
},
|
||||||
# attrs exposes declaration interface roughly compatible with dataclasses
|
|
||||||
# via attrs.define, attrs.frozen and attrs.mutable
|
|
||||||
# https://www.attrs.org/en/stable/names.html
|
|
||||||
'attr': {
|
|
||||||
'define': _dataclass,
|
|
||||||
'frozen': _dataclass,
|
|
||||||
},
|
|
||||||
'attrs': {
|
|
||||||
'define': _dataclass,
|
|
||||||
'frozen': _dataclass,
|
|
||||||
},
|
|
||||||
'os.path': {
|
'os.path': {
|
||||||
'dirname': _create_string_input_function(os.path.dirname),
|
'dirname': _create_string_input_function(os.path.dirname),
|
||||||
'abspath': _create_string_input_function(os.path.abspath),
|
'abspath': _create_string_input_function(os.path.abspath),
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -47,6 +47,7 @@ setup(name='jedi',
|
|||||||
'colorama',
|
'colorama',
|
||||||
'Django',
|
'Django',
|
||||||
'attrs',
|
'attrs',
|
||||||
|
'typing_extensions',
|
||||||
],
|
],
|
||||||
'qa': [
|
'qa': [
|
||||||
# latest version on 2025-06-16
|
# latest version on 2025-06-16
|
||||||
|
|||||||
@@ -318,40 +318,511 @@ def test_wraps_signature(Script, code, signature):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'start, start_params', [
|
"start, start_params, include_params",
|
||||||
['@dataclass\nclass X:', []],
|
[
|
||||||
['@dataclass(eq=True)\nclass X:', []],
|
["@dataclass\nclass X:", [], True],
|
||||||
[dedent('''
|
["@dataclass(eq=True)\nclass X:", [], True],
|
||||||
|
[
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
class Y():
|
class Y():
|
||||||
y: int
|
y: int
|
||||||
@dataclass
|
@dataclass
|
||||||
class X(Y):'''), []],
|
class X(Y):"""
|
||||||
[dedent('''
|
),
|
||||||
|
[],
|
||||||
|
True,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
@dataclass
|
@dataclass
|
||||||
class Y():
|
class Y():
|
||||||
y: int
|
y: int
|
||||||
z = 5
|
z = 5
|
||||||
@dataclass
|
@dataclass
|
||||||
class X(Y):'''), ['y']],
|
class X(Y):"""
|
||||||
]
|
),
|
||||||
|
["y"],
|
||||||
|
True,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
@dataclass
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
class Z(Y): # Not included
|
||||||
|
z = 5
|
||||||
|
@dataclass
|
||||||
|
class X(Z):"""
|
||||||
|
),
|
||||||
|
["y"],
|
||||||
|
True,
|
||||||
|
],
|
||||||
|
# init=False
|
||||||
|
[
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
@dataclass(init=False)
|
||||||
|
class X:"""
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
False,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
@dataclass(eq=True, init=False)
|
||||||
|
class X:"""
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
False,
|
||||||
|
],
|
||||||
|
# custom init
|
||||||
|
[
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
@dataclass()
|
||||||
|
class X:
|
||||||
|
def __init__(self, toto: str):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
["toto"],
|
||||||
|
False,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"direct_transformed",
|
||||||
|
"transformed_with_params",
|
||||||
|
"subclass_transformed",
|
||||||
|
"both_transformed",
|
||||||
|
"intermediate_not_transformed",
|
||||||
|
"init_false",
|
||||||
|
"init_false_multiple",
|
||||||
|
"custom_init",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
def test_dataclass_signature(Script, skip_pre_python37, start, start_params):
|
def test_dataclass_signature(
|
||||||
|
Script, skip_pre_python37, start, start_params, include_params, environment
|
||||||
|
):
|
||||||
|
if environment.version_info < (3, 8):
|
||||||
|
# Final is not yet supported
|
||||||
|
price_type = "float"
|
||||||
|
price_type_infer = "float"
|
||||||
|
else:
|
||||||
|
price_type = "Final[float]"
|
||||||
|
price_type_infer = "object"
|
||||||
|
|
||||||
|
code = dedent(
|
||||||
|
f"""
|
||||||
|
name: str
|
||||||
|
foo = 3
|
||||||
|
blob: ClassVar[str]
|
||||||
|
price: {price_type}
|
||||||
|
quantity: int = 0.0
|
||||||
|
|
||||||
|
X("""
|
||||||
|
)
|
||||||
|
|
||||||
|
code = (
|
||||||
|
"from dataclasses import dataclass\n"
|
||||||
|
+ "from typing import ClassVar, Final\n"
|
||||||
|
+ start
|
||||||
|
+ code
|
||||||
|
)
|
||||||
|
|
||||||
|
sig, = Script(code).get_signatures()
|
||||||
|
expected_params = (
|
||||||
|
[*start_params, "name", "price", "quantity"]
|
||||||
|
if include_params
|
||||||
|
else [*start_params]
|
||||||
|
)
|
||||||
|
assert [p.name for p in sig.params] == expected_params
|
||||||
|
|
||||||
|
if include_params:
|
||||||
|
quantity, = sig.params[-1].infer()
|
||||||
|
assert quantity.name == 'int'
|
||||||
|
price, = sig.params[-2].infer()
|
||||||
|
assert price.name == price_type_infer
|
||||||
|
|
||||||
|
|
||||||
|
dataclass_transform_cases = [
|
||||||
|
# Attributes on the decorated class and its base classes
|
||||||
|
# are not considered to be fields.
|
||||||
|
# 1/ Declare dataclass transformer
|
||||||
|
# Base Class
|
||||||
|
['@dataclass_transform\nclass X:', [], False],
|
||||||
|
# Base Class with params
|
||||||
|
['@dataclass_transform(eq_default=True)\nclass X:', [], False],
|
||||||
|
# Subclass
|
||||||
|
[dedent('''
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
@dataclass_transform
|
||||||
|
class X(Y):'''), [], False],
|
||||||
|
# 2/ Declare dataclass transformed
|
||||||
|
# Class based
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
class X(Y):'''), [], True],
|
||||||
|
# Class based with params
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform(eq_default=True)
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
class X(Y):'''), [], True],
|
||||||
|
# Decorator based
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model():
|
||||||
|
pass
|
||||||
|
@create_model
|
||||||
|
class X:'''), [], True],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model():
|
||||||
|
pass
|
||||||
|
class Y:
|
||||||
|
y: int
|
||||||
|
@create_model
|
||||||
|
class X(Y):'''), [], True],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model():
|
||||||
|
pass
|
||||||
|
@create_model
|
||||||
|
class Y:
|
||||||
|
y: int
|
||||||
|
@create_model
|
||||||
|
class X(Y):'''), ["y"], True],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model():
|
||||||
|
pass
|
||||||
|
@create_model
|
||||||
|
class Y:
|
||||||
|
y: int
|
||||||
|
class Z(Y):
|
||||||
|
z: int
|
||||||
|
@create_model
|
||||||
|
class X(Z):'''), ["y"], True],
|
||||||
|
# Metaclass based
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class ModelMeta():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
class ModelBase(metaclass=ModelMeta):
|
||||||
|
t: int
|
||||||
|
p = 5
|
||||||
|
class X(ModelBase):'''), [], True],
|
||||||
|
# 3/ Init custom init
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform()
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
class X(Y):
|
||||||
|
def __init__(self, toto: str):
|
||||||
|
pass
|
||||||
|
'''), ["toto"], False],
|
||||||
|
# 4/ init=false
|
||||||
|
# Class based
|
||||||
|
# WARNING: Unsupported
|
||||||
|
# [dedent('''
|
||||||
|
# @dataclass_transform
|
||||||
|
# class Y():
|
||||||
|
# y: int
|
||||||
|
# z = 5
|
||||||
|
# def __init_subclass__(
|
||||||
|
# cls,
|
||||||
|
# *,
|
||||||
|
# init: bool = False,
|
||||||
|
# )
|
||||||
|
# class X(Y):'''), [], False],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
def __init_subclass__(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
init: bool = False,
|
||||||
|
)
|
||||||
|
class X(Y, init=True):'''), [], True],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
def __init_subclass__(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
init: bool = False,
|
||||||
|
)
|
||||||
|
class X(Y, init=False):'''), [], False],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
class X(Y, init=False):'''), [], False],
|
||||||
|
# Decorator based
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model(init=False):
|
||||||
|
pass
|
||||||
|
@create_model()
|
||||||
|
class X:'''), [], False],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model(init=False):
|
||||||
|
pass
|
||||||
|
@create_model(init=True)
|
||||||
|
class X:'''), [], True],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model(init=False):
|
||||||
|
pass
|
||||||
|
@create_model(init=False)
|
||||||
|
class X:'''), [], False],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model():
|
||||||
|
pass
|
||||||
|
@create_model(init=False)
|
||||||
|
class X:'''), [], False],
|
||||||
|
# Metaclass based
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class ModelMeta():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
name,
|
||||||
|
bases,
|
||||||
|
namespace,
|
||||||
|
*,
|
||||||
|
init: bool = False,
|
||||||
|
):
|
||||||
|
...
|
||||||
|
class ModelBase(metaclass=ModelMeta):
|
||||||
|
t: int
|
||||||
|
p = 5
|
||||||
|
class X(ModelBase):'''), [], False],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class ModelMeta():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
name,
|
||||||
|
bases,
|
||||||
|
namespace,
|
||||||
|
*,
|
||||||
|
init: bool = False,
|
||||||
|
):
|
||||||
|
...
|
||||||
|
class ModelBase(metaclass=ModelMeta):
|
||||||
|
t: int
|
||||||
|
p = 5
|
||||||
|
class X(ModelBase, init=True):'''), [], True],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class ModelMeta():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
name,
|
||||||
|
bases,
|
||||||
|
namespace,
|
||||||
|
*,
|
||||||
|
init: bool = False,
|
||||||
|
):
|
||||||
|
...
|
||||||
|
class ModelBase(metaclass=ModelMeta):
|
||||||
|
t: int
|
||||||
|
p = 5
|
||||||
|
class X(ModelBase, init=False):'''), [], False],
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class ModelMeta():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
class ModelBase(metaclass=ModelMeta):
|
||||||
|
t: int
|
||||||
|
p = 5
|
||||||
|
class X(ModelBase, init=False):'''), [], False],
|
||||||
|
# 4/ Other parameters
|
||||||
|
# Class based
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
class X(Y, eq=True):'''), [], True],
|
||||||
|
# Decorator based
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
def create_model():
|
||||||
|
pass
|
||||||
|
@create_model(eq=True)
|
||||||
|
class X:'''), [], True],
|
||||||
|
# Metaclass based
|
||||||
|
[dedent('''
|
||||||
|
@dataclass_transform
|
||||||
|
class ModelMeta():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
class ModelBase(metaclass=ModelMeta):
|
||||||
|
t: int
|
||||||
|
p = 5
|
||||||
|
class X(ModelBase, eq=True):'''), [], True],
|
||||||
|
]
|
||||||
|
|
||||||
|
ids = [
|
||||||
|
"direct_transformer",
|
||||||
|
"transformer_with_params",
|
||||||
|
"subclass_transformer",
|
||||||
|
"base_transformed",
|
||||||
|
"base_transformed_with_params",
|
||||||
|
"decorator_transformed_direct",
|
||||||
|
"decorator_transformed_subclass",
|
||||||
|
"decorator_transformed_both",
|
||||||
|
"decorator_transformed_intermediate_not",
|
||||||
|
"metaclass_transformed",
|
||||||
|
"custom_init",
|
||||||
|
# "base_transformed_init_false_dataclass_init_default",
|
||||||
|
"base_transformed_init_false_dataclass_init_true",
|
||||||
|
"base_transformed_init_false_dataclass_init_false",
|
||||||
|
"base_transformed_init_default_dataclass_init_false",
|
||||||
|
"decorator_transformed_init_false_dataclass_init_default",
|
||||||
|
"decorator_transformed_init_false_dataclass_init_true",
|
||||||
|
"decorator_transformed_init_false_dataclass_init_false",
|
||||||
|
"decorator_transformed_init_default_dataclass_init_false",
|
||||||
|
"metaclass_transformed_init_false_dataclass_init_default",
|
||||||
|
"metaclass_transformed_init_false_dataclass_init_true",
|
||||||
|
"metaclass_transformed_init_false_dataclass_init_false",
|
||||||
|
"metaclass_transformed_init_default_dataclass_init_false",
|
||||||
|
"base_transformed_other_parameters",
|
||||||
|
"decorator_transformed_other_parameters",
|
||||||
|
"metaclass_transformed_other_parameters",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'start, start_params, include_params', dataclass_transform_cases, ids=ids
|
||||||
|
)
|
||||||
|
def test_extensions_dataclass_transform_signature(
|
||||||
|
Script, skip_pre_python37, start, start_params, include_params, environment
|
||||||
|
):
|
||||||
|
has_typing_ext = bool(Script('import typing_extensions').infer())
|
||||||
|
if not has_typing_ext:
|
||||||
|
raise pytest.skip("typing_extensions needed in target environment to run this test")
|
||||||
|
|
||||||
|
if environment.version_info < (3, 8):
|
||||||
|
# Final is not yet supported
|
||||||
|
price_type = "float"
|
||||||
|
price_type_infer = "float"
|
||||||
|
else:
|
||||||
|
price_type = "Final[float]"
|
||||||
|
price_type_infer = "object"
|
||||||
|
|
||||||
|
code = dedent(
|
||||||
|
f"""
|
||||||
|
name: str
|
||||||
|
foo = 3
|
||||||
|
blob: ClassVar[str]
|
||||||
|
price: {price_type}
|
||||||
|
quantity: int = 0.0
|
||||||
|
|
||||||
|
X("""
|
||||||
|
)
|
||||||
|
|
||||||
|
code = (
|
||||||
|
"from typing_extensions import dataclass_transform\n"
|
||||||
|
+ "from typing import ClassVar, Final\n"
|
||||||
|
+ start
|
||||||
|
+ code
|
||||||
|
)
|
||||||
|
|
||||||
|
(sig,) = Script(code).get_signatures()
|
||||||
|
expected_params = (
|
||||||
|
[*start_params, "name", "price", "quantity"]
|
||||||
|
if include_params
|
||||||
|
else [*start_params]
|
||||||
|
)
|
||||||
|
assert [p.name for p in sig.params] == expected_params
|
||||||
|
|
||||||
|
if include_params:
|
||||||
|
quantity, = sig.params[-1].infer()
|
||||||
|
assert quantity.name == 'int'
|
||||||
|
price, = sig.params[-2].infer()
|
||||||
|
assert price.name == price_type_infer
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataclass_transform_complete(Script):
|
||||||
|
script = Script('''\
|
||||||
|
@dataclass_transform
|
||||||
|
class Y():
|
||||||
|
y: int
|
||||||
|
z = 5
|
||||||
|
|
||||||
|
class X(Y):
|
||||||
|
name: str
|
||||||
|
foo = 3
|
||||||
|
|
||||||
|
def f(x: X):
|
||||||
|
x.na''')
|
||||||
|
completion, = script.complete()
|
||||||
|
assert completion.description == 'name: str'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"start, start_params, include_params", dataclass_transform_cases, ids=ids
|
||||||
|
)
|
||||||
|
def test_dataclass_transform_signature(
|
||||||
|
Script, skip_pre_python311, start, start_params, include_params
|
||||||
|
):
|
||||||
code = dedent('''
|
code = dedent('''
|
||||||
name: str
|
name: str
|
||||||
foo = 3
|
foo = 3
|
||||||
price: float
|
blob: ClassVar[str]
|
||||||
|
price: Final[float]
|
||||||
quantity: int = 0.0
|
quantity: int = 0.0
|
||||||
|
|
||||||
X(''')
|
X(''')
|
||||||
|
|
||||||
code = 'from dataclasses import dataclass\n' + start + code
|
code = (
|
||||||
|
"from typing import dataclass_transform\n"
|
||||||
|
+ "from typing import ClassVar, Final\n"
|
||||||
|
+ start
|
||||||
|
+ code
|
||||||
|
)
|
||||||
|
|
||||||
sig, = Script(code).get_signatures()
|
sig, = Script(code).get_signatures()
|
||||||
assert [p.name for p in sig.params] == start_params + ['name', 'price', 'quantity']
|
expected_params = (
|
||||||
|
[*start_params, "name", "price", "quantity"]
|
||||||
|
if include_params
|
||||||
|
else [*start_params]
|
||||||
|
)
|
||||||
|
assert [p.name for p in sig.params] == expected_params
|
||||||
|
|
||||||
|
if include_params:
|
||||||
quantity, = sig.params[-1].infer()
|
quantity, = sig.params[-1].infer()
|
||||||
assert quantity.name == 'int'
|
assert quantity.name == 'int'
|
||||||
price, = sig.params[-2].infer()
|
price, = sig.params[-2].infer()
|
||||||
assert price.name == 'float'
|
assert price.name == 'object'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -371,7 +842,8 @@ def test_dataclass_signature(Script, skip_pre_python37, start, start_params):
|
|||||||
z = 5
|
z = 5
|
||||||
@define
|
@define
|
||||||
class X(Y):'''), ['y']],
|
class X(Y):'''), ['y']],
|
||||||
]
|
],
|
||||||
|
ids=["define", "frozen", "define_customized", "define_subclass", "define_both"]
|
||||||
)
|
)
|
||||||
def test_attrs_signature(Script, skip_pre_python37, start, start_params):
|
def test_attrs_signature(Script, skip_pre_python37, start, start_params):
|
||||||
has_attrs = bool(Script('import attrs').infer())
|
has_attrs = bool(Script('import attrs').infer())
|
||||||
|
|||||||
Reference in New Issue
Block a user