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

Support dataclass transform
This commit is contained in:
Dave Halter
2025-09-03 13:15:39 +00:00
committed by GitHub
5 changed files with 905 additions and 83 deletions

View File

@@ -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):

View File

@@ -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 []

View File

@@ -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),

View File

@@ -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

View File

@@ -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())