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
This commit is contained in:
Mikhail Golubev
2024-06-25 11:57:05 +03:00
committed by intellij-monorepo-bot
parent a20dc6e783
commit 1da22d34fd
72 changed files with 2277 additions and 299 deletions

View File

@@ -526,13 +526,9 @@
<customClassStubType implementation="com.jetbrains.python.psi.impl.stubs.PyDataclassStubType"/>
<customDecoratorStubType implementation="com.jetbrains.python.psi.stubs.PyTestFixtureDecoratorStubType"/>
<customDecoratorStubType implementation="com.jetbrains.python.psi.stubs.PyFunctoolsWrapsDecoratorStubType"/>
<customDecoratorStubType implementation="com.jetbrains.python.psi.stubs.PyDataclassTransformDecoratorStubType"/>
<typeProvider implementation="com.jetbrains.python.psi.types.PyCollectionTypeByModificationsProvider" order="last"/>
<typeProvider implementation="com.jetbrains.python.codeInsight.decorator.PyDecoratedFunctionTypeProvider"
id="pyDecoratedFunctionTypeProvider"/>
<typeProvider implementation="com.jetbrains.python.codeInsight.decorator.PyFunctoolsWrapsDecoratedFunctionTypeProvider"
order="before pyDecoratedFunctionTypeProvider"/>
<!-- typing -->
<typeProvider implementation="com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider"/>
@@ -576,6 +572,10 @@
<canonicalPathProvider implementation="com.jetbrains.python.codeInsight.stdlib.PyStdlibCanonicalPathProvider"/>
<inspectionExtension implementation="com.jetbrains.python.inspections.stdlib.PyStdlibInspectionExtension"/>
<typeProvider implementation="com.jetbrains.python.codeInsight.stdlib.PyDataclassTypeProvider"/>
<typeProvider implementation="com.jetbrains.python.codeInsight.decorator.PyDecoratedFunctionTypeProvider"
id="pyDecoratedFunctionTypeProvider"/>
<typeProvider implementation="com.jetbrains.python.codeInsight.decorator.PyFunctoolsWrapsDecoratedFunctionTypeProvider"
order="before pyDecoratedFunctionTypeProvider"/>
<typeProvider implementation="com.jetbrains.python.codeInsight.stdlib.PyNamedTupleTypeProvider"/>
<!-- This provider should be able to override the results on anything else, including types coming from .pyi stubs -->
<typeProvider implementation="com.jetbrains.python.codeInsight.stdlib.PyStdlibTypeProvider" order="first"/>

View File

@@ -9,18 +9,21 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.util.QualifiedName
import com.jetbrains.python.PyNames
import com.jetbrains.python.codeInsight.PyDataclassParameters.Type
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.PyKnownDecoratorUtil.KnownDecorator
import com.jetbrains.python.psi.impl.PyCallExpressionHelper
import com.jetbrains.python.psi.impl.PyEvaluator
import com.jetbrains.python.psi.impl.StubAwareComputation
import com.jetbrains.python.psi.impl.stubs.PyDataclassFieldStubImpl
import com.jetbrains.python.psi.impl.stubs.PyDataclassStubImpl
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.resolve.PyResolveUtil
import com.jetbrains.python.psi.stubs.PyDataclassFieldStub
import com.jetbrains.python.psi.stubs.PyDataclassStub
import com.jetbrains.python.psi.types.PyCallableParameter
import com.jetbrains.python.psi.types.PyCallableParameterImpl
import com.jetbrains.python.psi.types.PyCallableTypeImpl
import com.jetbrains.python.psi.types.TypeEvalContext
import com.jetbrains.python.psi.stubs.PyDataclassTransformDecoratorStub
import com.jetbrains.python.psi.types.*
object PyDataclassNames {
@@ -86,6 +89,33 @@ object PyDataclassNames {
"attrs.fields_dict",
)
}
object DataclassTransform {
val DATACLASS_TRANSFORM_NAMES = setOf(
"typing.dataclass_transform",
"typing_extensions.dataclass_transform",
)
val DECORATOR_OR_CLASS_PARAMETERS = setOf(
"init",
"eq",
"order",
"unsafe_hash",
"frozen",
"match_args",
"kw_only",
"slots",
)
val FIELD_SPECIFIER_PARAMETERS = setOf(
"init",
"default",
"default_factory",
"factory",
"kw_only",
"alias",
)
}
}
/**
@@ -106,17 +136,25 @@ private val DECORATOR_AND_TYPE_AND_PARAMETERS = listOf(
Triple(KnownDecorator.ATTRS_FROZEN, PyDataclassParameters.PredefinedType.ATTRS, PyDataclassNames.Attrs.DECORATOR_PARAMETERS),
)
fun parseStdDataclassParameters(cls: PyClass, context: TypeEvalContext): PyDataclassParameters? {
return parseDataclassParameters(cls, context)?.takeIf { it.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD }
}
fun parseStdOrDataclassTransformDataclassParameters(cls: PyClass, context: TypeEvalContext): PyDataclassParameters? {
return parseDataclassParameters(cls, context)?.takeIf { it.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD ||
it.type.asPredefinedType == PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM }
}
fun parseDataclassParameters(cls: PyClass, context: TypeEvalContext): PyDataclassParameters? {
return PyUtil.getNullableParameterizedCachedValue(cls, context) {
StubAwareComputation.on(cls)
return@getNullableParameterizedCachedValue StubAwareComputation.on(cls)
.withCustomStub { stub -> stub.getCustomStub(PyDataclassStub::class.java) }
.overStub(::parseDataclassParametersFromStub)
.overAst { parseDataclassParametersFromAST(it, context) }
.overStub { dataclassStub -> resolveDataclassParameters(cls, dataclassStub ?: PyDataclassStubImpl.NON_PARAMETERIZED_DATACLASS_TRANSFORM_CANDIDATE_STUB, null, context) }
.overAst {
val (dataclassStub, dataclassParamArgMapping) = parseDataclassParametersFromAST(it, context)
?: (PyDataclassStubImpl.NON_PARAMETERIZED_DATACLASS_TRANSFORM_CANDIDATE_STUB to null)
return@overAst resolveDataclassParameters(cls, dataclassStub, dataclassParamArgMapping, context)
}
.withStubBuilder { PyDataclassStubImpl.create(it) }
.compute(context)
}
@@ -128,14 +166,15 @@ fun parseDataclassParameters(cls: PyClass, context: TypeEvalContext): PyDataclas
* @see parseStdDataclassParameters
* @see parseDataclassParameters
*/
fun parseDataclassParametersForStub(cls: PyClass): PyDataclassParameters? = parseDataclassParametersFromAST(cls, null)
fun parseDataclassParametersForStub(cls: PyClass): PyDataclassStub? = parseDataclassParametersFromAST(cls, null)?.first
fun resolvesToOmittedDefault(expression: PyExpression, type: Type): Boolean {
if (expression is PyReferenceExpression) {
val qNames = PyResolveUtil.resolveImportedElementQNameLocally(expression)
return when (type.asPredefinedType) {
PyDataclassParameters.PredefinedType.STD -> qNames.any { it.toString() == PyDataclassNames.Dataclasses.DATACLASSES_MISSING }
PyDataclassParameters.PredefinedType.STD, PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM ->
qNames.any { it.toString() == PyDataclassNames.Dataclasses.DATACLASSES_MISSING }
PyDataclassParameters.PredefinedType.ATTRS -> qNames.any { it.toString() in PyDataclassNames.Attrs.ATTRS_NOTHING }
else -> false
}
@@ -166,36 +205,83 @@ private fun decoratorAndTypeAndMarkedCallee(project: Project): List<Triple<Quali
}
}
private fun parseDataclassParametersFromAST(cls: PyClass, context: TypeEvalContext?): PyDataclassParameters? {
val decorators = cls.decoratorList ?: return null
private fun parseDataclassParametersFromAST(cls: PyClass, context: TypeEvalContext?): Pair<PyDataclassStub, DataclassParameterArgumentMapping>? {
val decorators = cls.decoratorList
val provided = PyDataclassParametersProvider.EP_NAME.extensionList.asSequence().mapNotNull {
it.getDataclassParameters(cls, context)
}.firstOrNull()
if (provided != null) return provided
for (decorator in decorators.decorators) {
val callee = (decorator.callee as? PyReferenceExpression) ?: continue
for (decoratorQualifiedName in PyResolveUtil.resolveImportedElementQNameLocally(callee)) {
val types = decoratorAndTypeAndMarkedCallee(cls.project)
val decoratorAndTypeAndMarkedCallee = types.firstOrNull { it.first == decoratorQualifiedName } ?: continue
val mapping = PyCallExpressionHelper.mapArguments(
decorator,
PyCallableTypeImpl(decoratorAndTypeAndMarkedCallee.third, null),
context ?: TypeEvalContext.codeInsightFallback(cls.project)
if (decorators != null) {
val provided = PyDataclassParametersProvider.EP_NAME.extensionList.asSequence().mapNotNull {
it.getDataclassParameters(cls, context)
}.firstOrNull()
if (provided != null) return Pair(
PyDataclassStubImpl(
type = provided.type.toString(),
decoratorName = null,
init = provided.init,
repr = provided.repr,
eq = provided.eq,
order = provided.order,
unsafeHash = provided.unsafeHash,
frozen = provided.frozen,
kwOnly = provided.kwOnly,
),
DataclassParameterArgumentMapping(
initArgument = provided.initArgument,
reprArgument = provided.reprArgument,
eqArgument = provided.eqArgument,
orderArgument = provided.orderArgument,
unsafeHashArgument = provided.unsafeHashArgument,
frozenArgument = provided.frozenArgument,
kwOnlyArgument = provided.kwOnlyArgument,
others = provided.others,
)
)
val builder = PyDataclassParametersBuilder(decoratorAndTypeAndMarkedCallee.second, decoratorAndTypeAndMarkedCallee.first, cls)
mapping
.mappedParameters
.entries
.forEach {
builder.update(it.value.name, it.key)
for (decorator in decorators.decorators) {
val callee = (decorator.callee as? PyReferenceExpression) ?: continue
for (decoratorQualifiedName in PyResolveUtil.resolveImportedElementQNameLocally(callee)) {
val types = decoratorAndTypeAndMarkedCallee(cls.project)
val decoratorAndTypeAndMarkedCallee = types.firstOrNull { it.first == decoratorQualifiedName } ?: continue
val mapping = PyCallExpressionHelper.mapArguments(
decorator,
PyCallableTypeImpl(decoratorAndTypeAndMarkedCallee.third, null),
context ?: TypeEvalContext.codeInsightFallback(cls.project)
)
val builder = PyDataclassParametersBuilder(decoratorAndTypeAndMarkedCallee.second, decoratorAndTypeAndMarkedCallee.first)
mapping
.mappedParameters
.entries
.forEach {
builder.update(it.value.name, it.key)
}
return builder.build()
}
// Process decorators that have dataclass_transform-compatible keyword arguments.
if (decorator.qualifiedName == null) continue
val decoratorKeywordArguments = decorator.arguments.filterIsInstance<PyKeywordArgument>()
if (decoratorKeywordArguments.map { it.name }.any { it in PyDataclassNames.DataclassTransform.DECORATOR_OR_CLASS_PARAMETERS }) {
val builder = PyDataclassParametersBuilder(PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM, decorator.qualifiedName!!)
decoratorKeywordArguments.forEach {
builder.update(it.keyword, it)
}
return builder.build()
}
}
}
// Process dataclass_transform-compatible keyword argument in the list of superclasses.
val superclassList = cls.superClassExpressionList
if (superclassList != null) {
val classKeywordArguments = superclassList.arguments.filterIsInstance<PyKeywordArgument>()
if (classKeywordArguments.map { it.name }.any { it in PyDataclassNames.DataclassTransform.DECORATOR_OR_CLASS_PARAMETERS }) {
val builder = PyDataclassParametersBuilder(PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM, null)
classKeywordArguments.forEach {
builder.update(it.keyword, it)
}
return builder.build()
}
}
@@ -203,21 +289,6 @@ private fun parseDataclassParametersFromAST(cls: PyClass, context: TypeEvalConte
return null
}
private fun parseDataclassParametersFromStub(stub: PyDataclassStub?): PyDataclassParameters? {
return stub?.let {
val type =
PyDataclassParametersProvider.EP_NAME.extensionList.map { e -> e.getType() }.firstOrNull { t -> t.name == it.type }
?: PyDataclassParameters.PredefinedType.values().firstOrNull { t -> t.name == it.type }
?: PyDataclassParameters.PredefinedType.STD
PyDataclassParameters(
it.initValue(), it.reprValue(), it.eqValue(), it.orderValue(), it.unsafeHashValue(), it.frozenValue(), it.kwOnly(),
null, null, null, null, null, null, null,
type, emptyMap()
)
}
}
/**
* Data describing dataclass.
*
@@ -227,22 +298,25 @@ private fun parseDataclassParametersFromStub(stub: PyDataclassStub?): PyDataclas
* This class also describes [type] of the dataclass and
* contains key-value pairs of other parameters and their expressions.
*/
data class PyDataclassParameters(val init: Boolean,
val repr: Boolean,
val eq: Boolean,
val order: Boolean,
val unsafeHash: Boolean,
val frozen: Boolean,
val kwOnly: Boolean,
val initArgument: PyExpression?,
val reprArgument: PyExpression?,
val eqArgument: PyExpression?,
val orderArgument: PyExpression?,
val unsafeHashArgument: PyExpression?,
val frozenArgument: PyExpression?,
val kwOnlyArgument: PyExpression?,
val type: Type,
val others: Map<String, PyExpression>) {
data class PyDataclassParameters(
val init: Boolean,
val repr: Boolean,
val eq: Boolean,
val order: Boolean,
val unsafeHash: Boolean,
val frozen: Boolean,
val kwOnly: Boolean,
val initArgument: PyExpression?,
val reprArgument: PyExpression?,
val eqArgument: PyExpression?,
val orderArgument: PyExpression?,
val unsafeHashArgument: PyExpression?,
val frozenArgument: PyExpression?,
val kwOnlyArgument: PyExpression?,
val type: Type,
val others: Map<String, PyExpression>,
val fieldSpecifiers: List<QualifiedName> = emptyList()
) {
interface Type {
val name: String
@@ -250,7 +324,7 @@ data class PyDataclassParameters(val init: Boolean,
}
enum class PredefinedType : Type {
STD, ATTRS;
STD, ATTRS, DATACLASS_TRANSFORM;
override val asPredefinedType: PredefinedType? = this
}
@@ -270,25 +344,14 @@ interface PyDataclassParametersProvider {
fun getDataclassParameters(cls: PyClass, context: TypeEvalContext?): PyDataclassParameters? = null
}
private class PyDataclassParametersBuilder(private val type: Type, decorator: QualifiedName, anchor: PsiElement) {
companion object {
private const val DEFAULT_INIT = true
private const val DEFAULT_REPR = true
private const val DEFAULT_EQ = true
private const val DEFAULT_ORDER = false
private const val DEFAULT_UNSAFE_HASH = false
private const val DEFAULT_FROZEN = false
private const val DEFAULT_KW_ONLY = false
}
private var init = DEFAULT_INIT
private var repr = DEFAULT_REPR
private var eq = DEFAULT_EQ
private var order = if (type.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS) DEFAULT_EQ else DEFAULT_ORDER
private var unsafeHash = DEFAULT_UNSAFE_HASH
private var frozen = decorator.toString() in PyDataclassNames.Attrs.ATTRS_FROZEN || DEFAULT_FROZEN
private var kwOnly = DEFAULT_KW_ONLY
private class PyDataclassParametersBuilder(private val type: Type, private val decorator: QualifiedName?) {
private var init: Boolean? = null
private var repr: Boolean? = null
private var eq: Boolean? = null
private var order: Boolean? = null
private var unsafeHash: Boolean? = null
private var frozen: Boolean? = null
private var kwOnly: Boolean? = null
private var initArgument: PyExpression? = null
private var reprArgument: PyExpression? = null
@@ -300,78 +363,73 @@ private class PyDataclassParametersBuilder(private val type: Type, decorator: Qu
private val others = mutableMapOf<String, PyExpression>()
init {
if (type.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS && decorator == KnownDecorator.ATTR_DATACLASS.qualifiedName) {
PyElementGenerator.getInstance(anchor.project)
.createExpressionFromText(LanguageLevel.forElement(anchor), PyNames.TRUE)
.also { others["auto_attribs"] = it }
}
}
fun update(name: String?, argument: PyExpression?) {
val value = PyUtil.peelArgument(argument)
when (name) {
"init" -> {
init = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_INIT
init = PyEvaluator.evaluateAsBooleanNoResolve(value)
initArgument = argument
return
}
"repr" -> {
repr = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_REPR
repr = PyEvaluator.evaluateAsBooleanNoResolve(value)
reprArgument = argument
return
}
"frozen" -> {
frozen = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_FROZEN
frozen = PyEvaluator.evaluateAsBooleanNoResolve(value)
frozenArgument = argument
return
}
"kw_only" -> {
kwOnly = PyEvaluator.evaluateAsBooleanNoResolve(value)
kwOnlyArgument = argument
return
}
}
if (type.asPredefinedType == PyDataclassParameters.PredefinedType.STD) {
if (type.asPredefinedType == PyDataclassParameters.PredefinedType.STD ||
type.asPredefinedType == PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM) {
when (name) {
"eq" -> {
eq = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_EQ
eq = PyEvaluator.evaluateAsBooleanNoResolve(value)
eqArgument = argument
return
}
"order" -> {
order = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_ORDER
order = PyEvaluator.evaluateAsBooleanNoResolve(value)
orderArgument = argument
return
}
"unsafe_hash" -> {
unsafeHash = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_UNSAFE_HASH
unsafeHash = PyEvaluator.evaluateAsBooleanNoResolve(value)
unsafeHashArgument = argument
return
}
"kw_only" -> {
kwOnly = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_KW_ONLY
kwOnlyArgument = argument
return
}
}
}
else if (type.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS) {
when (name) {
"eq" -> {
eq = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_EQ
eq = PyEvaluator.evaluateAsBooleanNoResolve(value)
eqArgument = argument
if (orderArgument == null) {
if (orderArgument == null && eqArgument != null) {
order = eq
orderArgument = eqArgument
}
return
}
"order" -> {
if (argument !is PyNoneLiteralExpression) {
order = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_EQ
order = PyEvaluator.evaluateAsBooleanNoResolve(value)
orderArgument = argument
}
return
}
"cmp" -> {
eq = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_EQ
eq = PyEvaluator.evaluateAsBooleanNoResolve(value)
eqArgument = argument
order = eq
@@ -379,15 +437,10 @@ private class PyDataclassParametersBuilder(private val type: Type, decorator: Qu
return
}
"hash" -> {
unsafeHash = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_UNSAFE_HASH
unsafeHash = PyEvaluator.evaluateAsBooleanNoResolve(value)
unsafeHashArgument = argument
return
}
"kw_only" -> {
kwOnly = PyEvaluator.evaluateAsBooleanNoResolve(value) ?: DEFAULT_KW_ONLY
kwOnlyArgument = argument
return
}
}
}
@@ -396,9 +449,271 @@ private class PyDataclassParametersBuilder(private val type: Type, decorator: Qu
}
}
fun build() = PyDataclassParameters(
init, repr, eq, order, unsafeHash, frozen, kwOnly,
initArgument, reprArgument, eqArgument, orderArgument, unsafeHashArgument, frozenArgument, kwOnlyArgument,
type, others
fun build(): Pair<PyDataclassStub, DataclassParameterArgumentMapping> =
Pair(
PyDataclassStubImpl(
type = type.name,
decoratorName = decorator,
init = init,
repr = repr,
eq = eq,
order = order,
unsafeHash = unsafeHash,
frozen = frozen,
kwOnly = kwOnly,
),
DataclassParameterArgumentMapping(
initArgument = initArgument,
reprArgument = reprArgument,
eqArgument = eqArgument,
orderArgument = orderArgument,
unsafeHashArgument = unsafeHashArgument,
frozenArgument = frozenArgument,
kwOnlyArgument = kwOnlyArgument,
others = others,
)
)
}
private data class DataclassParameterArgumentMapping(
val initArgument: PyExpression?,
val reprArgument: PyExpression?,
val eqArgument: PyExpression?,
val orderArgument: PyExpression?,
val unsafeHashArgument: PyExpression?,
val frozenArgument: PyExpression?,
val kwOnlyArgument: PyExpression?,
val others: Map<String, PyExpression>
)
@Suppress("NullableBooleanElvis")
private fun resolveDataclassParameters(
pyClass: PyClass,
stub: PyDataclassStub,
argumentMapping: DataclassParameterArgumentMapping?,
context: TypeEvalContext,
): PyDataclassParameters? {
val type =
PyDataclassParametersProvider.EP_NAME.extensionList.map { e -> e.getType() }.firstOrNull { t -> t.name == stub.type }
?: PyDataclassParameters.PredefinedType.entries.firstOrNull { t -> t.name == stub.type }
?: PyDataclassParameters.PredefinedType.STD
when (type.asPredefinedType) {
PyDataclassParameters.PredefinedType.STD -> {
return PyDataclassParameters(
init = stub.initValue() ?: true,
repr = stub.reprValue() ?: true,
eq = stub.eqValue() ?: true,
order = stub.orderValue() ?: false,
unsafeHash = stub.unsafeHashValue() ?: false,
frozen = stub.frozenValue() ?: false,
kwOnly = stub.kwOnly() ?: false,
initArgument = argumentMapping?.initArgument,
reprArgument = argumentMapping?.reprArgument,
eqArgument = argumentMapping?.eqArgument,
orderArgument = argumentMapping?.orderArgument,
unsafeHashArgument = argumentMapping?.unsafeHashArgument,
frozenArgument = argumentMapping?.frozenArgument,
kwOnlyArgument = argumentMapping?.kwOnlyArgument,
others = argumentMapping?.others ?: emptyMap(),
type = type,
fieldSpecifiers = listOf(QualifiedName.fromDottedString(PyDataclassNames.Dataclasses.DATACLASSES_FIELD)),
)
}
PyDataclassParameters.PredefinedType.ATTRS -> {
// TODO remove this hack, make it a proper field
val extraArguments = mutableMapOf<String, PyExpression>()
if (type.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS && stub.decoratorName() == KnownDecorator.ATTR_DATACLASS.qualifiedName) {
extraArguments["auto_attribs"] =
PyElementGenerator.getInstance(pyClass.project).createExpressionFromText(LanguageLevel.forElement(pyClass), PyNames.TRUE)
}
return PyDataclassParameters(
init = stub.initValue() ?: true,
repr = stub.reprValue() ?: true,
eq = stub.eqValue() ?: true,
order = stub.orderValue() ?: true,
unsafeHash = stub.unsafeHashValue() ?: false,
frozen = stub.frozenValue() ?: (stub.decoratorName()?.toString() in PyDataclassNames.Attrs.ATTRS_FROZEN),
kwOnly = stub.kwOnly() ?: false,
initArgument = argumentMapping?.initArgument,
reprArgument = argumentMapping?.reprArgument,
eqArgument = argumentMapping?.eqArgument,
orderArgument = argumentMapping?.orderArgument,
unsafeHashArgument = argumentMapping?.unsafeHashArgument,
frozenArgument = argumentMapping?.frozenArgument,
kwOnlyArgument = argumentMapping?.kwOnlyArgument,
others = (argumentMapping?.others ?: emptyMap()) + extraArguments,
type = type,
fieldSpecifiers = PyDataclassNames.Attrs.FIELD_FUNCTIONS.map(QualifiedName::fromDottedString),
)
}
PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM -> {
val dataclassTransformTargets = (pyClass.decoratorList?.decorators.orEmpty().asSequence().flatMap { resolveDecoratorStubSafe(it, context) }
+ sequence { yieldAll(pyClass.getAncestorClasses(context)) }
+ sequence { (pyClass.getMetaClassType(true, context) as? PyClassType)?.let { yield(it.pyClass) } })
val dataclassTransformDecorator: PyDecorator? = dataclassTransformTargets
.filterIsInstance<PyDecoratable>()
.flatMap { it.decoratorList?.decorators.orEmpty().asSequence() }
.filter { it.qualifiedName?.lastComponent == "dataclass_transform" }
.firstOrNull()
if (dataclassTransformDecorator != null) {
val dataclassTransformStub: PyDataclassTransformDecoratorStub? = StubAwareComputation.on(dataclassTransformDecorator)
.withCustomStub { dtStub -> dtStub.getCustomStub(PyDataclassTransformDecoratorStub::class.java) }
.overStub { dtStub -> dtStub }
.withStubBuilder(PyDataclassTransformDecoratorStub::create)
.compute(context)
if (dataclassTransformStub != null) {
val resolvedFieldSpecifiers = dataclassTransformStub.fieldSpecifiers
.flatMap { PyResolveUtil.resolveQualifiedNameInScope(it, ScopeUtil.getScopeOwner(dataclassTransformDecorator)!!, context) }
.filterIsInstance<PyQualifiedNameOwner>()
.mapNotNull { it.qualifiedName }
.map { QualifiedName.fromDottedString(it) }
return PyDataclassParameters(
init = stub.initValue() ?: true,
repr = stub.reprValue() ?: true,
eq = stub.eqValue() ?: dataclassTransformStub.eqDefault,
order = stub.orderValue() ?: dataclassTransformStub.orderDefault,
unsafeHash = stub.unsafeHashValue() ?: true,
frozen = stub.frozenValue() ?: dataclassTransformStub.frozenDefault,
kwOnly = stub.kwOnly() ?: dataclassTransformStub.kwOnlyDefault,
initArgument = argumentMapping?.initArgument,
reprArgument = argumentMapping?.reprArgument,
eqArgument = argumentMapping?.eqArgument,
orderArgument = argumentMapping?.orderArgument,
unsafeHashArgument = argumentMapping?.unsafeHashArgument,
frozenArgument = argumentMapping?.frozenArgument,
kwOnlyArgument = argumentMapping?.kwOnlyArgument,
others = argumentMapping?.others ?: emptyMap(),
type = type,
fieldSpecifiers = resolvedFieldSpecifiers,
)
}
}
return null
}
else -> {
// Non-standard dataclasses supported by third-party PyDataclassParametersProviders
return PyDataclassParameters(
init = stub.initValue() ?: true,
repr = stub.reprValue() ?: true,
eq = stub.eqValue() ?: true,
order = stub.orderValue() ?: false,
unsafeHash = stub.unsafeHashValue() ?: false,
frozen = stub.frozenValue() ?: false,
kwOnly = stub.kwOnly() ?: false,
initArgument = argumentMapping?.initArgument,
reprArgument = argumentMapping?.reprArgument,
eqArgument = argumentMapping?.eqArgument,
orderArgument = argumentMapping?.orderArgument,
unsafeHashArgument = argumentMapping?.unsafeHashArgument,
frozenArgument = argumentMapping?.frozenArgument,
kwOnlyArgument = argumentMapping?.kwOnlyArgument,
others = argumentMapping?.others ?: emptyMap(),
type = type,
)
}
}
}
private fun resolveDecoratorStubSafe(decorator: PyDecorator, context: TypeEvalContext): List<PsiElement> {
val resolveContext = PyResolveContext.defaultContext(context)
return StubAwareComputation.on(decorator)
.overAst { psi -> psi.multiResolveCalleeFunction(resolveContext) as List<PsiElement> }
.overStub { stub -> stub?.let { PyResolveUtil.resolveQualifiedNameInScope(it.qualifiedName, decorator.containingFile as ScopeOwner, context) } }
.overAstStubLike { psi -> psi.qualifiedName?.let { PyResolveUtil.resolveQualifiedNameInScope(it, decorator.containingFile as ScopeOwner, context) } }
.compute(context) ?: emptyList()
}
data class PyDataclassFieldParameters(
val hasDefault: Boolean,
val hasDefaultFactory: Boolean,
val initValue: Boolean,
val kwOnly: Boolean,
val alias: String?,
)
fun resolveDataclassFieldParameters(
dataclass: PyClass,
dataclassParams: PyDataclassParameters,
field: PyTargetExpression,
context: TypeEvalContext,
): PyDataclassFieldParameters? {
assert(field.containingClass == dataclass)
val assignedQName = field.assignedQName
if (assignedQName != null) {
val resolvesToMissingOrNothing = PyResolveUtil.resolveQualifiedNameInScope(assignedQName, ScopeUtil.getScopeOwner(dataclass)!!, context)
.filterIsInstance<PyQualifiedNameOwner>()
.map { it.qualifiedName }
.any { it == PyDataclassNames.Dataclasses.DATACLASSES_MISSING || it in PyDataclassNames.Attrs.ATTRS_NOTHING }
if (resolvesToMissingOrNothing) {
return PyDataclassFieldParameters(
hasDefault = false,
hasDefaultFactory = false,
initValue = dataclassParams.init,
kwOnly = dataclassParams.kwOnly,
alias = null,
)
}
}
// TODO test that the following would cause unstubbing
// val fieldStub = field.stub?.getCustomStub(PyDataclassFieldStub::class.java) ?: PyDataclassFieldStubImpl.create(field)
val fieldStub = if (field.stub != null) {
// TODO access the green stub here
field.stub.getCustomStub(PyDataclassFieldStub::class.java)
}
else {
PyDataclassFieldStubImpl.create(field)
}
if (dataclassParams.type.asPredefinedType != PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM) {
return fieldStub?.let {
PyDataclassFieldParameters(
hasDefault = fieldStub.hasDefault(),
hasDefaultFactory = fieldStub.hasDefaultFactory(),
initValue = fieldStub.initValue(),
kwOnly = fieldStub.kwOnly() ?: false,
alias = fieldStub.alias,
)
}
}
if (field.calleeName == null) return null
val fieldSpecifierDeclaration = PyResolveUtil.resolveQualifiedNameInScope(field.calleeName!!, ScopeUtil.getScopeOwner(dataclass)!!, context)
.mapNotNull {
when (it) {
is PyFunction -> it
// TODO add a test for constructors
is PyClass -> it.findInitOrNew(true, context)
else -> null
}
}
.firstOrNull {
val qualifiedName = it.qualifiedName
qualifiedName != null && QualifiedName.fromDottedString(qualifiedName) in dataclassParams.fieldSpecifiers
}
if (fieldSpecifierDeclaration == null) return null
return PyDataclassFieldParameters(
hasDefault = fieldStub?.hasDefault() ?: false,
hasDefaultFactory = fieldStub?.hasDefaultFactory() ?: false,
// TODO Should we delegate to dataclass parameters init here?
// TODO support overloading init with Literal types
initValue = fieldStub?.initValue() ?: getArgumentDefault("init", fieldSpecifierDeclaration) ?: true,
kwOnly = fieldStub?.kwOnly() ?: getArgumentDefault("kw_only", fieldSpecifierDeclaration) ?: dataclassParams.kwOnly,
alias = fieldStub?.alias,
)
}
private fun getArgumentDefault(paramName: String, function: PyFunction): Boolean? {
when (function.parameterList.findParameterByName(paramName)?.defaultValueText) {
PyNames.TRUE -> return true
PyNames.FALSE -> return false
else -> return null
}
}

View File

@@ -8,17 +8,13 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.python.PyNames
import com.jetbrains.python.ast.PyAstFunction
import com.jetbrains.python.codeInsight.*
import com.jetbrains.python.codeInsight.PyDataclassNames.Attrs
import com.jetbrains.python.codeInsight.PyDataclassNames.Dataclasses
import com.jetbrains.python.codeInsight.PyDataclassParameters
import com.jetbrains.python.codeInsight.parseDataclassParameters
import com.jetbrains.python.codeInsight.parseStdDataclassParameters
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyCallExpressionNavigator
import com.jetbrains.python.psi.impl.stubs.PyDataclassFieldStubImpl
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.stubs.PyDataclassFieldStub
import com.jetbrains.python.psi.types.*
import one.util.streamex.StreamEx
@@ -130,6 +126,7 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
val parameters = parseDataclassParameters(current, context)
if (parameters == null) {
// The base class decorated with @dataclass_transform gets filtered out already here, because for it we don't detect DataclassParameters
if (PyKnownDecoratorUtil.hasUnknownDecorator(current, context)) break else continue
}
else if (parameters.type.asPredefinedType == null) {
@@ -145,7 +142,7 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
.asReversed()
.asSequence()
.filterNot { PyTypingTypeProvider.isClassVar(it, context) }
.mapNotNull { fieldToParameter(current, it, parameters.type, ellipsis, context) }
.mapNotNull { fieldToParameter(current, it, parameters, ellipsis, context) }
.filterNot { it.first in seenNames }
.toList()
@@ -156,7 +153,9 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
fieldsInfo.forEachIndexed { index, (name, kwOnly, parameter) ->
// note: attributes are visited from inheritors to ancestors, in reversed order for every of them
if ((seenKeywordOnlyClass || index < indexOfKeywordOnlyAttribute || kwOnly) && name !in collected) {
if ((seenKeywordOnlyClass && (parameters.type == PyDataclassParameters.PredefinedType.ATTRS || kwOnly != false)
|| index < indexOfKeywordOnlyAttribute || kwOnly == true)
&& name !in collected) {
keywordOnly += name
}
@@ -164,7 +163,8 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
seenNames.add(name)
}
else if (!isKwOnlyMarkerField(parameter, context)) {
if (parameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD) {
if (parameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD ||
parameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM) {
// std: attribute that overrides ancestor's attribute does not change the order but updates type
collected[name] = collected.remove(name) ?: parameter
}
@@ -208,21 +208,23 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
return positionalOrKeyword + listOf(PyCallableParameterImpl.psi(singleStarParameter)) + keyword
}
private fun fieldToParameter(cls: PyClass,
field: PyTargetExpression,
dataclassType: PyDataclassParameters.Type,
ellipsis: PyNoneLiteralExpression,
context: TypeEvalContext): Triple<String, Boolean, PyCallableParameter?>? {
private fun fieldToParameter(
cls: PyClass,
field: PyTargetExpression,
dataclassParameters: PyDataclassParameters,
ellipsis: PyNoneLiteralExpression,
context: TypeEvalContext
): Triple<String, Boolean?, PyCallableParameter?>? {
val fieldName = field.name ?: return null
val stub = field.stub
val fieldStub = if (stub == null) PyDataclassFieldStubImpl.create(field) else stub.getCustomStub(PyDataclassFieldStub::class.java)
if (fieldStub != null && !fieldStub.initValue()) return Triple(fieldName, false, null)
if (fieldStub == null && field.annotationValue == null) return null // skip fields that are not annotated
val fieldParams = resolveDataclassFieldParameters(cls, dataclassParameters, field, context)
if (fieldParams != null && !fieldParams.initValue) return Triple(fieldName, false, null)
if (fieldParams == null && field.annotationValue == null) return null // skip fields that are not annotated
// TODO add support for name aliases
val parameterName =
fieldName.let {
if (dataclassType.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS && PyUtil.getInitialUnderscores(it) == 1) {
if (dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS && PyUtil.getInitialUnderscores(it) == 1) {
it.substring(1)
}
else it
@@ -230,12 +232,12 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
val parameter = PyCallableParameterImpl.nonPsi(
parameterName,
getTypeForParameter(cls, field, dataclassType, context),
getDefaultValueForParameter(cls, field, fieldStub, dataclassType, ellipsis, context),
getTypeForParameter(cls, field, dataclassParameters.type, context),
getDefaultValueForParameter(cls, field, fieldParams, dataclassParameters, ellipsis, context),
field
)
return Triple(parameterName, fieldStub?.kwOnly() == true, parameter)
return Triple(parameterName, fieldParams?.kwOnly, parameter)
}
private fun getTypeForParameter(cls: PyClass,
@@ -263,22 +265,24 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
return type
}
private fun getDefaultValueForParameter(cls: PyClass,
field: PyTargetExpression,
fieldStub: PyDataclassFieldStub?,
dataclassType: PyDataclassParameters.Type,
ellipsis: PyNoneLiteralExpression,
context: TypeEvalContext): PyExpression? {
return if (fieldStub == null) {
private fun getDefaultValueForParameter(
cls: PyClass,
field: PyTargetExpression,
fieldParams: PyDataclassFieldParameters?,
dataclassParams: PyDataclassParameters,
ellipsis: PyNoneLiteralExpression,
context: TypeEvalContext
): PyExpression? {
return if (fieldParams == null) {
when {
context.maySwitchToAST(field) -> field.findAssignedValue()
field.hasAssignedValue() -> ellipsis
else -> null
}
}
else if (fieldStub.hasDefault() ||
fieldStub.hasDefaultFactory() ||
dataclassType.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS &&
else if (fieldParams.hasDefault ||
fieldParams.hasDefaultFactory ||
dataclassParams.type.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS &&
methodDecoratedAsAttributeDefault(cls, field.name) != null) {
ellipsis
}

View File

@@ -11,19 +11,14 @@ import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.util.containers.ContainerUtil
import com.jetbrains.python.PyNames
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.codeInsight.*
import com.jetbrains.python.codeInsight.PyDataclassNames.Attrs
import com.jetbrains.python.codeInsight.PyDataclassNames.Dataclasses
import com.jetbrains.python.codeInsight.PyDataclassParameters
import com.jetbrains.python.codeInsight.parseDataclassParameters
import com.jetbrains.python.codeInsight.parseStdDataclassParameters
import com.jetbrains.python.codeInsight.resolvesToOmittedDefault
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.ParamHelper
import com.jetbrains.python.psi.impl.PyCallExpressionHelper
import com.jetbrains.python.psi.impl.PyEvaluator
import com.jetbrains.python.psi.impl.stubs.PyDataclassFieldStubImpl
import com.jetbrains.python.psi.stubs.PyDataclassFieldStub
import com.jetbrains.python.psi.types.*
import one.util.streamex.StreamEx
@@ -65,7 +60,8 @@ class PyDataclassInspection : PyInspection() {
val dataclassParameters = parseDataclassParameters(node, myTypeEvalContext)
if (dataclassParameters != null) {
if (dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD) {
if (dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD ||
dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM) {
processDataclassParameters(node, dataclassParameters)
val postInit = node.findMethodByName(Dataclasses.DUNDER_POST_INIT, false, myTypeEvalContext)
@@ -78,7 +74,7 @@ class PyDataclassInspection : PyInspection() {
processAsInitVar(element, postInit)?.let { localInitVars.add(it) }
}
processFieldFunctionCall(element)
processFieldFunctionCall(node, dataclassParameters, element)
}
true
@@ -95,43 +91,42 @@ class PyDataclassInspection : PyInspection() {
.findMethodByName(Attrs.DUNDER_POST_INIT, false, myTypeEvalContext)
?.also { processAttrsPostInitDefinition(it, dataclassParameters) }
processAttrsDefaultThroughDecorator(node)
processAttrsDefaultThroughDecorator(dataclassParameters, node)
processAttrsInitializersAndValidators(node)
processAttrIbFunctionCalls(node)
processAttrIbFunctionCalls(dataclassParameters, node)
}
processAnnotationsExistence(node, dataclassParameters)
PyNamedTupleInspection.inspectFieldsOrder(
node,
{
cls = node,
classFieldsFilter = {
val parameters = parseDataclassParameters(it, myTypeEvalContext)
parameters != null && !parameters.kwOnly
},
dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD,
myTypeEvalContext,
this::registerProblem,
{
val stub = it.stub
val fieldStub = if (stub == null) PyDataclassFieldStubImpl.create(it)
else stub.getCustomStub(PyDataclassFieldStub::class.java)
checkInheritedOrder = (dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD ||
dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM),
context = myTypeEvalContext,
callback = this::registerProblem,
fieldsFilter = {
val dataclassParams = parseDataclassParameters(it.containingClass!!, myTypeEvalContext)!!
val fieldParams = resolveDataclassFieldParameters(it.containingClass!!, dataclassParams, it, myTypeEvalContext)
(fieldStub == null || fieldStub.initValue() && !fieldStub.kwOnly()) &&
!(fieldStub == null && it.annotationValue == null) && // skip fields that are not annotated
!PyTypingTypeProvider.isClassVar(it, myTypeEvalContext) // skip classvars
return@inspectFieldsOrder (fieldParams == null || fieldParams.initValue && !fieldParams.kwOnly) &&
!(fieldParams == null && it.annotationValue == null) && // skip fields that are not annotated
!PyTypingTypeProvider.isClassVar(it, myTypeEvalContext) // skip classvars
},
{
val fieldStub = PyDataclassFieldStubImpl.create(it)
if (fieldStub != null) {
fieldStub.hasDefault() ||
fieldStub.hasDefaultFactory() ||
hasAssignedValue = {
val dataclassParams = parseDataclassParameters(it.containingClass!!, myTypeEvalContext)!!
val fieldParams = resolveDataclassFieldParameters(it.containingClass!!, dataclassParams, it, myTypeEvalContext)
if (fieldParams != null) {
fieldParams.hasDefault ||
fieldParams.hasDefaultFactory ||
dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.ATTRS &&
node.methods.any { m -> m.decoratorList?.findDecorator("${it.name}.default") != null }
}
else {
val assignedValue = it.findAssignedValue()
assignedValue != null && !resolvesToOmittedDefault(assignedValue, dataclassParameters.type)
it.hasAssignedValue()
}
}
)
@@ -327,7 +322,7 @@ class PyDataclassInspection : PyInspection() {
var frozenInHierarchy: Boolean? = null
for (current in StreamEx.of(cls).append(cls.getAncestorClasses(myTypeEvalContext))) {
val currentFrozen = parseStdDataclassParameters(current, myTypeEvalContext)?.frozen ?: continue
val currentFrozen = parseStdOrDataclassTransformDataclassParameters(current, myTypeEvalContext)?.frozen ?: continue
if (frozenInHierarchy == null) {
frozenInHierarchy = currentFrozen
@@ -431,7 +426,7 @@ class PyDataclassInspection : PyInspection() {
}
}
private fun processAttrsDefaultThroughDecorator(cls: PyClass) {
private fun processAttrsDefaultThroughDecorator(dataclassParameters: PyDataclassParameters, cls: PyClass) {
val initializers = mutableMapOf<String, MutableList<PyFunction>>()
cls.methods.forEach { method ->
@@ -449,10 +444,10 @@ class PyDataclassInspection : PyInspection() {
if (attribute != null) {
initializers.computeIfAbsent(name, { mutableListOf() }).add(method)
val stub = PyDataclassFieldStubImpl.create(attribute)
if (stub != null && (stub.hasDefault() || stub.hasDefaultFactory())) {
val fieldParams = resolveDataclassFieldParameters(cls, dataclassParameters, attribute, myTypeEvalContext)
if (fieldParams != null && (fieldParams.hasDefault || fieldParams.hasDefaultFactory)) {
registerProblem(method.nameIdentifier,
PyPsiBundle.message("INSP.dataclasses.attribute.default.set.using.method", "${stub.calleeName}()"),
PyPsiBundle.message("INSP.dataclasses.attribute.default.set.using.method", "${attribute.calleeName}()"),
ProblemHighlightType.GENERIC_ERROR)
}
}
@@ -508,9 +503,12 @@ class PyDataclassInspection : PyInspection() {
private fun processAnnotationsExistence(cls: PyClass, dataclassParameters: PyDataclassParameters) {
if (dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD ||
dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM ||
PyEvaluator.evaluateAsBoolean(PyUtil.peelArgument(dataclassParameters.others["auto_attribs"]), false)) {
cls.processClassLevelDeclarations { element, _ ->
if (element is PyTargetExpression && element.annotation == null && PyDataclassFieldStubImpl.create(element) != null) {
if (element is PyTargetExpression
&& element.annotation == null
&& resolveDataclassFieldParameters(cls, dataclassParameters, element, myTypeEvalContext) != null) {
registerProblem(element, PyPsiBundle.message("INSP.dataclasses.attribute.lacks.type.annotation", element.name),
ProblemHighlightType.GENERIC_ERROR)
}
@@ -520,15 +518,15 @@ class PyDataclassInspection : PyInspection() {
}
}
private fun processAttrIbFunctionCalls(cls: PyClass) {
cls.processClassLevelDeclarations { element, _ ->
private fun processAttrIbFunctionCalls(dataclassParameters: PyDataclassParameters, dataclass: PyClass) {
dataclass.processClassLevelDeclarations { element, _ ->
if (element is PyTargetExpression) {
val fieldParams = resolveDataclassFieldParameters(dataclass, dataclassParameters, element, myTypeEvalContext)
val call = element.findAssignedValue() as? PyCallExpression
val stub = PyDataclassFieldStubImpl.create(element)
if (call != null && stub != null) {
if (stub.hasDefaultFactory()) {
if (stub.hasDefault()) {
if (call != null && fieldParams != null) {
if (fieldParams.hasDefaultFactory) {
if (fieldParams.hasDefault) {
registerProblem(call.argumentList, PyPsiBundle.message("INSP.dataclasses.cannot.specify.both.default.and.factory"),
ProblemHighlightType.GENERIC_ERROR)
}
@@ -565,18 +563,18 @@ class PyDataclassInspection : PyInspection() {
return null
}
private fun processFieldFunctionCall(field: PyTargetExpression) {
val fieldStub = PyDataclassFieldStubImpl.create(field) ?: return
private fun processFieldFunctionCall(dataclass: PyClass, dataclassParameters: PyDataclassParameters, field: PyTargetExpression) {
val fieldStub = resolveDataclassFieldParameters(dataclass, dataclassParameters, field, myTypeEvalContext) ?: return
val call = field.findAssignedValue() as? PyCallExpression ?: return
if (PyTypingTypeProvider.isClassVar(field, myTypeEvalContext) || isInitVar(field)) {
if (fieldStub.hasDefaultFactory()) {
if (fieldStub.hasDefaultFactory) {
registerProblem(call.getKeywordArgument("default_factory"),
PyPsiBundle.message("INSP.dataclasses.field.cannot.have.default.factory"),
ProblemHighlightType.GENERIC_ERROR)
}
}
else if (fieldStub.hasDefault() && fieldStub.hasDefaultFactory()) {
else if (fieldStub.hasDefault && fieldStub.hasDefaultFactory) {
registerProblem(call.argumentList, PyPsiBundle.message("INSP.dataclasses.cannot.specify.both.default.and.default.factory"),
ProblemHighlightType.GENERIC_ERROR)
}

View File

@@ -60,7 +60,7 @@ public class PyFileElementType extends IStubFileElementType<PyFileStub> {
@Override
public int getStubVersion() {
// Don't forget to update versions of indexes that use the updated stub-based elements
return 93;
return 94;
}
@Nullable

View File

@@ -6,13 +6,16 @@ package com.jetbrains.python.psi.impl.stubs
import com.intellij.psi.stubs.StubInputStream
import com.intellij.psi.stubs.StubOutputStream
import com.intellij.psi.util.QualifiedName
import com.intellij.util.io.DataInputOutputUtil
import com.jetbrains.python.PyNames
import com.jetbrains.python.codeInsight.PyDataclassNames
import com.jetbrains.python.codeInsight.PyDataclassNames.Attrs
import com.jetbrains.python.codeInsight.PyDataclassNames.Dataclasses
import com.jetbrains.python.codeInsight.PyDataclassParameters
import com.jetbrains.python.codeInsight.resolvesToOmittedDefault
import com.jetbrains.python.psi.PyCallExpression
import com.jetbrains.python.psi.PyReferenceExpression
import com.jetbrains.python.psi.PyStringLiteralExpression
import com.jetbrains.python.psi.PyTargetExpression
import com.jetbrains.python.psi.impl.PyEvaluator
import com.jetbrains.python.psi.resolve.PyResolveUtil
@@ -20,16 +23,17 @@ import com.jetbrains.python.psi.stubs.PyDataclassFieldStub
import java.io.IOException
class PyDataclassFieldStubImpl private constructor(private val calleeName: QualifiedName,
private val parameters: FieldParameters) : PyDataclassFieldStub {
private val hasDefault: Boolean,
private val hasDefaultFactory: Boolean,
private val initValue: Boolean,
private val kwOnly: Boolean?,
private val alias: String?,
) : PyDataclassFieldStub {
companion object {
fun create(expression: PyTargetExpression): PyDataclassFieldStub? {
val value = expression.findAssignedValue() as? PyCallExpression ?: return null
val callee = value.callee as? PyReferenceExpression ?: return null
val calleeNameAndType = calculateCalleeNameAndType(callee) ?: return null
val parameters = analyzeArguments(value, calleeNameAndType.second) ?: return null
return PyDataclassFieldStubImpl(calleeNameAndType.first, parameters)
val callExpr = expression.findAssignedValue() as? PyCallExpression ?: return null
val predefinedType = calculateCalleeNameAndType(callExpr) ?: return null
return analyzeArguments(callExpr, predefinedType)
}
@Throws(IOException::class)
@@ -38,55 +42,96 @@ class PyDataclassFieldStubImpl private constructor(private val calleeName: Quali
val hasDefault = stream.readBoolean()
val hasDefaultFactory = stream.readBoolean()
val initValue = stream.readBoolean()
val kwOnly = stream.readBoolean()
val kwOnly = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
val alias = stream.readNameString()
return PyDataclassFieldStubImpl(
QualifiedName.fromDottedString(calleeName),
FieldParameters(hasDefault, hasDefaultFactory, initValue, kwOnly)
calleeName = QualifiedName.fromDottedString(calleeName),
hasDefault = hasDefault,
hasDefaultFactory = hasDefaultFactory,
initValue = initValue,
kwOnly = kwOnly,
alias = alias,
)
}
private fun calculateCalleeNameAndType(callee: PyReferenceExpression): Pair<QualifiedName, PyDataclassParameters.PredefinedType>? {
val qualifiedName = callee.asQualifiedName() ?: return null
private fun calculateCalleeNameAndType(fieldInitializer: PyCallExpression): PyDataclassParameters.PredefinedType? {
val callee = fieldInitializer.callee as? PyReferenceExpression ?: return null
for (originalQName in PyResolveUtil.resolveImportedElementQNameLocally(callee)) {
when (originalQName.toString()) {
Dataclasses.DATACLASSES_FIELD -> return qualifiedName to PyDataclassParameters.PredefinedType.STD
in Attrs.FIELD_FUNCTIONS -> return qualifiedName to PyDataclassParameters.PredefinedType.ATTRS
Dataclasses.DATACLASSES_FIELD -> return PyDataclassParameters.PredefinedType.STD
in Attrs.FIELD_FUNCTIONS -> return PyDataclassParameters.PredefinedType.ATTRS
}
}
// Any function call with these keyword arguments is a potential dataclass field initializer
if (PyDataclassNames.DataclassTransform.FIELD_SPECIFIER_PARAMETERS.any { fieldInitializer.getKeywordArgument(it) != null }) {
return PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM
}
return null
}
private fun analyzeArguments(call: PyCallExpression, type: PyDataclassParameters.PredefinedType): FieldParameters? {
private fun analyzeArguments(call: PyCallExpression, type: PyDataclassParameters.PredefinedType): PyDataclassFieldStub? {
val qualifiedName = (call.callee as? PyReferenceExpression)?.asQualifiedName() ?: return null
val initValue = PyEvaluator.evaluateAsBooleanNoResolve(call.getKeywordArgument("init"), true)
val kwOnly = PyEvaluator.evaluateAsBooleanNoResolve(call.getKeywordArgument("kw_only"))
val default = call.getKeywordArgument("default")
val defaultFactory = call.getKeywordArgument("default_factory")
val factory = call.getKeywordArgument("factory")
val alias = (call.getKeywordArgument("alias") as? PyStringLiteralExpression)?.stringValue
if (type == PyDataclassParameters.PredefinedType.STD) {
val default = call.getKeywordArgument("default")
val defaultFactory = call.getKeywordArgument("default_factory")
val kwOnly = PyEvaluator.evaluateAsBooleanNoResolve(call.getKeywordArgument("kw_only"), false)
return FieldParameters(default != null && !resolvesToOmittedDefault(default, type),
defaultFactory != null && !resolvesToOmittedDefault(defaultFactory, type),
initValue,
kwOnly)
return PyDataclassFieldStubImpl(
calleeName = qualifiedName,
hasDefault = default != null && !resolvesToOmittedDefault(default, type),
hasDefaultFactory = defaultFactory != null && !resolvesToOmittedDefault(defaultFactory, type),
initValue = initValue,
kwOnly = kwOnly,
alias = null
)
}
else if (type == PyDataclassParameters.PredefinedType.ATTRS) {
val default = call.getKeywordArgument("default")
val hasFactory = call.getKeywordArgument("factory").let { it != null && it.text != PyNames.NONE }
val kwOnly = PyEvaluator.evaluateAsBooleanNoResolve(call.getKeywordArgument("kw_only"), false)
val hasFactory = factory.let { it != null && it.text != PyNames.NONE }
if (default != null && !resolvesToOmittedDefault(default, type)) {
val callee = (default as? PyCallExpression)?.callee as? PyReferenceExpression
val hasFactoryInDefault =
callee != null &&
PyResolveUtil.resolveImportedElementQNameLocally(callee).any { it.toString() in Attrs.ATTRS_FACTORY }
return FieldParameters(!hasFactoryInDefault, hasFactory || hasFactoryInDefault, initValue, kwOnly)
return PyDataclassFieldStubImpl(
calleeName = qualifiedName,
hasDefault = !hasFactoryInDefault,
hasDefaultFactory = hasFactory || hasFactoryInDefault,
initValue = initValue,
kwOnly = kwOnly,
alias = alias
)
}
return FieldParameters(false, hasFactory, initValue, kwOnly)
return PyDataclassFieldStubImpl(
calleeName = qualifiedName,
hasDefault = false,
hasDefaultFactory = hasFactory,
initValue = initValue,
kwOnly = kwOnly,
alias = alias
)
}
else if (type == PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM) {
return PyDataclassFieldStubImpl(
calleeName = qualifiedName,
// dataclasses.MISSING is not mentioned in the spec, but because dataclasses.KW_ONLY is supported,
// this one is special-cases as well
hasDefault = default != null && !resolvesToOmittedDefault(default, type),
hasDefaultFactory = defaultFactory != null && !resolvesToOmittedDefault(defaultFactory, type) ||
factory != null && !resolvesToOmittedDefault(factory, type),
initValue = initValue, // TODO How should we handle custom field specifiers where init=False by default
kwOnly = kwOnly,
alias = alias
)
}
return null
@@ -99,29 +144,28 @@ class PyDataclassFieldStubImpl private constructor(private val calleeName: Quali
override fun serialize(stream: StubOutputStream) {
stream.writeName(calleeName.toString())
stream.writeBoolean(parameters.hasDefault)
stream.writeBoolean(parameters.hasDefaultFactory)
stream.writeBoolean(parameters.initValue)
stream.writeBoolean(parameters.kwOnly)
stream.writeBoolean(hasDefault)
stream.writeBoolean(hasDefaultFactory)
stream.writeBoolean(initValue)
DataInputOutputUtil.writeNullable(stream, kwOnly, stream::writeBoolean)
stream.writeName(alias)
}
override fun getCalleeName(): QualifiedName = calleeName
override fun hasDefault(): Boolean = parameters.hasDefault
override fun hasDefaultFactory(): Boolean = parameters.hasDefaultFactory
override fun initValue(): Boolean = parameters.initValue
override fun kwOnly(): Boolean = parameters.kwOnly
override fun hasDefault(): Boolean = hasDefault
override fun hasDefaultFactory(): Boolean = hasDefaultFactory
override fun initValue(): Boolean = initValue
override fun kwOnly(): Boolean? = kwOnly
override fun getAlias(): String? = alias
override fun toString(): String {
return "PyDataclassFieldStubType(callee=$calleeName" +
", hasDefault=${parameters.hasDefault}" +
", hasDefaultFactory=${parameters.hasDefaultFactory}" +
", initValue=${parameters.initValue}" +
", kwOnly=${parameters.kwOnly}" +
", hasDefault=${hasDefault}" +
", hasDefaultFactory=${hasDefaultFactory}" +
", initValue=${initValue}" +
", kwOnly=${kwOnly}" +
", alias=${alias}" +
")"
}
private data class FieldParameters(val hasDefault: Boolean,
val hasDefaultFactory: Boolean,
val initValue: Boolean,
val kwOnly: Boolean)
}

View File

@@ -3,6 +3,9 @@ package com.jetbrains.python.psi.impl.stubs
import com.intellij.psi.stubs.StubInputStream
import com.intellij.psi.stubs.StubOutputStream
import com.intellij.psi.util.QualifiedName
import com.intellij.util.io.DataInputOutputUtil
import com.jetbrains.python.codeInsight.PyDataclassParameters.PredefinedType
import com.jetbrains.python.codeInsight.parseDataclassParametersForStub
import com.jetbrains.python.psi.PyClass
import com.jetbrains.python.psi.stubs.PyDataclassStub
@@ -20,36 +23,48 @@ class PyDataclassStubType : PyCustomClassStubType<PyDataclassStub>() {
}
}
class PyDataclassStubImpl private constructor(private val type: String,
private val init: Boolean,
private val repr: Boolean,
private val eq: Boolean,
private val order: Boolean,
private val unsafeHash: Boolean,
private val frozen: Boolean,
private val kwOnly: Boolean) : PyDataclassStub {
class PyDataclassStubImpl(
private val type: String,
private val decoratorName: QualifiedName?,
private val init: Boolean?,
private val repr: Boolean?,
private val eq: Boolean?,
private val order: Boolean?,
private val unsafeHash: Boolean?,
private val frozen: Boolean?,
private val kwOnly: Boolean?,
) : PyDataclassStub {
companion object {
val NON_PARAMETERIZED_DATACLASS_TRANSFORM_CANDIDATE_STUB = PyDataclassStubImpl(
type = PredefinedType.DATACLASS_TRANSFORM.name,
decoratorName = null,
init = null,
repr = null,
eq = null,
order = null,
unsafeHash = null,
frozen = null,
kwOnly = null
)
fun create(cls: PyClass): PyDataclassStub? {
return parseDataclassParametersForStub(cls)?.let {
PyDataclassStubImpl(it.type.toString(), it.init, it.repr, it.eq, it.order, it.unsafeHash, it.frozen, it.kwOnly)
}
return parseDataclassParametersForStub(cls)
}
@Throws(IOException::class)
fun deserialize(stream: StubInputStream): PyDataclassStub? {
val type = stream.readNameString() ?: return null
val decoratorName = QualifiedName.deserialize(stream)
val init = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
val repr = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
val eq = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
val order = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
val unsafeHash = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
val frozen = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
val kwOnly = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
val init = stream.readBoolean()
val repr = stream.readBoolean()
val eq = stream.readBoolean()
val order = stream.readBoolean()
val unsafeHash = stream.readBoolean()
val frozen = stream.readBoolean()
val kwOnly = stream.readBoolean()
return PyDataclassStubImpl(type, init, repr, eq, order, unsafeHash, frozen, kwOnly)
return PyDataclassStubImpl(type, decoratorName, init, repr, eq, order, unsafeHash, frozen, kwOnly)
}
}
@@ -57,21 +72,23 @@ class PyDataclassStubImpl private constructor(private val type: String,
override fun serialize(stream: StubOutputStream) {
stream.writeName(type)
stream.writeBoolean(init)
stream.writeBoolean(repr)
stream.writeBoolean(eq)
stream.writeBoolean(order)
stream.writeBoolean(unsafeHash)
stream.writeBoolean(frozen)
stream.writeBoolean(kwOnly)
QualifiedName.serialize(decoratorName, stream)
DataInputOutputUtil.writeNullable(stream, init, stream::writeBoolean)
DataInputOutputUtil.writeNullable(stream, repr, stream::writeBoolean)
DataInputOutputUtil.writeNullable(stream, eq, stream::writeBoolean)
DataInputOutputUtil.writeNullable(stream, order, stream::writeBoolean)
DataInputOutputUtil.writeNullable(stream, unsafeHash, stream::writeBoolean)
DataInputOutputUtil.writeNullable(stream, frozen, stream::writeBoolean)
DataInputOutputUtil.writeNullable(stream, kwOnly, stream::writeBoolean)
}
override fun getType(): String = type
override fun initValue(): Boolean = init
override fun reprValue(): Boolean = repr
override fun eqValue(): Boolean = eq
override fun orderValue(): Boolean = order
override fun unsafeHashValue(): Boolean = unsafeHash
override fun frozenValue(): Boolean = frozen
override fun kwOnly(): Boolean = kwOnly
override fun decoratorName(): QualifiedName? = decoratorName
override fun initValue(): Boolean? = init
override fun reprValue(): Boolean? = repr
override fun eqValue(): Boolean? = eq
override fun orderValue(): Boolean? = order
override fun unsafeHashValue(): Boolean? = unsafeHash
override fun frozenValue(): Boolean? = frozen
override fun kwOnly(): Boolean? = kwOnly
}

View File

@@ -4,6 +4,7 @@
package com.jetbrains.python.psi.stubs;
import com.jetbrains.python.psi.impl.stubs.CustomTargetExpressionStub;
import org.jetbrains.annotations.Nullable;
public interface PyDataclassFieldStub extends CustomTargetExpressionStub {
@@ -23,7 +24,13 @@ public interface PyDataclassFieldStub extends CustomTargetExpressionStub {
boolean initValue();
/**
* @return true if field is used in `__init__` as a keyword-only parameter (attrs).
* Whether the corresponding field should be used in `__init__` as a keyword-only parameter.
* <p>
* When {@code null}, this property of the field depends on the value of {@code kw_only} argument of
* a {@code @dataclass_transform}-powered decorator or a base class and {@code kw_only_default} parameter default
* of {@code @dataclass_transform} itself.
*/
boolean kwOnly();
@Nullable Boolean kwOnly();
@Nullable String getAlias();
}

View File

@@ -1,8 +1,10 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.psi.stubs;
import com.intellij.psi.util.QualifiedName;
import com.jetbrains.python.psi.impl.stubs.PyCustomClassStub;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface PyDataclassStub extends PyCustomClassStub {
@@ -12,45 +14,47 @@ public interface PyDataclassStub extends PyCustomClassStub {
@NotNull
String getType();
@Nullable QualifiedName decoratorName();
/**
* @return value of `init` parameter or
* its default value if it is not specified or could not be evaluated.
*/
boolean initValue();
@Nullable Boolean initValue();
/**
* @return value of `repr` parameter or
* its default value if it is not specified or could not be evaluated.
*/
boolean reprValue();
@Nullable Boolean reprValue();
/**
* @return value of `eq` (std) or `cmp` (attrs) parameter or
* its default value if it is not specified or could not be evaluated.
*/
boolean eqValue();
@Nullable Boolean eqValue();
/**
* @return value of `order` (std) or `cmp` (attrs) parameter or
* its default value if it is not specified or could not be evaluated.
*/
boolean orderValue();
@Nullable Boolean orderValue();
/**
* @return value of `unsafe_hash` (std) or `hash` (attrs) parameter or
* its default value if it is not specified or could not be evaluated.
*/
boolean unsafeHashValue();
@Nullable Boolean unsafeHashValue();
/**
* @return value of `frozen` parameter or
* its default value if it is not specified or could not be evaluated.
*/
boolean frozenValue();
@Nullable Boolean frozenValue();
/**
* @return value of `kw_only` (attrs) parameter or
* its default value if it is not specified or could not be evaluated.
*/
boolean kwOnly();
@Nullable Boolean kwOnly();
}

View File

@@ -0,0 +1,77 @@
package com.jetbrains.python.psi.stubs
import com.intellij.psi.stubs.StubInputStream
import com.intellij.psi.stubs.StubOutputStream
import com.intellij.psi.util.QualifiedName
import com.intellij.util.io.DataInputOutputUtil
import com.jetbrains.python.codeInsight.PyDataclassNames
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyEvaluator
import com.jetbrains.python.psi.impl.stubs.PyCustomDecoratorStub
import com.jetbrains.python.psi.resolve.PyResolveUtil
import java.io.IOException
/**
* Represents arguments of `typing.dataclass_transform` or `typing_extensions.dataclass_transform` decorators
* applied to another decorator function or a base class for creating new dataclasses.
*
* Omitted arguments are expected to be replaced by their default values by the caller creating a new instance.
*
* See https://docs.python.org/3.13/library/typing.html#typing.dataclass_transform.
*/
data class PyDataclassTransformDecoratorStub(
val eqDefault: Boolean,
val orderDefault: Boolean,
val kwOnlyDefault: Boolean,
val frozenDefault: Boolean,
val fieldSpecifiers: List<QualifiedName>,
) : PyCustomDecoratorStub {
override fun getTypeClass(): Class<PyDataclassTransformDecoratorStubType> {
return PyDataclassTransformDecoratorStubType::class.java
}
@Throws(IOException::class)
override fun serialize(stream: StubOutputStream) {
stream.writeBoolean(eqDefault)
stream.writeBoolean(orderDefault)
stream.writeBoolean(kwOnlyDefault)
stream.writeBoolean(frozenDefault)
DataInputOutputUtil.writeSeq(stream, fieldSpecifiers) { QualifiedName.serialize(it, stream) }
}
companion object {
fun deserialize(stream: StubInputStream): PyDataclassTransformDecoratorStub {
val eqDefault = stream.readBoolean()
val orderDefault = stream.readBoolean()
val kwOnlyDefault = stream.readBoolean()
val frozenDefault = stream.readBoolean()
val fieldSpecifiers = DataInputOutputUtil.readSeq(stream) { QualifiedName.deserialize(stream)!! }
return PyDataclassTransformDecoratorStub(
eqDefault = eqDefault,
orderDefault = orderDefault,
kwOnlyDefault = kwOnlyDefault,
frozenDefault = frozenDefault,
fieldSpecifiers = fieldSpecifiers,
)
}
fun create(decorator: PyDecorator): PyDataclassTransformDecoratorStub? {
val decoratorName = decorator.callee as? PyReferenceExpression ?: return null
val importedAsDataclassTransform = PyResolveUtil.resolveImportedElementQNameLocally(decoratorName)
.any { it.toString() in PyDataclassNames.DataclassTransform.DATACLASS_TRANSFORM_NAMES }
if (importedAsDataclassTransform) {
val fieldSpecifierList = PyUtil.peelArgument(decorator.getKeywordArgument("field_specifiers")) as? PyTupleExpression
return PyDataclassTransformDecoratorStub(
eqDefault = PyEvaluator.evaluateAsBooleanNoResolve(decorator.getKeywordArgument("eq_default"), true),
orderDefault = PyEvaluator.evaluateAsBooleanNoResolve(decorator.getKeywordArgument("order_default"), false),
kwOnlyDefault = PyEvaluator.evaluateAsBooleanNoResolve(decorator.getKeywordArgument("kw_only_default"), false),
frozenDefault = PyEvaluator.evaluateAsBooleanNoResolve(decorator.getKeywordArgument("frozen_default"), false),
fieldSpecifiers = fieldSpecifierList?.elements?.mapNotNull { (it as? PyReferenceExpression)?.asQualifiedName() } ?: emptyList(),
)
}
return null
}
}
}

View File

@@ -0,0 +1,13 @@
package com.jetbrains.python.psi.stubs
import com.intellij.psi.stubs.StubInputStream
import com.jetbrains.python.psi.PyDecorator
import com.jetbrains.python.psi.impl.stubs.PyCustomDecoratorStubType
import java.io.IOException
class PyDataclassTransformDecoratorStubType : PyCustomDecoratorStubType<PyDataclassTransformDecoratorStub> {
override fun createStub(decorator: PyDecorator): PyDataclassTransformDecoratorStub? = PyDataclassTransformDecoratorStub.create(decorator)
@Throws(IOException::class)
override fun deserializeStub(stream: StubInputStream): PyDataclassTransformDecoratorStub = PyDataclassTransformDecoratorStub.deserialize(stream)
}

View File

@@ -0,0 +1,3 @@
from dt_class import RecordViaBaseClass
RecordViaBaseClass(<warning descr="Unexpected argument">1</warning>, <warning descr="Unexpected argument">"foo"</warning><warning descr="Parameter 'id' unfilled"><warning descr="Parameter 'name' unfilled">)</warning></warning>

View File

@@ -0,0 +1,8 @@
from typing import dataclass_transform
@dataclass_transform()
class DataclassBase:
def __init_subclass__(cls, **kwargs):
import dataclasses
cls.__dir__ = dataclasses.dataclass(**kwargs)(cls).__dir__

View File

@@ -0,0 +1,6 @@
from dt_base import DataclassBase
class RecordViaBaseClass(DataclassBase, kw_only=True):
id: int
name: str

View File

@@ -0,0 +1,3 @@
from dt_class import RecordViaDecorator
RecordViaDecorator(<warning descr="Unexpected argument">1</warning>, <warning descr="Unexpected argument">"foo"</warning><warning descr="Parameter 'id' unfilled"><warning descr="Parameter 'name' unfilled">)</warning></warning>

View File

@@ -0,0 +1,7 @@
from dt_decorator import my_dt_decorator
@my_dt_decorator(kw_only=True)
class RecordViaDecorator:
id: int
name: str

View File

@@ -0,0 +1,7 @@
from typing import dataclass_transform, Callable
@dataclass_transform()
def my_dt_decorator(**kwargs) -> Callable[[type], type]:
import dataclasses
return dataclasses.dataclass(**kwargs) # type: ignore

View File

@@ -0,0 +1,3 @@
from dt_class import RecordViaMetaClass
RecordViaMetaClass(<warning descr="Unexpected argument">1</warning>, <warning descr="Unexpected argument">"foo"</warning><warning descr="Parameter 'id' unfilled"><warning descr="Parameter 'name' unfilled">)</warning></warning>

View File

@@ -0,0 +1,5 @@
from dt_meta import DataclassMeta
class DataclassBase(metaclass=DataclassMeta):
pass

View File

@@ -0,0 +1,6 @@
from dt_base import DataclassBase
class RecordViaMetaClass(DataclassBase, kw_only=True):
id: int
name: str

View File

@@ -0,0 +1,17 @@
from typing import dataclass_transform
@dataclass_transform()
class DataclassMeta(type):
def __new__(
cls,
name,
bases,
namespace,
**kwargs,
):
import dataclasses
return dataclasses.dataclass(**kwargs)(super().__new__(cls, name, bases, namespace)) # type: ignore

View File

@@ -0,0 +1,26 @@
from decorator import my_dataclass
@my_dataclass(order=False)
class Test1:
def __gt__(self, other):
pass
@my_dataclass()
class Test2:
def __gt__(self, other):
pass
print(Test1() < Test1())
print(Test2() < Test2())
print(Test1() > Test1())
print(Test2() > Test2())
print(Test1 < Test1)
print(Test2 < Test2)
print(Test1 > Test1)
print(Test2 > Test2)

View File

@@ -0,0 +1,13 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(order_default=True)
def my_dataclass_order_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,23 @@
from decorator import my_dataclass_order_default, my_dataclass
@my_dataclass_order_default()
class A1:
x: int = 0
@my_dataclass()
class A2:
y: int = 0
print(A1(1) <error descr="'__lt__' not supported between instances of 'A1' and 'A2'"><</error> A2(2))
print(A1(1) <error descr="'__le__' not supported between instances of 'A1' and 'A2'"><=</error> A2(2))
print(A1(1) <error descr="'__gt__' not supported between instances of 'A1' and 'A2'">></error> A2(2))
print(A1(1) <error descr="'__ge__' not supported between instances of 'A1' and 'A2'">>=</error> A2(2))
print(A1 < A2)
print(A1 <= A2)
print(A1 > A2)
print(A1 >= A2)

View File

@@ -0,0 +1,13 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(order_default=True)
def my_dataclass_order_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,35 @@
from decorator import my_dataclass_order_default, my_dataclass
@my_dataclass(order=True)
class A1:
x: int = 0
@my_dataclass_order_default()
class A2:
y: int = 0
print(A1(1) < A1(2))
print(A1(1) <= A1(2))
print(A1(1) > A1(2))
print(A1(1) >= A1(2))
print(A1(1) <error descr="'__lt__' not supported between instances of 'A1' and 'A2'"><</error> A2(2))
print(A1(1) <error descr="'__le__' not supported between instances of 'A1' and 'A2'"><=</error> A2(2))
print(A1(1) <error descr="'__gt__' not supported between instances of 'A1' and 'A2'">></error> A2(2))
print(A1(1) <error descr="'__ge__' not supported between instances of 'A1' and 'A2'">>=</error> A2(2))
print(A1 < A1)
print(A1 <= A1)
print(A1 > A1)
print(A1 >= A1)
print(A1 < A2)
print(A1 <= A2)
print(A1 > A2)
print(A1 >= A2)

View File

@@ -0,0 +1,13 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(order_default=True)
def my_dataclass_order_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,35 @@
from decorator import my_dataclass_order_default, my_dataclass
@my_dataclass()
class A1:
x: int = 0
@my_dataclass_order_default(order=False)
class A2:
y: int = 0
print(A1(1) <error descr="'__lt__' not supported between instances of 'A1'"><</error> A1(2))
print(A1(1) <error descr="'__le__' not supported between instances of 'A1'"><=</error> A1(2))
print(A1(1) <error descr="'__gt__' not supported between instances of 'A1'">></error> A1(2))
print(A1(1) <error descr="'__ge__' not supported between instances of 'A1'">>=</error> A1(2))
print(A1(1) <error descr="'__lt__' not supported between instances of 'A1' and 'A2'"><</error> A2(2))
print(A1(1) <error descr="'__le__' not supported between instances of 'A1' and 'A2'"><=</error> A2(2))
print(A1(1) <error descr="'__gt__' not supported between instances of 'A1' and 'A2'">></error> A2(2))
print(A1(1) <error descr="'__ge__' not supported between instances of 'A1' and 'A2'">>=</error> A2(2))
print(A1 < A1)
print(A1 <= A1)
print(A1 > A1)
print(A1 >= A1)
print(A1 < A2)
print(A1 <= A2)
print(A1 > A2)
print(A1 >= A2)

View File

@@ -0,0 +1,13 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(order_default=True)
def my_dataclass_order_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,8 @@
import dataclasses
from mod import Base
@dataclasses.dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'Base'">Sub</error>(Base):
field_no_default: int

View File

@@ -0,0 +1,7 @@
import dataclasses
@dataclasses.dataclass
class Base:
base_field_no_default: int = dataclasses.MISSING
base_field_default: int = 42

View File

@@ -0,0 +1,13 @@
from typing import ClassVar
from decorator import my_field, my_dataclass
@my_dataclass()
class E1:
a: int = my_field(default=1)
b1: int = my_field(default_factory=int)
b2: int = my_field(factory=int)
c1: int = my_field<error descr="Cannot specify both 'default' and 'default_factory'">(default=1, default_factory=int)</error>
c2: int = my_field<error descr="Cannot specify both 'default' and 'default_factory'">(default=1, factory=int)</error>
d: ClassVar[int] = my_field(default_factory=<error descr="Field cannot have a default factory">int</error>)

View File

@@ -0,0 +1,12 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
def my_field(**kwargs):
...
@dataclass_transform(field_specifiers=(my_field,))
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,8 @@
from decorator import my_dataclass, my_field
@my_dataclass()
class A1:
<error descr="Attribute 'a' lacks a type annotation">a</error> = my_field()
b = 1
c: int = 1

View File

@@ -0,0 +1,12 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
def my_field(**kwargs):
...
@dataclass_transform(field_specifiers=(my_field,))
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,20 @@
from bases import A1, A3, A41, A42
from decorator import my_dataclass
@my_dataclass()
class <error descr="Non-default argument(s) follows default argument(s) defined in 'A1'">B1</error>(A1):
y1: str
y2: str = "1"
@my_dataclass()
class B3(A3):
y1: str
@my_dataclass()
class B4<error descr="Inherited non-default argument(s) defined in A41 follows inherited default argument defined in A42">(A41, A42)</error>:
pass

View File

@@ -0,0 +1,22 @@
from decorator import my_dataclass
@my_dataclass()
class A1:
x1: int
x2: int = 1
@my_dataclass()
class A3:
x1: int
@my_dataclass()
class A41:
field1: int
@my_dataclass()
class A42:
field2: str = "1"

View File

@@ -0,0 +1,12 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
def my_field(**kwargs):
...
@dataclass_transform(field_specifiers=(my_field,))
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,93 @@
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

View File

@@ -0,0 +1,12 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
def my_field(**kwargs):
...
@dataclass_transform(field_specifiers=(my_field,))
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,75 @@
from decorator import my_dataclass, my_dataclass_kw_only_default, my_field, my_filed_kw_only_default
@my_dataclass(kw_only=True)
class KwOnlyExplicitClassParam:
kw_only_default: int = 42
kw_only_no_default: int
@my_dataclass()
class C1(KwOnlyExplicitClassParam):
not_kw_only_no_default: int
@my_dataclass_kw_only_default()
class KwOnlyImplicitClassParam:
kw_only_default: int = 42
kw_only_no_default: int
@my_dataclass()
class C2(KwOnlyImplicitClassParam):
not_kw_only_no_default: int
@my_dataclass()
class KwOnlyExplicitFieldParam:
kw_only_default: int = my_field(default=42, kw_only=True)
kw_only_no_default: int
@my_dataclass()
class C3(KwOnlyExplicitFieldParam):
not_kw_only_no_default: int
@my_dataclass()
class KwOnlyImplicitFieldParam:
kw_only_default: int = my_filed_kw_only_default(default=42)
kw_only_no_default: int
@my_dataclass()
class C3(KwOnlyImplicitFieldParam):
not_kw_only_no_default: int
@my_dataclass()
class BaseNotKwOnlyDefault:
not_kw_only_default: int = 42
@my_dataclass()
class <error descr="Non-default argument(s) follows default argument(s) defined in 'BaseNotKwOnlyDefault'">SubNotKwOnly</error>(BaseNotKwOnlyDefault):
not_kw_only_no_default: int
@my_dataclass(kw_only=True)
class SubKwOnlyExplicitClassParam(BaseNotKwOnlyDefault):
kw_only_no_default: int
@my_dataclass_kw_only_default()
class SubKwOnlyImplicitClassParam(BaseNotKwOnlyDefault):
kw_only_no_default: int
@my_dataclass()
class SubKwOnlyExplicitFieldParam(BaseNotKwOnlyDefault):
kw_only_no_default: int = my_field(kw_only=True)
@my_dataclass()
class SubKwOnlyExplicitFieldParam(BaseNotKwOnlyDefault):
kw_only_no_default: int = my_filed_kw_only_default()

View File

@@ -0,0 +1,22 @@
import dataclasses
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
def my_field(*, default=dataclasses.MISSING, kw_only=False):
...
def my_filed_kw_only_default(*, default=dataclasses.MISSING, kw_only=True):
...
@dataclass_transform(field_specifiers=(my_field, my_filed_kw_only_default))
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(kw_only_default=True, field_specifiers=(my_field, my_filed_kw_only_default))
def my_dataclass_kw_only_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,84 @@
from decorator import my_dataclass, my_dataclass_order_default, my_dataclass_frozen_default, my_dataclass_eq_default
@my_dataclass()
class ImplicitInit:
field: int
def __init__(self, field):
...
@my_dataclass(<warning descr="'init' is ignored if the class already defines '__init__' method">init=True</warning>)
class ExplicitInit:
field: int
def __init__(self, field):
...
@my_dataclass()
class ImplicitEq:
field: int
def __eq__(self, other):
pass
# Unclear semantic
@my_dataclass_eq_default()
class ImplicitEq2:
field: int
def __eq__(self, other):
pass
@my_dataclass(<warning descr="'eq' is ignored if the class already defines '__eq__' method">eq=True</warning>)
class ExplicitEq:
field: int
def __eq__(self, other):
pass
# Unclear semantic
@my_dataclass_order_default()
class ImplicitOrder:
field: int
def __gt__(self, other):
pass
@my_dataclass(<error descr="'order' should be False if the class defines one of order methods">order=True</error>)
class ExplicitOrder:
field: int
def __gt__(self, other):
pass
@my_dataclass(<error descr="'unsafe_hash' should be False if the class defines '__hash__'">unsafe_hash=True</error>)
class ExplicitUnsafeHash:
field: int
def __hash__(self):
pass
# Unclear semantic
@my_dataclass_frozen_default()
class ImplicitFrozen:
field: int
def __setattr__(self, name, val):
pass
@my_dataclass(<error descr="'frozen' should be False if the class defines '__setattr__' or '__delattr__'">frozen=True</error>)
class ExplicitFrozen:
field: int
def __setattr__(self, name, val):
pass

View File

@@ -0,0 +1,23 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(frozen_default=True)
def my_dataclass_frozen_default(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(order_default=True)
def my_dataclass_order_default(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(eq_default=True)
def my_dataclass_eq_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,8 @@
import dataclasses
from mod import Base
@dataclasses.dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'Base'">Sub</error>(Base):
field_no_default: int

View File

@@ -0,0 +1,6 @@
import dataclasses
@dataclasses.dataclass
class Base:
field_default: int = 42

View File

@@ -0,0 +1,22 @@
from bases import BaseFrozenExplicit, BaseNonFrozenImplicit, BaseFrozenWithFrozenDefault
from decorator import my_dataclass, my_dataclass_frozen_default
@my_dataclass()
class <error descr="Frozen dataclasses can not inherit non-frozen one and vice versa">B1</error>(BaseFrozenExplicit):
b: str = "2"
@my_dataclass(<error descr="Frozen dataclasses can not inherit non-frozen one and vice versa">frozen=True</error>)
class B2(BaseNonFrozenImplicit):
b: str = "2"
@my_dataclass()
class <error descr="Frozen dataclasses can not inherit non-frozen one and vice versa">B1</error>(BaseFrozenWithFrozenDefault):
b: str = "2"
@my_dataclass_frozen_default()
class <error descr="Frozen dataclasses can not inherit non-frozen one and vice versa">B2</error>(BaseNonFrozenImplicit):
b: str = "2"

View File

@@ -0,0 +1,16 @@
from decorator import my_dataclass, my_dataclass_frozen_default
@my_dataclass(frozen=True)
class BaseFrozenExplicit:
a: int = 1
@my_dataclass()
class BaseNonFrozenImplicit:
a: int = 1
@my_dataclass_frozen_default()
class BaseFrozenWithFrozenDefault:
a: int = 1

View File

@@ -0,0 +1,12 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(frozen_default=True)
def my_dataclass_frozen_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,26 @@
from collections import OrderedDict
from typing import ClassVar
from decorator import my_dataclass, field
@my_dataclass()
class A:
a: list[int] = <error descr="Mutable default '[]' is not allowed. Use 'default_factory'">[]</error>
b: list[int] = <error descr="Mutable default 'list()' is not allowed. Use 'default_factory'">list()</error>
c: set[int] = <error descr="Mutable default '{1}' is not allowed. Use 'default_factory'">{1}</error>
d: set[int] = <error descr="Mutable default 'set()' is not allowed. Use 'default_factory'">set()</error>
e: tuple[int, ...] = ()
f: tuple[int, ...] = tuple()
g: ClassVar[list[int]] = []
h: ClassVar = []
i: dict[int, int] = <error descr="Mutable default '{1: 2}' is not allowed. Use 'default_factory'">{1: 2}</error>
j: dict[int, int] = <error descr="Mutable default 'dict()' is not allowed. Use 'default_factory'">dict()</error>
k = []
l = list()
m: dict[int, int] = <error descr="Mutable default 'OrderedDict()' is not allowed. Use 'default_factory'">OrderedDict()</error>
n: frozenset[int] = frozenset()
o: list = field(default_factory=list)
a2: type[list[int]] = list
b2: type[set[int]] = set
c2: type[tuple[int, ...]] = tuple

View File

@@ -0,0 +1,12 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
def field(**kwargs):
...
@dataclass_transform(field_specifiers=(field,))
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,69 @@
from decorator import my_dataclass, my_dataclass_frozen_default
@my_dataclass()
class B1:
x: int
y: str
z: float = 0.0
B1.x = 5
b1 = B1(1, "2")
b1.x = 2
b1.y = "3"
b1.z = 1.0
del b1.x
del b1.y
del b1.z
@my_dataclass(frozen=False)
class B2:
x: int
y: str
z: float = 0.0
B2.x = 5
b2 = B2(1, "2")
b2.x = 2
b2.y = "3"
b2.z = 1.0
del b2.x
del b2.y
del b2.z
@my_dataclass(frozen=True)
class B3:
x: int
y: str
z: float = 0.0
B3.x = 5
b3 = B3(1, "2")
<error descr="'B3' object attribute 'x' is read-only">b3.x</error> = 2
<error descr="'B3' object attribute 'y' is read-only">b3.y</error> = "3"
<error descr="'B3' object attribute 'z' is read-only">b3.z</error> = 1.0
del <error descr="'B3' object attribute 'x' is read-only">b3.x</error>
del <error descr="'B3' object attribute 'y' is read-only">b3.y</error>
del <error descr="'B3' object attribute 'z' is read-only">b3.z</error>
@my_dataclass_frozen_default()
class B4:
x: int
y: str
z: float = 0.0
B4.x = 5
b4 = B4(1, "2")
<error descr="'B4' object attribute 'x' is read-only">b4.x</error> = 2
<error descr="'B4' object attribute 'y' is read-only">b4.y</error> = "3"
<error descr="'B4' object attribute 'z' is read-only">b4.z</error> = 1.0
del <error descr="'B4' object attribute 'x' is read-only">b4.x</error>
del <error descr="'B4' object attribute 'y' is read-only">b4.y</error>
del <error descr="'B4' object attribute 'z' is read-only">b4.z</error>

View File

@@ -0,0 +1,13 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(frozen_default=True)
def my_dataclass_frozen_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,11 @@
from bases import BaseFrozenWithFrozenDefault
from decorator import my_dataclass
@my_dataclass(frozen=True)
class B1(BaseFrozenWithFrozenDefault):
b: str = "1"
<error descr="'BaseFrozenWithFrozenDefault' object attribute 'a' is read-only">BaseFrozenWithFrozenDefault().a</error> = 2
<error descr="'B1' object attribute 'a' is read-only">B1().a</error> = 2
<error descr="'B1' object attribute 'b' is read-only">B1().b</error> = "2"

View File

@@ -0,0 +1,6 @@
from decorator import my_dataclass, my_dataclass_frozen_default
@my_dataclass_frozen_default()
class BaseFrozenWithFrozenDefault:
a: int = 1

View File

@@ -0,0 +1,12 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(frozen_default=True)
def my_dataclass_frozen_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -0,0 +1,21 @@
from decorator import my_dataclass_order_default, my_dataclass
@my_dataclass(<error descr="'eq' must be true if 'order' is true">eq=False</error>, order=True)
class A1:
x: int
@my_dataclass(eq=False)
class A2:
x: int
@my_dataclass(eq=False, order=False)
class A3:
x: int
@my_dataclass_order_default(<error descr="'eq' must be true if 'order' is true">eq=False</error>)
class A4:
x: int

View File

@@ -0,0 +1,13 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@dataclass_transform(order_default=True)
def my_dataclass_order_default(**kwargs) -> Callable[[type[T]], type[T]]:
...

View File

@@ -30,3 +30,10 @@ class Derived3(Base3):
Derived3(<arg3>)
Base3(<arg4>)
@dataclass(kw_only=True)
class Base4:
a: int = field(kw_only=False)
b: int
Base4(<arg5>)

View File

@@ -0,0 +1,24 @@
from typing import Callable, dataclass_transform
def field(**kwargs):
...
def not_field(**kwargs):
...
@dataclass_transform(field_specifiers=(field,))
def my_dataclass(**kwargs) -> Callable[[type], type]:
...
@my_dataclass()
class Dataclass:
field1: int = field()
field2: int = field(default=42)
field3: int = not_field()
field4: int = not_field(default=42)
Dataclass(<arg1>)

View File

@@ -0,0 +1,21 @@
from typing import Callable, dataclass_transform
def field_init_default_false(init: bool = False):
...
def field(**kwargs):
...
@dataclass_transform(field_specifiers=(field_init_default_false, field))
def my_dataclass(**kwargs) -> Callable[[type], type]:
...
@my_dataclass()
class Dataclass:
not_init_spec_default: int = field_init_default_false()
not_init_spec_param: int = field(init=False)
init_spec_param: int = field(init=True)
init_inferred: int = field()
Dataclass(<arg1>)

View File

@@ -0,0 +1,76 @@
from typing import Callable, dataclass_transform
def field_kw_only_default_false(kw_only: bool = False):
...
def field_kw_only_default_true(kw_only: bool = True):
...
def field_no_own_params():
...
@dataclass_transform(field_specifiers=(field_kw_only_default_false, field_kw_only_default_true, field_no_own_params))
def my_dataclass(**kwargs) -> Callable[[type], type]:
...
@dataclass_transform(kw_only_default=True, field_specifiers=(field_kw_only_default_false, field_kw_only_default_true, field_no_own_params))
def my_dataclass_kw_only_default_true(**kwargs) -> Callable[[type], type]:
...
@dataclass_transform(kw_only_default=False, field_specifiers=(field_kw_only_default_false, field_kw_only_default_true, field_no_own_params))
def my_dataclass_kw_only_default_false(**kwargs) -> Callable[[type], type]:
...
@my_dataclass(kw_only=True)
class DataclassKwOnlyTrue:
not_kw_only_spec_default: int = field_kw_only_default_false()
not_kw_only_spec_arg: int = field_kw_only_default_true(kw_only=False)
kw_only_inferred: int = field_no_own_params()
kw_only_spec_default: int = field_kw_only_default_true()
kw_only_spec_arg: int = field_kw_only_default_false(kw_only=True)
@my_dataclass_kw_only_default_true()
class DataclassKwOnlyDefaultTrue:
not_kw_only_spec_default: int = field_kw_only_default_false()
not_kw_only_spec_arg: int = field_kw_only_default_true(kw_only=False)
kw_only_inferred: int = field_no_own_params()
kw_only_spec_default: int = field_kw_only_default_true()
kw_only_spec_arg: int = field_kw_only_default_false(kw_only=True)
@my_dataclass(kw_only=False)
class DataclassKwOnlyFalse:
not_kw_only_spec_default: int = field_kw_only_default_false()
not_kw_only_spec_arg: int = field_kw_only_default_true(kw_only=False)
not_kw_only_inferred: int = field_no_own_params()
kw_only_spec_default: int = field_kw_only_default_true()
kw_only_spec_arg: int = field_kw_only_default_false(kw_only=True)
@my_dataclass_kw_only_default_false()
class DataclassKwOnlyDefaultFalse:
not_kw_only_spec_default: int = field_kw_only_default_false()
not_kw_only_spec_arg: int = field_kw_only_default_true(kw_only=False)
not_kw_only_inferred: int = field_no_own_params()
kw_only_spec_default: int = field_kw_only_default_true()
kw_only_spec_arg: int = field_kw_only_default_false(kw_only=True)
@my_dataclass()
class DataclassImplicitKwOnlyFalse:
not_kw_only_spec_default: int = field_kw_only_default_false()
not_kw_only_spec_arg: int = field_kw_only_default_true(kw_only=False)
not_kw_only_inferred: int = field_no_own_params()
kw_only_spec_default: int = field_kw_only_default_true()
kw_only_spec_arg: int = field_kw_only_default_false(kw_only=True)
DataclassKwOnlyTrue(<arg1>)
DataclassKwOnlyDefaultTrue(<arg2>)
DataclassKwOnlyFalse(<arg3>)
DataclassKwOnlyDefaultFalse(<arg4>)
DataclassImplicitKwOnlyFalse(<arg5>)

View File

@@ -0,0 +1,22 @@
from typing import dataclass_transform, Callable, TypeVar
T = TypeVar("T")
@dataclass_transform()
def my_dataclass(**kwargs) -> Callable[[type[T]], type[T]]:
...
@my_dataclass()
class Super:
super_attr: int
@my_dataclass()
class Sub(Super):
sub_attr: int
super_attr: str
Sub(<arg1>)

View File

@@ -0,0 +1,2 @@
class Customer:
attr = field(default_factory=list, init=False, kw_only=False, alias="alias")

View File

@@ -0,0 +1,13 @@
import typing
@typing.dataclass_transform(kw_only_default=True, order_default=True)
def create_model(
*,
frozen: bool = False,
kw_only: bool = True,
) -> Callable[[Type[_T]], Type[_T]]: ...
@create_model(frozen=True, kw_only=False)
class CustomerModel:
id: int
name: str

View File

@@ -0,0 +1,23 @@
import typing
@typing.dataclass_transform(eq_default=True, order_default=True)
class ModelBase:
def __init_subclass__(
cls,
*,
init: bool = True,
frozen: bool = False,
eq: bool = True,
order: bool = True,
):
...
class CustomerModel(
ModelBase,
init=False,
frozen=True,
eq=False,
order=False,
):
id: int
name: str

View File

@@ -0,0 +1,8 @@
from typing import dataclass_transform
from mod1 import field1
import mod2
from mod3 import *
@dataclass_transform(eq_default=False, order_default=True, field_specifiers=(field1, mod2.field2, field3)
def func()
...

View File

@@ -1195,12 +1195,13 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
// PY-49946
public void testInitializingDataclassKwOnlyOnField() {
final Map<String, PsiElement> marks = loadTest(4);
final Map<String, PsiElement> marks = loadTest(5);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("b: int, *, a: int", new String[]{"b: int, "});
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("a: int, *, b: int", new String[]{"a: int, "});
feignCtrlP(marks.get("<arg3>").getTextOffset()).check("*, a: int, b: int", new String[]{"*, a: int"});
feignCtrlP(marks.get("<arg4>").getTextOffset()).check("*, a: int", new String[]{"*, a: int"});
feignCtrlP(marks.get("<arg5>").getTextOffset()).check("a: int, *, b: int", new String[]{"a: int, "});
}
// PY-53693
@@ -1215,6 +1216,38 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
feignCtrlP(marks.get("<arg6>").getTextOffset()).check("a: str", new String[]{"a: str"});
}
// PY-54560
public void testInitializingDataclassTransformFieldSpecifierKwOnlyArgument() {
final Map<String, PsiElement> marks = loadTest(5);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("not_kw_only_spec_default: int, not_kw_only_spec_arg: int, *, kw_only_inferred: int, kw_only_spec_default: int, kw_only_spec_arg: int", new String[]{"not_kw_only_spec_default: int, "});
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("not_kw_only_spec_default: int, not_kw_only_spec_arg: int, *, kw_only_inferred: int, kw_only_spec_default: int, kw_only_spec_arg: int", new String[]{"not_kw_only_spec_default: int, "});
feignCtrlP(marks.get("<arg3>").getTextOffset()).check("not_kw_only_spec_default: int, not_kw_only_spec_arg: int, not_kw_only_inferred: int, *, kw_only_spec_default: int, kw_only_spec_arg: int", new String[]{"not_kw_only_spec_default: int, "});
feignCtrlP(marks.get("<arg4>").getTextOffset()).check("not_kw_only_spec_default: int, not_kw_only_spec_arg: int, not_kw_only_inferred: int, *, kw_only_spec_default: int, kw_only_spec_arg: int", new String[]{"not_kw_only_spec_default: int, "});
feignCtrlP(marks.get("<arg5>").getTextOffset()).check("not_kw_only_spec_default: int, not_kw_only_spec_arg: int, not_kw_only_inferred: int, *, kw_only_spec_default: int, kw_only_spec_arg: int", new String[]{"not_kw_only_spec_default: int, "});
}
// PY-54560
public void testInitializingDataclassTransformFieldSpecifierInitArgument() {
final Map<String, PsiElement> marks = loadTest(1);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("init_spec_param: int, init_inferred: int", new String[]{"init_spec_param: int, "});
}
// PY-54560
public void testInitializingDataclassTransformDistinguishingFieldSpecifierFromDefaults() {
final Map<String, PsiElement> marks = loadTest(1);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("field1: int, field2: int = ..., field3: int = not_field(), field4: int = not_field(default=42)", new String[]{"field1: int, "});
}
// PY-54560
public void testInitializingDataclassTransformOverridingAncestorFieldType() {
final Map<String, PsiElement> marks = loadTest(1);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("super_attr: str, sub_attr: int", new String[]{"super_attr: str, "});
}
// PY-49946
public void testInitializingDataclassKwOnlyOnClassOverridingHierarchy() {
final Map<String, PsiElement> marks = loadTest(3);

View File

@@ -981,7 +981,7 @@ public class PyStubsTest extends PyTestCase {
assertTrue(file.findTopLevelClass("Foo1").getStub().getCustomStub(PyDataclassStub.class).kwOnly());
assertFalse(file.findTopLevelClass("Foo2").getStub().getCustomStub(PyDataclassStub.class).kwOnly());
assertFalse(file.findTopLevelClass("Foo3").getStub().getCustomStub(PyDataclassStub.class).kwOnly());
assertNull(file.findTopLevelClass("Foo3").getStub().getCustomStub(PyDataclassStub.class).kwOnly());
assertNotParsed(file);
}
@@ -991,7 +991,7 @@ public class PyStubsTest extends PyTestCase {
final PyFile file = getTestFile();
final PyClass cls = file.findTopLevelClass("Foo");
assertFalse(cls.findClassAttribute("bar1", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly());
assertNull(cls.findClassAttribute("bar1", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly());
assertTrue(cls.findClassAttribute("bar2", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly());
assertFalse(cls.findClassAttribute("bar3", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly());
@@ -1111,7 +1111,7 @@ public class PyStubsTest extends PyTestCase {
assertTrue(file.findTopLevelClass("Foo1").getStub().getCustomStub(PyDataclassStub.class).kwOnly());
assertFalse(file.findTopLevelClass("Foo2").getStub().getCustomStub(PyDataclassStub.class).kwOnly());
assertFalse(file.findTopLevelClass("Foo3").getStub().getCustomStub(PyDataclassStub.class).kwOnly());
assertNull(file.findTopLevelClass("Foo3").getStub().getCustomStub(PyDataclassStub.class).kwOnly());
assertNotParsed(file);
}
@@ -1121,7 +1121,7 @@ public class PyStubsTest extends PyTestCase {
final PyFile file = getTestFile();
final PyClass cls = file.findTopLevelClass("Foo");
assertFalse(cls.findClassAttribute("bar1", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly());
assertNull(cls.findClassAttribute("bar1", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly());
assertTrue(cls.findClassAttribute("bar2", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly());
assertFalse(cls.findClassAttribute("bar3", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly());
@@ -1159,7 +1159,81 @@ public class PyStubsTest extends PyTestCase {
assertNotNull(typeAliasStatement);
doTestTypeParameterStub(typeAliasStatement, file);
}
// PY-54560
public void testDataclassTransformDecoratorStub() {
PyFile file = getTestFile();
PyFunction decoratedFunction = file.findTopLevelFunction("func");
assertNotNull(decoratedFunction);
PyDecorator decorator = assertOneElement(decoratedFunction.getDecoratorList().getDecorators());
PyDataclassTransformDecoratorStub customStub = decorator.getStub().getCustomStub(PyDataclassTransformDecoratorStub.class);
assertNotNull(customStub);
assertFalse(customStub.getEqDefault());
assertTrue(customStub.getOrderDefault());
assertFalse(customStub.getKwOnlyDefault());
assertFalse(customStub.getFrozenDefault());
assertEquals(
List.of(
QualifiedName.fromDottedString("field1"),
QualifiedName.fromDottedString("mod2.field2"),
QualifiedName.fromDottedString("field3")
),
customStub.getFieldSpecifiers());
assertNotParsed(file);
}
// PY-54560
public void testDataclassFieldSpecifierStub() {
PyFile file = getTestFile();
@Nullable PyClass dataclass = file.findTopLevelClass("Customer");
assertNotNull(dataclass);
@Nullable PyTargetExpression attribute = dataclass.findClassAttribute("attr", false, TypeEvalContext.codeAnalysis(myFixture.getProject(), myFixture.getFile()));
assertNotNull(attribute);
PyDataclassFieldStub dataclassFieldStub = attribute.getStub().getCustomStub(PyDataclassFieldStub.class);
assertNotNull(dataclassFieldStub);
assertFalse(dataclassFieldStub.hasDefault());
assertTrue(dataclassFieldStub.hasDefaultFactory());
assertFalse(dataclassFieldStub.initValue());
assertFalse(dataclassFieldStub.kwOnly());
assertEquals(dataclassFieldStub.getAlias(), "alias");
assertNotParsed(file);
}
// PY-54560
public void testDataclassStubForClassDecoratedWithDataclassTransformFactoryFunction() {
PyFile file = getTestFile();
PyClass modelClass = file.findTopLevelClass("CustomerModel");
assertNotNull(modelClass);
PyDataclassStub customStub = modelClass.getStub().getCustomStub(PyDataclassStub.class);
assertNotNull(customStub);
assertEquals("DATACLASS_TRANSFORM", customStub.getType());
assertNull(customStub.initValue());
assertNull(customStub.reprValue());
assertNull(customStub.eqValue());
assertNull(customStub.orderValue());
assertNull(customStub.unsafeHashValue());
assertTrue(customStub.frozenValue());
assertFalse(customStub.kwOnly());
assertNotParsed(file);
}
// PY-54560
public void testDataclassStubForClassExtendingDataclassTransformFactoryBase() {
PyFile file = getTestFile();
PyClass modelClass = file.findTopLevelClass("CustomerModel");
assertNotNull(modelClass);
PyDataclassStub customStub = modelClass.getStub().getCustomStub(PyDataclassStub.class);
assertNotNull(customStub);
assertEquals("DATACLASS_TRANSFORM", customStub.getType());
assertFalse(customStub.initValue());
assertNull(customStub.reprValue());
assertFalse(customStub.eqValue());
assertFalse(customStub.orderValue());
assertNull(customStub.unsafeHashValue());
assertTrue(customStub.frozenValue());
assertNull(customStub.kwOnly());
assertNotParsed(file);
}
private void doTestTypingTypedDictArguments() {
doTestTypedDict("name", Arrays.asList("x", "y"), Arrays.asList("str", "int"), QualifiedName.fromComponents("TypedDict"));

View File

@@ -5518,6 +5518,192 @@ public class PyTypingTest extends PyTestCase {
""");
}
public void testDataclassTransformConstructorSignature() {
doTestExpressionUnderCaret("(id: int, name: str) -> MyClass", """
from typing import dataclass_transform
@dataclass_transform()
def deco(cls):
...
@deco
class MyClass:
id: int
name: str
MyCl<caret>ass()
""");
}
public void testDataclassTransformConstructorSignatureDecoratedBaseClassAttributeExcluded() {
doTestExpressionUnderCaret("(id: int, name: str) -> SubSub", """
from typing import dataclass_transform
@dataclass_transform()
class Base:
excluded: int
class Sub(Base):
id: int
class SubSub(Sub):
name: str
Sub<caret>Sub()
""");
}
public void testDataclassTransformConstructorSignatureMetaClassBaseClassAttributeNotExcluded() {
doTestExpressionUnderCaret("(included: int, id: int, name: str) -> SubSub", """
from typing import dataclass_transform
@dataclass_transform()
class Meta(type):
pass
class Base(metaclass=Meta):
included: int
class Sub(Base):
id: int
class SubSub(Sub):
name: str
Sub<caret>Sub()
""");
}
public void testDataclassTransformOverloads() {
doTestExpressionUnderCaret("(id: int, name: str) -> MyClass", """
from typing import dataclass_transform, overload
@overload
def deco(name: str):
...
@dataclass_transform()
@overload
def deco(cls: type):
...
@overload
def deco():
...
def deco(*args, **kwargs):
...
@deco
class MyClass:
id: int
name: str
MyCl<caret>ass()
""");
}
public void testDataclassTransformOwnKwOnlyOmittedAndTakenFromKwOnlyDefault() {
doTestExpressionUnderCaret("(Any, id: int, name: str) -> MyClass", """
from typing import dataclass_transform, Callable
@dataclass_transform(kw_only_default=True)
def deco(**kwargs) -> Callable[[type], type]:
...
@deco(frozen=True)
class MyClass:
id: int
name: str
My<caret>Class()
""");
}
public void testDataclassTransformFieldSpecifierKwOnlyDefaultOverridesDecoratorsKwOnly() {
doTestExpressionUnderCaret("(id: str, Any, addr: list[str]) -> Order", """
from typing import Callable, dataclass_transform
def my_field(kw_only=False):
...
@dataclass_transform(field_specifiers=(my_field,))
def my_dataclass(**kwargs) -> Callable[[type], type]:
...
@my_dataclass(kw_only=True)
class Order:
id: str = my_field()
addr: list[str]
Ord<caret>er()
""");
}
public void testDataclassTransformFieldSpecifierKwOnlyDefaultOverridesDecoratorsKwOnlyDefault() {
doTestExpressionUnderCaret("(id: str, Any, addr: list[str]) -> Order", """
from typing import Callable, dataclass_transform
def my_field(kw_only=False):
...
@dataclass_transform(kw_only_default=True, field_specifiers=(my_field,))
def my_dataclass(**kwargs) -> Callable[[type], type]:
...
@my_dataclass()
class Order:
id: str = my_field()
addr: list[str]
Ord<caret>er()
""");
}
public void testDataclassTransformFieldSpecifierKwOnlyOverridesDecoratorsKwOnly() {
doTestExpressionUnderCaret("(id: str, Any, addr: list[str]) -> Order", """
from typing import Callable, dataclass_transform
def my_field(kw_only=False):
...
@dataclass_transform(field_specifiers=(my_field,))
def my_dataclass(**kwargs) -> Callable[[type], type]:
...
@my_dataclass(kw_only=True)
class Order:
id: str = my_field(kw_only=False)
addr: list[str]
Ord<caret>er()
""");
}
public void testDataclassTransformFieldSpecifierKwOnlyOverridesDecoratorsKwOnlyDefault() {
doTestExpressionUnderCaret("(id: str, Any, addr: list[str]) -> Order", """
from typing import Callable, dataclass_transform
def my_field(kw_only=False):
...
@dataclass_transform(kw_only_default=True, field_specifiers=(my_field,))
def my_dataclass(**kwargs) -> Callable[[type], type]:
...
@my_dataclass()
class Order:
id: str = my_field(kw_only=False)
addr: list[str]
Ord<caret>er()
""");
}
private void doTestNoInjectedText(@NotNull String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
@@ -5547,6 +5733,15 @@ public class PyTypingTest extends PyTestCase {
assertType("Failed in user initiated context", expectedType, expr, userInitiated);
}
private void doTestExpressionUnderCaret(@NotNull String expectedType, @NotNull String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
PyExpression expr = PsiTreeUtil.getParentOfType(myFixture.getFile().findElementAt(myFixture.getCaretOffset()), PyExpression.class);
TypeEvalContext codeAnalysis = TypeEvalContext.codeAnalysis(expr.getProject(), expr.getContainingFile());
TypeEvalContext userInitiated = TypeEvalContext.userInitiated(expr.getProject(), expr.getContainingFile()).withTracing();
assertType("Failed in code analysis context", expectedType, expr, codeAnalysis);
assertType("Failed in user initiated context", expectedType, expr, userInitiated);
}
private void doMultiFileStubAwareTest(@NotNull final String expectedType, @NotNull final String text) {
myFixture.copyDirectoryToProject("types/" + getTestName(false), "");
myFixture.configureByText(PythonFileType.INSTANCE, text);

View File

@@ -342,4 +342,16 @@ public class Py3ArgumentListInspectionTest extends PyInspectionTestCase {
public void testFunctoolsWrapsMultiFile() {
doMultiFileTest();
}
public void testInitByDataclassTransformOnDecorator() {
doMultiFileTest();
}
public void testInitByDataclassTransformOnBaseClass() {
doMultiFileTest();
}
public void testInitByDataclassTransformOnMetaClass() {
doMultiFileTest();
}
}

View File

@@ -18,16 +18,31 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase {
doTest();
}
// PY-54560
public void testMutatingFrozenDataclassTransform() {
doMultiFileTest();
}
// PY-28506
public void testFrozenInheritance() {
doTest();
}
// PY-54560
public void testFrozenInheritanceDataclassTransform() {
doMultiFileTest();
}
// PY-28506, PY-31762
public void testMutatingFrozenInInheritance() {
doTest();
}
// PY-54560
public void testMutatingFrozenInInheritanceDataclassTransform() {
doMultiFileTest();
}
// PY-28506, PY-31762
public void testMutatingFrozenInMixedInheritance() {
doTest();
@@ -38,26 +53,46 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase {
doTest();
}
// PY-54560
public void testOrderAndNotEqDataclassTransform() {
doMultiFileTest();
}
// PY-27398
public void testDefaultFieldValue() {
doTest();
}
// PY-54560
public void testMutableDefaultFieldValueDataclassTransform() {
doMultiFileTest();
}
// PY-27398
public void testFieldsOrder() {
doTest();
}
// PY-26354
public void testAttrsFieldsOrder() {
doTest();
}
// PY-54560
public void testDataclassTransformFieldsOrder() {
doMultiFileTest();
}
// PY-28506, PY-31762
public void testFieldsOrderInInheritance() {
doTest();
}
// PY-54560
public void testDataclassTransformFieldOrderInInheritance() {
doMultiFileTest();
}
// PY-28506, PY-31762
public void testFieldsOrderInMixedInheritance() {
doTest();
@@ -118,6 +153,26 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase {
doTest();
}
// PY-54560
public void testComparisonForOrderedDataclassTransform() {
doMultiFileTest();
}
// PY-54560
public void testComparisonForUnorderedDataclassTransform() {
doMultiFileTest();
}
// PY-54560
public void testComparisonForOrderedAndUnorderedDataclassTransform() {
doMultiFileTest();
}
// PY-54560
public void testComparisonForManuallyOrderedDataclassTransform() {
doMultiFileTest();
}
// PY-32078
public void testComparisonForManuallyOrderedAttrs() {
doTestByText("""
@@ -250,11 +305,21 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase {
doTest();
}
// PY-54560
public void testDataclassTransformDefaultAndDefaultFactory() {
doMultiFileTest();
}
// PY-27398
public void testUselessInitReprEq() {
doTest();
}
// PY-54560
public void testDataclassTransformUselessInitEqUnsafeHashOrderFrozen() {
doMultiFileTest();
}
// PY-27398
public void testUselessOrder() {
doTest();
@@ -305,6 +370,11 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase {
doTest();
}
// PY-54560
public void testDataclassTransformFieldLackingTypeAnnotation() {
doMultiFileTest();
}
// PY-26354
public void testAttrsLackingTypeAnnotation() {
doTest();
@@ -325,6 +395,11 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase {
doTest();
}
// PY-54560
public void testDataclassTransformKwOnlyFieldOrderInInheritance() {
doMultiFileTest();
}
// PY-49946
public void testFieldsOrderOverridden() {
doTest();
@@ -338,6 +413,14 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase {
assertProjectFilesNotParsed(myFixture.getFile());
}
public void testFieldOrderInheritanceMultifile() {
doMultiFileTest();
}
public void testDataclassMissingHandlingMultifile() {
doMultiFileTest();
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {