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

add unittest #47

Merged
merged 17 commits into from
Aug 25, 2019
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
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ cache:
- "$HOME/.gradle/caches/"
- "$HOME/.gradle/wrapper/"
install: skip
script: travis_wait ./gradlew buildPlugin
script: travis_wait ./gradlew buildPlugin test jacocoTestReport
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
- rm -fr $HOME/.gradle/caches/*/fileHashes/
after_success:
- bash <(curl -s https://codecov.io/bash)
deploy:
- provider: releases

Expand Down
15 changes: 13 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ allprojects {
apply plugin: "org.jetbrains.intellij"
apply plugin: "java"
apply plugin: "kotlin"
apply plugin: "jacoco"
repositories {
mavenCentral()
}
Expand All @@ -45,17 +46,27 @@ allprojects {
kotlinOptions {
jvmTarget = "1.8"
languageVersion = "1.3"
apiVersion = "1.23"
apiVersion = "1.3"
}
}

dependencies {
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
testCompile group: 'junit', name: 'junit', version: '4.11'
}

jacocoTestReport {
reports {
xml.enabled true
html.enabled true
}
}


sourceCompatibility = 1.8
targetCompatibility = 1.8


}

sourceSets {
Expand All @@ -64,6 +75,6 @@ sourceSets {
resources.srcDir 'resources'
}
test {
java.srcDir 'test'
java.srcDir 'testSrc'
}
}
8 changes: 8 additions & 0 deletions pydantic-pycharm-plugin.iml
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PLUGIN_MODULE" version="4">
<component name="DevKit.ModuleBuildProperties" url="file://$MODULE_DIR$/resources/META-INF/plugin.xml" />
<component name="FacetManager">
<facet type="Python" name="Python">
<configuration sdkName="Python 3.7 (pydantic-pycharm-plugin)" />
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/testData" type="java-test-resource" />
<excludePattern pattern="out" />
</content>
<orderEntry type="jdk" jdkName="JavaSDK" jdkType="IDEA JDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Python 3.7 (pydantic-pycharm-plugin) interpreter library" level="application" />
</component>
</module>
8 changes: 7 additions & 1 deletion resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<idea-plugin url="https://github.com/koxudaxi/pydantic-pycharm-plugin">
<id>com.koxudaxi.pydantic</id>
<name>Pydantic</name>
<version>0.0.14</version>
<version>0.0.15</version>
<vendor email="koaxudai@gmail.com">Koudai Aono @koxudaxi</vendor>
<change-notes><![CDATA[
<h2>version 0.0.15</h2>
<p>Features</p>
<ul>
<li>Support to detect types by default value on Schema [#49] <li>
<li>Improve inner logic [#47] </li>
</ul>
<h2>version 0.0.14</h2>
<p>Features</p>
<ul>
Expand Down
46 changes: 30 additions & 16 deletions src/com/koxudaxi/pydantic/Pydantic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,59 @@ package com.koxudaxi.pydantic
import com.intellij.psi.util.QualifiedName
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.resolve.PyResolveUtil
import com.jetbrains.python.psi.types.PyCallableTypeImpl
import com.jetbrains.python.psi.types.TypeEvalContext

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 SCHEMA_Q_NAME = "pydantic.schema.Schema"
const val FIELD_Q_NAME = "pydantic.field.Field"
const val BASE_SETTINGS_Q_NAME = "pydantic.env_settings.BaseSettings"

fun getPyClassByPyCallExpression(pyCallExpression: PyCallExpression): PyClass? {
return pyCallExpression.callee?.reference?.resolve() as? PyClass
}

fun getPyClassByPyKeywordArgument(pyKeywordArgument: PyKeywordArgument): PyClass? {
val pyCallExpression = pyKeywordArgument.parent?.parent as? PyCallExpression ?: return null
return pyCallExpression.callee?.reference?.resolve() as? PyClass ?: return null
return getPyClassByPyCallExpression(pyCallExpression)
}

fun isPydanticModel(pyClass: PyClass, context: TypeEvalContext? = null): Boolean {
return isSubClassOfPydanticBaseModel(pyClass, context) || isPydanticDataclass(pyClass)
}

fun isPydanticBaseModel(pyClass: PyClass): Boolean {
return pyClass.qualifiedName == "pydantic.main.BaseModel"
return pyClass.qualifiedName == BASE_MODEL_Q_NAME
}

fun isSubClassOfPydanticBaseModel(pyClass: PyClass, context: TypeEvalContext?): Boolean {
return pyClass.isSubclass("pydantic.main.BaseModel", context)
return pyClass.isSubclass(BASE_MODEL_Q_NAME, context)
}

fun isPydanticDataclass(pyClass: PyClass): Boolean {
val decorators = pyClass.decoratorList?.decorators ?: return false
for (decorator in decorators) {
val callee = (decorator.callee as? PyReferenceExpression) ?: continue
fun isBaseSetting(pyClass: PyClass, context: TypeEvalContext): Boolean {
return pyClass.isSubclass(BASE_SETTINGS_Q_NAME, context)
}

for (decoratorQualifiedName in PyResolveUtil.resolveImportedElementQNameLocally(callee)) {
if (decoratorQualifiedName == QualifiedName.fromDottedString("pydantic.dataclasses.dataclass")) return true
fun hasDecorator(pyElement: PyElement, refName: String): Boolean {
if (pyElement is PyDecoratable) {
pyElement.decoratorList?.decorators?.mapNotNull { it.callee as? PyReferenceExpression }?.forEach {
PyResolveUtil.resolveImportedElementQNameLocally(it).forEach { decoratorQualifiedName ->
if (decoratorQualifiedName == QualifiedName.fromDottedString(refName)) return true
}
}
}
return false
}

fun isPydanticField(pyClass: PyClass, context: TypeEvalContext? = null): Boolean {
return pyClass.isSubclass("pydantic.schema.Schema", context) || pyClass.isSubclass("pydantic.field.Field", context)
fun isPydanticDataclass(pyClass: PyClass): Boolean {
return hasDecorator(pyClass, DATA_CLASS_Q_NAME)
}

fun isPydanticField(pyClass: PyClass, context: TypeEvalContext): Boolean {
return pyClass.isSubclass(SCHEMA_Q_NAME, context) || pyClass.isSubclass(FIELD_Q_NAME, context)
}

fun hasClassMethodDecorator(pyFunction: PyFunction, context: TypeEvalContext): Boolean {
return pyFunction.decoratorList?.decorators?.firstOrNull { pyDecorator ->
(context.getType(pyDecorator) as? PyCallableTypeImpl)?.getReturnType(context)?.name == "classmethod" && pyDecorator.name != "classmethod"
} != null
fun validatorMethod(pyFunction: PyFunction): Boolean {
return hasDecorator(pyFunction, VALIDATOR_Q_NAME)
}
14 changes: 9 additions & 5 deletions src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,23 @@ class PydanticFieldRenameFactory : AutomaticRenamerFactory {
when (element) {
is PyTargetExpression -> if (element.name != null) {
val pyClass = element.containingClass
addAllElement(pyClass, element.name!!, added)
suggestAllNames(element.name!!, newName)
if (pyClass is PyClass) {
addAllElement(pyClass, element.name!!, added)
}
suggestAllNames(element.name, newName)

}
is PyKeywordArgument -> if (element.name != null) {
val pyClass = getPyClassByPyKeywordArgument(element)
addAllElement(pyClass, element.name!!, added)
if (pyClass is PyClass) {
addAllElement(pyClass, element.name!!, added)
}
suggestAllNames(element.name!!, newName)
}
}
}

private fun addAllElement(pyClass: PyClass?, elementName: String, added: MutableSet<PyClass>) {
if (pyClass == null) return
private fun addAllElement(pyClass: PyClass, elementName: String, added: MutableSet<PyClass>) {
added.add(pyClass)
addClassAttributes(pyClass, elementName)
addKeywordArguments(pyClass, elementName)
Expand Down
7 changes: 3 additions & 4 deletions src/com/koxudaxi/pydantic/PydanticFieldSearchExecutor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ private fun searchDirectReferenceField(pyClass: PyClass, elementName: String, co
return false
}

private fun searchAllElementReference(pyClass: PyClass?, elementName: String, added: MutableSet<PyClass>, consumer: Processor<in PsiReference>) {
if (pyClass == null) return
private fun searchAllElementReference(pyClass: PyClass, elementName: String, added: MutableSet<PyClass>, consumer: Processor<in PsiReference>) {
added.add(pyClass)
searchField(pyClass, elementName, consumer)
searchKeywordArgument(pyClass, elementName, consumer)
pyClass.getAncestorClasses(null).forEach { ancestorClass ->
if (isPydanticBaseModel(ancestorClass) && !added.contains(ancestorClass)){
pyClass.getAncestorClasses(null).forEach { ancestorClass ->
if (isPydanticBaseModel(ancestorClass) && !added.contains(ancestorClass)) {
searchField(pyClass, elementName, consumer)
}
}
Expand Down
5 changes: 1 addition & 4 deletions src/com/koxudaxi/pydantic/PydanticIgnoreInspection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ class PydanticIgnoreInspection : PyInspectionExtension() {

override fun ignoreMethodParameters(function: PyFunction, context: TypeEvalContext): Boolean {
val pyClass = function.containingClass ?: return false
if (isPydanticModel(pyClass, context) && hasClassMethodDecorator(function, context)) {
return true
}
return false
return isPydanticModel(pyClass, context) && validatorMethod(function)
}
}
18 changes: 10 additions & 8 deletions src/com/koxudaxi/pydantic/PydanticInspection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import com.jetbrains.python.inspections.quickfix.RenameParameterQuickFix
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyReferenceExpressionImpl
import com.jetbrains.python.psi.impl.PyStarArgumentImpl
import com.jetbrains.python.psi.impl.references.PyReferenceImpl

class PydanticInspection : PyInspection() {

Expand All @@ -23,16 +22,19 @@ class PydanticInspection : PyInspection() {
private class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) {

override fun visitPyFunction(node: PyFunction?) {
super.visitPyFunction(node)

val pyClass = node?.parent?.parent as? PyClass ?: return
if (!isPydanticModel(pyClass, myTypeEvalContext) || !hasClassMethodDecorator(node, myTypeEvalContext)) return
if (!isPydanticModel(pyClass, myTypeEvalContext) || !validatorMethod(node)) return
val paramList = node.parameterList
val params = paramList.parameters
val firstParam = params.firstOrNull()
if (firstParam == null && node.modifier != PyFunction.Modifier.STATICMETHOD) {
if (firstParam == null) {
registerProblem(paramList, PyBundle.message("INSP.must.have.first.parameter", PyNames.CANONICAL_CLS),
ProblemHighlightType.GENERIC_ERROR)
} else if (firstParam!!.asNamed?.name != PyNames.CANONICAL_CLS) {
registerProblem(PyUtil.sure(params[0]),
} else if (firstParam.asNamed?.isSelf == true && firstParam.asNamed?.name != PyNames.CANONICAL_CLS) {

registerProblem(PyUtil.sure(firstParam),
PyBundle.message("INSP.usually.named.\$0", PyNames.CANONICAL_CLS),
ProblemHighlightType.WEAK_WARNING, null,
RenameParameterQuickFix(PyNames.CANONICAL_CLS))
Expand All @@ -43,10 +45,10 @@ class PydanticInspection : PyInspection() {
override fun visitPyCallExpression(node: PyCallExpression?) {
super.visitPyCallExpression(node)

if (node != null) {
val pyClass: PyClass = (node.callee?.reference as? PyReferenceImpl)?.resolve() as? PyClass ?: return
if (node != null) { // $COVERAGE-IGNORE$
val pyClass: PyClass = getPyClassByPyCallExpression(node) ?: return
if (!isPydanticModel(pyClass, myTypeEvalContext)) return
if ((node.callee as PyReferenceExpressionImpl).isQualified) return
if ((node.callee as PyReferenceExpressionImpl).isQualified) return // $COVERAGE-IGNORE$
for (argument in node.arguments) {
if (argument is PyKeywordArgument) {
continue
Expand Down
37 changes: 23 additions & 14 deletions src/com/koxudaxi/pydantic/PydanticTypeProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,42 +103,52 @@ class PydanticTypeProvider : PyTypeProviderBase() {
return PyCallableTypeImpl(collected.values.reversed(), clsType.toInstance())
}

private fun hasAnnotationValue(field: PyTargetExpression): Boolean {
return field.annotationValue != null
}

private fun fieldToParameter(field: PyTargetExpression,
ellipsis: PyNoneLiteralExpression,
context: TypeEvalContext,
pyClass: PyClass): PyCallableParameter? {

if (field.annotationValue == null && !field.hasAssignedValue()) return null // skip fields that are invalid syntax
if (!hasAnnotationValue(field) && !field.hasAssignedValue()) return null // skip fields that are invalid syntax

val defaultValueFromField = getDefaultValueForParameter(field, ellipsis, context)
val defaultValue = when {
pyClass.isSubclass("pydantic.env_settings.BaseSettings", context) -> ellipsis
else -> getDefaultValueForParameter(field, ellipsis, context)
isBaseSetting(pyClass, context) -> ellipsis
else -> defaultValueFromField
}

return PyCallableParameterImpl.nonPsi(field.name,
getTypeForParameter(field, context),
defaultValue)
val typeForParameter = if (!hasAnnotationValue(field) && defaultValueFromField is PyTypedElement) {
// get type from default value
context.getType(defaultValueFromField)
} else {
// get type from annotation
getTypeForParameter(field, context)
}

return PyCallableParameterImpl.nonPsi(field.name, typeForParameter, defaultValue)
}

private fun getTypeForParameter(field: PyTargetExpression,
context: TypeEvalContext): PyType? {

return context.getType(field)

}

private fun getDefaultValueForParameter(field: PyTargetExpression,
ellipsis: PyNoneLiteralExpression,
context: TypeEvalContext): PyExpression? {

val value = field.findAssignedValue()
when {
value == null -> {
when (val value = field.findAssignedValue()) {
null -> {
val annotation = (field.annotation?.value as? PySubscriptionExpressionImpl) ?: return null

when {
annotation.qualifier?.text == "Optional" -> return ellipsis
annotation.qualifier?.text == "Union" -> for (child in annotation.children) {
annotation.qualifier == null -> return value
annotation.qualifier!!.text == "Optional" -> return ellipsis
annotation.qualifier!!.text == "Union" -> for (child in annotation.children) {
if (child is PyTupleExpression) {
for (type in child.children) {
if (type is PyNoneLiteralExpression) {
Expand All @@ -150,8 +160,7 @@ class PydanticTypeProvider : PyTypeProviderBase() {
}
return value
}
field.hasAssignedValue() -> return getDefaultValueByAssignedValue(field, ellipsis, context)
else -> return null
else -> return getDefaultValueByAssignedValue(field, ellipsis, context)
}
}

Expand Down
8 changes: 8 additions & 0 deletions testData/ignoreinspection/baseModel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from pydantic import BaseModel, validator


class A(BaseModel):
a: str

def vali<caret>date_a(cls):
pass
13 changes: 13 additions & 0 deletions testData/ignoreinspection/pythonDecorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pydantic import BaseModel, validator

def deco(func):
def inner():
return func()
return inner

class A(BaseModel):
a: str

@deco
def vali<caret>date_a(cls):
pass
3 changes: 3 additions & 0 deletions testData/ignoreinspection/pythonFunction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

def vali<caret>date_a(cls):
pass
6 changes: 6 additions & 0 deletions testData/ignoreinspection/pythonMethod.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

class A:
a: str

def vali<caret>date_a(cls):
pass
Loading