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

support dynamic model #175

Merged
merged 5 commits into from
Aug 20, 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 7 additions & 1 deletion resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<idea-plugin url="https://github.com/koxudaxi/pydantic-pycharm-plugin">
<id>com.koxudaxi.pydantic</id>
<name>Pydantic</name>
<version>0.1.10</version>
<version>0.1.11</version>
<vendor email="koaxudai@gmail.com">Koudai Aono @koxudaxi</vendor>
<change-notes><![CDATA[
<h2>version 0.1.11</h2>
<p>Features</p>
<ul>
<li>Support dynamic model [#175]</li>
</ul>
<h2>version 0.1.10</h2>
<p>BugFixes</p>
<ul>
Expand Down Expand Up @@ -261,6 +266,7 @@
<inspectionExtension implementation="com.koxudaxi.pydantic.PydanticIgnoreInspection"/>
<pyDataclassParametersProvider implementation="com.koxudaxi.pydantic.PydanticParametersProvider"/>
<pyAnnotator implementation="com.koxudaxi.pydantic.PydanticAnnotator"/>
<pyClassMembersProvider implementation="com.koxudaxi.pydantic.PydanticDynamicModelMemberProvider"/>
</extensions>

<resource-bundle/>
Expand Down
18 changes: 17 additions & 1 deletion src/com/koxudaxi/pydantic/Pydantic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, KotlinVersion> = hashMapOf()
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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? {
Expand Down
15 changes: 15 additions & 0 deletions src/com/koxudaxi/pydantic/PydanticDynamicModel.kt
Original file line number Diff line number Diff line change
@@ -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<PyClassLikeType> {
return baseModel.getType(context)?.let {
mutableListOf(it)
} ?: mutableListOf()
}
}
10 changes: 10 additions & 0 deletions src/com/koxudaxi/pydantic/PydanticDynamicModelClassType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.koxudaxi.pydantic


import com.jetbrains.python.codeInsight.PyCustomMember
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.types.PyClassTypeImpl

class PydanticDynamicModelClassType(source: PyClass, isDefinition: Boolean, val members: List<PyCustomMember>, private val memberResolver: Map<String, PyElement>) : PyClassTypeImpl(source, isDefinition) {
fun resolveMember(name: String): PyElement? = memberResolver[name]
}
20 changes: 20 additions & 0 deletions src/com/koxudaxi/pydantic/PydanticDynamicModelMemberProvider.kt
Original file line number Diff line number Diff line change
@@ -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() {
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<PyCustomMember> {
if (clazz !is PydanticDynamicModelClassType) return mutableListOf()
return clazz.members.toMutableList()
}
}
165 changes: 153 additions & 12 deletions src/com/koxudaxi/pydantic/PydanticTypeProvider.kt
Original file line number Diff line number Diff line change
@@ -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.*
Expand All @@ -15,9 +17,10 @@ class PydanticTypeProvider : PyTypeProviderBase() {
return getPydanticTypeForCallee(referenceExpression, context)
}

override fun getCallType(function: PyFunction, callSite: PyCallSiteExpression, context: TypeEvalContext): Ref<PyType>? {
return when (function.qualifiedName) {
CON_LIST_Q_NAME -> Ref.create(createConListPyType(callSite, context) ?: PyCollectionTypeImpl.createTypeByQName(callSite as PsiElement, LIST_Q_NAME, true))
override fun getCallType(pyFunction: PyFunction, callSite: PyCallSiteExpression, context: TypeEvalContext): Ref<PyType>? {
return when (pyFunction.qualifiedName) {
CON_LIST_Q_NAME -> Ref.create(createConListPyType(callSite, context)
?: PyCollectionTypeImpl.createTypeByQName(callSite as PsiElement, LIST_Q_NAME, true))
else -> null
}
}
Expand Down Expand Up @@ -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()
} ?: getPydanticDynamicModelTypeForTargetExpression(it, context, true)
else -> null
}
}
Expand All @@ -115,10 +119,102 @@ 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 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)
val pyFunction = PyUtil.filterTopPriorityResults(resolveResults).asSequence().filterIsInstance<PyFunction>().map { it.takeIf { pyFunction -> isPydanticCreateModel(pyFunction) } }.firstOrNull()
?: return null


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<String, Triple<PyCallableParameter, PyCustomMember, PyElement>>()
// TODO get config
// val config = getConfig(pyClass, context, true)
val baseClass = when (val baseArgument = pyArgumentList.getKeywordArgument("__base__")?.valueExpression) {
is PyReferenceExpression -> {
PyUtil.filterTopPriorityResults(getResolveElements(baseArgument, context))
.filterIsInstance<PyClass>().firstOrNull { isPydanticModel(it, false, context) }
}
is PyClass -> baseArgument
else -> null
}?.let { baseClass ->
val baseClassCollected = linkedMapOf<String, Triple<PyCallableParameter, PyCustomMember, PyElement>>()
(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, 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
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<PyTargetExpression>()
.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)
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 {
val parameter = fieldToParameter(it, context, 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
Expand Down Expand Up @@ -183,6 +279,51 @@ class PydanticTypeProvider : PyTypeProviderBase() {
)
}

internal fun fieldToParameter(field: PyExpression,
context: TypeEvalContext,
config: HashMap<String, Any?>,
typed: Boolean = true): PyCallableParameter? {
var type: PyType? = null
var defaultValue: PyExpression? = null
when (val tupleValue = PsiTreeUtil.findChildOfType(field, PyTupleExpression::class.java)) {
is PyTupleExpression -> {
tupleValue.toList().let {
if (it.size > 1) {
type = when (val typeValue = it[0]) {
is PyType -> typeValue
is PyReferenceExpression -> {
val resolveResults = getResolveElements(typeValue, context)
PyUtil.filterTopPriorityResults(resolveResults)
.filterIsInstance<PyClass>()
.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? {

Expand Down
2 changes: 1 addition & 1 deletion testData/mock/pydanticv1/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 11 additions & 0 deletions testData/mock/pydanticv1/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions testData/mock/stub/typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ class List:
@classmethod
def __getitem__(cls, item):
pass

Type = _alias(type, CT_co, inst=False)