Files
Mikhail Golubev 1da22d34fd PY-54560 Support PEP-681 dataclass_transform
`dataclass_transform` support posed a number of challenges to the current
AST/PSI stubs separation in our architecture. For the standard "dataclasses"
module and the "attrs" package API, we could rely on well-known names
and defaults to recognize and preserve the information about decorator
arguments and field specifier arguments in PSI stubs. With `dataclass_transform`
however, effectively any decorator call or extending any base class can indicate
using a "magical" API that should generate dataclass-like entities.
At the moment of building PSI stubs we can't be sure, because we can't leave
the boundaries of the current file to resolve these names and determine if these
decorators and base classes are decorated themselves with `dataclass_transform`.

To support that, we instead rely on well-known keyword argument names documented
in the spec, e.g. "kw_only" or "frozen". In other words, whenever we encounter
any decorator call or a superclass list with such keyword arguments, we generate
a `PyDataclassStub` stub for the corresponding class.

The same thing is happening with class attribute initializers, i.e. whenever
we see a function call with argument such as "default" or "kw_only" in their
RHS, we generate a `PyDataclassFieldStub` for the corresponding target expression.
Both of these stub interfaces now can contain null values for the corresponding
properties if they were not specified directly in the class definition.

Finally, for the `dataclass_transform` decorator itself, a new custom decorator stub
was introduced -- `PyDataclassTransformDecoratorStub`, it preserves its keyword
arguments, such as "keyword_only_default" or "frozen_default" controlling
the default properties of generated dataclasses.

Later, when we need concluded information about specific dataclass properties,
e.g. in `PyDataclassTypeProvider` to generate a constructor signature, or in
`PyDataclassInspection`, we try to "resolve" this incomplete information from stubs
into finalized `PyDataclassParameters` and `PyDataclassFieldParameters` that
contain non-null versions of the same fields. The main entry points for that
are `resolveDataclassParameters` and `resolveDataclassFieldParameters`.
These methods additionally handle the situations where decorators, superclass
lists and field specifiers lack any keyword arguments, and thus, there were no
automatically created custom stubs for them.

All the existing usages of `PyDataclassStub` and `PyDataclassFieldStub`
were updated to operate on `PyDataclassParameters` and `PyDataclassFieldParameters`
instead.

Counterparts of the tests on various inspection checks for the standard dataclasses
definitions were added for dataclasses created with `dataclass_transform`, even
though the spec is unclear on some aspects the expected type checker semantics, e.g.
if combining "eq=False" and "order=True" or specifying both "default" and
"default_factory" for a field should be reported.
I tried to follow common sense when enabling existing checks for such arbitrary
user-defined dataclass APIs.

GitOrigin-RevId: 4180a1e32b5e4025fc4e3ed49bb8d67af0d60e66
2024-09-09 11:34:15 +00:00

94 lines
1.6 KiB
Python

import dataclasses
from typing import ClassVar
from decorator import my_dataclass, my_field
@my_dataclass()
class A1:
bar1: int
<error descr="Fields with a default value must come after any fields without a default.">baz1</error>: int = 1
foo1: int
<error descr="Fields with a default value must come after any fields without a default.">bar2</error>: int = 2
baz2: int
foo2: int = 3
@my_dataclass()
class A2:
bar: int
baz: str = ""
foo: int = 5
@my_dataclass()
class A3:
bar1: int
baz1: ClassVar[int] = 1
foo1: int
bar2: ClassVar[int] = 2
baz2: int
foo2: int = 3
@my_dataclass()
class A4:
bar1: int
baz1: ClassVar = 1
foo1: int
bar2: ClassVar = 2
baz2: int
foo2: int = 3
@my_dataclass()
class B1:
a: int = my_field()
b: int
@my_dataclass()
class B2:
<error descr="Fields with a default value must come after any fields without a default.">a</error>: int = my_field(default=1)
b: int = my_field()
@my_dataclass()
class B3:
<error descr="Fields with a default value must come after any fields without a default.">a</error>: int = my_field(default_factory=int)
b: int = my_field()
@my_dataclass()
class C1:
x: int = dataclasses.MISSING
y: int
@my_dataclass()
class C2:
x: int = my_field(default=dataclasses.MISSING)
y: int
C2(1, 2)
@my_dataclass()
class C3:
x: int = my_field(default_factory=dataclasses.MISSING)
y: int
C3(1, 2)
@my_dataclass()
class D1:
x: int = 0
y: int = my_field(init=False)
@my_dataclass()
class E1:
foo = "bar" # <- has no type annotation, so doesn't count.
baz: str