From 2c3326101a44d8cf9d630eaefbd58e1c2b3a1fba Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 18 Aug 2020 05:31:33 +0900 Subject: [PATCH 1/5] support dynamic model --- resources/META-INF/plugin.xml | 1 + src/com/koxudaxi/pydantic/Pydantic.kt | 18 ++- .../pydantic/PydanticDynamicModelClassType.kt | 11 ++ .../PydanticDynamicModelMemberProvider.kt | 20 +++ .../koxudaxi/pydantic/PydanticTypeProvider.kt | 137 ++++++++++++++++-- 5 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt create mode 100644 src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 18b26367..a2cde040 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -261,6 +261,7 @@ + diff --git a/src/com/koxudaxi/pydantic/Pydantic.kt b/src/com/koxudaxi/pydantic/Pydantic.kt index f4d2b4c7..3b053312 100644 --- a/src/com/koxudaxi/pydantic/Pydantic.kt +++ b/src/com/koxudaxi/pydantic/Pydantic.kt @@ -45,11 +45,14 @@ const val CON_INT_Q_NAME = "pydantic.types.conint" const val CON_LIST_Q_NAME = "pydantic.types.conlist" const val CON_STR_Q_NAME = "pydantic.types.constr" const val LIST_Q_NAME = "builtins.list" +const val CREATE_MODEL = "pydantic.main.create_model" val VERSION_QUALIFIED_NAME = QualifiedName.fromDottedString(VERSION_Q_NAME) val BASE_CONFIG_QUALIFIED_NAME = QualifiedName.fromDottedString(BASE_CONFIG_Q_NAME) +val BASE_MODEL_QUALIFIED_NAME = QualifiedName.fromDottedString(BASE_MODEL_Q_NAME) + val VERSION_SPLIT_PATTERN: Pattern = Pattern.compile("[.a-zA-Z]")!! val pydanticVersionCache: HashMap = hashMapOf() @@ -124,6 +127,11 @@ internal fun isDataclassField(pyFunction: PyFunction): Boolean { return pyFunction.qualifiedName == DATACLASS_FIELD_Q_NAME } + +internal fun isPydanticCreateModel(pyFunction: PyFunction): Boolean { + return pyFunction.qualifiedName == CREATE_MODEL +} + internal fun isDataclassMissing(pyTargetExpression: PyTargetExpression): Boolean { return pyTargetExpression.qualifiedName == DATACLASS_MISSING } @@ -317,10 +325,18 @@ fun getFieldName(field: PyTargetExpression, fun getPydanticBaseConfig(project: Project, context: TypeEvalContext): PyClass? { + return getPyClassFromQualifiedName(BASE_CONFIG_QUALIFIED_NAME, project, context) +} + +fun getPydanticBaseModel(project: Project, context: TypeEvalContext): PyClass? { + return getPyClassFromQualifiedName(BASE_MODEL_QUALIFIED_NAME, project, context) +} + +fun getPyClassFromQualifiedName(qualifiedName: QualifiedName, project: Project, context: TypeEvalContext): PyClass? { val module = project.modules.firstOrNull() ?: return null val pythonSdk = module.pythonSdk val contextAnchor = ModuleBasedContextAnchor(module) - return BASE_CONFIG_QUALIFIED_NAME.resolveToElement(QNameResolveContext(contextAnchor, pythonSdk, context)) as? PyClass + return qualifiedName.resolveToElement(QNameResolveContext(contextAnchor, pythonSdk, context)) as? PyClass } fun getPyClassByAttribute(pyPsiElement: PsiElement?): PyClass? { diff --git a/src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt b/src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt new file mode 100644 index 00000000..948f1da3 --- /dev/null +++ b/src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt @@ -0,0 +1,11 @@ +package com.koxudaxi.pydantic + + +import com.jetbrains.python.codeInsight.PyCustomMember +import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.types.PyCallableParameter +import com.jetbrains.python.psi.types.PyClassTypeImpl + +class PydanticDynamicModelClassType(source: PyClass, isDefinition: Boolean, val members: List, private val memberResolver: Map) : PyClassTypeImpl(source, isDefinition) { + fun resolveMember(name: String): PyElement? = memberResolver[name] +} \ No newline at end of file diff --git a/src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt b/src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt new file mode 100644 index 00000000..cf226944 --- /dev/null +++ b/src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt @@ -0,0 +1,20 @@ +package com.koxudaxi.pydantic + +import com.intellij.psi.PsiElement +import com.jetbrains.python.codeInsight.PyCustomMember +import com.jetbrains.python.psi.resolve.PyResolveContext +import com.jetbrains.python.psi.types.* + +class PydanticDynamicModelMemberProvider : PyClassMembersProviderBase(), PyOverridingClassMembersProvider { + override fun resolveMember(type: PyClassType, name: String, location: PsiElement?, resolveContext: PyResolveContext): PsiElement? { + if (type is PydanticDynamicModelClassType) { + type.resolveMember(name)?.let { return it } + } + return super.resolveMember(type, name, location, resolveContext) + } + + override fun getMembers(clazz: PyClassType?, location: PsiElement?, context: TypeEvalContext): MutableCollection { + if (clazz !is PydanticDynamicModelClassType) return mutableListOf() + return clazz.members.toMutableList() + } +} diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index f40aa855..3c0cf8d6 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -1,8 +1,10 @@ package com.koxudaxi.pydantic +import com.intellij.icons.AllIcons import com.intellij.openapi.util.Ref import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.codeInsight.PyCustomMember import com.jetbrains.python.psi.* import com.jetbrains.python.psi.impl.* import com.jetbrains.python.psi.types.* @@ -17,7 +19,8 @@ class PydanticTypeProvider : PyTypeProviderBase() { override fun getCallType(function: PyFunction, callSite: PyCallSiteExpression, context: TypeEvalContext): Ref? { return when (function.qualifiedName) { - CON_LIST_Q_NAME -> Ref.create(createConListPyType(callSite, context) ?: PyCollectionTypeImpl.createTypeByQName(callSite as PsiElement, LIST_Q_NAME, true)) + CON_LIST_Q_NAME -> Ref.create(createConListPyType(callSite, context) + ?: PyCollectionTypeImpl.createTypeByQName(callSite as PsiElement, LIST_Q_NAME, true)) else -> null } } @@ -93,14 +96,15 @@ class PydanticTypeProvider : PyTypeProviderBase() { pyClassType.isDefinition }.map { filteredPyClassType -> getPydanticTypeForClass(filteredPyClassType.pyClass, context, true) }.firstOrNull() } - it is PyTargetExpression -> (it as? PyTypedElement)?.let { pyTypedElement -> - context.getType(pyTypedElement) - ?.let { pyType -> getPyClassTypeByPyTypes(pyType) } - ?.filter { pyClassType -> pyClassType.isDefinition } - ?.map { filteredPyClassType -> - getPydanticTypeForClass(filteredPyClassType.pyClass, context, true) - }?.firstOrNull() - } + it is PyTargetExpression -> (it as? PyTypedElement) + ?.let { pyTypedElement -> + context.getType(pyTypedElement) + ?.let { pyType -> getPyClassTypeByPyTypes(pyType) } + ?.filter { pyClassType -> pyClassType.isDefinition } + ?.map { filteredPyClassType -> + getPydanticTypeForClass(filteredPyClassType.pyClass, context, true) + }?.firstOrNull() + } ?: getPydanticTypeForCreateModel(it, context, true) else -> null } } @@ -115,10 +119,79 @@ class PydanticTypeProvider : PyTypeProviderBase() { val typeArgumentList = argumentList.getKeywordArgument("item_type") ?: argumentList.arguments[0] // TODO support PySubscriptionExpression val typeArgumentListType = context.getType(typeArgumentList) ?: return null - val typeArgumentListReturnType = (typeArgumentListType as? PyCallableType)?.getReturnType(context) ?: return null + val typeArgumentListReturnType = (typeArgumentListType as? PyCallableType)?.getReturnType(context) + ?: return null return PyCollectionTypeImpl.createTypeByQName(pyCallExpression as PsiElement, LIST_Q_NAME, true, listOf(typeArgumentListReturnType)) } + private fun getPydanticTypeForCreateModel(pyTargetExpression: PyTargetExpression, context: TypeEvalContext, init: Boolean = false): PyCallableType? { + val pyCallExpression = pyTargetExpression.findAssignedValue() as? PyCallExpression ?: return null + val referenceExpression = (pyCallExpression.callee as? PyReferenceExpression) ?: return null + val resolveResults = getResolveElements(referenceExpression, context) + if (!PyUtil.filterTopPriorityResults(resolveResults).any { (it as? PyFunction)?.let { pyFunction -> isPydanticCreateModel(pyFunction) } == true }) return null + + val project = pyCallExpression.project + + + val typed = !init || getInstance(project).currentInitTyped + val collected = linkedMapOf>() + val pydanticVersion = getPydanticVersion(project, context) + // TODO get config +// val config = getConfig(pyClass, context, true) + val baseClass = when (val baseArgument = pyCallExpression.getKeywordArgument("__base__")) { + is PyReferenceExpression -> { + PyUtil.filterTopPriorityResults(getResolveElements(baseArgument, context)) + .filterIsInstance().firstOrNull { isPydanticModel(it, false, context) } + } + is PyClass -> baseArgument + else -> null + }?.let { baseClass -> + val baseClassCollected = linkedMapOf>() + (context.getType(baseClass) as? PyClassLikeType).let { baseClassType -> + for (currentType in StreamEx.of(baseClassType).append(baseClass.getAncestorTypes(context))) { + if (currentType !is PyClassType) continue + val current = currentType.pyClass + if (!isPydanticModel(current, false, context)) continue + getClassVariables(current, context) + .map { Pair(fieldToParameter(it, context, pydanticVersion, hashMapOf(), typed), it) } + .filter { (parameter, _) -> parameter?.name?.let { !collected.containsKey(it) } ?: false } + .forEach { (parameter, field) -> + parameter?.name?.let { name -> + val type = parameter.getType(context) + val member = PyCustomMember(name, null) { type } + .toPsiElement(field) + .withIcon(AllIcons.Nodes.Field) + baseClassCollected[name] = Triple(parameter, member, field) + } + } + } + } + baseClassCollected.entries.reversed().forEach { + collected[it.key] = it.value + } + baseClass + } ?: getPydanticBaseModel(project, context) ?: return null + + val modelClass = PyPsiFacade.getInstance(baseClass.project).createClassByQName(BASE_MODEL_Q_NAME, baseClass) + ?: return null + + pyCallExpression.arguments + .filter { it is PyKeywordArgument || (it as? PyStarArgumentImpl)?.isKeyword == true } + .filterNot { it.name?.startsWith("_") == true || it.name == "model_name" } + .forEach { + val parameter = fieldToParameter(it, context, pydanticVersion, hashMapOf(), typed)!! + parameter.name?.let { name -> + val type = parameter.getType(context) + val member = PyCustomMember(name, null) { type } + .toPsiElement(it) + .withIcon(AllIcons.Nodes.Field) + collected[name] = Triple(parameter, member, it) + } + } + + val modelClassType = PydanticDynamicModelClassType(modelClass, false, collected.values.map { it.second }, collected.entries.map { it.key to it.value.third }.toMap()) + return PyCallableTypeImpl(collected.values.map { it.first }, modelClassType.toInstance()) + } fun getPydanticTypeForClass(pyClass: PyClass, context: TypeEvalContext, init: Boolean = false): PyCallableType? { if (!isPydanticModel(pyClass, false, context)) return null @@ -183,6 +256,50 @@ class PydanticTypeProvider : PyTypeProviderBase() { ) } + internal fun fieldToParameter(field: PyExpression, + context: TypeEvalContext, + pydanticVersion: KotlinVersion?, + config: HashMap, + typed: Boolean = true): PyCallableParameter? { + var type: PyType? + var defaultValue: PyExpression? + when (val tupleValue = PsiTreeUtil.findChildOfType(field, PyTupleExpression::class.java)) { + is PyTupleExpression -> { + tupleValue.toList().let { + type = when (val typeValue = it[0]) { + is PyType -> typeValue + is PyReferenceExpression -> { + val resolveResults = getResolveElements(typeValue, context) + PyUtil.filterTopPriorityResults(resolveResults) + .filterIsInstance() + .map { pyClass -> pyClass.getType(context)?.getReturnType(context) } + .firstOrNull() + } + else -> null + } + defaultValue = it[1] + } + } + else -> { + type = context.getType(field) + defaultValue = (field as? PyKeywordArgumentImpl)?.valueExpression + } + } + val typeForParameter = when { + !typed -> null + else -> { + type + } + } + + return PyCallableParameterImpl.nonPsi( + field.name, +// getFieldName(field, context, config, pydanticVersion), + typeForParameter, + defaultValue + ) + } + private fun getTypeForParameter(field: PyTargetExpression, context: TypeEvalContext): PyType? { From 006455aebff80178f0fea2f2c011b0bcc299c475 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 20 Aug 2020 06:12:46 +0900 Subject: [PATCH 2/5] support members for dynamic model --- resources/META-INF/plugin.xml | 7 +- .../koxudaxi/pydantic/PydanticDynamicModel.kt | 15 +++++ .../pydantic/PydanticDynamicModelClassType.kt | 1 - .../PydanticDynamicModelMemberProvider.kt | 2 +- .../koxudaxi/pydantic/PydanticTypeProvider.kt | 65 ++++++++++++------- 5 files changed, 65 insertions(+), 25 deletions(-) create mode 100644 src/com/koxudaxi/pydantic/PydanticDynamicModel.kt diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index a2cde040..c1d43e34 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -1,9 +1,14 @@ com.koxudaxi.pydantic Pydantic - 0.1.10 + 0.1.11 Koudai Aono @koxudaxi version 0.1.11 +

Features

+
    +
  • Support dynamic model [#175]
  • +

version 0.1.10

BugFixes

    diff --git a/src/com/koxudaxi/pydantic/PydanticDynamicModel.kt b/src/com/koxudaxi/pydantic/PydanticDynamicModel.kt new file mode 100644 index 00000000..898d0ad9 --- /dev/null +++ b/src/com/koxudaxi/pydantic/PydanticDynamicModel.kt @@ -0,0 +1,15 @@ +package com.koxudaxi.pydantic + +import com.intellij.lang.ASTNode +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.impl.PyClassImpl +import com.jetbrains.python.psi.types.PyClassLikeType +import com.jetbrains.python.psi.types.TypeEvalContext + +class PydanticDynamicModel(astNode: ASTNode, val baseModel: PyClass) : PyClassImpl(astNode) { + override fun getSuperClassTypes(context: TypeEvalContext): MutableList { + return baseModel.getType(context)?.let { + mutableListOf(it) + } ?: mutableListOf() + } +} \ No newline at end of file diff --git a/src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt b/src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt index 948f1da3..8fd85018 100644 --- a/src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt +++ b/src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt @@ -3,7 +3,6 @@ package com.koxudaxi.pydantic import com.jetbrains.python.codeInsight.PyCustomMember import com.jetbrains.python.psi.* -import com.jetbrains.python.psi.types.PyCallableParameter import com.jetbrains.python.psi.types.PyClassTypeImpl class PydanticDynamicModelClassType(source: PyClass, isDefinition: Boolean, val members: List, private val memberResolver: Map) : PyClassTypeImpl(source, isDefinition) { diff --git a/src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt b/src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt index cf226944..816e89d6 100644 --- a/src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt @@ -5,7 +5,7 @@ import com.jetbrains.python.codeInsight.PyCustomMember import com.jetbrains.python.psi.resolve.PyResolveContext import com.jetbrains.python.psi.types.* -class PydanticDynamicModelMemberProvider : PyClassMembersProviderBase(), PyOverridingClassMembersProvider { +class PydanticDynamicModelMemberProvider : PyClassMembersProviderBase() { override fun resolveMember(type: PyClassType, name: String, location: PsiElement?, resolveContext: PyResolveContext): PsiElement? { if (type is PydanticDynamicModelClassType) { type.resolveMember(name)?.let { return it } diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index 3c0cf8d6..457f4918 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -17,10 +17,16 @@ class PydanticTypeProvider : PyTypeProviderBase() { return getPydanticTypeForCallee(referenceExpression, context) } - override fun getCallType(function: PyFunction, callSite: PyCallSiteExpression, context: TypeEvalContext): Ref? { - return when (function.qualifiedName) { + override fun getCallType(pyFunction: PyFunction, callSite: PyCallSiteExpression, context: TypeEvalContext): Ref? { + return when (pyFunction.qualifiedName) { CON_LIST_Q_NAME -> Ref.create(createConListPyType(callSite, context) ?: PyCollectionTypeImpl.createTypeByQName(callSite as PsiElement, LIST_Q_NAME, true)) + CREATE_MODEL -> when (callSite) { + is PyCallExpression -> callSite.argumentList?.let { + Ref.create(getPydanticDynamicModelTypeForFunction(pyFunction, it, context)?.getReturnType(context)) + } + else -> null + } else -> null } } @@ -104,7 +110,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { ?.map { filteredPyClassType -> getPydanticTypeForClass(filteredPyClassType.pyClass, context, true) }?.firstOrNull() - } ?: getPydanticTypeForCreateModel(it, context, true) + } ?: getPydanticDynamicModelTypeForTargetExpression(it, context, true) else -> null } } @@ -124,21 +130,30 @@ class PydanticTypeProvider : PyTypeProviderBase() { return PyCollectionTypeImpl.createTypeByQName(pyCallExpression as PsiElement, LIST_Q_NAME, true, listOf(typeArgumentListReturnType)) } - private fun getPydanticTypeForCreateModel(pyTargetExpression: PyTargetExpression, context: TypeEvalContext, init: Boolean = false): PyCallableType? { + private fun getPydanticDynamicModelTypeForTargetExpression(pyTargetExpression: PyTargetExpression, context: TypeEvalContext, init: Boolean = false): PyCallableType? { val pyCallExpression = pyTargetExpression.findAssignedValue() as? PyCallExpression ?: return null + return getPydanticDynamicModelTypeForTargetExpression(pyCallExpression, context, init) + } + + private fun getPydanticDynamicModelTypeForTargetExpression(pyCallExpression: PyCallExpression, context: TypeEvalContext, init: Boolean = false): PyCallableType? { + val argumentList = pyCallExpression.argumentList ?: return null val referenceExpression = (pyCallExpression.callee as? PyReferenceExpression) ?: return null val resolveResults = getResolveElements(referenceExpression, context) - if (!PyUtil.filterTopPriorityResults(resolveResults).any { (it as? PyFunction)?.let { pyFunction -> isPydanticCreateModel(pyFunction) } == true }) return null + val pyFunction = PyUtil.filterTopPriorityResults(resolveResults).asSequence().filterIsInstance().map { it.takeIf { pyFunction -> isPydanticCreateModel(pyFunction) } }.firstOrNull() + ?: return null - val project = pyCallExpression.project + return getPydanticDynamicModelTypeForFunction(pyFunction, argumentList, context, init) + } + private fun getPydanticDynamicModelTypeForFunction(pyFunction: PyFunction, pyArgumentList: PyArgumentList, context: TypeEvalContext, init: Boolean = false): PyCallableType? { + val project = pyFunction.project val typed = !init || getInstance(project).currentInitTyped val collected = linkedMapOf>() val pydanticVersion = getPydanticVersion(project, context) // TODO get config // val config = getConfig(pyClass, context, true) - val baseClass = when (val baseArgument = pyCallExpression.getKeywordArgument("__base__")) { + val baseClass = when (val baseArgument = pyArgumentList.getKeywordArgument("__base__")?.valueExpression) { is PyReferenceExpression -> { PyUtil.filterTopPriorityResults(getResolveElements(baseArgument, context)) .filterIsInstance().firstOrNull { isPydanticModel(it, false, context) } @@ -172,10 +187,14 @@ class PydanticTypeProvider : PyTypeProviderBase() { baseClass } ?: getPydanticBaseModel(project, context) ?: return null - val modelClass = PyPsiFacade.getInstance(baseClass.project).createClassByQName(BASE_MODEL_Q_NAME, baseClass) - ?: return null + val modelNameArgument = pyArgumentList.getKeywordArgument("__model_name")?.valueExpression + ?: pyArgumentList.arguments.firstOrNull() ?: return null + val modelName = PyPsiUtils.strValue(modelNameArgument) ?: return null + val langLevel = LanguageLevel.forElement(pyFunction) + val dynamicModelClassText = "class ${modelName}: pass" + val modelClass = PydanticDynamicModel(PyElementGenerator.getInstance(project).createFromText(langLevel, PyClass::class.java, dynamicModelClassText).node, baseClass) - pyCallExpression.arguments + pyArgumentList.arguments .filter { it is PyKeywordArgument || (it as? PyStarArgumentImpl)?.isKeyword == true } .filterNot { it.name?.startsWith("_") == true || it.name == "model_name" } .forEach { @@ -261,23 +280,25 @@ class PydanticTypeProvider : PyTypeProviderBase() { pydanticVersion: KotlinVersion?, config: HashMap, typed: Boolean = true): PyCallableParameter? { - var type: PyType? - var defaultValue: PyExpression? + var type: PyType? = null + var defaultValue: PyExpression? = null when (val tupleValue = PsiTreeUtil.findChildOfType(field, PyTupleExpression::class.java)) { is PyTupleExpression -> { tupleValue.toList().let { - type = when (val typeValue = it[0]) { - is PyType -> typeValue - is PyReferenceExpression -> { - val resolveResults = getResolveElements(typeValue, context) - PyUtil.filterTopPriorityResults(resolveResults) - .filterIsInstance() - .map { pyClass -> pyClass.getType(context)?.getReturnType(context) } - .firstOrNull() + if (it.size > 1) { + type = when (val typeValue = it[0]) { + is PyType -> typeValue + is PyReferenceExpression -> { + val resolveResults = getResolveElements(typeValue, context) + PyUtil.filterTopPriorityResults(resolveResults) + .filterIsInstance() + .map { pyClass -> pyClass.getType(context)?.getReturnType(context) } + .firstOrNull() + } + else -> null } - else -> null + defaultValue = it[1] } - defaultValue = it[1] } } else -> { From 832e80d6eaf69b70a09423803c52357c1c1e0ae1 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 20 Aug 2020 06:22:47 +0900 Subject: [PATCH 3/5] remove unused code --- src/com/koxudaxi/pydantic/PydanticTypeProvider.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index 457f4918..f34681ad 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -150,7 +150,6 @@ class PydanticTypeProvider : PyTypeProviderBase() { val project = pyFunction.project val typed = !init || getInstance(project).currentInitTyped val collected = linkedMapOf>() - val pydanticVersion = getPydanticVersion(project, context) // TODO get config // val config = getConfig(pyClass, context, true) val baseClass = when (val baseArgument = pyArgumentList.getKeywordArgument("__base__")?.valueExpression) { @@ -168,7 +167,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { val current = currentType.pyClass if (!isPydanticModel(current, false, context)) continue getClassVariables(current, context) - .map { Pair(fieldToParameter(it, context, pydanticVersion, hashMapOf(), typed), it) } + .map { Pair(fieldToParameter(it, context, hashMapOf(), typed), it) } .filter { (parameter, _) -> parameter?.name?.let { !collected.containsKey(it) } ?: false } .forEach { (parameter, field) -> parameter?.name?.let { name -> @@ -198,7 +197,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { .filter { it is PyKeywordArgument || (it as? PyStarArgumentImpl)?.isKeyword == true } .filterNot { it.name?.startsWith("_") == true || it.name == "model_name" } .forEach { - val parameter = fieldToParameter(it, context, pydanticVersion, hashMapOf(), typed)!! + val parameter = fieldToParameter(it, context, hashMapOf(), typed)!! parameter.name?.let { name -> val type = parameter.getType(context) val member = PyCustomMember(name, null) { type } @@ -277,7 +276,6 @@ class PydanticTypeProvider : PyTypeProviderBase() { internal fun fieldToParameter(field: PyExpression, context: TypeEvalContext, - pydanticVersion: KotlinVersion?, config: HashMap, typed: Boolean = true): PyCallableParameter? { var type: PyType? = null From 0078d2098972d41d337a0a00242689d70ecc8961 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 20 Aug 2020 06:34:15 +0900 Subject: [PATCH 4/5] support referenced model name --- src/com/koxudaxi/pydantic/PydanticTypeProvider.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index f34681ad..1477240c 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -188,7 +188,14 @@ class PydanticTypeProvider : PyTypeProviderBase() { val modelNameArgument = pyArgumentList.getKeywordArgument("__model_name")?.valueExpression ?: pyArgumentList.arguments.firstOrNull() ?: return null - val modelName = PyPsiUtils.strValue(modelNameArgument) ?: return null + val modelName = when (modelNameArgument) { + is PyReferenceExpression -> PyUtil.filterTopPriorityResults(getResolveElements(modelNameArgument, context)) + .filterIsInstance() + .map { it.findAssignedValue() } + .firstOrNull() + .let { PyPsiUtils.strValue(it) } + else -> PyPsiUtils.strValue(modelNameArgument) + } ?: return null val langLevel = LanguageLevel.forElement(pyFunction) val dynamicModelClassText = "class ${modelName}: pass" val modelClass = PydanticDynamicModel(PyElementGenerator.getInstance(project).createFromText(langLevel, PyClass::class.java, dynamicModelClassText).node, baseClass) From cc60823d6222eb5daebbce82b29bbe069ccc2587 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Aug 2020 01:34:03 +0900 Subject: [PATCH 5/5] update documents --- README.md | 2 ++ docs/index.md | 3 +++ .../koxudaxi/pydantic/PydanticTypeProvider.kt | 20 +++++++++---------- testData/mock/pydanticv1/__init__.py | 2 +- testData/mock/pydanticv1/main.py | 11 ++++++++++ testData/mock/stub/typing/__init__.py | 2 ++ 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 291de4b5..fabdb80e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ You can install the stable version on PyCharm's `Marketplace` (Preference -> Plu * (After PyCharm 2020.1 and this plugin version 0.1.0, PyCharm treats `pydantic.dataclasses.dataclass` as third-party dataclass.) * Exclude a feature which is inserting unfilled arguments with a QuickFix +### pydantic.create_model [experimental] +* Support minimum features for a model which is created by create_model ## Contribute diff --git a/docs/index.md b/docs/index.md index 798a0d9e..f06f3443 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,6 +26,9 @@ I got interviewed about this plugin for [JetBrains' PyCharm Blog](https://blog.j * (After PyCharm 2020.1 and this plugin version 0.1.0, PyCharm treats `pydantic.dataclasses.dataclass` as third-party dataclass.) * Exclude a feature which is inserting unfilled arguments with a QuickFix +### pydantic.create_model [experimental] +* Support minimum features for a model which is created by create_model + ## Demo ![demo1](demo1.gif) diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index 1477240c..f9b9a0a0 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -21,12 +21,6 @@ class PydanticTypeProvider : PyTypeProviderBase() { return when (pyFunction.qualifiedName) { CON_LIST_Q_NAME -> Ref.create(createConListPyType(callSite, context) ?: PyCollectionTypeImpl.createTypeByQName(callSite as PsiElement, LIST_Q_NAME, true)) - CREATE_MODEL -> when (callSite) { - is PyCallExpression -> callSite.argumentList?.let { - Ref.create(getPydanticDynamicModelTypeForFunction(pyFunction, it, context)?.getReturnType(context)) - } - else -> null - } else -> null } } @@ -185,9 +179,10 @@ class PydanticTypeProvider : PyTypeProviderBase() { } baseClass } ?: getPydanticBaseModel(project, context) ?: return null - - val modelNameArgument = pyArgumentList.getKeywordArgument("__model_name")?.valueExpression - ?: pyArgumentList.arguments.firstOrNull() ?: return null + var modelNameIsPositionalArgument = true + val modelNameArgument = pyArgumentList.getKeywordArgument("__model_name")?.valueExpression?.apply { + modelNameIsPositionalArgument = false + } ?: pyArgumentList.arguments.firstOrNull() ?: return null val modelName = when (modelNameArgument) { is PyReferenceExpression -> PyUtil.filterTopPriorityResults(getResolveElements(modelNameArgument, context)) .filterIsInstance() @@ -199,8 +194,11 @@ class PydanticTypeProvider : PyTypeProviderBase() { val langLevel = LanguageLevel.forElement(pyFunction) val dynamicModelClassText = "class ${modelName}: pass" val modelClass = PydanticDynamicModel(PyElementGenerator.getInstance(project).createFromText(langLevel, PyClass::class.java, dynamicModelClassText).node, baseClass) - - pyArgumentList.arguments + val argumentWithoutModelName = when (modelNameIsPositionalArgument) { + true -> pyArgumentList.arguments.asSequence().drop(1) + else -> pyArgumentList.arguments.asSequence() + } + argumentWithoutModelName .filter { it is PyKeywordArgument || (it as? PyStarArgumentImpl)?.isKeyword == true } .filterNot { it.name?.startsWith("_") == true || it.name == "model_name" } .forEach { diff --git a/testData/mock/pydanticv1/__init__.py b/testData/mock/pydanticv1/__init__.py index 5491a851..9f3b5c2c 100644 --- a/testData/mock/pydanticv1/__init__.py +++ b/testData/mock/pydanticv1/__init__.py @@ -1,4 +1,4 @@ -from .main import BaseModel, BaseConfig +from .main import BaseModel, BaseConfig, create_model from .class_validators import validator, root_validator from .fields import Field, Schema from .env_settings import BaseSettings diff --git a/testData/mock/pydanticv1/main.py b/testData/mock/pydanticv1/main.py index 16a7a2ab..44f96b01 100644 --- a/testData/mock/pydanticv1/main.py +++ b/testData/mock/pydanticv1/main.py @@ -47,3 +47,14 @@ class BaseConfig: json_loads = json.loads json_dumps = json.dumps json_encoders = {} + +def create_model( + model_name: str, + *, + __config__: Type[BaseConfig] = None, + __base__: Type[BaseModel] = None, + __module__: Optional[str] = None, + __validators__: Dict[str, classmethod] = None, + **field_definitions: Any, +) -> Type[BaseModel]: + pass \ No newline at end of file diff --git a/testData/mock/stub/typing/__init__.py b/testData/mock/stub/typing/__init__.py index ef7219d2..1abe5dd2 100644 --- a/testData/mock/stub/typing/__init__.py +++ b/testData/mock/stub/typing/__init__.py @@ -23,3 +23,5 @@ class List: @classmethod def __getitem__(cls, item): pass + +Type = _alias(type, CT_co, inst=False) \ No newline at end of file