Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix detecting validators decorated methods #196

Merged
merged 1 commit into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<h2>version 0.1.14</h2>
<p>BugFixes</p>
<ul>
<li>Fix detecting validators decorated methods [#196]</li>
<li>Remove stub deletion error [#190]</li>
</ul>
<h2>version 0.1.13</h2>
Expand Down
48 changes: 37 additions & 11 deletions src/com/koxudaxi/pydantic/Pydantic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ import java.util.regex.Pattern

const val BASE_MODEL_Q_NAME = "pydantic.main.BaseModel"
const val DATA_CLASS_Q_NAME = "pydantic.dataclasses.dataclass"
const val VALIDATOR_Q_NAME = "pydantic.validator"
const val ROOT_VALIDATOR_Q_NAME = "pydantic.root_validator"
const val DATA_CLASS_SHORT_Q_NAME = "pydantic.dataclass"
const val VALIDATOR_Q_NAME = "pydantic.class_validators.validator"
const val VALIDATOR_SHORT_Q_NAME = "pydantic.validator"
const val ROOT_VALIDATOR_Q_NAME = "pydantic.class_validators.root_validator"
const val ROOT_VALIDATOR_SHORT_Q_NAME = "pydantic.root_validator"
const val SCHEMA_Q_NAME = "pydantic.schema.Schema"
const val FIELD_Q_NAME = "pydantic.fields.Field"
const val DATACLASS_FIELD_Q_NAME = "dataclasses.field"
const val DEPRECATED_SCHEMA_Q_NAME = "pydantic.fields.Schema"
const val BASE_SETTINGS_Q_NAME = "pydantic.env_settings.BaseSettings"
const val VERSION_Q_NAME = "pydantic.version.VERSION"
const val BASE_CONFIG_Q_NAME = "pydantic.BaseConfig"
const val BASE_CONFIG_Q_NAME = "pydantic.main.BaseConfig"
const val DATACLASS_MISSING = "dataclasses.MISSING"
const val CON_BYTES_Q_NAME = "pydantic.types.conbytes"
const val CON_DECIMAL_Q_NAME = "pydantic.types.condecimal"
Expand All @@ -50,6 +53,30 @@ val BASE_CONFIG_QUALIFIED_NAME = QualifiedName.fromDottedString(BASE_CONFIG_Q_NA

val BASE_MODEL_QUALIFIED_NAME = QualifiedName.fromDottedString(BASE_MODEL_Q_NAME)

val VALIDATOR_QUALIFIED_NAME = QualifiedName.fromDottedString(VALIDATOR_Q_NAME)

val VALIDATOR_SHORT_QUALIFIED_NAME = QualifiedName.fromDottedString(VALIDATOR_SHORT_Q_NAME)

val ROOT_VALIDATOR_QUALIFIED_NAME = QualifiedName.fromDottedString(ROOT_VALIDATOR_Q_NAME)

val ROOT_VALIDATOR_SHORT_QUALIFIED_NAME = QualifiedName.fromDottedString(ROOT_VALIDATOR_SHORT_Q_NAME)

val DATA_CLASS_QUALIFIED_NAME = QualifiedName.fromDottedString(DATA_CLASS_Q_NAME)

val DATA_CLASS_SHORT_QUALIFIED_NAME = QualifiedName.fromDottedString(DATA_CLASS_SHORT_Q_NAME)

val DATA_CLASS_QUALIFIED_NAMES = listOf(
DATA_CLASS_QUALIFIED_NAME,
DATA_CLASS_SHORT_QUALIFIED_NAME
)

val VALIDATOR_QUALIFIED_NAMES = listOf(
VALIDATOR_QUALIFIED_NAME,
VALIDATOR_SHORT_QUALIFIED_NAME,
ROOT_VALIDATOR_QUALIFIED_NAME,
ROOT_VALIDATOR_SHORT_QUALIFIED_NAME
)

val VERSION_SPLIT_PATTERN: Pattern = Pattern.compile("[.a-zA-Z]")!!

val pydanticVersionCache: HashMap<String, KotlinVersion> = hashMapOf()
Expand Down Expand Up @@ -99,17 +126,16 @@ internal fun isBaseSetting(pyClass: PyClass, context: TypeEvalContext): Boolean
return pyClass.isSubclass(BASE_SETTINGS_Q_NAME, context)
}

internal fun hasDecorator(pyDecoratable: PyDecoratable, refName: String): Boolean {
pyDecoratable.decoratorList?.decorators?.mapNotNull { it.callee as? PyReferenceExpression }?.forEach {
PyResolveUtil.resolveImportedElementQNameLocally(it).forEach { decoratorQualifiedName ->
if (decoratorQualifiedName == QualifiedName.fromDottedString(refName)) return true
internal fun hasDecorator(pyDecoratable: PyDecoratable, refNames: List<QualifiedName>): Boolean {
return pyDecoratable.decoratorList?.decorators?.mapNotNull { it.callee as? PyReferenceExpression }?.any {
PyResolveUtil.resolveImportedElementQNameLocally(it).any { decoratorQualifiedName ->
refNames.any { refName -> decoratorQualifiedName == refName }
}
}
return false
} ?: false
}

internal fun isPydanticDataclass(pyClass: PyClass): Boolean {
return hasDecorator(pyClass, DATA_CLASS_Q_NAME)
return hasDecorator(pyClass, DATA_CLASS_QUALIFIED_NAMES)
}

internal fun isPydanticSchema(pyClass: PyClass, context: TypeEvalContext): Boolean {
Expand All @@ -134,7 +160,7 @@ internal fun isDataclassMissing(pyTargetExpression: PyTargetExpression): Boolean
}

internal fun isValidatorMethod(pyFunction: PyFunction): Boolean {
return hasDecorator(pyFunction, VALIDATOR_Q_NAME) || hasDecorator(pyFunction, ROOT_VALIDATOR_Q_NAME)
return hasDecorator(pyFunction, VALIDATOR_QUALIFIED_NAMES)
}

internal fun isConfigClass(pyClass: PyClass): Boolean {
Expand Down
10 changes: 10 additions & 0 deletions testData/ignoreinspection/validatorFullPath.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from pydantic import BaseModel
from pydantic.class_validators import validator


class A(BaseModel):
a: str

@validator('a')
def vali<caret>date_a(cls):
pass
36 changes: 36 additions & 0 deletions testData/inspection/rootValidatorSelf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,42 @@ class A(BaseModel):
d: str
e: str

@root_validator('a')
def validate_a(<weak_warning descr="Usually first parameter of such methods is named 'cls'">self</weak_warning>):
pass

@root_validator('b')
def validate_b(<weak_warning descr="Usually first parameter of such methods is named 'cls'">fles</weak_warning>):
pass

@root_validator('c')
def validate_b(*args):
pass

@root_validator('d')
def validate_c(**kwargs):
pass

@root_validator('e')
def validate_e<error descr="Method must have a first parameter, usually called 'cls'">()</error>:
pass

def dummy(self):
pass

@check
def task(self):
pass

from pydantic.class_validators import root_validator

class B(BaseModel):
a: str
b: str
c: str
d: str
e: str

@root_validator('a')
def validate_a(<weak_warning descr="Usually first parameter of such methods is named 'cls'">self</weak_warning>):
pass
Expand Down
4 changes: 4 additions & 0 deletions testSrc/com/koxudaxi/pydantic/PydanticIgnoreInspectionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ open class PydanticIgnoreInspectionTest : PydanticTestCase() {
doTest(true)
}

fun testValidatorFullPath() {
doTest(true)
}

fun testBaseModel() {
doTest(false)
}
Expand Down