From 91986e49c5c816fcfee9af55c0bf30480a5d6413 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Tue, 10 Dec 2024 11:43:26 +0200 Subject: [PATCH] PY-77433 Fix resolving qualified names in field_specifiers argument of @dataclass_transform Previously, we mistakenly tried to resolve qualified names listed in the `field_specifiers` argument of @dataclass_transform in the same scope where the dataclass itself is defined, not where the actual decorator application is located. Thus, if in the file where the dataclass is defined, a field specifier was imported differently than where @dataclass_transform was applied, we couldn't recognize a field specifier call in the RHS of an assignment as such and took it for an ordinary field default value. In particular, this is what happened with pydantic dataclasses. `pydantic.fields.Field` is usually imported as just `pydantic.Field` where user dataclasses are defined, but imported with an alias and set in the `field_specifiers` argument as `PydanticModelField` in `pydantic._internal._model_construction` where `ModelMetaclass` is defined. It was accidentally broken in f15a07836e7aeac7c46b489b4742e8248a0e6ef4 to support decorating class methods with dataclass_transform (see testData/inspections/PyDataclassInspection/DataclassTransformFieldsOrder/decorator.py). Until PyResolveUtil.resolveQualifiedNameInScope automatically traverses through containing scopes looking for a name, the file containing decorator application seems like a safe trade-off for the scope, because field specifiers are normally defined or imported somewhere at the top level. GitOrigin-RevId: de9afeb0831a52f058453fe678de229d41c26a4d --- .../com/jetbrains/python/codeInsight/PyDataclasses.kt | 2 +- .../InitByDataclassTransformOnBaseClass/dt_base.py | 4 +++- .../InitByDataclassTransformOnBaseClass/dt_class.py | 6 +++++- .../InitByDataclassTransformOnBaseClass/dt_field.py | 10 ++++++++++ .../InitByDataclassTransformOnDecorator/dt_class.py | 4 +++- .../dt_decorator.py | 4 +++- .../InitByDataclassTransformOnDecorator/dt_field.py | 10 ++++++++++ .../InitByDataclassTransformOnMetaClass/dt_class.py | 4 +++- .../InitByDataclassTransformOnMetaClass/dt_field.py | 10 ++++++++++ .../InitByDataclassTransformOnMetaClass/dt_meta.py | 4 +++- 10 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_field.py create mode 100644 python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_field.py create mode 100644 python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_field.py diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyDataclasses.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyDataclasses.kt index 4d682985a0c2..526bdb24e219 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyDataclasses.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyDataclasses.kt @@ -566,7 +566,7 @@ private fun resolveDataclassParameters( if (dataclassTransformStub != null) { val resolvedFieldSpecifiers = dataclassTransformStub.fieldSpecifiers - .flatMap { PyResolveUtil.resolveQualifiedNameInScope(it, ScopeUtil.getScopeOwner(pyClass)!!, context) } + .flatMap { PyResolveUtil.resolveQualifiedNameInScope(it, dataclassTransformDecorator.containingFile as ScopeOwner, context) } .filterIsInstance() .mapNotNull { it.qualifiedName } .map { QualifiedName.fromDottedString(it) } diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_base.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_base.py index 74d32f17f4fb..9cc4619c626e 100644 --- a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_base.py +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_base.py @@ -1,7 +1,9 @@ from typing import dataclass_transform +import dt_field -@dataclass_transform() + +@dataclass_transform(field_specifiers=(dt_field.DataclassField,)) class DataclassBase: def __init_subclass__(cls, **kwargs): import dataclasses diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_class.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_class.py index aadcb0155f74..4e6fbd8d9640 100644 --- a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_class.py +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_class.py @@ -1,6 +1,10 @@ from dt_base import DataclassBase +from dt_field import DataclassField class RecordViaBaseClass(DataclassBase, kw_only=True): id: int - name: str + name: str = DataclassField() + address: str | None = DataclassField(default=None) + + diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_field.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_field.py new file mode 100644 index 000000000000..a02202968b9a --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnBaseClass/dt_field.py @@ -0,0 +1,10 @@ +import dataclasses + + +class DataclassField: + def __init__(self, + *, + kw_only=dataclasses.MISSING, + default=dataclasses.MISSING, + default_factory=dataclasses.MISSING): + ... diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_class.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_class.py index 95f276126c91..5dbbcbf82de2 100644 --- a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_class.py +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_class.py @@ -1,7 +1,9 @@ from dt_decorator import my_dt_decorator +from dt_field import DataclassField @my_dt_decorator(kw_only=True) class RecordViaDecorator: id: int - name: str + name: str = DataclassField() + address: str | None = DataclassField(default=None) diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_decorator.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_decorator.py index cffe715eaa33..f3cbfd104ebc 100644 --- a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_decorator.py +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_decorator.py @@ -1,7 +1,9 @@ from typing import dataclass_transform, Callable +import dt_field -@dataclass_transform() + +@dataclass_transform(field_specifiers=(dt_field.DataclassField,)) def my_dt_decorator(**kwargs) -> Callable[[type], type]: import dataclasses return dataclasses.dataclass(**kwargs) # type: ignore diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_field.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_field.py new file mode 100644 index 000000000000..a02202968b9a --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnDecorator/dt_field.py @@ -0,0 +1,10 @@ +import dataclasses + + +class DataclassField: + def __init__(self, + *, + kw_only=dataclasses.MISSING, + default=dataclasses.MISSING, + default_factory=dataclasses.MISSING): + ... diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_class.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_class.py index d5fd64d8a15f..fda887b422a1 100644 --- a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_class.py +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_class.py @@ -1,6 +1,8 @@ from dt_base import DataclassBase +from dt_field import DataclassField class RecordViaMetaClass(DataclassBase, kw_only=True): id: int - name: str + name: str = DataclassField() + address: str | None = DataclassField(default=None) diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_field.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_field.py new file mode 100644 index 000000000000..a02202968b9a --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_field.py @@ -0,0 +1,10 @@ +import dataclasses + + +class DataclassField: + def __init__(self, + *, + kw_only=dataclasses.MISSING, + default=dataclasses.MISSING, + default_factory=dataclasses.MISSING): + ... diff --git a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_meta.py b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_meta.py index 57fbcab10c50..73fa013d57ae 100644 --- a/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_meta.py +++ b/python/testData/inspections/PyArgumentListInspection/InitByDataclassTransformOnMetaClass/dt_meta.py @@ -1,7 +1,9 @@ from typing import dataclass_transform +import dt_field -@dataclass_transform() + +@dataclass_transform(field_specifiers=(dt_field.DataclassField,)) class DataclassMeta(type): def __new__( cls,