From ab764e7c2354439fb761b8b569d5576134e26b04 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 13 Apr 2021 00:52:09 +0900 Subject: [PATCH 1/5] Support PyCharm 2021.1 --- build.gradle | 26 ++++++++++++------- resources/META-INF/plugin.xml | 9 ++++--- src/com/koxudaxi/pydantic/Pydantic.kt | 8 +++--- .../koxudaxi/pydantic/PydanticAnnotator.kt | 3 +-- .../pydantic/PydanticConfigPanel.java | 8 +++--- .../pydantic/PydanticFieldRenameFactory.kt | 2 +- .../koxudaxi/pydantic/PydanticInitializer.kt | 8 +++--- .../koxudaxi/pydantic/PydanticInspection.kt | 15 +++-------- .../pydantic/PydanticParametersProvider.kt | 6 ++--- .../koxudaxi/pydantic/PydanticTypeProvider.kt | 16 +++++------- .../com/jetbrains/python/PythonMockSdk.java | 2 +- 11 files changed, 51 insertions(+), 52 deletions(-) diff --git a/build.gradle b/build.gradle index 265e6760..7ef7cc59 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = "1.3.70" + ext.kotlin_version = "1.4.32" repositories { mavenCentral() } @@ -10,18 +10,25 @@ buildscript { plugins { - id "org.jetbrains.intellij" version "0.6.5" + id "org.jetbrains.intellij" version "0.7.2" } intellij { pluginName project.name - version "2020.2" + version "2021.1" type "PC" updateSinceUntilBuild false downloadSources true plugins "python-ce" } + +patchPluginXml { + sinceBuild "211.6693.111" +// untilBuild "211.*" +} + + allprojects { apply plugin: "org.jetbrains.intellij" apply plugin: "kotlin" @@ -33,24 +40,25 @@ allprojects { compileKotlin { kotlinOptions { jvmTarget = "11" - languageVersion = "1.3" - apiVersion = "1.3" + languageVersion = "1.4" + apiVersion = "1.4" } } compileTestKotlin { kotlinOptions { jvmTarget = "11" - languageVersion = "1.3" - apiVersion = "1.3" + languageVersion = "1.4" + apiVersion = "1.4" } } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" compile "org.apache.tuweni:tuweni-toml:1.1.0" compile group: 'org.ini4j', name: 'ini4j', version: '0.5.4' - testCompile group: 'junit', name: 'junit', version: '4.13.2' - compile group: 'org.jetbrains', name: 'annotations', version: '20.0.0' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.1' + compile group: 'org.jetbrains', name: 'annotations', version: '20.1.0' } jacocoTestReport { diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 081d4bdc..fe794b19 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -1,9 +1,14 @@ com.koxudaxi.pydantic Pydantic - 0.2.1 + 0.3.0 Koudai Aono @koxudaxi version 0.3.0 +

Features

+
    +
  • Support PyCharm 2021.1 [#]
  • +

version 0.2.1

Features

    @@ -299,8 +304,6 @@
]]> - - com.intellij.modules.lang com.intellij.modules.python diff --git a/src/com/koxudaxi/pydantic/Pydantic.kt b/src/com/koxudaxi/pydantic/Pydantic.kt index 1279c273..2b5bdf64 100644 --- a/src/com/koxudaxi/pydantic/Pydantic.kt +++ b/src/com/koxudaxi/pydantic/Pydantic.kt @@ -17,9 +17,7 @@ import com.jetbrains.python.psi.impl.* import com.jetbrains.python.psi.resolve.PyResolveContext import com.jetbrains.python.psi.resolve.PyResolveUtil import com.jetbrains.python.psi.types.* -import com.jetbrains.python.sdk.PythonSdkUtil -import com.jetbrains.python.sdk.associatedModule -import com.jetbrains.python.sdk.pythonSdk +import com.jetbrains.python.sdk.* import com.jetbrains.python.statistics.modules import java.util.regex.Pattern @@ -272,7 +270,7 @@ fun getSdk(project: Project): Sdk? { fun getPsiElementByQualifiedName(qualifiedName: QualifiedName, project: Project, context: TypeEvalContext): PsiElement? { val pythonSdk = getSdk(project) ?: return null - val module = pythonSdk.associatedModule ?: project.modules.firstOrNull() ?: return null + val module = project.modules.firstOrNull { pythonSdk.isAssociatedWithModule(it) } ?: return null val contextAnchor = ModuleBasedContextAnchor(module) return qualifiedName.resolveToElement(QNameResolveContext(contextAnchor, pythonSdk, context)) } @@ -503,7 +501,7 @@ internal fun getFieldFromAnnotated(annotated: PyExpression, context: TypeEvalCon ?.let {getFieldFromPyExpression(it, context, null) } -internal fun getTypeExpressionFromAnnotated(annotated: PyExpression, context: TypeEvalContext): PyExpression? = +internal fun getTypeExpressionFromAnnotated(annotated: PyExpression): PyExpression? = annotated.children .filterIsInstance () .firstOrNull() diff --git a/src/com/koxudaxi/pydantic/PydanticAnnotator.kt b/src/com/koxudaxi/pydantic/PydanticAnnotator.kt index e440646f..f189286a 100644 --- a/src/com/koxudaxi/pydantic/PydanticAnnotator.kt +++ b/src/com/koxudaxi/pydantic/PydanticAnnotator.kt @@ -12,9 +12,8 @@ import com.jetbrains.python.validation.PyAnnotator class PydanticAnnotator : PyAnnotator() { private val pydanticTypeProvider = PydanticTypeProvider() - override fun visitPyCallExpression(node: PyCallExpression?) { + override fun visitPyCallExpression(node: PyCallExpression) { super.visitPyCallExpression(node) - if (node == null) return annotatePydanticModelCallableExpression(node) } diff --git a/src/com/koxudaxi/pydantic/PydanticConfigPanel.java b/src/com/koxudaxi/pydantic/PydanticConfigPanel.java index 5ab98446..f2e6f02e 100644 --- a/src/com/koxudaxi/pydantic/PydanticConfigPanel.java +++ b/src/com/koxudaxi/pydantic/PydanticConfigPanel.java @@ -62,10 +62,10 @@ private void setHyperlinkHtml(JTextPane jTextPane, String html) { if (kit instanceof HTMLEditorKit) { StyleSheet css = ((HTMLEditorKit) kit).getStyleSheet(); - css.addRule("a, a:link {color:#" + ColorUtil.toHex(JBUI.CurrentTheme.Link.linkColor()) + ";}"); - css.addRule("a:visited {color:#" + ColorUtil.toHex(JBUI.CurrentTheme.Link.linkVisitedColor()) + ";}"); - css.addRule("a:hover {color:#" + ColorUtil.toHex(JBUI.CurrentTheme.Link.linkHoverColor()) + ";}"); - css.addRule("a:active {color:#" + ColorUtil.toHex(JBUI.CurrentTheme.Link.linkPressedColor()) + ";}"); + css.addRule("a, a:link {color:#" + ColorUtil.toHex(JBUI.CurrentTheme.Link.Foreground.ENABLED) + ";}"); + css.addRule("a:visited {color:#" + ColorUtil.toHex(JBUI.CurrentTheme.Link.Foreground.VISITED) + ";}"); + css.addRule("a:hover {color:#" + ColorUtil.toHex(JBUI.CurrentTheme.Link.Foreground.HOVERED) + ";}"); + css.addRule("a:active {color:#" + ColorUtil.toHex(JBUI.CurrentTheme.Link.Foreground.PRESSED) + ";}"); css.addRule("body {background-color:#" + ColorUtil.toHex(JBUI.CurrentTheme.DefaultTabs.background()) + ";}"); } diff --git a/src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt b/src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt index d2e20ea7..ddc7521f 100644 --- a/src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt +++ b/src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt @@ -30,7 +30,7 @@ class PydanticFieldRenameFactory : AutomaticRenamerFactory { return false } - override fun getOptionName(): String? { + override fun getOptionName(): String { return "Rename fields in hierarchy" } diff --git a/src/com/koxudaxi/pydantic/PydanticInitializer.kt b/src/com/koxudaxi/pydantic/PydanticInitializer.kt index 0f99c0d6..9b7a249b 100644 --- a/src/com/koxudaxi/pydantic/PydanticInitializer.kt +++ b/src/com/koxudaxi/pydantic/PydanticInitializer.kt @@ -42,7 +42,7 @@ class PydanticInitializer : StartupActivity { invokeAfterPsiEvents { LocalFileSystem.getInstance() .findFileByPath(configService.pyprojectToml ?: defaultPyProjectToml) - ?.also { loadPyprojecToml(project, it, configService) } + ?.also { loadPyprojectToml(project, it, configService) } ?: run { clearPyProjectTomlConfig(configService) } LocalFileSystem.getInstance() .findFileByPath(configService.mypyIni ?: defaultMypyIni) @@ -76,7 +76,7 @@ class PydanticInitializer : StartupActivity { .asSequence() .forEach { when (it.path) { - pyprojectToml -> loadPyprojecToml(project, it, configService) + pyprojectToml -> loadPyprojectToml(project, it, configService) mypyIni -> loadMypyIni(it, configService) } } @@ -123,7 +123,7 @@ class PydanticInitializer : StartupActivity { } } - private fun loadPyprojecToml(project: Project, config: VirtualFile, configService: PydanticConfigService) { + private fun loadPyprojectToml(project: Project, config: VirtualFile, configService: PydanticConfigService) { val result: TomlParseResult = Toml.parse(config.inputStream) val table = result.getTableOrEmpty("tool.pydantic-pycharm-plugin") @@ -183,6 +183,6 @@ class PydanticInitializer : StartupActivity { else -> runnable() } } - ApplicationManager.getApplication().invokeLater(wrapper, { false }) + ApplicationManager.getApplication().invokeLater(wrapper) { false } } } diff --git a/src/com/koxudaxi/pydantic/PydanticInspection.kt b/src/com/koxudaxi/pydantic/PydanticInspection.kt index 4afad8ff..804af570 100644 --- a/src/com/koxudaxi/pydantic/PydanticInspection.kt +++ b/src/com/koxudaxi/pydantic/PydanticInspection.kt @@ -4,14 +4,11 @@ import com.intellij.codeInspection.LocalInspectionToolSession import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor -import com.jetbrains.python.PyBundle import com.jetbrains.python.PyNames import com.jetbrains.python.inspections.PyInspection import com.jetbrains.python.inspections.PyInspectionVisitor import com.jetbrains.python.inspections.quickfix.RenameParameterQuickFix import com.jetbrains.python.psi.* -import com.jetbrains.python.psi.impl.PyCallExpressionImpl -import com.jetbrains.python.psi.impl.PySubscriptionExpressionImpl import com.jetbrains.python.psi.impl.PyTargetExpressionImpl import com.jetbrains.python.psi.resolve.PyResolveContext import com.jetbrains.python.psi.types.PyClassType @@ -28,10 +25,9 @@ class PydanticInspection : PyInspection() { val pydanticConfigService = PydanticConfigService.getInstance(holder.project) - override fun visitPyFunction(node: PyFunction?) { + override fun visitPyFunction(node: PyFunction) { super.visitPyFunction(node) - if (node == null) return val pyClass = getPyClassByAttribute(node) ?: return if (!isPydanticModel(pyClass, false, myTypeEvalContext) || !isValidatorMethod(node)) return val paramList = node.parameterList @@ -49,19 +45,17 @@ class PydanticInspection : PyInspection() { } - override fun visitPyCallExpression(node: PyCallExpression?) { + override fun visitPyCallExpression(node: PyCallExpression) { super.visitPyCallExpression(node) - if (node == null) return inspectPydanticModelCallableExpression(node) inspectFromOrm(node) } - override fun visitPyAssignmentStatement(node: PyAssignmentStatement?) { + override fun visitPyAssignmentStatement(node: PyAssignmentStatement) { super.visitPyAssignmentStatement(node) - if (node == null) return if (pydanticConfigService.currentWarnUntypedFields) { inspectWarnUntypedFields(node) } @@ -70,10 +64,9 @@ class PydanticInspection : PyInspection() { inspectAnnotatedAssignedField(node) } - override fun visitPyTypeDeclarationStatement(node: PyTypeDeclarationStatement?) { + override fun visitPyTypeDeclarationStatement(node: PyTypeDeclarationStatement) { super.visitPyTypeDeclarationStatement(node) - if (node == null) return inspectAnnotatedField(node) } diff --git a/src/com/koxudaxi/pydantic/PydanticParametersProvider.kt b/src/com/koxudaxi/pydantic/PydanticParametersProvider.kt index 8e5364ee..7e7b6977 100644 --- a/src/com/koxudaxi/pydantic/PydanticParametersProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticParametersProvider.kt @@ -10,17 +10,17 @@ import com.jetbrains.python.psi.types.PyCallableParameterImpl class PydanticParametersProvider: PyDataclassParametersProvider { - override fun getType(): PyDataclassParameters.Type = PyDanticType + override fun getType(): PyDataclassParameters.Type = PydanticType override fun getDecoratorAndTypeAndParameters(project: Project): Triple> { val generator = PyElementGenerator.getInstance(project) val ellipsis = generator.createEllipsis() val parameters = mutableListOf(PyCallableParameterImpl.psi(generator.createSingleStarParameter())) parameters.addAll(DATACLASS_ARGUMENTS.map { name -> PyCallableParameterImpl.nonPsi(name, null, ellipsis) }) - return Triple(DATACLASS_QUALIFIED_NAME, PyDanticType, parameters) + return Triple(DATACLASS_QUALIFIED_NAME, PydanticType, parameters) } - private object PyDanticType: PyDataclassParameters.Type { + private object PydanticType: PyDataclassParameters.Type { override val name: String = "pydantic" override val asPredefinedType: PyDataclassParameters.PredefinedType = PyDataclassParameters.PredefinedType.STD } diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index 4e278ecb..22ffd238 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -249,7 +249,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { } }.firstOrNull() } - is PyClass -> baseArgument.takeIf { isPydanticModel(baseArgument, false, context) } as? PyClass + is PyClass -> baseArgument.takeIf { isPydanticModel(baseArgument, false, context) } else -> null } ?.let { baseClass -> @@ -264,9 +264,9 @@ class PydanticTypeProvider : PyTypeProviderBase() { continue } baseClassCollected.putAll(getClassVariables(current, context) - .mapNotNull { it to fieldToParameter(it, context, hashMapOf(), typed) } + .mapNotNull { it to fieldToParameter(it, context, typed) } .mapNotNull { (field, parameter) -> - parameter?.name?.let { name -> Triple(field, parameter, name) } + parameter.name?.let { name -> Triple(field, parameter, name) } } .filterNot { (_, _, name) -> collected.containsKey(name) } .map { (field, parameter, name) -> @@ -288,7 +288,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { .filter { (name, _) -> isValidFieldName(name) && !name.startsWith('_') } .filter { (name, _) -> (newVersion || name != "model_name") } .map { (name, field) -> - val parameter = fieldToParameter(field, context, hashMapOf(), typed)!! + val parameter = fieldToParameter(field, context, typed) name to PydanticDynamicModel.createAttribute(name, parameter, field, context, false) } ) @@ -372,12 +372,11 @@ class PydanticTypeProvider : PyTypeProviderBase() { ) } - internal fun fieldToParameter( + private fun fieldToParameter( field: PyExpression, context: TypeEvalContext, - config: HashMap, typed: Boolean = true - ): PyCallableParameter? { + ): PyCallableParameter { var type: PyType? = null var defaultValue: PyExpression? = null when (val tupleValue = PsiTreeUtil.findChildOfType(field, PyTupleExpression::class.java)) { @@ -403,7 +402,6 @@ class PydanticTypeProvider : PyTypeProviderBase() { return PyCallableParameterImpl.nonPsi( field.name, -// getFieldName(field, context, config, pydanticVersion), typeForParameter, defaultValue ) @@ -445,7 +443,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { ANNOTATED_Q_NAME -> return getFieldFromAnnotated(pyExpression, context) ?.takeIf { it.arguments.any { arg -> arg.name == "default_factory" } } ?: value - ?: getTypeExpressionFromAnnotated(pyExpression, context)?.let { + ?: getTypeExpressionFromAnnotated(pyExpression)?.let { parseAnnotation(it, context) } else -> return value diff --git a/testSrc/com/jetbrains/python/PythonMockSdk.java b/testSrc/com/jetbrains/python/PythonMockSdk.java index ba661413..a46d634b 100644 --- a/testSrc/com/jetbrains/python/PythonMockSdk.java +++ b/testSrc/com/jetbrains/python/PythonMockSdk.java @@ -48,7 +48,7 @@ public static Sdk create(final String version, final VirtualFile @NotNull ... ad final VirtualFile typeShedDir = PyTypeShed.INSTANCE.getDirectory(); PyTypeShed.INSTANCE .findRootsForLanguageLevel(level) - .forEach(path -> ContainerUtil.putIfNotNull(classes, typeShedDir.findFileByRelativePath(path), roots)); + .forEach(path -> ContainerUtil.putIfNotNull(classes, typeShedDir.findFileByRelativePath(path.getPath()), roots)); String mock_stubs_path = mock_path + PythonSdkUtil.SKELETON_DIR_NAME; ContainerUtil.putIfNotNull(classes, LocalFileSystem.getInstance().refreshAndFindFileByPath(mock_stubs_path), roots); From e431e34551aee8b3f2e719613eb3828a7bf3b55c Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 13 Apr 2021 01:54:05 +0900 Subject: [PATCH 2/5] Pass tests --- src/com/koxudaxi/pydantic/Pydantic.kt | 219 +++++++++++------- .../bin/python3.7 => MockSdk/bin/python} | 0 testData/inspection/readOnlyProperty.py | 2 +- testData/typeinspection/fieldBroken.py | 4 +- .../com/jetbrains/python/PythonMockSdk.java | 89 ++++--- .../fixtures/PyLightProjectDescriptor.java | 29 ++- .../pydantic/PydanticCompletionTest.kt | 2 +- .../pydantic/PydanticCompletionV0Test.kt | 1 + .../pydantic/PydanticInitializerTest.kt | 7 +- .../com/koxudaxi/pydantic/PydanticTestCase.kt | 11 +- 10 files changed, 216 insertions(+), 148 deletions(-) rename testData/{MockSdk3.7/bin/python3.7 => MockSdk/bin/python} (100%) diff --git a/src/com/koxudaxi/pydantic/Pydantic.kt b/src/com/koxudaxi/pydantic/Pydantic.kt index 2b5bdf64..6f900e36 100644 --- a/src/com/koxudaxi/pydantic/Pydantic.kt +++ b/src/com/koxudaxi/pydantic/Pydantic.kt @@ -70,15 +70,15 @@ 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 + 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 + 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]")!! @@ -90,24 +90,28 @@ enum class ConfigType { } val DEFAULT_CONFIG = mapOf( - "allow_population_by_alias" to false, - "allow_population_by_field_name" to false, - "orm_mode" to false, - "allow_mutation" to true, - "keep_untouched" to listOf() + "allow_population_by_alias" to false, + "allow_population_by_field_name" to false, + "orm_mode" to false, + "allow_mutation" to true, + "keep_untouched" to listOf() ) val CONFIG_TYPES = mapOf( - "allow_population_by_alias" to ConfigType.BOOLEAN, - "allow_population_by_field_name" to ConfigType.BOOLEAN, - "orm_mode" to ConfigType.BOOLEAN, - "allow_mutation" to ConfigType.BOOLEAN, - "keep_untouched" to ConfigType.LIST_PYTYPE + "allow_population_by_alias" to ConfigType.BOOLEAN, + "allow_population_by_field_name" to ConfigType.BOOLEAN, + "orm_mode" to ConfigType.BOOLEAN, + "allow_mutation" to ConfigType.BOOLEAN, + "keep_untouched" to ConfigType.LIST_PYTYPE ) const val CUSTOM_ROOT_FIELD = "__root__" -fun getPyClassByPyCallExpression(pyCallExpression: PyCallExpression, includeDataclass: Boolean, context: TypeEvalContext): PyClass? { +fun getPyClassByPyCallExpression( + pyCallExpression: PyCallExpression, + includeDataclass: Boolean, + context: TypeEvalContext, +): PyClass? { val callee = pyCallExpression.callee ?: return null val pyType = when (val type = context.getType(callee)) { is PyClass -> return type @@ -123,7 +127,8 @@ fun getPyClassByPyKeywordArgument(pyKeywordArgument: PyKeywordArgument, context: } fun isPydanticModel(pyClass: PyClass, includeDataclass: Boolean, context: TypeEvalContext? = null): Boolean { - return (isSubClassOfPydanticBaseModel(pyClass, context) || (includeDataclass && isPydanticDataclass(pyClass))) && !isPydanticBaseModel(pyClass) + return (isSubClassOfPydanticBaseModel(pyClass, + context) || (includeDataclass && isPydanticDataclass(pyClass))) && !isPydanticBaseModel(pyClass) } fun isPydanticBaseModel(pyClass: PyClass): Boolean { @@ -191,36 +196,39 @@ internal fun isPydanticRegex(stringLiteralExpression: StringLiteralExpression): val context = TypeEvalContext.userInitiated(referenceExpression.project, referenceExpression.containingFile) val resolveResults = getResolveElements(referenceExpression, context) return PyUtil.filterTopPriorityResults(resolveResults) - .filterIsInstance() - .filter { pyFunction -> isPydanticField(pyFunction) || isConStr(pyFunction) } - .any() + .filterIsInstance() + .filter { pyFunction -> isPydanticField(pyFunction) || isConStr(pyFunction) } + .any() } internal fun getClassVariables(pyClass: PyClass, context: TypeEvalContext): Sequence { return pyClass.classAttributes - .asReversed() - .asSequence() - .filterNot { PyTypingTypeProvider.isClassVar(it, context) } + .asReversed() + .asSequence() + .filterNot { PyTypingTypeProvider.isClassVar(it, context) } } -private fun getAliasedFieldName(field: PyTargetExpression, context: TypeEvalContext, pydanticVersion: KotlinVersion?): String? { +private fun getAliasedFieldName( + field: PyTargetExpression, + context: TypeEvalContext, + pydanticVersion: KotlinVersion?, +): String? { val fieldName = field.name val assignedField = field.findAssignedValue()?.let { getFieldFromPyExpression(it, context, pydanticVersion) - } ?: - (field.annotation?.value as? PySubscriptionExpression) - ?.takeIf { getQualifiedName(it, context) == ANNOTATED_Q_NAME } - ?.let { getFieldFromAnnotated(it, context) } + } ?: (field.annotation?.value as? PySubscriptionExpression) + ?.takeIf { getQualifiedName(it, context) == ANNOTATED_Q_NAME } + ?.let { getFieldFromAnnotated(it, context) } ?: return fieldName return when (val alias = assignedField.getKeywordArgument("alias")) { - is StringLiteralExpression -> alias.stringValue - is PyReferenceExpression -> ((alias.reference.resolve() as? PyTargetExpressionImpl) - ?.findAssignedValue() as? StringLiteralExpression)?.stringValue - //TODO Support dynamic assigned Value. eg: Schema(..., alias=get_alias_name(field_name)) - else -> fieldName - } ?: fieldName - } + is StringLiteralExpression -> alias.stringValue + is PyReferenceExpression -> ((alias.reference.resolve() as? PyTargetExpressionImpl) + ?.findAssignedValue() as? StringLiteralExpression)?.stringValue + //TODO Support dynamic assigned Value. eg: Schema(..., alias=get_alias_name(field_name)) + else -> fieldName + } ?: fieldName +} fun getResolveElements(referenceExpression: PyReferenceExpression, context: TypeEvalContext): Array { @@ -240,15 +248,19 @@ fun getPyClassTypeByPyTypes(pyType: PyType): List { fun isPydanticSchemaByPsiElement(psiElement: PsiElement, context: TypeEvalContext): Boolean { - return PsiTreeUtil.getContextOfType(psiElement, PyClass::class.java) - ?.let { isPydanticSchema(it, context) } ?: false + return (psiElement as? PyClass ?: PsiTreeUtil.getContextOfType(psiElement, PyClass::class.java)) + ?.let { isPydanticSchema(it, context) } ?: false + } -inline fun validatePsiElementByFunction(psiElement: PsiElement, validator: (T) -> Boolean): Boolean { +inline fun validatePsiElementByFunction( + psiElement: PsiElement, + validator: (T) -> Boolean, +): Boolean { return when { T::class.java.isInstance(psiElement) -> validator(psiElement as T) else -> PsiTreeUtil.getContextOfType(psiElement, T::class.java) - ?.let { validator(it) } ?: false + ?.let { validator(it) } ?: false } } @@ -268,18 +280,24 @@ fun getSdk(project: Project): Sdk? { return project.pythonSdk ?: project.modules.mapNotNull { PythonSdkUtil.findPythonSdk(it) }.firstOrNull() } -fun getPsiElementByQualifiedName(qualifiedName: QualifiedName, project: Project, context: TypeEvalContext): PsiElement? { +fun getPsiElementByQualifiedName( + qualifiedName: QualifiedName, + project: Project, + context: TypeEvalContext, +): PsiElement? { val pythonSdk = getSdk(project) ?: return null - val module = project.modules.firstOrNull { pythonSdk.isAssociatedWithModule(it) } ?: return null + val module = project.modules.firstOrNull { pythonSdk.isAssociatedWithModule(it) } ?: project.modules.firstOrNull() + ?: return null val contextAnchor = ModuleBasedContextAnchor(module) return qualifiedName.resolveToElement(QNameResolveContext(contextAnchor, pythonSdk, context)) } fun getPydanticVersion(project: Project, context: TypeEvalContext): KotlinVersion? { val version = getPsiElementByQualifiedName(VERSION_QUALIFIED_NAME, project, context) as? PyTargetExpression - ?: return null - val versionString = (version.findAssignedValue()?.lastChild?.firstChild?.nextSibling as? PyStringLiteralExpression)?.stringValue - ?:(version.findAssignedValue() as? PyStringLiteralExpressionImpl)?.stringValue ?: return null + ?: return null + val versionString = + (version.findAssignedValue()?.lastChild?.firstChild?.nextSibling as? PyStringLiteralExpression)?.stringValue + ?: (version.findAssignedValue() as? PyStringLiteralExpressionImpl)?.stringValue ?: return null return pydanticVersionCache.getOrPut(versionString) { val versionList = versionString.split(VERSION_SPLIT_PATTERN).map { it.toIntOrNull() ?: 0 } val pydanticVersion = when { @@ -334,16 +352,16 @@ fun getConfigValue(name: String, value: Any?, context: TypeEvalContext): Any? { fun getConfig(pyClass: PyClass, context: TypeEvalContext, setDefault: Boolean): HashMap { val config = hashMapOf() pyClass.getAncestorClasses(context) - .reversed() - .filter { isPydanticModel(it, false) } - .map { getConfig(it, context, false) } - .forEach { - it.entries.forEach { entry -> - if (entry.value != null) { - config[entry.key] = getConfigValue(entry.key, entry.value, context) - } + .reversed() + .filter { isPydanticModel(it, false) } + .map { getConfig(it, context, false) } + .forEach { + it.entries.forEach { entry -> + if (entry.value != null) { + config[entry.key] = getConfigValue(entry.key, entry.value, context) } } + } pyClass.nestedClasses.firstOrNull { isConfigClass(it) }?.let { it.classAttributes.forEach { attribute -> attribute.findAssignedValue()?.let { value -> @@ -364,10 +382,12 @@ fun getConfig(pyClass: PyClass, context: TypeEvalContext, setDefault: Boolean): return config } -fun getFieldName(field: PyTargetExpression, - context: TypeEvalContext, - config: HashMap, - pydanticVersion: KotlinVersion?): String? { +fun getFieldName( + field: PyTargetExpression, + context: TypeEvalContext, + config: HashMap, + pydanticVersion: KotlinVersion?, +): String? { return when (pydanticVersion?.major) { 0 -> when { @@ -404,7 +424,8 @@ fun getPyClassByAttribute(pyPsiElement: PsiElement?): PyClass? { fun createPyClassTypeImpl(qualifiedName: String, project: Project, context: TypeEvalContext): PyClassTypeImpl? { var psiElement = getPsiElementByQualifiedName(QualifiedName.fromDottedString(qualifiedName), project, context) if (psiElement == null) { - psiElement = getPsiElementByQualifiedName(QualifiedName.fromDottedString("builtins.$qualifiedName"), project, context) + psiElement = + getPsiElementByQualifiedName(QualifiedName.fromDottedString("builtins.$qualifiedName"), project, context) ?: return null } return PyClassTypeImpl.createTypeByQName(psiElement, qualifiedName, false) @@ -417,7 +438,8 @@ fun getPydanticPyClass(pyCallExpression: PyCallExpression, context: TypeEvalCont } fun getParentOfPydanticCallableExpression(file: PsiFile, offset: Int, context: TypeEvalContext): PyCallExpression? { - var pyCallExpression: PyCallExpression? = PsiTreeUtil.getParentOfType(file.findElementAt(offset), PyCallExpression::class.java, true) + var pyCallExpression: PyCallExpression? = + PsiTreeUtil.getParentOfType(file.findElementAt(offset), PyCallExpression::class.java, true) while (pyCallExpression != null && getPydanticPyClass(pyCallExpression, context) == null) { pyCallExpression = PsiTreeUtil.getParentOfType(pyCallExpression, PyCallExpression::class.java, true) } @@ -426,7 +448,7 @@ fun getParentOfPydanticCallableExpression(file: PsiFile, offset: Int, context: T fun getPydanticCallExpressionAtCaret(file: PsiFile, editor: Editor, context: TypeEvalContext): PyCallExpression? { return getParentOfPydanticCallableExpression(file, editor.caretModel.offset, context) - ?: getParentOfPydanticCallableExpression(file, editor.caretModel.offset - 1, context) + ?: getParentOfPydanticCallableExpression(file, editor.caretModel.offset - 1, context) } @@ -437,10 +459,16 @@ fun addKeywordArgument(pyCallExpression: PyCallExpression, pyKeywordArgument: Py } } -fun getPydanticUnFilledArguments(pyClass: PyClass?, pyCallExpression: PyCallExpression, pydanticTypeProvider: PydanticTypeProvider, context: TypeEvalContext): List { +fun getPydanticUnFilledArguments( + pyClass: PyClass?, + pyCallExpression: PyCallExpression, + pydanticTypeProvider: PydanticTypeProvider, + context: TypeEvalContext, +): List { val pydanticClass = pyClass ?: getPydanticPyClass(pyCallExpression, context) ?: return emptyList() val pydanticType = pydanticTypeProvider.getPydanticTypeForClass(pydanticClass, context, true) ?: return emptyList() - val currentArguments = pyCallExpression.arguments.filter { it is PyKeywordArgument || (it as? PyStarArgumentImpl)?.isKeyword == true } + val currentArguments = + pyCallExpression.arguments.filter { it is PyKeywordArgument || (it as? PyStarArgumentImpl)?.isKeyword == true } .mapNotNull { it.name }.toSet() return pydanticType.getParameters(context)?.filterNot { currentArguments.contains(it.name) } ?: emptyList() } @@ -455,55 +483,69 @@ fun getPyTypeFromPyExpression(pyExpression: PyExpression, context: TypeEvalConte is PyReferenceExpression -> { val resolveResults = getResolveElements(pyExpression, context) PyUtil.filterTopPriorityResults(resolveResults) - .filterIsInstance() - .map { pyClass -> pyClass.getType(context)?.getReturnType(context) } - .firstOrNull() + .filterIsInstance() + .map { pyClass -> pyClass.getType(context)?.getReturnType(context) } + .firstOrNull() } else -> null } } -internal fun hasTargetPyType(pyExpression: PyExpression, targetPyTypes: List, context: TypeEvalContext): Boolean { +internal fun hasTargetPyType( + pyExpression: PyExpression, + targetPyTypes: List, + context: TypeEvalContext, +): Boolean { val callee = (pyExpression as? PyCallExpression)?.callee ?: return false val pyType = getPyTypeFromPyExpression(callee, context) ?: return false val defaultValueTypeClassQName = pyType.declarationElement?.qualifiedName ?: return false return targetPyTypes.any { it.declarationElement?.qualifiedName == defaultValueTypeClassQName } } -internal fun isUntouchedClass(pyExpression: PyExpression?, config: HashMap, context: TypeEvalContext):Boolean { +internal fun isUntouchedClass( + pyExpression: PyExpression?, + config: HashMap, + context: TypeEvalContext, +): Boolean { if (pyExpression == null) return false - val keepUntouchedClasses = (config["keep_untouched"] as? List<*>)?.filterIsInstance()?.toList() ?: return false + val keepUntouchedClasses = + (config["keep_untouched"] as? List<*>)?.filterIsInstance()?.toList() ?: return false if (keepUntouchedClasses.isNullOrEmpty()) return false return (hasTargetPyType(pyExpression, keepUntouchedClasses, context)) } -internal fun getFieldFromPyExpression(psiElement: PsiElement, context: TypeEvalContext, pydanticVersion: KotlinVersion?): PyCallExpression? { +internal fun getFieldFromPyExpression( + psiElement: PsiElement, + context: TypeEvalContext, + pydanticVersion: KotlinVersion?, +): PyCallExpression? { val callee = (psiElement as? PyCallExpression) ?.let { it.callee as? PyReferenceExpression } ?: return null val results = getResolveElements(callee, context) val versionZero = pydanticVersion?.major == 0 - if (!PyUtil.filterTopPriorityResults(results).any{ + if (!PyUtil.filterTopPriorityResults(results).any { when { versionZero -> isPydanticSchemaByPsiElement(it, context) else -> isPydanticFieldByPsiElement(it) } - }) return null + }) return null return psiElement } internal fun getFieldFromAnnotated(annotated: PyExpression, context: TypeEvalContext): PyCallExpression? = annotated.children - .filterIsInstance () + .filterIsInstance() .firstOrNull() ?.children ?.getOrNull(1) - ?.let {getFieldFromPyExpression(it, context, null) + ?.let { + getFieldFromPyExpression(it, context, null) } internal fun getTypeExpressionFromAnnotated(annotated: PyExpression): PyExpression? = annotated.children - .filterIsInstance () + .filterIsInstance() .firstOrNull() ?.children ?.getOrNull(0) @@ -512,20 +554,21 @@ internal fun getTypeExpressionFromAnnotated(annotated: PyExpression): PyExpressi internal fun getDefaultFromField(field: PyCallExpression): PyExpression? = field.getKeywordArgument("default") ?: field.getArgument(0, PyExpression::class.java).takeIf { it?.name == null } -internal fun getDefaultFactoryFromField(field: PyCallExpression): PyExpression? = field.getKeywordArgument("default_factory") - -internal fun getQualifiedName(pyExpression: PyExpression, context: TypeEvalContext) : String? { - return when(pyExpression) { - is PySubscriptionExpression -> pyExpression.qualifier?.let { getQualifiedName(it, context) } - is PyReferenceExpression -> { - val resolveResults = getResolveElements(pyExpression, context) - return PyUtil.filterTopPriorityResults(resolveResults) - .filterIsInstance() - .mapNotNull { it.qualifiedName } - .firstOrNull() - } - else -> { - return null +internal fun getDefaultFactoryFromField(field: PyCallExpression): PyExpression? = + field.getKeywordArgument("default_factory") + +internal fun getQualifiedName(pyExpression: PyExpression, context: TypeEvalContext): String? { + return when (pyExpression) { + is PySubscriptionExpression -> pyExpression.qualifier?.let { getQualifiedName(it, context) } + is PyReferenceExpression -> { + val resolveResults = getResolveElements(pyExpression, context) + return PyUtil.filterTopPriorityResults(resolveResults) + .filterIsInstance() + .mapNotNull { it.qualifiedName } + .firstOrNull() + } + else -> { + return null } } } \ No newline at end of file diff --git a/testData/MockSdk3.7/bin/python3.7 b/testData/MockSdk/bin/python similarity index 100% rename from testData/MockSdk3.7/bin/python3.7 rename to testData/MockSdk/bin/python diff --git a/testData/inspection/readOnlyProperty.py b/testData/inspection/readOnlyProperty.py index b32a3a2d..2fc0b865 100644 --- a/testData/inspection/readOnlyProperty.py +++ b/testData/inspection/readOnlyProperty.py @@ -46,7 +46,7 @@ class Config: allow_mutation=False G.abc = G.abc.lower() -G.abc.lower() = 'efg' +G.abc.lower() = 'efg' class H: diff --git a/testData/typeinspection/fieldBroken.py b/testData/typeinspection/fieldBroken.py index a53178cc..065ef3e7 100644 --- a/testData/typeinspection/fieldBroken.py +++ b/testData/typeinspection/fieldBroken.py @@ -7,6 +7,6 @@ class A(BaseModel): a b = c = Schema() - d: - e: = '123' + d: + e: = '123' A(a=int(123), b=str('123'), c=str('456'), d=str('789')) diff --git a/testSrc/com/jetbrains/python/PythonMockSdk.java b/testSrc/com/jetbrains/python/PythonMockSdk.java index a46d634b..f79ab586 100644 --- a/testSrc/com/jetbrains/python/PythonMockSdk.java +++ b/testSrc/com/jetbrains/python/PythonMockSdk.java @@ -1,4 +1,4 @@ -// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python; import com.intellij.openapi.projectRoots.Sdk; @@ -20,73 +20,95 @@ import org.jetbrains.annotations.Nullable; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; +import java.util.List; /** * @author yole */ -public class PythonMockSdk { - @NonNls private static final String MOCK_SDK_NAME = "Mock Python SDK"; +public final class PythonMockSdk { private PythonMockSdk() { } - public static Sdk create(final String version, final VirtualFile @NotNull ... additionalRoots) { - final String mock_path = PythonTestUtil.getTestDataPath() + "/MockSdk" + version + "/"; + public static @NotNull Sdk create(@NotNull String name) { + return create(name, LanguageLevel.getLatest()); + } - String sdkHome = new File(mock_path, "bin/python" + version).getPath(); + public static @NotNull Sdk create(@NotNull LanguageLevel level, VirtualFile @NotNull ... additionalRoots) { + return create("MockSdk", level, additionalRoots); + } + + private static @NotNull Sdk create(@NotNull String name, @NotNull LanguageLevel level, VirtualFile @NotNull ... additionalRoots) { + return create(name, new PyMockSdkType(level), level, additionalRoots); + } + + public static @NotNull Sdk create(@NotNull String pathSuffix, @NotNull SdkTypeId sdkType, @NotNull LanguageLevel level, VirtualFile @NotNull ... additionalRoots) { + String sdkName = "Mock " + PyNames.PYTHON_SDK_ID_NAME + " " + level.toPythonVersion(); + return create(sdkName, pathSuffix, sdkType, level, additionalRoots); + } + + public static @NotNull Sdk create(@NotNull String name, @NotNull String pathSuffix, @NotNull SdkTypeId sdkType, @NotNull LanguageLevel level, VirtualFile @NotNull ... additionalRoots) { + final String mockSdkPath = PythonTestUtil.getTestDataPath() + "/" + pathSuffix; MultiMap roots = MultiMap.create(); - OrderRootType classes = OrderRootType.CLASSES; + roots.putValues(OrderRootType.CLASSES, createRoots(mockSdkPath, level)); + roots.putValues(OrderRootType.CLASSES, Arrays.asList(additionalRoots)); - ContainerUtil.putIfNotNull(classes, LocalFileSystem.getInstance().refreshAndFindFileByIoFile(new File(mock_path, "Lib")), roots); + MockSdk sdk = new MockSdk( + name, + mockSdkPath + "/bin/python", + toVersionString(level), + roots, + sdkType + ); + + // com.jetbrains.python.psi.resolve.PythonSdkPathCache.getInstance() corrupts SDK, so have to clone + return sdk.clone(); + } - ContainerUtil.putIfNotNull(classes, PyUserSkeletonsUtil.getUserSkeletonsDirectory(), roots); + private static @NotNull List createRoots(@NotNull @NonNls String mockSdkPath, @NotNull LanguageLevel level) { + final var result = new ArrayList(); - final LanguageLevel level = LanguageLevel.fromPythonVersion(version); - final VirtualFile typeShedDir = PyTypeShed.INSTANCE.getDirectory(); - PyTypeShed.INSTANCE - .findRootsForLanguageLevel(level) - .forEach(path -> ContainerUtil.putIfNotNull(classes, typeShedDir.findFileByRelativePath(path.getPath()), roots)); + final var localFS = LocalFileSystem.getInstance(); + ContainerUtil.addIfNotNull(result, localFS.refreshAndFindFileByIoFile(new File(mockSdkPath, "Lib"))); + ContainerUtil.addIfNotNull(result, localFS.refreshAndFindFileByIoFile(new File(mockSdkPath, PythonSdkUtil.SKELETON_DIR_NAME))); - String mock_stubs_path = mock_path + PythonSdkUtil.SKELETON_DIR_NAME; - ContainerUtil.putIfNotNull(classes, LocalFileSystem.getInstance().refreshAndFindFileByPath(mock_stubs_path), roots); + ContainerUtil.addIfNotNull(result, PyUserSkeletonsUtil.getUserSkeletonsDirectory()); - roots.putValues(classes, Arrays.asList(additionalRoots)); + result.addAll(PyTypeShed.INSTANCE.findRootsForLanguageLevel(level)); - MockSdk sdk = new MockSdk(MOCK_SDK_NAME + " " + version, sdkHome, "Python " + version + " Mock SDK", roots, new PyMockSdkType(version)); + return result; + } - // com.jetbrains.python.psi.resolve.PythonSdkPathCache.getInstance() corrupts SDK, so have to clone - return sdk.clone(); + private static @NotNull String toVersionString(@NotNull LanguageLevel level) { + return "Python " + level.toPythonVersion(); } - private static class PyMockSdkType implements SdkTypeId { + private static final class PyMockSdkType implements SdkTypeId { @NotNull - private final String myVersionString; - private final String mySdkIdName; + private final LanguageLevel myLevel; - private PyMockSdkType(@NotNull String string) { - mySdkIdName = PyNames.PYTHON_SDK_ID_NAME; - myVersionString = string; + private PyMockSdkType(@NotNull LanguageLevel level) { + myLevel = level; } @NotNull @Override public String getName() { - return mySdkIdName; + return PyNames.PYTHON_SDK_ID_NAME; } @Nullable @Override - public String getVersionString(@NotNull Sdk sdk) { - return myVersionString; + public @NotNull String getVersionString(@NotNull Sdk sdk) { + return toVersionString(myLevel); } @Override public void saveAdditionalData(@NotNull SdkAdditionalData additionalData, @NotNull Element additional) { - } @Nullable @@ -94,10 +116,5 @@ public void saveAdditionalData(@NotNull SdkAdditionalData additionalData, @NotNu public SdkAdditionalData loadAdditionalData(@NotNull Sdk currentSdk, @NotNull Element additional) { return null; } - - @Override - public boolean isLocalSdk(@NotNull Sdk sdk) { - return false; - } } } diff --git a/testSrc/com/jetbrains/python/fixtures/PyLightProjectDescriptor.java b/testSrc/com/jetbrains/python/fixtures/PyLightProjectDescriptor.java index 609b84a0..fe7df591 100644 --- a/testSrc/com/jetbrains/python/fixtures/PyLightProjectDescriptor.java +++ b/testSrc/com/jetbrains/python/fixtures/PyLightProjectDescriptor.java @@ -24,34 +24,45 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.testFramework.LightProjectDescriptor; import com.jetbrains.python.PythonMockSdk; +import com.jetbrains.python.psi.LanguageLevel; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Project descriptor (extracted from {@link com.jetbrains.python.fixtures.PyTestCase}) and should be used with it. * @author Ilya.Kazakevich */ public class PyLightProjectDescriptor extends LightProjectDescriptor { - private final String myPythonVersion; - public PyLightProjectDescriptor(String pythonVersion) { - myPythonVersion = pythonVersion; - } + @Nullable + private final String myName; @NotNull - @Override - public String getModuleTypeId() { - return "EMPTY_MODULE"; + private final LanguageLevel myLevel; + + public PyLightProjectDescriptor(@NotNull LanguageLevel level) { + this(null, level); + } + + public PyLightProjectDescriptor(@NotNull String name) { + this(name, LanguageLevel.getLatest()); + } + + private PyLightProjectDescriptor(@Nullable String name, @NotNull LanguageLevel level) { + myName = name; + myLevel = level; } @Override public Sdk getSdk() { - return PythonMockSdk.create(myPythonVersion, getAdditionalRoots()); + return myName == null ? PythonMockSdk.create(myLevel, getAdditionalRoots()) : PythonMockSdk.create(myName); } /** * @return additional roots to add to mock python + * @apiNote ignored when name is provided. */ - protected VirtualFile [] getAdditionalRoots() { + protected VirtualFile @NotNull [] getAdditionalRoots() { return VirtualFile.EMPTY_ARRAY; } diff --git a/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt b/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt index cd6a37e4..51a60e8a 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt @@ -12,7 +12,7 @@ open class PydanticCompletionTest : PydanticTestCase() { val excludes = listOf( "__annotations__", "__base__", "__bases__", "__basicsize__", "__dict__", "__dictoffset__", "__flags__", "__itemsize__", "__mro__", "__name__", "__qualname__", "__slots__", "__text_signature__", - "__weakrefoffset__", "Ellipsis", "EnvironmentError", "IOError", "NotImplemented", "List", "Type", "Annotated", "MISSING") + "__weakrefoffset__", "Ellipsis", "EnvironmentError", "IOError", "NotImplemented", "List", "Type", "Annotated", "MISSING", "WindowsError") val actual = myFixture!!.completeBasic().filter { it!!.psiElement is PyTargetExpression }.filterNot { diff --git a/testSrc/com/koxudaxi/pydantic/PydanticCompletionV0Test.kt b/testSrc/com/koxudaxi/pydantic/PydanticCompletionV0Test.kt index f01d7f3f..ba9b6065 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticCompletionV0Test.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticCompletionV0Test.kt @@ -60,6 +60,7 @@ open class PydanticCompletionV0Test : PydanticTestCase(version = "v0") { Pair("EnvironmentError", "builtins"), Pair("IOError", "builtins"), Pair("NotImplemented", "builtins"), + Pair("WindowsError", "builtins"), Pair("b_id", "null") ) ) diff --git a/testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt b/testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt index 4eb0a1ec..2961eccc 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt @@ -3,7 +3,10 @@ package com.koxudaxi.pydantic import com.intellij.codeInspection.ProblemHighlightType import com.intellij.openapi.application.invokeLater import java.io.File +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.createTempFile +@ExperimentalPathApi open class PydanticInitializerTest : PydanticTestCase() { lateinit var pydanticConfigService: PydanticConfigService lateinit var testMethodName: String @@ -18,7 +21,7 @@ open class PydanticInitializerTest : PydanticTestCase() { private fun setUpPyProjectToml(runnable: () -> Unit) { setUpConfig() - val target = createTempFile(testMethodName) + val target = createTempFile(testMethodName).toFile() try { val source = File("${myFixture!!.testDataPath}/${testDataMethodPath.toLowerCase()}/pyproject.toml") pydanticConfigService.pyprojectToml = target.path @@ -34,7 +37,7 @@ open class PydanticInitializerTest : PydanticTestCase() { private fun setUpMypyIni(runnable: () -> Unit) { setUpConfig() - val target = createTempFile(testMethodName) + val target = createTempFile(testMethodName).toFile() try { val source = File("${myFixture!!.testDataPath}/${testDataMethodPath.toLowerCase()}/mypy.ini") pydanticConfigService.mypyIni = target.path diff --git a/testSrc/com/koxudaxi/pydantic/PydanticTestCase.kt b/testSrc/com/koxudaxi/pydantic/PydanticTestCase.kt index 31780539..3ec1a6f0 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticTestCase.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticTestCase.kt @@ -1,33 +1,26 @@ package com.koxudaxi.pydantic -import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.impl.FilePropertyPusher -import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile -import com.intellij.testFramework.LightProjectDescriptor import com.intellij.testFramework.PsiTestUtil.addSourceRoot import com.intellij.testFramework.PsiTestUtil.removeSourceRoot import com.intellij.testFramework.UsefulTestCase import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl -import com.intellij.testFramework.writeChild import com.jetbrains.python.PyNames -import com.jetbrains.python.PythonDialectsTokenSetProvider import com.jetbrains.python.fixtures.PyLightProjectDescriptor import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher -import com.jetbrains.python.psi.search.PySearchUtilBase import com.jetbrains.python.sdk.PythonSdkUtil abstract class PydanticTestCase(version: String = "v1") : UsefulTestCase() { protected var myFixture: CodeInsightTestFixture? = null - private val PYTHON_3_MOCK_SDK = "3.7" - private val projectDescriptor: PyLightProjectDescriptor = PyLightProjectDescriptor(PYTHON_3_MOCK_SDK) + private val projectDescriptor: PyLightProjectDescriptor = PyLightProjectDescriptor(LanguageLevel.getLatest()) private val testDataPath: String = "testData" private val mockPath: String = "mock" private val pydanticMockPath: String = "$mockPath/pydantic$version" @@ -84,7 +77,7 @@ abstract class PydanticTestCase(version: String = "v1") : UsefulTestCase() { .also { sdk.sdkModificator.addRoot(it, OrderRootType.CLASSES) } libDir.createChildDirectory(null, PyNames.SITE_PACKAGES) } -// PythonDialectsTokenSetProvider.getInstance().dispose() + setLanguageLevel(LanguageLevel.PYTHON37) } From 6451d04e8c20ad70a80795bce9811ea32b294f86 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 13 Apr 2021 02:32:50 +0900 Subject: [PATCH 3/5] Fix CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a5036fbf..bfd99676 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -32,7 +32,7 @@ jobs: uses: ChrisCarini/intellij-platform-plugin-verifier-action@v0.0.2 with: ide-versions: | - pycharmPC:2020.2 + pycharmPC:2021.1 pycharmPC:LATEST-EAP-SNAPSHOT - name: setup python uses: actions/setup-python@v1 From 93fb36d306067b0cc40a01bb9b8ec363faa70d95 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Wed, 14 Apr 2021 04:15:21 +0900 Subject: [PATCH 4/5] Add PydanticDataclassTypeProvider.kt --- resources/META-INF/plugin.xml | 1 + src/com/koxudaxi/pydantic/Pydantic.kt | 20 ++--- .../pydantic/PydanticDataclassTypeProvider.kt | 83 +++++++++++++++++++ .../koxudaxi/pydantic/PydanticTypeProvider.kt | 46 +++++----- 4 files changed, 115 insertions(+), 35 deletions(-) create mode 100644 src/com/koxudaxi/pydantic/PydanticDataclassTypeProvider.kt diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index fe794b19..8a4b2d1d 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -332,6 +332,7 @@ + diff --git a/src/com/koxudaxi/pydantic/Pydantic.kt b/src/com/koxudaxi/pydantic/Pydantic.kt index 6f900e36..b47649bc 100644 --- a/src/com/koxudaxi/pydantic/Pydantic.kt +++ b/src/com/koxudaxi/pydantic/Pydantic.kt @@ -194,8 +194,7 @@ internal fun isPydanticRegex(stringLiteralExpression: StringLiteralExpression): val pyCallExpression = pyKeywordArgument.parent.parent as? PyCallExpression ?: return false val referenceExpression = pyCallExpression.callee as? PyReferenceExpression ?: return false val context = TypeEvalContext.userInitiated(referenceExpression.project, referenceExpression.containingFile) - val resolveResults = getResolveElements(referenceExpression, context) - return PyUtil.filterTopPriorityResults(resolveResults) + return getResolvedPsiElements(referenceExpression, context) .filterIsInstance() .filter { pyFunction -> isPydanticField(pyFunction) || isConStr(pyFunction) } .any() @@ -238,6 +237,11 @@ fun getResolveElements(referenceExpression: PyReferenceExpression, context: Type } + +fun getResolvedPsiElements(referenceExpression: PyReferenceExpression, context: TypeEvalContext): List { + return getResolveElements(referenceExpression, context).let { PyUtil.filterTopPriorityResults(it) } +} + fun getPyClassTypeByPyTypes(pyType: PyType): List { return when (pyType) { is PyUnionType -> pyType.members.mapNotNull { it }.flatMap { getPyClassTypeByPyTypes(it) } @@ -325,8 +329,7 @@ fun isValidFieldName(name: String?): Boolean { fun getConfigValue(name: String, value: Any?, context: TypeEvalContext): Any? { if (value is PyReferenceExpression) { - val resolveResults = getResolveElements(value, context) - val targetExpression = PyUtil.filterTopPriorityResults(resolveResults).firstOrNull() ?: return null + val targetExpression = getResolvedPsiElements(value, context).firstOrNull() ?: return null val assignedValue = (targetExpression as? PyTargetExpression)?.findAssignedValue() ?: return null return getConfigValue(name, assignedValue, context) } @@ -481,8 +484,7 @@ fun getPyTypeFromPyExpression(pyExpression: PyExpression, context: TypeEvalConte return when (pyExpression) { is PyType -> pyExpression is PyReferenceExpression -> { - val resolveResults = getResolveElements(pyExpression, context) - PyUtil.filterTopPriorityResults(resolveResults) + getResolvedPsiElements(pyExpression, context) .filterIsInstance() .map { pyClass -> pyClass.getType(context)?.getReturnType(context) } .firstOrNull() @@ -522,9 +524,8 @@ internal fun getFieldFromPyExpression( val callee = (psiElement as? PyCallExpression) ?.let { it.callee as? PyReferenceExpression } ?: return null - val results = getResolveElements(callee, context) val versionZero = pydanticVersion?.major == 0 - if (!PyUtil.filterTopPriorityResults(results).any { + if (!getResolvedPsiElements(callee, context).any { when { versionZero -> isPydanticSchemaByPsiElement(it, context) else -> isPydanticFieldByPsiElement(it) @@ -561,8 +562,7 @@ internal fun getQualifiedName(pyExpression: PyExpression, context: TypeEvalConte return when (pyExpression) { is PySubscriptionExpression -> pyExpression.qualifier?.let { getQualifiedName(it, context) } is PyReferenceExpression -> { - val resolveResults = getResolveElements(pyExpression, context) - return PyUtil.filterTopPriorityResults(resolveResults) + return getResolvedPsiElements(pyExpression, context) .filterIsInstance() .mapNotNull { it.qualifiedName } .firstOrNull() diff --git a/src/com/koxudaxi/pydantic/PydanticDataclassTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticDataclassTypeProvider.kt new file mode 100644 index 00000000..dee56f3f --- /dev/null +++ b/src/com/koxudaxi/pydantic/PydanticDataclassTypeProvider.kt @@ -0,0 +1,83 @@ +package com.koxudaxi.pydantic + +import com.intellij.psi.PsiElement +import com.jetbrains.python.codeInsight.stdlib.PyDataclassTypeProvider +import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.impl.* +import com.jetbrains.python.psi.types.* + +/** + * `PydanticDataclassTypeProvider` gets actual pydantic dataclass types + * + * PyCharm 2021.1 detects decorated object type by type-hint of decorators. + * Unfortunately, pydantic.dataclasses.dataclass returns `Dataclass` type. + * `Dataclass` is not an actual model, which is Stub type for static-type checking. + * But, PyCharm can detect actual dataclass type by parsing the type definition. + * `PydanticDataclassTypeProvider` ignore `Dataclass` and get actual dataclass type using `PyDataclassTypeProvider` + * + */ +class PydanticDataclassTypeProvider : PyTypeProviderBase() { + private val pyDataclassTypeProvider = PyDataclassTypeProvider() + + override fun getReferenceExpressionType( + referenceExpression: PyReferenceExpression, + context: TypeEvalContext, + ): PyType? { + return getPydanticDataclass(referenceExpression, context) + } + + + private fun getDataclassCallableType( + referenceTarget: PsiElement, + context: TypeEvalContext, + callSite: PyCallExpression? = null, + ): PyCallableType? { + return pyDataclassTypeProvider.getReferenceType( + referenceTarget, + context, + callSite ?: PyCallExpressionImpl(referenceTarget.node) + )?.get() as? PyCallableType + } + + private fun getPydanticDataclassType( + referenceTarget: PsiElement, + context: TypeEvalContext, + pyReferenceExpression: PyReferenceExpression, + definition: Boolean, + ): PyType? { + val callSite = PyCallExpressionNavigator.getPyCallExpressionByCallee(pyReferenceExpression) + val dataclassCallableType = getDataclassCallableType(referenceTarget, context, callSite) ?: return null + val dataclassType = (dataclassCallableType).getReturnType(context) as? PyClassType ?: return null + if (!isPydanticDataclass(dataclassType.pyClass)) return null + + return when { + callSite is PyCallExpression && definition -> dataclassCallableType + definition -> (dataclassType.declarationElement as? PyTypedElement)?.let { context.getType(it) } + else -> dataclassType + } + } + + + private fun getPydanticDataclass(referenceExpression: PyReferenceExpression, context: TypeEvalContext): PyType? { + return getResolvedPsiElements(referenceExpression, context) + .asSequence() + .mapNotNull { + when { + it is PyClass && isPydanticDataclass(it) -> + getPydanticDataclassType(it, context, referenceExpression, true) + it is PyTargetExpression -> (it as? PyTypedElement) + ?.let { pyTypedElement -> context.getType(pyTypedElement) } + ?.let { pyType -> getPyClassTypeByPyTypes(pyType) } + ?.filter { pyClassType -> isPydanticDataclass(pyClassType.pyClass) } + ?.mapNotNull { pyClassType -> + getPydanticDataclassType(pyClassType.pyClass, + context, + referenceExpression, + pyClassType.isDefinition) + } + ?.firstOrNull() + else -> null + } + }.firstOrNull() + } +} diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index 22ffd238..3a6f4592 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -12,7 +12,7 @@ import one.util.streamex.StreamEx class PydanticTypeProvider : PyTypeProviderBase() { override fun getReferenceExpressionType( referenceExpression: PyReferenceExpression, - context: TypeEvalContext + context: TypeEvalContext, ): PyType? { return getPydanticTypeForCallee(referenceExpression, context) } @@ -20,7 +20,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { override fun getCallType( pyFunction: PyFunction, callSite: PyCallSiteExpression, - context: TypeEvalContext + context: TypeEvalContext, ): Ref? { return when (pyFunction.qualifiedName) { CON_LIST_Q_NAME -> Ref.create( @@ -37,7 +37,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { override fun getReferenceType( referenceTarget: PsiElement, context: TypeEvalContext, - anchor: PsiElement? + anchor: PsiElement?, ): Ref? { if (referenceTarget is PyTargetExpression) { val pyClass = getPyClassByAttribute(referenceTarget.parent) ?: return null @@ -70,7 +70,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { pyClass: PyClass, context: TypeEvalContext, ellipsis: PyNoneLiteralExpression, - pydanticVersion: KotlinVersion? + pydanticVersion: KotlinVersion?, ): Ref? { return pyClass.findClassAttribute(name, false, context) ?.let { return getRefTypeFromField(it, ellipsis, context, pyClass, pydanticVersion) } @@ -91,7 +91,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun getRefTypeFromField( pyTargetExpression: PyTargetExpression, ellipsis: PyNoneLiteralExpression, context: TypeEvalContext, pyClass: PyClass, - pydanticVersion: KotlinVersion? + pydanticVersion: KotlinVersion?, ): Ref? { return fieldToParameter( pyTargetExpression, @@ -107,13 +107,11 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun getPydanticTypeForCallee( referenceExpression: PyReferenceExpression, - context: TypeEvalContext + context: TypeEvalContext, ): PyType? { if (PyCallExpressionNavigator.getPyCallExpressionByCallee(referenceExpression) == null) return null - val resolveResults = getResolveElements(referenceExpression, context) - - return PyUtil.filterTopPriorityResults(resolveResults) + return getResolvedPsiElements(referenceExpression, context) .asSequence() .map { when { @@ -171,7 +169,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun getPydanticDynamicModelPyClass( pyTargetExpression: PyTargetExpression, - context: TypeEvalContext + context: TypeEvalContext, ): PyClass? { val pyCallableType = getPydanticDynamicModelTypeForTargetExpression(pyTargetExpression, context) ?: return null @@ -180,7 +178,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun getPydanticDynamicModelTypeForTargetExpression( pyTargetExpression: PyTargetExpression, - context: TypeEvalContext + context: TypeEvalContext, ): PydanticDynamicModelClassType? { val pyCallExpression = pyTargetExpression.findAssignedValue() as? PyCallExpression ?: return null return getPydanticDynamicModelTypeForTargetExpression(pyCallExpression, context) @@ -188,13 +186,12 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun getPydanticDynamicModelTypeForTargetExpression( pyCallExpression: PyCallExpression, - context: TypeEvalContext + context: TypeEvalContext, ): PydanticDynamicModelClassType? { val arguments = pyCallExpression.arguments.toList() if (arguments.isEmpty()) return null val referenceExpression = (pyCallExpression.callee as? PyReferenceExpression) ?: return null - val resolveResults = getResolveElements(referenceExpression, context) - val pyFunction = PyUtil.filterTopPriorityResults(resolveResults) + val pyFunction = getResolvedPsiElements(referenceExpression, context) .asSequence() .filterIsInstance() .map { it.takeIf { pyFunction -> isPydanticCreateModel(pyFunction) } }.firstOrNull() @@ -207,7 +204,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun getPydanticDynamicModelTypeForFunction( pyFunction: PyFunction, pyArguments: List, - context: TypeEvalContext + context: TypeEvalContext, ): PydanticDynamicModelClassType? { val project = pyFunction.project val typed = getInstance(project).currentInitTyped @@ -227,7 +224,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { pyArguments.firstOrNull() } ?: return null val modelName = when (modelNameArgument) { - is PyReferenceExpression -> PyUtil.filterTopPriorityResults(getResolveElements(modelNameArgument, context)) + is PyReferenceExpression -> getResolvedPsiElements(modelNameArgument, context) .filterIsInstance() .map { it.findAssignedValue() } .firstOrNull() @@ -240,7 +237,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { val baseClass = when (val baseArgument = (keywordArguments["__base__"] as? PyKeywordArgument)?.valueExpression) { is PyReferenceExpression -> { - PyUtil.filterTopPriorityResults(getResolveElements(baseArgument, context)) + getResolvedPsiElements(baseArgument, context) .map { when (it) { is PyTargetExpression -> getPydanticDynamicModelPyClass(it, context) @@ -342,7 +339,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { pydanticVersion: KotlinVersion?, config: HashMap, typed: Boolean = true, - isDataclass: Boolean = false + isDataclass: Boolean = false, ): PyCallableParameter? { if (!isValidField(field, context)) return null if (!hasAnnotationValue(field) && !field.hasAssignedValue()) return null // skip fields that are invalid syntax @@ -375,7 +372,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun fieldToParameter( field: PyExpression, context: TypeEvalContext, - typed: Boolean = true + typed: Boolean = true, ): PyCallableParameter { var type: PyType? = null var defaultValue: PyExpression? = null @@ -409,7 +406,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun getTypeForParameter( field: PyTargetExpression, - context: TypeEvalContext + context: TypeEvalContext, ): PyType? { return context.getType(field) @@ -420,7 +417,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { ellipsis: PyNoneLiteralExpression, context: TypeEvalContext, pydanticVersion: KotlinVersion?, - isDataclass: Boolean + isDataclass: Boolean, ): PyExpression? { val value = field.findAssignedValue() @@ -460,7 +457,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { ellipsis: PyNoneLiteralExpression, context: TypeEvalContext, pydanticVersion: KotlinVersion?, - isDataclass: Boolean + isDataclass: Boolean, ): PyExpression? { val assignedValue = field.findAssignedValue()!! @@ -514,15 +511,14 @@ class PydanticTypeProvider : PyTypeProviderBase() { private fun getDefaultValueForDataclass( assignedValue: PyCallExpression, context: TypeEvalContext, - argumentName: String + argumentName: String, ): PyExpression? { val defaultValue = assignedValue.getKeywordArgument(argumentName) return when { defaultValue == null -> null defaultValue.text == "..." -> null defaultValue is PyReferenceExpression -> { - val resolveResults = getResolveElements(defaultValue, context) - PyUtil.filterTopPriorityResults(resolveResults).any { isDataclassMissingByPsiElement(it) }.let { + getResolvedPsiElements(defaultValue, context).any { isDataclassMissingByPsiElement(it) }.let { return when { it -> null else -> defaultValue From 00b58b8f0c1b63ede6b918fc799b169b734c1a91 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 16 Apr 2021 04:00:56 +0900 Subject: [PATCH 5/5] Add unittest --- .../pydantic/PydanticDataclassTypeProvider.kt | 2 +- testData/mock/pydanticv18/dataclasses.py | 68 +++++++++- testData/typeinspectionv18/dataclass.py | 127 ++++++++++++++++++ .../pydantic/PydanticTypeInspectionV18Test.kt | 3 + 4 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 testData/typeinspectionv18/dataclass.py diff --git a/src/com/koxudaxi/pydantic/PydanticDataclassTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticDataclassTypeProvider.kt index dee56f3f..0125c7b6 100644 --- a/src/com/koxudaxi/pydantic/PydanticDataclassTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticDataclassTypeProvider.kt @@ -30,7 +30,7 @@ class PydanticDataclassTypeProvider : PyTypeProviderBase() { private fun getDataclassCallableType( referenceTarget: PsiElement, context: TypeEvalContext, - callSite: PyCallExpression? = null, + callSite: PyCallExpression?, ): PyCallableType? { return pyDataclassTypeProvider.getReferenceType( referenceTarget, diff --git a/testData/mock/pydanticv18/dataclasses.py b/testData/mock/pydanticv18/dataclasses.py index 092e1cd8..1a6a4053 100644 --- a/testData/mock/pydanticv18/dataclasses.py +++ b/testData/mock/pydanticv18/dataclasses.py @@ -1,2 +1,66 @@ -def dataclass(): - pass \ No newline at end of file +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, TypeVar, Union, overload + +if TYPE_CHECKING: + from .main import BaseConfig, BaseModel # noqa: F401 + + DataclassT = TypeVar('DataclassT', bound='Dataclass') + + class Dataclass: + __pydantic_model__: Type[BaseModel] + __initialised__: bool + __post_init_original__: Optional[Callable[..., None]] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + + def __call__(self: 'DataclassT', *args: Any, **kwargs: Any) -> 'DataclassT': + pass + + +@overload +def dataclass( + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: Type[Any] = None, +) -> Callable[[Type[Any]], Type['Dataclass']]: + ... + + +@overload +def dataclass( + _cls: Type[Any], + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: Type[Any] = None, +) -> Type['Dataclass']: + ... + + +def dataclass( + _cls: Optional[Type[Any]] = None, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: Type[Any] = None, +) -> Union[Callable[[Type[Any]], Type['Dataclass']], Type['Dataclass']]: + + def wrap(cls: Type[Any]) -> Type['Dataclass']: + pass + if _cls is None: + return wrap + + return wrap(_cls) diff --git a/testData/typeinspectionv18/dataclass.py b/testData/typeinspectionv18/dataclass.py new file mode 100644 index 00000000..994f6648 --- /dev/null +++ b/testData/typeinspectionv18/dataclass.py @@ -0,0 +1,127 @@ +from typing import * + +import pydantic + +@pydantic.dataclasses.dataclass +class MyDataclass: + a: str + b: int + + def func_a(self) -> str: + return self.a + + def func_b(self) -> int: + return self.b + + def func_c(self) -> int: + return self.a + + def func_d(self) -> str: + return self.b + +@pydantic.dataclasses.dataclass +class ChildDataclass(MyDataclass): + c: str + d: int + +MyDataclass(a='apple', b=1) +MyDataclass(a=2, b='orange') + +ChildDataclass(a='apple', b=1, c='berry', d=3) +ChildDataclass(a=2, b='orange', c=4, d='cherry') + + +a: MyDataclass = MyDataclass() +b: Type[MyDataclass] = MyDataclass + +c: MyDataclass = MyDataclass +d: Type[MyDataclass] = MyDataclass() + +aa: Union[str, MyDataclass] = MyDataclass() +bb: Union[str, Type[MyDataclass]] = MyDataclass + +cc: Union[str, MyDataclass] = MyDataclass +dd: Union[str, Type[MyDataclass]] = MyDataclass() + +aaa: ChildDataclass = ChildDataclass() +bbb: Type[ChildDataclass] = ChildDataclass + +ccc: ChildDataclass = ChildDataclass +ddd: Type[ChildDataclass] = ChildDataclass() + + +e: str = MyDataclass(a='apple', b=1).a +f: int = MyDataclass(a='apple', b=1).b + +g: int = MyDataclass(a='apple', b=1).a +h: str = MyDataclass(a='apple', b=1).b + + +ee: str = ChildDataclass(a='apple', b=1, c='orange', d=2).a +ff: int = ChildDataclass(a='apple', b=1, c='orange', d=2).d + +gg: int = ChildDataclass(a='apple', b=1, c='orange', d=2).a +hh: str = ChildDataclass(a='apple', b=1, c='orange', d=2).d + +i: MyDataclass = MyDataclass(a='apple', b=1) +j: str = i.a +k: int = i.b + +l: int = i.a +m: str = i.b + + +ii: ChildDataclass = ChildDataclass(a='apple', b=1, c='orange', d=2) +jj: str = i.a +kk: int = i.d + +ll: int = ii.a +mm: str = ii.d + +def my_fn_1() -> MyDataclass: + return MyDataclass() + +def my_fn_2() -> Type[MyDataclass]: + return MyDataclass + +def my_fn_3() -> MyDataclass: + return MyDataclass + +def my_fn_4() -> Type[MyDataclass]: + return MyDataclass() + +def my_fn_5() -> Union[str, MyDataclass]: + return MyDataclass() + +def my_fn_6() -> Type[str, MyDataclass]: + return MyDataclass + +def my_fn_7() -> Union[str, MyDataclass]: + return MyDataclass + +def my_fn_8() -> Union[str, Type[MyDataclass]]: + return MyDataclass() + +def my_fn_9() -> ChildDataclass: + return ChildDataclass() + +def my_fn_10() -> Type[ChildDataclass]: + return ChildDataclass + +def my_fn_11() -> ChildDataclass: + return ChildDataclass + +def my_fn_12() -> Type[ChildDataclass]: + return ChildDataclass() + +def my_fn_13() -> Union[str, ChildDataclass]: + return ChildDataclass() + +def my_fn_14() -> Type[str, ChildDataclass]: + return ChildDataclass + +def my_fn_7() -> Union[str, ChildDataclass]: + return ChildDataclass + +def my_fn_8() -> Union[str, Type[ChildDataclass]]: + return ChildDataclass() diff --git a/testSrc/com/koxudaxi/pydantic/PydanticTypeInspectionV18Test.kt b/testSrc/com/koxudaxi/pydantic/PydanticTypeInspectionV18Test.kt index eaca8cdd..2c502686 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticTypeInspectionV18Test.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticTypeInspectionV18Test.kt @@ -12,4 +12,7 @@ open class PydanticTypeInspectionV18Test : PydanticInspectionBase("v18") { fun testDynamicModel() { doTest() } + fun testDataclass() { + doTest() + } } \ No newline at end of file