diff --git a/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml b/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml
index 0743e9e1dace..8bb1c12111b7 100644
--- a/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml
+++ b/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml
@@ -128,7 +128,7 @@
-
+
diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
index d8a5c70ccb83..fa4f31eb719a 100644
--- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties
+++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
@@ -442,7 +442,6 @@ INSP.pep8.naming=PEP 8 naming convention violation
INSP.pep8.naming.excluded.base.classes=Excluded base classes
INSP.pep8.naming.ignored.errors=Ignored errors
INSP.shadowing.builtins=Shadowing built-ins
-INSP.dataclass.definition.and.usages=Dataclass definition and usages
INSP.dunder.slots=Definition of __slots__ in a class
INSP.final.classes.methods.and.variables=Final classes, methods and variables
INSP.global.undefined=Global variable is undefined at the module level
@@ -868,6 +867,39 @@ INSP.class.$0.superclass.$1.already.caught=''{0}'', superclass of exception clas
INSP.NAME.global.undefined=Global variable is undefined at the module level
INSP.NAME.global.$0.undefined=Global variable ''{0}'' is undefined at the module level
+#PyDataclassInspection
+INSP.NAME.dataclass.definition.and.usages=Dataclass definition and usages
+INSP.dataclasses.operator.not.supported.between.instances.of.class=''{0}'' not supported between instances of ''{1}''
+INSP.dataclasses.operator.not.supported.between.instances.of.classes=''{0}'' not supported between instances of ''{1}'' and ''{2}''
+INSP.dataclasses.object.could.have.no.attribute.because.it.declared.as.init.only=''{0}'' object could have no attribute ''{1}'' because it is declared as init-only
+INSP.dataclasses.object.attribute.read.only=''{0}'' object attribute ''{1}'' is read-only
+INSP.dataclasses.eq.must.be.true.if.order.true='eq' must be true if 'order' is true
+INSP.dataclasses.argument.ignored.if.class.already.defines.method=''{0}'' is ignored if the class already defines ''{1}'' method
+INSP.dataclasses.order.argument.should.be.false.if.class.defines.one.of.order.methods='order' should be false if the class defines one of order methods
+INSP.dataclasses.frozen.attribute.should.be.false.if.class.defines.setattr.or.delattr='frozen' should be false if the class defines '__setattr__' or '__delattr__'
+INSP.dataclasses.unsafe.hash.attribute.should.be.false.if.class.defines.hash='unsafe_hash' should be false if the class defines '__hash__'
+INSP.dataclasses.frozen.dataclasses.can.not.inherit.non.frozen.one=Frozen dataclasses can not inherit non-frozen one and vice versa
+INSP.dataclasses.method.is.ignored.if.class.already.defines.parameter=''{0}'' is ignored if the class already defines ''{1}'' parameter
+INSP.dataclasses.hash.ignored.if.class.already.defines.cmp.or.order.or.frozen.parameters='__hash__' is ignored if the class already defines 'cmp/order' and 'frozen' parameters
+INSP.dataclasses.mutable.attribute.default.not.allowed.use.default.factory=Mutable default ''{0}'' is not allowed. Use ''default_factory''
+INSP.dataclasses.attribute.default.is.set.using.attr.ib=A default is set using 'attr.ib()'
+INSP.dataclasses.attribute.default.set.using.method=A default is set using ''{0}''
+INSP.dataclasses.method.should.take.only.n.parameter=''{0}'' should take only {1} {1,choice,1#parameter|2#parameters}
+INSP.dataclasses.attribute.lacks.type.annotation=Attribute ''{0}'' lacks a type annotation
+INSP.dataclasses.cannot.specify.both.default.and.factory=Cannot specify both 'default' and 'factory'
+INSP.dataclasses.attribute.useless.until.post.init.declared=Attribute ''{0}'' is useless until ''__post_init__'' is declared
+INSP.dataclasses.field.cannot.have.default.factory=Field cannot have a default factory
+INSP.dataclasses.cannot.specify.both.default.and.default.factory=Cannot specify both 'default' and 'default_factory'
+INSP.dataclasses.post.init.would.not.be.called.until.init.parameter.set.to.true='__post_init__' would not be called until 'init' parameter is set to True
+INSP.dataclasses.post.init.should.take.all.init.only.variables.including.inherited.in.same.order.they.defined='__post_init__' should take all init-only variables (incl. inherited) in the same order as they are defined
+INSP.dataclasses.post.init.should.take.all.init.only.variables.in.same.order.they.defined='__post_init__' should take all init-only variables in the same order as they are defined
+INSP.dataclasses.attrs.post.init.would.not.be.called.until.init.parameter.set.to.true='__attrs_post_init__' would not be called until 'init' parameter is set to True
+INSP.dataclasses.attrs.post.init.should.not.take.any.parameters.except.self='__attrs_post_init__' should not take any parameters except 'self'
+INSP.dataclasses.method.should.be.called.on.dataclass.instances.or.types=''{0}'' method should be called on dataclass instances or types
+INSP.dataclasses.method.should.be.called.on.dataclass.instances=''{0}'' method should be called on dataclass instances
+INSP.dataclasses.method.should.be.called.on.attrs.instances=''{0}'' method should be called on attrs instances
+INSP.dataclasses.method.should.be.called.on.attrs.types=''{0}'' method should be called on attrs types
+
# PyHighlightingAnnotator
INSP.python.trailing.suffix.not.support=Python does not support a trailing ''{0}''
diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt
index 03f5e81b5fff..5719e70ebad5 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt
+++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt
@@ -11,6 +11,7 @@ import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.containers.isNullOrEmpty
import com.jetbrains.python.PyNames
+import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.codeInsight.*
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.psi.*
@@ -162,7 +163,7 @@ class PyDataclassInspection : PyInspection() {
if (leftClass == rightClass) {
if (leftOrder == ClassOrder.DC_UNORDERED && rightOrder != ClassOrder.MANUALLY) {
registerProblem(node.psiOperator,
- "'$leftOperator' not supported between instances of '${leftClass.name}'",
+ PyPsiBundle.message("INSP.dataclasses.operator.not.supported.between.instances.of.class", leftOperator, leftClass.name),
ProblemHighlightType.GENERIC_ERROR)
}
}
@@ -176,7 +177,7 @@ class PyDataclassInspection : PyInspection() {
rightClass.isSubclass(leftClass, myTypeEvalContext)) return // attrs allows to compare ancestor and its subclass
registerProblem(node.psiOperator,
- "'$leftOperator' not supported between instances of '${leftClass.name}' and '${rightClass.name}'",
+ PyPsiBundle.message("INSP.dataclasses.operator.not.supported.between.instances.of.classes", leftOperator, leftClass.name, rightClass.name),
ProblemHighlightType.GENERIC_ERROR)
}
}
@@ -223,7 +224,7 @@ class PyDataclassInspection : PyInspection() {
if (resolved.isNotEmpty() && resolved.asSequence().map { it.element }.all { it is PyTargetExpression && isInitVar(it) }) {
registerProblem(node.lastChild,
- "'${cls.name}' object could have no attribute '${node.name}' because it is declared as init-only",
+ PyPsiBundle.message("INSP.dataclasses.object.could.have.no.attribute.because.it.declared.as.init.only", cls.name, node.name),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
}
}
@@ -237,7 +238,7 @@ class PyDataclassInspection : PyInspection() {
.mapNotNull { parseDataclassParameters(it, myTypeEvalContext) }
.any { it.frozen }) {
registerProblem(expression,
- "'${cls.name}' object attribute '${expression.name}' is read-only",
+ PyPsiBundle.message("INSP.dataclasses.object.attribute.read.only", cls.name, expression.name),
ProblemHighlightType.GENERIC_ERROR)
}
}
@@ -276,7 +277,7 @@ class PyDataclassInspection : PyInspection() {
private fun processDataclassParameters(cls: PyClass, dataclassParameters: PyDataclassParameters) {
if (!dataclassParameters.eq && dataclassParameters.order) {
- registerProblem(dataclassParameters.eqArgument, "'eq' must be true if 'order' is true", ProblemHighlightType.GENERIC_ERROR)
+ registerProblem(dataclassParameters.eqArgument, PyPsiBundle.message("INSP.dataclasses.eq.must.be.true.if.order.true"), ProblemHighlightType.GENERIC_ERROR)
}
var initMethodExists = false
@@ -316,25 +317,25 @@ class PyDataclassInspection : PyInspection() {
useless.forEach {
registerProblem(it.first,
- "'${it.second}' is ignored if the class already defines '${it.third}' method",
+ PyPsiBundle.message("INSP.dataclasses.argument.ignored.if.class.already.defines.method", it.second, it.third),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
}
if (dataclassParameters.order && orderMethodsExist) {
registerProblem(dataclassParameters.orderArgument,
- "'order' should be false if the class defines one of order methods",
+ PyPsiBundle.message("INSP.dataclasses.order.argument.should.be.false.if.class.defines.one.of.order.methods"),
ProblemHighlightType.GENERIC_ERROR)
}
if (dataclassParameters.frozen && mutatingMethodsExist) {
registerProblem(dataclassParameters.frozenArgument,
- "'frozen' should be false if the class defines '__setattr__' or '__delattr__'",
+ PyPsiBundle.message("INSP.dataclasses.frozen.attribute.should.be.false.if.class.defines.setattr.or.delattr"),
ProblemHighlightType.GENERIC_ERROR)
}
if (dataclassParameters.unsafeHash && hashMethodExists) {
registerProblem(dataclassParameters.unsafeHashArgument,
- "'unsafe_hash' should be false if the class defines '${PyNames.HASH}'",
+ PyPsiBundle.message("INSP.dataclasses.unsafe.hash.attribute.should.be.false.if.class.defines.hash"),
ProblemHighlightType.GENERIC_ERROR)
}
@@ -347,7 +348,7 @@ class PyDataclassInspection : PyInspection() {
}
else if (frozenInHierarchy != currentFrozen) {
registerProblem(dataclassParameters.frozenArgument ?: cls.nameIdentifier,
- "Frozen dataclasses can not inherit non-frozen one and vice versa",
+ PyPsiBundle.message("INSP.dataclasses.frozen.dataclasses.can.not.inherit.non.frozen.one"),
ProblemHighlightType.GENERIC_ERROR)
}
}
@@ -405,14 +406,14 @@ class PyDataclassInspection : PyInspection() {
problems.forEach {
it.first?.apply {
registerProblem(nameIdentifier,
- "'$name' is ignored if the class already defines '${it.second}' parameter",
+ PyPsiBundle.message("INSP.dataclasses.method.is.ignored.if.class.already.defines.parameter", name, it.second),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
}
}
if (dataclassParameters.order && dataclassParameters.frozen && hashMethod != null) {
registerProblem(hashMethod?.nameIdentifier,
- "'${PyNames.HASH}' is ignored if the class already defines 'cmp/order' and 'frozen' parameters",
+ PyPsiBundle.message("INSP.dataclasses.hash.ignored.if.class.already.defines.cmp.or.order.or.frozen.parameters"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
}
}
@@ -423,7 +424,7 @@ class PyDataclassInspection : PyInspection() {
val value = field.findAssignedValue()
if (PyUtil.isForbiddenMutableDefault(value, myTypeEvalContext)) {
registerProblem(value,
- "Mutable default '${value?.text}' is not allowed. Use 'default_factory'",
+ PyPsiBundle.message("INSP.dataclasses.mutable.attribute.default.not.allowed.use.default.factory", value?.text),
ProblemHighlightType.GENERIC_ERROR)
}
}
@@ -448,7 +449,9 @@ class PyDataclassInspection : PyInspection() {
val stub = PyDataclassFieldStubImpl.create(attribute)
if (stub != null && (stub.hasDefault() || stub.hasDefaultFactory())) {
- registerProblem(method.nameIdentifier, "A default is set using 'attr.ib()'", ProblemHighlightType.GENERIC_ERROR)
+ registerProblem(method.nameIdentifier,
+ PyPsiBundle.message("INSP.dataclasses.attribute.default.is.set.using.attr.ib"),
+ ProblemHighlightType.GENERIC_ERROR)
}
}
}
@@ -461,7 +464,9 @@ class PyDataclassInspection : PyInspection() {
sameAttrInitializers
.asSequence()
.drop(1)
- .forEach { registerProblem(it.nameIdentifier, "A default is set using '${first.name}'", ProblemHighlightType.GENERIC_ERROR) }
+ .forEach { registerProblem(it.nameIdentifier,
+ PyPsiBundle.message("INSP.dataclasses.attribute.default.set.using.method", first.name),
+ ProblemHighlightType.GENERIC_ERROR) }
}
}
@@ -485,8 +490,7 @@ class PyDataclassInspection : PyInspection() {
val actualParameters = method.parameterList
if (actualParameters.parameters.size != expectedParameters) {
- val message = "'${method.name}' should take only $expectedParameters parameter" +
- if (expectedParameters > 1) "s" else ""
+ val message = PyPsiBundle.message("INSP.dataclasses.method.should.take.only.n.parameter", method.name, expectedParameters)
registerProblem(actualParameters, message, ProblemHighlightType.GENERIC_ERROR)
}
@@ -505,7 +509,8 @@ class PyDataclassInspection : PyInspection() {
PyEvaluator.evaluateAsBoolean(PyUtil.peelArgument(dataclassParameters.others["auto_attribs"]), false)) {
cls.processClassLevelDeclarations { element, _ ->
if (element is PyTargetExpression && element.annotation == null && PyDataclassFieldStubImpl.create(element) != null) {
- registerProblem(element, "Attribute '${element.name}' lacks a type annotation", ProblemHighlightType.GENERIC_ERROR)
+ registerProblem(element, PyPsiBundle.message("INSP.dataclasses.attribute.lacks.type.annotation", element.name),
+ ProblemHighlightType.GENERIC_ERROR)
}
true
@@ -522,7 +527,8 @@ class PyDataclassInspection : PyInspection() {
if (call != null && stub != null) {
if (stub.hasDefaultFactory()) {
if (stub.hasDefault()) {
- registerProblem(call.argumentList, "Cannot specify both 'default' and 'factory'", ProblemHighlightType.GENERIC_ERROR)
+ registerProblem(call.argumentList, PyPsiBundle.message("INSP.dataclasses.cannot.specify.both.default.and.factory"),
+ ProblemHighlightType.GENERIC_ERROR)
}
else {
// at least covers the following case: `attr.ib(default=attr.Factory(...), factory=...)`
@@ -531,7 +537,8 @@ class PyDataclassInspection : PyInspection() {
val factory = call.getKeywordArgument("factory")
if (default != null && factory != null && !resolvesToOmittedDefault(default, PyDataclassParameters.PredefinedType.ATTRS)) {
- registerProblem(call.argumentList, "Cannot specify both 'default' and 'factory'", ProblemHighlightType.GENERIC_ERROR)
+ registerProblem(call.argumentList, PyPsiBundle.message("INSP.dataclasses.cannot.specify.both.default.and.factory"),
+ ProblemHighlightType.GENERIC_ERROR)
}
}
}
@@ -546,7 +553,7 @@ class PyDataclassInspection : PyInspection() {
if (isInitVar(field)) {
if (postInit == null) {
registerProblem(field,
- "Attribute '${field.name}' is useless until '$DUNDER_POST_INIT' is declared",
+ PyPsiBundle.message("INSP.dataclasses.attribute.useless.until.post.init.declared", field.name),
ProblemHighlightType.LIKE_UNUSED_SYMBOL)
}
@@ -563,12 +570,13 @@ class PyDataclassInspection : PyInspection() {
if (PyTypingTypeProvider.isClassVar(field, myTypeEvalContext) || isInitVar(field)) {
if (fieldStub.hasDefaultFactory()) {
registerProblem(call.getKeywordArgument("default_factory"),
- "Field cannot have a default factory",
+ PyPsiBundle.message("INSP.dataclasses.field.cannot.have.default.factory"),
ProblemHighlightType.GENERIC_ERROR)
}
}
else if (fieldStub.hasDefault() && fieldStub.hasDefaultFactory()) {
- registerProblem(call.argumentList, "Cannot specify both 'default' and 'default_factory'", ProblemHighlightType.GENERIC_ERROR)
+ registerProblem(call.argumentList, PyPsiBundle.message("INSP.dataclasses.cannot.specify.both.default.and.default.factory"),
+ ProblemHighlightType.GENERIC_ERROR)
}
}
@@ -578,7 +586,7 @@ class PyDataclassInspection : PyInspection() {
localInitVars: List) {
if (!dataclassParameters.init) {
registerProblem(postInit.nameIdentifier,
- "'$DUNDER_POST_INIT' would not be called until 'init' parameter is set to True",
+ PyPsiBundle.message("INSP.dataclasses.post.init.would.not.be.called.until.init.parameter.set.to.true"),
ProblemHighlightType.LIKE_UNUSED_SYMBOL)
return
@@ -602,10 +610,14 @@ class PyDataclassInspection : PyInspection() {
val implicitParameters = postInit.getParameters(myTypeEvalContext)
val parameters = if (implicitParameters.isEmpty()) emptyList() else ContainerUtil.subList(implicitParameters, 1)
- val message = "'$DUNDER_POST_INIT' " +
- "should take all init-only variables" +
- "${if (allInitVars.size != localInitVars.size) " (incl. inherited)" else ""} " +
- "in the same order as they are defined"
+
+ val message = if (allInitVars.size != localInitVars.size) {
+ PyPsiBundle.message("INSP.dataclasses.post.init.should.take.all.init.only.variables.including.inherited.in.same.order.they.defined")
+ }
+ else {
+ PyPsiBundle.message("INSP.dataclasses.post.init.should.take.all.init.only.variables.in.same.order.they.defined")
+ }
+
if (parameters.size != allInitVars.size) {
registerProblem(postInit.parameterList, message, ProblemHighlightType.GENERIC_ERROR)
@@ -622,13 +634,13 @@ class PyDataclassInspection : PyInspection() {
private fun processAttrsPostInitDefinition(postInit: PyFunction, dataclassParameters: PyDataclassParameters) {
if (!dataclassParameters.init) {
registerProblem(postInit.nameIdentifier,
- "'$DUNDER_ATTRS_POST_INIT' would not be called until 'init' parameter is set to True",
+ PyPsiBundle.message("INSP.dataclasses.attrs.post.init.would.not.be.called.until.init.parameter.set.to.true"),
ProblemHighlightType.LIKE_UNUSED_SYMBOL)
}
if (postInit.getParameters(myTypeEvalContext).size != 1) {
registerProblem(postInit.parameterList,
- "'$DUNDER_ATTRS_POST_INIT' should not take any parameters except '${PyNames.CANONICAL_SELF}'",
+ PyPsiBundle.message("INSP.dataclasses.attrs.post.init.should.not.take.any.parameters.except.self"),
ProblemHighlightType.GENERIC_ERROR)
}
}
@@ -641,7 +653,12 @@ class PyDataclassInspection : PyInspection() {
val type = myTypeEvalContext.getType(argument)
val allowSubclass = calleeQName != "dataclasses.asdict"
if (!isExpectedDataclass(type, PyDataclassParameters.PredefinedType.STD, allowDefinition, true, allowSubclass)) {
- val message = "'$calleeQName' method should be called on dataclass instances" + if (allowDefinition) " or types" else ""
+ val message = if (allowDefinition) {
+ PyPsiBundle.message("INSP.dataclasses.method.should.be.called.on.dataclass.instances.or.types", calleeQName)
+ }
+ else {
+ PyPsiBundle.message("INSP.dataclasses.method.should.be.called.on.dataclass.instances", calleeQName)
+ }
registerProblem(argument, message)
}
@@ -654,7 +671,12 @@ class PyDataclassInspection : PyInspection() {
val type = myTypeEvalContext.getType(argument)
if (!isExpectedDataclass(type, PyDataclassParameters.PredefinedType.ATTRS, !instance, instance, true)) {
- val message = "'$calleeQName' method should be called on attrs " + if (instance) "instances" else "types"
+ val message = if (instance) {
+ PyPsiBundle.message("INSP.dataclasses.method.should.be.called.on.attrs.instances", calleeQName)
+ }
+ else {
+ PyPsiBundle.message("INSP.dataclasses.method.should.be.called.on.attrs.types", calleeQName)
+ }
registerProblem(argument, message)
}