mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-76811 Conformance test failure: dataclasses_slots.py
- support @dataclass(slots=True) - add inspection for conflict with explicit __slots__ - add quickfix for new inspection - add/adjust tests GitOrigin-RevId: 5ac6efeb6c1e209b641365e2f22bdca74ae6484a
This commit is contained in:
committed by
intellij-monorepo-bot
parent
fc84fd71df
commit
3914253777
@@ -287,6 +287,8 @@ public interface PyClass extends PyAstClass, PsiNameIdentifierOwner, PyCompoundS
|
||||
/**
|
||||
* Returns the list of names in the class' __slots__ attribute, or null if the class
|
||||
* does not define such an attribute.
|
||||
* <p>
|
||||
* Does not include synthesized slot attributes from <code>@dataclass(slots=True)</code>.
|
||||
*
|
||||
* @return the list of names or null.
|
||||
*/
|
||||
|
||||
+18
-3
@@ -1,14 +1,29 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>Reports invalid usages of a class with <code>__slots__</code> definitions.</p>
|
||||
<p><b>Example:</b></p>
|
||||
<p><b>Example when accessing undefined attributes:</b></p>
|
||||
<pre><code>
|
||||
class Foo:
|
||||
__slots__ = ['foo', 'bar']
|
||||
|
||||
def __init__(self):
|
||||
self.x = 3 # error: 'x' is not defined in __slots__
|
||||
</code></pre>
|
||||
|
||||
foo = Foo()
|
||||
foo.baz = 'spam'
|
||||
<p><b>Example of conflicting attributes:</b></p>
|
||||
<pre><code>
|
||||
class A:
|
||||
__slots__ = ("x",)
|
||||
x = 42 # error: conflict with "x" listed in __slots__
|
||||
</code></pre>
|
||||
|
||||
<p><b>Example <code>slots=True</code>:</b></p>
|
||||
<pre><code>
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(slots=True) # error: __slots__ is also defined in Foo
|
||||
class Foo:
|
||||
__slots__ = ['a']
|
||||
</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -582,7 +582,7 @@
|
||||
<inspectionExtension implementation="com.jetbrains.python.codeInsight.typing.PyTypingInspectionExtension"/>
|
||||
<customPackageIdentifier implementation="com.jetbrains.python.pyi.PyiCustomPackageIdentifier"/>
|
||||
<knownDecoratorProvider implementation="com.jetbrains.python.psi.PyStdKnownDecoratorProvider"/>
|
||||
<pyClassMembersProvider implementation="com.jetbrains.python.codeInsight.PyAttrsClassMembersProvider"/>
|
||||
<pyClassMembersProvider implementation="com.jetbrains.python.codeInsight.PyDataclassClassMembersProvider"/>
|
||||
|
||||
<!-- IPython -->
|
||||
<pyReferenceResolveProvider implementation="com.jetbrains.python.psi.resolve.PyIPythonBuiltinReferenceResolveProvider"/>
|
||||
|
||||
@@ -1011,9 +1011,11 @@ INSP.NAME.deprecated.function.class.or.module=Deprecated function, class, or mod
|
||||
INSP.deprecation.abc.decorator.deprecated.use.alternative=''{0}'' is deprecated since Python 3.3. Use ''{1}'' with ''{2}'' instead
|
||||
|
||||
# PyDunderSlotsInspection
|
||||
INSP.NAME.dunder.slots=Invalid usages of classes with '__slots__' definitions
|
||||
INSP.dunder.slots.name.in.slots.conflicts.with.class.variable=''{0}'' in __slots__ conflicts with a class variable
|
||||
INSP.dunder.slots.class.object.attribute.read.only=''{0}'' object attribute ''{1}'' is read-only
|
||||
INSP.NAME.dunder.slots=Invalid usages of classes with '__slots__' definitions
|
||||
INSP.dunder.slots.name.in.slots.conflicts.with.class.variable=''{0}'' in '__slots__' conflicts with a class variable
|
||||
INSP.dunder.slots.class.object.missing.attribute=''{0}'' object has no attribute ''{1}''
|
||||
INSP.dunder.slots.enabled.twice=A data class that explicitly defines '__slots__' must not be configured with 'slots=True'
|
||||
QFIX.dunder.slots.enabled.twice=Remove slots=True
|
||||
|
||||
# PyFinalInspection
|
||||
INSP.NAME.final.classes.methods.and.variables=Invalid usages of final classes, methods, and variables
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
// Copyright 2000-2025 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.codeInsight;
|
||||
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.psi.PyClass;
|
||||
import com.jetbrains.python.psi.impl.PyBuiltinCache;
|
||||
import com.jetbrains.python.psi.types.PyClassMembersProviderBase;
|
||||
import com.jetbrains.python.psi.types.PyClassType;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.jetbrains.python.codeInsight.PyDataclassesKt.parseDataclassParameters;
|
||||
|
||||
/**
|
||||
* Adds member __attrs_attrs__ caused by decorator @attrs.define
|
||||
*/
|
||||
public final class PyAttrsClassMembersProvider extends PyClassMembersProviderBase {
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<PyCustomMember> getMembers(PyClassType clazz, PsiElement location, @NotNull TypeEvalContext context) {
|
||||
PyClass pyClass = clazz.getPyClass();
|
||||
PyDataclassParameters dataclassParameters = parseDataclassParameters(pyClass, context);
|
||||
boolean hasAttrs = dataclassParameters != null && dataclassParameters.getType() == PyDataclassParameters.PredefinedType.ATTRS;
|
||||
if (hasAttrs) {
|
||||
PyBuiltinCache builtinCache = PyBuiltinCache.getInstance(pyClass);
|
||||
PyClass objectClass = builtinCache.getClass(PyNames.OBJECT);
|
||||
return List.of(new PyCustomMember("__attrs_attrs__", objectClass));
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright 2000-2025 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.codeInsight;
|
||||
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.util.CachedValueProvider;
|
||||
import com.intellij.psi.util.CachedValuesManager;
|
||||
import com.intellij.psi.util.PsiModificationTracker;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.codeInsight.PyDataclassNames.Dataclasses;
|
||||
import com.jetbrains.python.psi.PyClass;
|
||||
import com.jetbrains.python.psi.impl.PyBuiltinCache;
|
||||
import com.jetbrains.python.psi.types.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import static com.jetbrains.python.codeInsight.PyDataclassesKt.parseDataclassParameters;
|
||||
|
||||
/**
|
||||
* Adds members for dataclass-like classes.
|
||||
*/
|
||||
public final class PyDataclassClassMembersProvider extends PyClassMembersProviderBase {
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<PyCustomMember> getMembers(PyClassType clazz, PsiElement location, @NotNull TypeEvalContext context) {
|
||||
PyClass pyClass = clazz.getPyClass();
|
||||
PyDataclassParameters dataclassParameters = parseDataclassParameters(pyClass, context);
|
||||
|
||||
return CachedValuesManager.getCachedValue(pyClass, () -> {
|
||||
Collection<PyCustomMember> result = new ArrayList<>();
|
||||
|
||||
// Adds member __slots__ caused by dataclass.slots
|
||||
if (dataclassParameters != null && dataclassParameters.getSlots()) {
|
||||
PyBuiltinCache builtinCache = PyBuiltinCache.getInstance(pyClass);
|
||||
PyType strType = builtinCache.getStrType();
|
||||
|
||||
if (strType != null) {
|
||||
// creating the following PyTupleType requires caching the result to avoid Idempotency Errors at runtime
|
||||
PyTupleType tupleOfStrings = PyTupleType.createHomogeneous(pyClass, strType);
|
||||
if (tupleOfStrings != null) {
|
||||
String qNameTuple = tupleOfStrings.getPyClass().getQualifiedName();
|
||||
PyCustomMember slotsMember = new PyCustomMember(Dataclasses.DUNDER_SLOTS, qNameTuple, ignored -> tupleOfStrings);
|
||||
result.add(slotsMember);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds member __attrs_attrs__ caused by decorator @attrs.define
|
||||
boolean hasAttrs = dataclassParameters != null && dataclassParameters.getType() == PyDataclassParameters.PredefinedType.ATTRS;
|
||||
if (hasAttrs) {
|
||||
PyBuiltinCache builtinCache = PyBuiltinCache.getInstance(pyClass);
|
||||
PyClass objectClass = builtinCache.getClass(PyNames.OBJECT);
|
||||
result.add(new PyCustomMember("__attrs_attrs__", objectClass));
|
||||
}
|
||||
|
||||
return CachedValueProvider.Result.create(result, PsiModificationTracker.MODIFICATION_COUNT);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,6 @@ 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.PyKnownDecorator
|
||||
import com.jetbrains.python.psi.impl.PyEvaluator
|
||||
import com.jetbrains.python.psi.impl.StubAwareComputation
|
||||
import com.jetbrains.python.psi.impl.mapArguments
|
||||
@@ -28,26 +27,27 @@ import com.jetbrains.python.psi.types.*
|
||||
|
||||
object PyDataclassNames {
|
||||
object Dataclasses {
|
||||
const val DATACLASSES_MISSING = "dataclasses.MISSING"
|
||||
const val DATACLASSES_INITVAR = "dataclasses.InitVar"
|
||||
const val DATACLASSES_FIELDS = "dataclasses.fields"
|
||||
const val DATACLASSES_ASDICT = "dataclasses.asdict"
|
||||
const val DATACLASSES_FIELD = "dataclasses.field"
|
||||
const val DATACLASSES_REPLACE = "dataclasses.replace"
|
||||
const val DATACLASSES_KW_ONLY = "dataclasses.KW_ONLY"
|
||||
const val DUNDER_POST_INIT = "__post_init__"
|
||||
val DECORATOR_PARAMETERS = listOf("init", "repr", "eq", "order", "unsafe_hash", "frozen", "match_args", "kw_only")
|
||||
val HELPER_FUNCTIONS = setOf(DATACLASSES_FIELDS, DATACLASSES_ASDICT, "dataclasses.astuple", DATACLASSES_REPLACE)
|
||||
const val DATACLASSES_MISSING: String = "dataclasses.MISSING"
|
||||
const val DATACLASSES_INITVAR: String = "dataclasses.InitVar"
|
||||
const val DATACLASSES_FIELDS: String = "dataclasses.fields"
|
||||
const val DATACLASSES_ASDICT: String = "dataclasses.asdict"
|
||||
const val DATACLASSES_FIELD: String = "dataclasses.field"
|
||||
const val DATACLASSES_REPLACE: String = "dataclasses.replace"
|
||||
const val DATACLASSES_KW_ONLY: String = "dataclasses.KW_ONLY"
|
||||
const val DUNDER_POST_INIT: String = "__post_init__"
|
||||
const val DUNDER_SLOTS: String = PyNames.SLOTS
|
||||
val DECORATOR_PARAMETERS: List<String> = listOf("init", "repr", "eq", "order", "unsafe_hash", "frozen", "match_args", "kw_only", "slots")
|
||||
val HELPER_FUNCTIONS: Set<String> = setOf(DATACLASSES_FIELDS, DATACLASSES_ASDICT, "dataclasses.astuple", DATACLASSES_REPLACE)
|
||||
}
|
||||
|
||||
object Attrs {
|
||||
val ATTRS_NOTHING = setOf("attr.NOTHING", "attrs.NOTHING")
|
||||
val ATTRS_FACTORY = setOf("attr.Factory", "attrs.Factory")
|
||||
val ATTRS_ASSOC = setOf("attr.assoc", "attrs.assoc")
|
||||
val ATTRS_EVOLVE = setOf("attr.evolve", "attrs.evolve")
|
||||
val ATTRS_FROZEN = setOf("attr.frozen", "attrs.frozen")
|
||||
const val DUNDER_POST_INIT = "__attrs_post_init__"
|
||||
val DECORATOR_PARAMETERS = listOf(
|
||||
val ATTRS_NOTHING: Set<String> = setOf("attr.NOTHING", "attrs.NOTHING")
|
||||
val ATTRS_FACTORY: Set<String> = setOf("attr.Factory", "attrs.Factory")
|
||||
val ATTRS_ASSOC: Set<String> = setOf("attr.assoc", "attrs.assoc")
|
||||
val ATTRS_EVOLVE: Set<String> = setOf("attr.evolve", "attrs.evolve")
|
||||
val ATTRS_FROZEN: Set<String> = setOf("attr.frozen", "attrs.frozen")
|
||||
const val DUNDER_POST_INIT: String = "__attrs_post_init__"
|
||||
val DECORATOR_PARAMETERS: List<String> = listOf(
|
||||
"these",
|
||||
"repr_ns",
|
||||
"repr",
|
||||
@@ -66,14 +66,14 @@ object PyDataclassNames {
|
||||
"order",
|
||||
"match_args",
|
||||
)
|
||||
val FIELD_FUNCTIONS = setOf(
|
||||
val FIELD_FUNCTIONS: Set<String> = setOf(
|
||||
"attr.ib",
|
||||
"attr.attr",
|
||||
"attr.attrib",
|
||||
"attr.field",
|
||||
"attrs.field",
|
||||
)
|
||||
val INSTANCE_HELPER_FUNCTIONS = setOf(
|
||||
val INSTANCE_HELPER_FUNCTIONS: Set<String> = setOf(
|
||||
"attr.asdict",
|
||||
"attr.astuple",
|
||||
"attr.assoc",
|
||||
@@ -83,7 +83,7 @@ object PyDataclassNames {
|
||||
"attrs.assoc",
|
||||
"attrs.evolve",
|
||||
)
|
||||
val CLASS_HELPERS_FUNCTIONS = setOf(
|
||||
val CLASS_HELPERS_FUNCTIONS: Set<String> = setOf(
|
||||
"attr.fields",
|
||||
"attr.fields_dict",
|
||||
"attrs.fields",
|
||||
@@ -92,9 +92,9 @@ object PyDataclassNames {
|
||||
}
|
||||
|
||||
object DataclassTransform {
|
||||
const val DATACLASS_TRANSFORM_NAME = "dataclass_transform"
|
||||
const val DATACLASS_TRANSFORM_NAME: String = "dataclass_transform"
|
||||
|
||||
val DECORATOR_OR_CLASS_PARAMETERS = setOf(
|
||||
val DECORATOR_OR_CLASS_PARAMETERS: Set<String> = setOf(
|
||||
"init",
|
||||
"eq",
|
||||
"order",
|
||||
@@ -104,8 +104,8 @@ object PyDataclassNames {
|
||||
"kw_only",
|
||||
"slots",
|
||||
)
|
||||
|
||||
val FIELD_SPECIFIER_PARAMETERS = setOf(
|
||||
|
||||
val FIELD_SPECIFIER_PARAMETERS: Set<String> = setOf(
|
||||
"init",
|
||||
"default",
|
||||
"default_factory",
|
||||
@@ -139,15 +139,20 @@ fun parseStdDataclassParameters(cls: PyClass, context: TypeEvalContext): PyDatac
|
||||
}
|
||||
|
||||
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 }
|
||||
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) {
|
||||
return@getNullableParameterizedCachedValue StubAwareComputation.on(cls)
|
||||
.withCustomStub { stub -> stub.getCustomStub(PyDataclassStub::class.java) }
|
||||
.overStub { dataclassStub -> resolveDataclassParameters(cls, dataclassStub ?: PyDataclassStubImpl.NON_PARAMETERIZED_DATACLASS_TRANSFORM_CANDIDATE_STUB, null, 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)
|
||||
@@ -222,6 +227,7 @@ private fun parseDataclassParametersFromAST(cls: PyClass, context: TypeEvalConte
|
||||
frozen = provided.frozen,
|
||||
matchArgs = provided.matchArgs,
|
||||
kwOnly = provided.kwOnly,
|
||||
slots = provided.slots,
|
||||
),
|
||||
DataclassParameterArgumentMapping(
|
||||
initArgument = provided.initArgument,
|
||||
@@ -232,6 +238,7 @@ private fun parseDataclassParametersFromAST(cls: PyClass, context: TypeEvalConte
|
||||
frozenArgument = provided.frozenArgument,
|
||||
matchArgsArgument = provided.matchArgsArgument,
|
||||
kwOnlyArgument = provided.kwOnlyArgument,
|
||||
slotsArgument = provided.slotsArgument,
|
||||
others = provided.others,
|
||||
)
|
||||
)
|
||||
@@ -306,6 +313,7 @@ data class PyDataclassParameters(
|
||||
val frozen: Boolean,
|
||||
val matchArgs: Boolean,
|
||||
val kwOnly: Boolean,
|
||||
val slots: Boolean,
|
||||
val initArgument: PyExpression?,
|
||||
val reprArgument: PyExpression?,
|
||||
val eqArgument: PyExpression?,
|
||||
@@ -314,9 +322,10 @@ data class PyDataclassParameters(
|
||||
val frozenArgument: PyExpression?,
|
||||
val matchArgsArgument: PyExpression?,
|
||||
val kwOnlyArgument: PyExpression?,
|
||||
val slotsArgument: PyExpression?,
|
||||
val type: Type,
|
||||
val others: Map<String, PyExpression>,
|
||||
val fieldSpecifiers: List<QualifiedName> = emptyList()
|
||||
val fieldSpecifiers: List<QualifiedName> = emptyList(),
|
||||
) {
|
||||
|
||||
interface Type {
|
||||
@@ -354,6 +363,7 @@ private class PyDataclassParametersBuilder(private val type: Type, private val d
|
||||
private var frozen: Boolean? = null
|
||||
private var matchArgs: Boolean? = null
|
||||
private var kwOnly: Boolean? = null
|
||||
private var slots: Boolean? = null
|
||||
|
||||
private var initArgument: PyExpression? = null
|
||||
private var reprArgument: PyExpression? = null
|
||||
@@ -363,6 +373,7 @@ private class PyDataclassParametersBuilder(private val type: Type, private val d
|
||||
private var frozenArgument: PyExpression? = null
|
||||
private var matchArgsArgument: PyExpression? = null
|
||||
private var kwOnlyArgument: PyExpression? = null
|
||||
private var slotsArgument: PyExpression? = null
|
||||
|
||||
private val others = mutableMapOf<String, PyExpression>()
|
||||
|
||||
@@ -395,6 +406,11 @@ private class PyDataclassParametersBuilder(private val type: Type, private val d
|
||||
kwOnlyArgument = argument
|
||||
return
|
||||
}
|
||||
"slots" -> {
|
||||
slots = PyEvaluator.evaluateAsBooleanNoResolve(value)
|
||||
slotsArgument = argument
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (type.asPredefinedType == PyDataclassParameters.PredefinedType.STD ||
|
||||
@@ -470,6 +486,7 @@ private class PyDataclassParametersBuilder(private val type: Type, private val d
|
||||
frozen = frozen,
|
||||
matchArgs = matchArgs,
|
||||
kwOnly = kwOnly,
|
||||
slots = slots,
|
||||
),
|
||||
DataclassParameterArgumentMapping(
|
||||
initArgument = initArgument,
|
||||
@@ -480,6 +497,7 @@ private class PyDataclassParametersBuilder(private val type: Type, private val d
|
||||
frozenArgument = frozenArgument,
|
||||
matchArgsArgument = matchArgsArgument,
|
||||
kwOnlyArgument = kwOnlyArgument,
|
||||
slotsArgument = slotsArgument,
|
||||
others = others,
|
||||
)
|
||||
)
|
||||
@@ -494,7 +512,8 @@ private data class DataclassParameterArgumentMapping(
|
||||
val frozenArgument: PyExpression?,
|
||||
val matchArgsArgument: PyExpression?,
|
||||
val kwOnlyArgument: PyExpression?,
|
||||
val others: Map<String, PyExpression>
|
||||
val slotsArgument: PyExpression?,
|
||||
val others: Map<String, PyExpression>,
|
||||
)
|
||||
|
||||
@Suppress("NullableBooleanElvis")
|
||||
@@ -509,7 +528,7 @@ private fun resolveDataclassParameters(
|
||||
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(
|
||||
@@ -521,6 +540,7 @@ private fun resolveDataclassParameters(
|
||||
frozen = stub.frozenValue() ?: false,
|
||||
matchArgs = stub.matchArgsValue() ?: true,
|
||||
kwOnly = stub.kwOnly() ?: false,
|
||||
slots = stub.slotsValue() ?: false,
|
||||
initArgument = argumentMapping?.initArgument,
|
||||
reprArgument = argumentMapping?.reprArgument,
|
||||
eqArgument = argumentMapping?.eqArgument,
|
||||
@@ -529,6 +549,7 @@ private fun resolveDataclassParameters(
|
||||
frozenArgument = argumentMapping?.frozenArgument,
|
||||
matchArgsArgument = argumentMapping?.matchArgsArgument,
|
||||
kwOnlyArgument = argumentMapping?.kwOnlyArgument,
|
||||
slotsArgument = argumentMapping?.slotsArgument,
|
||||
others = argumentMapping?.others ?: emptyMap(),
|
||||
type = type,
|
||||
fieldSpecifiers = listOf(QualifiedName.fromDottedString(PyDataclassNames.Dataclasses.DATACLASSES_FIELD)),
|
||||
@@ -551,6 +572,7 @@ private fun resolveDataclassParameters(
|
||||
frozen = stub.frozenValue() ?: (stub.decoratorName()?.toString() in PyDataclassNames.Attrs.ATTRS_FROZEN),
|
||||
matchArgs = stub.matchArgsValue() ?: true,
|
||||
kwOnly = stub.kwOnly() ?: false,
|
||||
slots = stub.slotsValue() ?: false,
|
||||
initArgument = argumentMapping?.initArgument,
|
||||
reprArgument = argumentMapping?.reprArgument,
|
||||
eqArgument = argumentMapping?.eqArgument,
|
||||
@@ -559,6 +581,7 @@ private fun resolveDataclassParameters(
|
||||
frozenArgument = argumentMapping?.frozenArgument,
|
||||
matchArgsArgument = argumentMapping?.matchArgsArgument,
|
||||
kwOnlyArgument = argumentMapping?.kwOnlyArgument,
|
||||
slotsArgument = argumentMapping?.slotsArgument,
|
||||
others = (argumentMapping?.others ?: emptyMap()) + extraArguments,
|
||||
type = type,
|
||||
fieldSpecifiers = PyDataclassNames.Attrs.FIELD_FUNCTIONS.map(QualifiedName::fromDottedString),
|
||||
@@ -597,6 +620,7 @@ private fun resolveDataclassParameters(
|
||||
frozen = stub.frozenValue() ?: dataclassTransformStub.frozenDefault,
|
||||
matchArgs = stub.matchArgsValue() ?: true,
|
||||
kwOnly = stub.kwOnly() ?: dataclassTransformStub.kwOnlyDefault,
|
||||
slots = stub.slotsValue() ?: false,
|
||||
initArgument = argumentMapping?.initArgument,
|
||||
reprArgument = argumentMapping?.reprArgument,
|
||||
eqArgument = argumentMapping?.eqArgument,
|
||||
@@ -605,6 +629,7 @@ private fun resolveDataclassParameters(
|
||||
frozenArgument = argumentMapping?.frozenArgument,
|
||||
matchArgsArgument = argumentMapping?.matchArgsArgument,
|
||||
kwOnlyArgument = argumentMapping?.kwOnlyArgument,
|
||||
slotsArgument = argumentMapping?.slotsArgument,
|
||||
others = argumentMapping?.others ?: emptyMap(),
|
||||
type = type,
|
||||
fieldSpecifiers = resolvedFieldSpecifiers,
|
||||
@@ -623,6 +648,7 @@ private fun resolveDataclassParameters(
|
||||
unsafeHash = stub.unsafeHashValue() ?: false,
|
||||
frozen = stub.frozenValue() ?: false,
|
||||
kwOnly = stub.kwOnly() ?: false,
|
||||
slots = stub.slotsValue() ?: false,
|
||||
matchArgs = stub.matchArgsValue() ?: true,
|
||||
initArgument = argumentMapping?.initArgument,
|
||||
reprArgument = argumentMapping?.reprArgument,
|
||||
@@ -632,6 +658,7 @@ private fun resolveDataclassParameters(
|
||||
matchArgsArgument = argumentMapping?.matchArgsArgument,
|
||||
frozenArgument = argumentMapping?.frozenArgument,
|
||||
kwOnlyArgument = argumentMapping?.kwOnlyArgument,
|
||||
slotsArgument = argumentMapping?.slotsArgument,
|
||||
others = argumentMapping?.others ?: emptyMap(),
|
||||
type = type,
|
||||
)
|
||||
|
||||
+6
-10
@@ -19,16 +19,12 @@ import com.intellij.lang.annotation.HighlightSeverity;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiReferenceBase;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.jetbrains.python.psi.PsiReferenceEx;
|
||||
import com.jetbrains.python.psi.PyClass;
|
||||
import com.jetbrains.python.psi.PyExpression;
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression;
|
||||
import com.jetbrains.python.psi.PyTargetExpression;
|
||||
import com.jetbrains.python.psi.PyUtil;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import java.util.Objects;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
public class PyDunderSlotsReference extends PsiReferenceBase<PyStringLiteralExpression> implements PsiReferenceEx {
|
||||
public PyDunderSlotsReference(@NotNull PyStringLiteralExpression element) {
|
||||
@@ -43,11 +39,11 @@ public class PyDunderSlotsReference extends PsiReferenceBase<PyStringLiteralExpr
|
||||
|
||||
@Override
|
||||
public boolean isReferenceTo(@NotNull PsiElement element) {
|
||||
if (element instanceof PyExpression && PyUtil.isInstanceAttribute((PyExpression)element)) {
|
||||
PyClass elementClass = PsiTreeUtil.getParentOfType(element, PyClass.class);
|
||||
if (element instanceof PyTargetExpression targetExpression && PyUtil.isInstanceAttribute(targetExpression)) {
|
||||
PyClass elementClass = PsiTreeUtil.getParentOfType(targetExpression, PyClass.class);
|
||||
PyClass referenceClass = PsiTreeUtil.getParentOfType(myElement, PyClass.class);
|
||||
if (referenceClass != null && referenceClass.isSubclass(elementClass, null)) {
|
||||
String elementName = ((PyTargetExpression) element).getReferencedName();
|
||||
String elementName = targetExpression.getReferencedName();
|
||||
String referenceName = myElement.getStringValue();
|
||||
if (Objects.equals(elementName, referenceName)) {
|
||||
return true;
|
||||
|
||||
+2
-1
@@ -114,7 +114,7 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
|
||||
cls: PyClass,
|
||||
dataclassParams: PyDataclassParameters?,
|
||||
context: TypeEvalContext,
|
||||
): Sequence<InitVarInfo>? {
|
||||
): List<InitVarInfo>? {
|
||||
if (dataclassParams == null || !dataclassParams.init) {
|
||||
return null
|
||||
}
|
||||
@@ -133,6 +133,7 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
|
||||
+69
-5
@@ -2,10 +2,18 @@
|
||||
package com.jetbrains.python.inspections
|
||||
|
||||
import com.intellij.codeInspection.LocalInspectionToolSession
|
||||
import com.intellij.codeInspection.ProblemDescriptor
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.modcommand.ModCommand
|
||||
import com.intellij.modcommand.ModCommandQuickFix
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.psi.util.parentOfType
|
||||
import com.jetbrains.python.PyNames
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.PyTokenTypes
|
||||
import com.jetbrains.python.codeInsight.parseDataclassParameters
|
||||
import com.jetbrains.python.psi.*
|
||||
import com.jetbrains.python.psi.impl.PyPsiUtils
|
||||
import com.jetbrains.python.psi.types.PyClassType
|
||||
@@ -13,9 +21,11 @@ import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
|
||||
class PyDunderSlotsInspection : PyInspection() {
|
||||
|
||||
override fun buildVisitor(holder: ProblemsHolder,
|
||||
isOnTheFly: Boolean,
|
||||
session: LocalInspectionToolSession): PsiElementVisitor = Visitor(
|
||||
override fun buildVisitor(
|
||||
holder: ProblemsHolder,
|
||||
isOnTheFly: Boolean,
|
||||
session: LocalInspectionToolSession,
|
||||
): PsiElementVisitor = Visitor(
|
||||
holder,PyInspectionVisitor.getContext(session))
|
||||
|
||||
private class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
|
||||
@@ -23,6 +33,21 @@ class PyDunderSlotsInspection : PyInspection() {
|
||||
override fun visitPyClass(node: PyClass) {
|
||||
super.visitPyClass(node)
|
||||
|
||||
val params = parseDataclassParameters(node, myTypeEvalContext)
|
||||
if (params != null && params.slots) {
|
||||
val explicitSlotsAttr = node.findClassAttribute(PyNames.SLOTS, false, myTypeEvalContext)
|
||||
if (explicitSlotsAttr != null) {
|
||||
val anchor = params.slotsArgument ?: node.nameIdentifier ?: node
|
||||
val message = PyPsiBundle.message("INSP.dunder.slots.enabled.twice")
|
||||
if (params.slotsArgument != null) {
|
||||
registerProblem(anchor, message, RemoveSlotsKwargQuickFix())
|
||||
}
|
||||
else {
|
||||
registerProblem(anchor, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!LanguageLevel.forElement(node).isPython2) {
|
||||
when (val slots = findSlotsValue(node)) {
|
||||
is PySequenceExpression -> slots
|
||||
@@ -55,7 +80,8 @@ class PyDunderSlotsInspection : PyInspection() {
|
||||
|
||||
val classAttribute = pyClass.findClassAttribute(name, false, myTypeEvalContext)
|
||||
if (classAttribute != null && classAttribute.hasAssignedValue()) {
|
||||
registerProblem(slot, PyPsiBundle.message("INSP.dunder.slots.name.in.slots.conflicts.with.class.variable", name))
|
||||
val message = PyPsiBundle.message("INSP.dunder.slots.name.in.slots.conflicts.with.class.variable", name)
|
||||
registerProblem(slot, message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +95,47 @@ class PyDunderSlotsInspection : PyInspection() {
|
||||
|
||||
val qualifierType = myTypeEvalContext.getType(qualifier)
|
||||
if (qualifierType is PyClassType && !qualifierType.isAttributeWritable(targetName, myTypeEvalContext)) {
|
||||
registerProblem(target, PyPsiBundle.message("INSP.dunder.slots.class.object.attribute.read.only", qualifierType.name, targetName))
|
||||
val message = PyPsiBundle.message("INSP.dunder.slots.class.object.missing.attribute", qualifierType.name, targetName)
|
||||
registerProblem(target, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RemoveSlotsKwargQuickFix : ModCommandQuickFix() {
|
||||
override fun getFamilyName(): String = PyPsiBundle.message("QFIX.dunder.slots.enabled.twice")
|
||||
|
||||
override fun perform(project: Project, descriptor: ProblemDescriptor): ModCommand {
|
||||
val element = descriptor.psiElement
|
||||
val kwArg = element.parentOfType<PyKeywordArgument>()
|
||||
?: element as? PyKeywordArgument
|
||||
?: return ModCommand.nop()
|
||||
|
||||
// Only remove if it's the 'slots' kwarg inside an argument list
|
||||
if (kwArg.keyword != "slots") return ModCommand.nop()
|
||||
if (kwArg.parent !is PyArgumentList) return ModCommand.nop()
|
||||
|
||||
return ModCommand.psiUpdate(kwArg) { kwArgCopy ->
|
||||
// Remove neighboring comma to keep argument list syntax correct
|
||||
val commaAfter = PsiTreeUtil.skipWhitespacesForward(kwArgCopy)
|
||||
?.takeIf { it.node.elementType == PyTokenTypes.COMMA }
|
||||
val commaBefore = PsiTreeUtil.skipWhitespacesBackward(kwArgCopy)
|
||||
?.takeIf { it.node.elementType == PyTokenTypes.COMMA }
|
||||
|
||||
// Prefer removing the trailing comma, otherwise remove the leading one
|
||||
if (commaAfter != null) {
|
||||
commaAfter.delete()
|
||||
kwArgCopy.delete()
|
||||
}
|
||||
else if (commaBefore != null) {
|
||||
kwArgCopy.delete()
|
||||
commaBefore.delete()
|
||||
}
|
||||
else {
|
||||
kwArgCopy.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ private class CachedFile(
|
||||
private val myTypeCache: MutableMap<String, PyClassType?> = mutableMapOf(),
|
||||
) {
|
||||
fun getClassType(name: @NonNls String): PyClassType? {
|
||||
return synchronized(this) {
|
||||
return synchronized(myTypeCache) {
|
||||
if (myModificationStamp != file.modificationStamp) {
|
||||
myTypeCache.clear()
|
||||
myModificationStamp = file.modificationStamp
|
||||
@@ -316,7 +316,7 @@ private class CachedFile(
|
||||
?.also { it.assertValid(name) }
|
||||
} ?: resolveTopLevel(name)?.also {
|
||||
synchronized(myTypeCache) {
|
||||
myTypeCache.put(name, it)
|
||||
myTypeCache[name] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.intellij.psi.tree.TokenSet;
|
||||
import com.intellij.psi.util.CachedValueProvider.Result;
|
||||
import com.intellij.psi.util.*;
|
||||
import com.intellij.ui.IconManager;
|
||||
import com.intellij.ui.PlatformIcons;
|
||||
import com.intellij.util.ArrayFactory;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
@@ -28,9 +29,11 @@ import com.intellij.util.containers.MultiMap;
|
||||
import com.jetbrains.python.*;
|
||||
import com.jetbrains.python.ast.PyAstFunction.Modifier;
|
||||
import com.jetbrains.python.ast.impl.PyUtilCore;
|
||||
import com.jetbrains.python.codeInsight.PyDataclassParameters;
|
||||
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
|
||||
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
|
||||
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
|
||||
import com.jetbrains.python.codeInsight.stdlib.PyDataclassTypeProvider;
|
||||
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
|
||||
import com.jetbrains.python.documentation.docstrings.DocStringUtil;
|
||||
import com.jetbrains.python.psi.*;
|
||||
@@ -56,8 +59,10 @@ import java.util.*;
|
||||
|
||||
import static com.intellij.openapi.util.text.StringUtil.join;
|
||||
import static com.intellij.openapi.util.text.StringUtil.notNullize;
|
||||
import static com.jetbrains.python.codeInsight.PyDataclassesKt.parseDataclassParameters;
|
||||
import static com.jetbrains.python.psi.PyUtil.as;
|
||||
import static com.jetbrains.python.psi.impl.PyDeprecationUtilKt.extractDeprecationMessageFromDecorator;
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
|
||||
public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyClass {
|
||||
@@ -120,7 +125,7 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
|
||||
|
||||
@Override
|
||||
public Icon getIcon(int flags) {
|
||||
return IconManager.getInstance().getPlatformIcon(com.intellij.ui.PlatformIcons.Class);
|
||||
return IconManager.getInstance().getPlatformIcon(PlatformIcons.Class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -236,9 +241,16 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
|
||||
|
||||
if (!cls.isNewStyleClass(contextToUse)) return null;
|
||||
|
||||
final List<String> ownSlots = cls.getOwnSlots();
|
||||
if (ownSlots == null || ownSlots.contains(PyNames.DUNDER_DICT)) {
|
||||
return null;
|
||||
List<String> ownSlots = cls.getOwnSlots();
|
||||
if (ownSlots != null && ownSlots.contains(PyNames.DUNDER_DICT)) {
|
||||
return null; // not "viably slotted" due to __dict__ in __slots__
|
||||
}
|
||||
if (ownSlots == null) {
|
||||
// check @dataclass(slots=True)
|
||||
ownSlots = getOwnSlotsSynthesized(contextToUse);
|
||||
}
|
||||
if (ownSlots == null) {
|
||||
return null; // not "viably slotted"
|
||||
}
|
||||
|
||||
result.addAll(ownSlots);
|
||||
@@ -266,6 +278,39 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns null if this class is not made "viably slotted" via <code>@dataclass(slots=True)</code>.
|
||||
* Returns a list of all valid slotted attribute names otherwise.
|
||||
*/
|
||||
private @Nullable List<@NotNull String> getOwnSlotsSynthesized(@NotNull TypeEvalContext context) {
|
||||
PyDataclassParameters dcParams = parseDataclassParameters(this, context);
|
||||
if (dcParams != null && dcParams.getSlots()) {
|
||||
List<String> result = new ArrayList<>();
|
||||
var initVars = PyDataclassTypeProvider.Companion.getInitVars(this, dcParams, context);
|
||||
var initVarTargets = initVars == null ? emptySet() : ContainerUtil.map2Set(initVars, iv -> iv.getTargetExpression());
|
||||
var attributes = getClassAttributes();
|
||||
|
||||
for (PyTargetExpression target : attributes) {
|
||||
final String name = target.getName();
|
||||
if (name == null) continue;
|
||||
|
||||
// Include only dataclass instance fields:
|
||||
// - must be an annotated field (PEP 526), not an unannotated class attribute
|
||||
// - must not be typing.ClassVar
|
||||
// - must not be dataclasses.InitVar
|
||||
final boolean isAnnotatedField = target.getAnnotation() != null || target.getParent() instanceof PyTypeDeclarationStatement;
|
||||
if (!isAnnotatedField) continue;
|
||||
if (PyTypingTypeProvider.isClassVar(target, context)) continue;
|
||||
if (initVarTargets.contains(target)) continue;
|
||||
|
||||
result.add(name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<@NotNull String> getOwnMatchArgs() {
|
||||
final PyClassStub stub = getStub();
|
||||
@@ -1023,7 +1068,7 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PyTargetExpression> getClassAttributes() {
|
||||
public @NotNull List<PyTargetExpression> getClassAttributes() {
|
||||
final List<PyTargetExpression> result = new ArrayList<>();
|
||||
PyClassStub stub = getStub();
|
||||
if (stub != null) {
|
||||
@@ -1132,12 +1177,12 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
|
||||
}
|
||||
PyFunction initMethod = findMethodByName(PyNames.INIT, false, null);
|
||||
if (initMethod != null) {
|
||||
collectInstanceAttributes(initMethod, result, Collections.emptySet(), scopesToSkip);
|
||||
collectInstanceAttributes(initMethod, result, emptySet(), scopesToSkip);
|
||||
}
|
||||
}
|
||||
|
||||
public static void collectInstanceAttributes(@NotNull PyFunction method, final @NotNull Map<String, PyTargetExpression> result) {
|
||||
collectInstanceAttributes(method, result, Collections.emptySet(), Collections.emptyMap());
|
||||
collectInstanceAttributes(method, result, emptySet(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
private static void collectInstanceAttributes(@NotNull PyFunction method,
|
||||
|
||||
@@ -34,10 +34,11 @@ class PyDataclassStubImpl(
|
||||
private val frozen: Boolean?,
|
||||
private val matchArgs: Boolean?,
|
||||
private val kwOnly: Boolean?,
|
||||
private val slots: Boolean?,
|
||||
) : PyDataclassStub {
|
||||
|
||||
companion object {
|
||||
val NON_PARAMETERIZED_DATACLASS_TRANSFORM_CANDIDATE_STUB = PyDataclassStubImpl(
|
||||
val NON_PARAMETERIZED_DATACLASS_TRANSFORM_CANDIDATE_STUB: PyDataclassStub = PyDataclassStubImpl(
|
||||
type = PredefinedType.DATACLASS_TRANSFORM.name,
|
||||
decoratorName = null,
|
||||
init = null,
|
||||
@@ -47,7 +48,8 @@ class PyDataclassStubImpl(
|
||||
unsafeHash = null,
|
||||
frozen = null,
|
||||
matchArgs = null,
|
||||
kwOnly = null
|
||||
kwOnly = null,
|
||||
slots = null
|
||||
)
|
||||
|
||||
fun create(cls: PyClass): PyDataclassStub? {
|
||||
@@ -66,8 +68,9 @@ class PyDataclassStubImpl(
|
||||
val frozen = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
|
||||
val matchArgs = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
|
||||
val kwOnly = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
|
||||
val slots = DataInputOutputUtil.readNullable(stream, stream::readBoolean)
|
||||
|
||||
return PyDataclassStubImpl(type, decoratorName, init, repr, eq, order, unsafeHash, frozen, matchArgs, kwOnly)
|
||||
return PyDataclassStubImpl(type, decoratorName, init, repr, eq, order, unsafeHash, frozen, matchArgs, kwOnly, slots)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +87,7 @@ class PyDataclassStubImpl(
|
||||
DataInputOutputUtil.writeNullable(stream, frozen, stream::writeBoolean)
|
||||
DataInputOutputUtil.writeNullable(stream, matchArgs, stream::writeBoolean)
|
||||
DataInputOutputUtil.writeNullable(stream, kwOnly, stream::writeBoolean)
|
||||
DataInputOutputUtil.writeNullable(stream, slots, stream::writeBoolean)
|
||||
}
|
||||
|
||||
override fun getType(): String = type
|
||||
@@ -96,6 +100,7 @@ class PyDataclassStubImpl(
|
||||
override fun frozenValue(): Boolean? = frozen
|
||||
override fun matchArgsValue(): Boolean? = matchArgs
|
||||
override fun kwOnly(): Boolean? = kwOnly
|
||||
override fun slotsValue(): Boolean? = slots
|
||||
|
||||
override fun toString(): String {
|
||||
return "PyDataclassStub(" +
|
||||
@@ -108,7 +113,8 @@ class PyDataclassStubImpl(
|
||||
"unsafeHash=$unsafeHash, " +
|
||||
"frozen=$frozen, " +
|
||||
"matchArgs=$matchArgs, " +
|
||||
"kwOnly=$kwOnly" +
|
||||
"kwOnly=$kwOnly, " +
|
||||
"slots=$slots" +
|
||||
")"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,4 +63,10 @@ public interface PyDataclassStub extends PyCustomClassStub {
|
||||
* its default value if it is not specified or could not be evaluated.
|
||||
*/
|
||||
@Nullable Boolean kwOnly();
|
||||
|
||||
/**
|
||||
* @return value of `slots` parameter or
|
||||
* its default value if it is not specified or could not be evaluated.
|
||||
*/
|
||||
@Nullable Boolean slotsValue();
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@ C.attr = 'spam'
|
||||
print(C.attr)
|
||||
|
||||
c = C()
|
||||
<warning descr="'C' object attribute 'attr' is read-only">c.attr</warning> = 'spam'
|
||||
<warning descr="'C' object has no attribute 'attr'">c.attr</warning> = 'spam'
|
||||
print(c.attr)
|
||||
+1
-1
@@ -9,5 +9,5 @@ C.attr = 'spam'
|
||||
print(C.attr)
|
||||
|
||||
c = C()
|
||||
<warning descr="'C' object attribute 'attr' is read-only">c.attr</warning> = 'spam'
|
||||
<warning descr="'C' object has no attribute 'attr'">c.attr</warning> = 'spam'
|
||||
print(c.attr)
|
||||
+1
-1
@@ -9,5 +9,5 @@ C.attr = 'spam'
|
||||
print(C.attr)
|
||||
|
||||
c = C()
|
||||
<warning descr="'C' object attribute 'attr' is read-only">c.attr</warning> = 'spam'
|
||||
<warning descr="'C' object has no attribute 'attr'">c.attr</warning> = 'spam'
|
||||
print(c.attr)
|
||||
+1
-1
@@ -9,5 +9,5 @@ C.attr = 'spam'
|
||||
print(C.attr)
|
||||
|
||||
c = C()
|
||||
<warning descr="'C' object attribute 'attr' is read-only">c.attr</warning> = 'spam'
|
||||
<warning descr="'C' object has no attribute 'attr'">c.attr</warning> = 'spam'
|
||||
print(c.attr)
|
||||
@@ -6,5 +6,5 @@ Foo.attr = 'spam'
|
||||
print(Foo.attr)
|
||||
|
||||
foo = Foo()
|
||||
<warning descr="'Foo' object attribute 'attr' is read-only">foo.attr</warning> = 'spam'
|
||||
<warning descr="'Foo' object has no attribute 'attr'">foo.attr</warning> = 'spam'
|
||||
print(foo.attr)
|
||||
+1
-1
@@ -6,5 +6,5 @@ Foo.attr = 'spam'
|
||||
print(Foo.attr)
|
||||
|
||||
foo = Foo()
|
||||
<warning descr="'Foo' object attribute 'attr' is read-only">foo.attr</warning> = 'spam'
|
||||
<warning descr="'Foo' object has no attribute 'attr'">foo.attr</warning> = 'spam'
|
||||
print(foo.attr)
|
||||
+1
-1
@@ -6,5 +6,5 @@ Foo.attr = 'spam'
|
||||
print(Foo.attr)
|
||||
|
||||
foo = Foo()
|
||||
<warning descr="'Foo' object attribute 'attr' is read-only">foo.attr</warning> = 'spam'
|
||||
<warning descr="'Foo' object has no attribute 'attr'">foo.attr</warning> = 'spam'
|
||||
print(foo.attr)
|
||||
+1
-1
@@ -9,5 +9,5 @@ C.attr = 'spam'
|
||||
print(C.attr)
|
||||
|
||||
c = C()
|
||||
<warning descr="'C' object attribute 'attr' is read-only">c.attr</warning> = 'spam'
|
||||
<warning descr="'C' object has no attribute 'attr'">c.attr</warning> = 'spam'
|
||||
print(c.attr)
|
||||
+1
-1
@@ -9,5 +9,5 @@ C.attr = 'spam'
|
||||
print(C.attr)
|
||||
|
||||
c = C()
|
||||
<warning descr="'C' object attribute 'attr' is read-only">c.attr</warning> = 'spam'
|
||||
<warning descr="'C' object has no attribute 'attr'">c.attr</warning> = 'spam'
|
||||
print(c.attr)
|
||||
+1
-1
@@ -9,5 +9,5 @@ C.attr = 'spam'
|
||||
print(C.attr)
|
||||
|
||||
c = C()
|
||||
<warning descr="'C' object attribute 'attr' is read-only">c.attr</warning> = 'spam'
|
||||
<warning descr="'C' object has no attribute 'attr'">c.attr</warning> = 'spam'
|
||||
print(c.attr)
|
||||
+1
-1
@@ -9,5 +9,5 @@ C.attr = 'spam'
|
||||
print(C.attr)
|
||||
|
||||
c = C()
|
||||
<warning descr="'C' object attribute 'attr' is read-only">c.attr</warning> = 'spam'
|
||||
<warning descr="'C' object has no attribute 'attr'">c.attr</warning> = 'spam'
|
||||
print(c.attr)
|
||||
@@ -0,0 +1,13 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Foo1:
|
||||
bar: str = "hello"
|
||||
|
||||
@dataclass(slots=False)
|
||||
class Foo2:
|
||||
bar: str = "hello"
|
||||
|
||||
@dataclass
|
||||
class Foo3:
|
||||
bar: str = "hello"
|
||||
@@ -12,7 +12,6 @@ constructors_call_new.py
|
||||
constructors_call_type.py
|
||||
constructors_callable.py
|
||||
dataclasses_order.py
|
||||
dataclasses_slots.py
|
||||
dataclasses_transform_converter.py
|
||||
dataclasses_transform_field.py
|
||||
dataclasses_transform_meta.py
|
||||
|
||||
@@ -1138,6 +1138,17 @@ public class PyStubsTest extends PyTestCase {
|
||||
assertNotParsed(file);
|
||||
}
|
||||
|
||||
// PY-76811
|
||||
public void testDataclassSlotsOnClass() {
|
||||
final PyFile file = getTestFile();
|
||||
|
||||
assertTrue(file.findTopLevelClass("Foo1").getStub().getCustomStub(PyDataclassStub.class).slotsValue());
|
||||
assertFalse(file.findTopLevelClass("Foo2").getStub().getCustomStub(PyDataclassStub.class).slotsValue());
|
||||
assertNull(file.findTopLevelClass("Foo3").getStub().getCustomStub(PyDataclassStub.class).slotsValue());
|
||||
|
||||
assertNotParsed(file);
|
||||
}
|
||||
|
||||
// PY-62608
|
||||
public void testTypeParameterListInFunctionDeclaration() {
|
||||
PyFile file = getTestFile();
|
||||
|
||||
@@ -568,8 +568,8 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase {
|
||||
|
||||
class Color(Enum):
|
||||
R = 1
|
||||
G == 2
|
||||
B == 3
|
||||
G = 2
|
||||
B = 3
|
||||
RED = R
|
||||
BLUE = B
|
||||
|
||||
|
||||
@@ -3,12 +3,18 @@ package com.jetbrains.python.inspections;
|
||||
|
||||
import com.intellij.openapi.application.WriteAction;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.jetbrains.python.PyPsiBundle;
|
||||
import com.jetbrains.python.PythonFileType;
|
||||
import com.jetbrains.python.fixtures.PyInspectionTestCase;
|
||||
import com.jetbrains.python.psi.LanguageLevel;
|
||||
import com.jetbrains.python.psi.PyClass;
|
||||
import com.jetbrains.python.psi.PyFile;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.List;
|
||||
|
||||
public class PyDunderSlotsInspectionTest extends PyInspectionTestCase {
|
||||
|
||||
@@ -302,6 +308,106 @@ public class PyDunderSlotsInspectionTest extends PyInspectionTestCase {
|
||||
self.a = a""");
|
||||
}
|
||||
|
||||
// PY-76811
|
||||
public void testDataclassSlotsEnabledTwice() {
|
||||
doTestByText("""
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(<warning descr="A data class that explicitly defines '__slots__' must not be configured with 'slots=True'">slots=True</warning>)
|
||||
class C:
|
||||
__slots__ = ("a",)
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-76811
|
||||
public void testDataclassSlotsEnabledTwiceQuickFix() {
|
||||
String before = """
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(<warning descr="A data class that explicitly defines '__slots__' must not be configured with 'slots=True'">slots=<caret>True</warning>)
|
||||
class C:
|
||||
__slots__ = ("a",)
|
||||
""";
|
||||
|
||||
String after = """
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass()
|
||||
class C:
|
||||
__slots__ = ("a",)
|
||||
""";
|
||||
|
||||
myFixture.configureByText(PythonFileType.INSTANCE, before);
|
||||
configureInspection();
|
||||
var action = myFixture.findSingleIntention(PyPsiBundle.message("QFIX.dunder.slots.enabled.twice"));
|
||||
myFixture.launchAction(action);
|
||||
myFixture.checkResult(after);
|
||||
}
|
||||
|
||||
// PY-76811
|
||||
public void testDataclassSlotsExcludesInitVarClassVarUnannotatedAndNonFields() {
|
||||
String code = """
|
||||
from dataclasses import dataclass, InitVar
|
||||
from typing import ClassVar
|
||||
|
||||
@dataclass(slots=True)
|
||||
class C1:
|
||||
a: int # should be slotted
|
||||
b: ClassVar[int] # excluded (ClassVar)
|
||||
c = 1 # excluded (unannotated class attr)
|
||||
d: InitVar[int] # excluded (InitVar)
|
||||
|
||||
# Non-field members should never become slots
|
||||
def meth(self): # excluded (method)
|
||||
pass
|
||||
|
||||
@property # excluded (property)
|
||||
def p(self) -> int:
|
||||
return 0
|
||||
""";
|
||||
|
||||
PyClass cls = loadClass("C1", code);
|
||||
TypeEvalContext ctx = TypeEvalContext.codeAnalysis(myFixture.getProject(), cls.getContainingFile());
|
||||
|
||||
List<String> slots = cls.getSlots(ctx);
|
||||
assertNotNull("Slots should be computed for dataclass(slots=True)", slots);
|
||||
|
||||
// Only 'a' should be present
|
||||
assertEquals(List.of("a"), slots);
|
||||
}
|
||||
|
||||
// PY-76811
|
||||
public void testDataclassSlotsExcludesInitVarAndClassVarViaAliases() {
|
||||
String code = """
|
||||
import dataclasses as dc
|
||||
import typing as t
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class C2:
|
||||
ok: int # should be slotted
|
||||
cv: t.ClassVar[str] # excluded
|
||||
iv: dc.InitVar[bytes] # excluded
|
||||
u = object() # excluded (unannotated)
|
||||
""";
|
||||
|
||||
PyClass cls = loadClass("C2", code);
|
||||
TypeEvalContext ctx = TypeEvalContext.codeAnalysis(myFixture.getProject(), cls.getContainingFile());
|
||||
|
||||
List<String> slots = cls.getSlots(ctx);
|
||||
assertNotNull("Slots should be computed for dataclass(slots=True)", slots);
|
||||
|
||||
assertEquals(List.of("ok"), slots);
|
||||
}
|
||||
|
||||
private @NotNull PyClass loadClass(@NotNull String className, @NotNull String code) {
|
||||
myFixture.configureByText(PythonFileType.INSTANCE, code);
|
||||
PyFile pyFile = (PyFile)myFixture.getFile();
|
||||
PyClass cls = pyFile.findTopLevelClass(className);
|
||||
assertNotNull("Class " + className + " should be found", cls);
|
||||
return cls;
|
||||
}
|
||||
|
||||
|
||||
private void doTestPy2() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON27, this::doTest);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ private val inspections
|
||||
PyCallingNonCallableInspection(),
|
||||
PyClassVarInspection(),
|
||||
PyDataclassInspection(),
|
||||
PyDunderSlotsInspection(),
|
||||
PyEnumInspection(),
|
||||
PyFinalInspection(),
|
||||
//PyInitNewSignatureInspection(), // False negative constructors_consistency.py
|
||||
|
||||
Reference in New Issue
Block a user