Skip to content

Commit

Permalink
Fix type-map edge case for parsable-type and acceptable-type (#118)
Browse files Browse the repository at this point in the history
* fix type map

* fix logic to get qualifiedName

* update history

* fix qualifiedName type

* fix indexed file error

* fix unittest
  • Loading branch information
koxudaxi authored Apr 29, 2020
1 parent 47a2abd commit 2430460
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 48 deletions.
7 changes: 6 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.4</version>
<version>0.1.5</version>
<vendor email="koaxudai@gmail.com">Koudai Aono @koxudaxi</vendor>
<change-notes><![CDATA[
<h2>version 0.1.5</h2>
<p>BugFixes<p>
<ul>
<li>Fix type-map edge case for parsable-type and acceptable-type [#118]</li>
</ul>
<h2>version 0.1.4</h2>
<p>BugFixes<p>
<ul>
Expand Down
69 changes: 45 additions & 24 deletions src/com/koxudaxi/pydantic/PydanticInitializer.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.koxudaxi.pydantic

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.startup.StartupActivity
Expand All @@ -11,14 +12,16 @@ import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
import com.intellij.openapi.vfs.newvfs.events.VFileCopyEvent
import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent
import com.intellij.psi.util.QualifiedName
import com.jetbrains.python.psi.PyQualifiedNameOwner
import com.jetbrains.python.psi.types.TypeEvalContext
import org.apache.tuweni.toml.Toml
import org.apache.tuweni.toml.TomlArray
import org.apache.tuweni.toml.TomlParseResult
import org.apache.tuweni.toml.TomlTable
import org.ini4j.Ini
import org.ini4j.IniPreferences
import java.io.StringReader

import com.intellij.openapi.project.NoAccessDuringPsiEvents

class PydanticInitializer : StartupActivity {

Expand All @@ -33,18 +36,20 @@ class PydanticInitializer : StartupActivity {
private fun initializeFileLoader(project: Project, configService: PydanticConfigService) {
val defaultPyProjectToml = getDefaultPyProjectTomlPath(project)
val defaultMypyIni = getDefaultMypyIniPath(project)
when (val pyprojectToml = LocalFileSystem.getInstance()
.findFileByPath(configService.pyprojectToml ?: defaultPyProjectToml)
) {
is VirtualFile -> loadPyprojecToml(pyprojectToml, configService)
else -> clearPyProjectTomlConfig(configService)
}
invokeAfterPsiEvents {
when (val pyprojectToml = LocalFileSystem.getInstance()
.findFileByPath(configService.pyprojectToml ?: defaultPyProjectToml)
) {
is VirtualFile -> loadPyprojecToml(project, pyprojectToml, configService)
else -> clearPyProjectTomlConfig(configService)
}

when (val mypyIni = LocalFileSystem.getInstance()
.findFileByPath(configService.mypyIni ?: defaultMypyIni)
) {
is VirtualFile -> loadMypyIni(mypyIni, configService)
else -> clearMypyIniConfig(configService)
when (val mypyIni = LocalFileSystem.getInstance()
.findFileByPath(configService.mypyIni ?: defaultMypyIni)
) {
is VirtualFile -> loadMypyIni(mypyIni, configService)
else -> clearMypyIniConfig(configService)
}
}
VirtualFileManager.getInstance().addAsyncFileListener(
{ events ->
Expand All @@ -61,10 +66,12 @@ class PydanticInitializer : StartupActivity {
if (projectFiles.count() == 0) return
val pyprojectToml = configService.pyprojectToml ?: defaultPyProjectToml
val mypyIni = configService.mypyIni ?: defaultMypyIni
projectFiles.forEach {
when (it.path) {
pyprojectToml -> loadPyprojecToml(it, configService)
mypyIni -> loadMypyIni(it, configService)
invokeAfterPsiEvents {
projectFiles.forEach {
when (it.path) {
pyprojectToml -> loadPyprojecToml(project, it, configService)
mypyIni -> loadMypyIni(it, configService)
}
}
}
}
Expand Down Expand Up @@ -106,7 +113,7 @@ class PydanticInitializer : StartupActivity {
}
}

private fun loadPyprojecToml(config: VirtualFile, configService: PydanticConfigService) {
private fun loadPyprojecToml(project: Project, config: VirtualFile, configService: PydanticConfigService) {
val result: TomlParseResult = Toml.parse(config.inputStream)

val table = result.getTableOrEmpty("tool.pydantic-pycharm-plugin")
Expand All @@ -115,8 +122,9 @@ class PydanticInitializer : StartupActivity {
return
}

configService.parsableTypeMap = getTypeMap("parsable-types", table)
configService.acceptableTypeMap = getTypeMap("acceptable-types", table)
val context = TypeEvalContext.codeAnalysis(project, null)
configService.parsableTypeMap = getTypeMap(project, "parsable-types", table, context)
configService.acceptableTypeMap = getTypeMap(project, "acceptable-types", table, context)

configService.parsableTypeHighlightType = getHighlightLevel(table, "parsable-type-highlight", ProblemHighlightType.WARNING)
configService.acceptableTypeHighlightType = getHighlightLevel(table, "acceptable-type-highlight", ProblemHighlightType.WEAK_WARNING)
Expand All @@ -139,17 +147,20 @@ class PydanticInitializer : StartupActivity {
}
}

private fun getTypeMap(path: String, table: TomlTable): MutableMap<String, List<String>> {

private fun getTypeMap(project: Project, path: String, table: TomlTable, context: TypeEvalContext): MutableMap<String, List<String>> {
val temporaryTypeMap = mutableMapOf<String, List<String>>()

val parsableTypeTable = table.getTableOrEmpty(path).toMap()
parsableTypeTable.entries.forEach { (key, value) ->
val name = when (val psiElement = getPsiElementByQualifiedName(QualifiedName.fromDottedString(key), project, context)) {
is PyQualifiedNameOwner -> psiElement.qualifiedName!!
else -> key
}
run {
if (value is TomlArray) {
value.toList().filterIsInstance<String>().let {
if (it.isNotEmpty()) {
temporaryTypeMap[key] = it
temporaryTypeMap[name] = it
}
}
}
Expand All @@ -162,4 +173,14 @@ class PydanticInitializer : StartupActivity {
val configService = PydanticConfigService.getInstance(project)
initializeFileLoader(project, configService)
}
}

private fun invokeAfterPsiEvents(runnable: () -> Unit) {
val wrapper = {
when {
NoAccessDuringPsiEvents.isInsideEventProcessing() -> invokeAfterPsiEvents(runnable)
else -> runnable()
}
}
ApplicationManager.getApplication().invokeLater(wrapper, {false})
}
}
2 changes: 1 addition & 1 deletion testData/initializer/pyprojecttoml/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ acceptable-type-highlight = "warning"
[tool.pydantic-pycharm-plugin.parsable-types]
## datetime.datetime field may parse int
"datetime.datetime" = ["int"]

"pydantic.HttpUrl" = ["str"]
[tool.pydantic-pycharm-plugin.acceptable-types]
# str field accepts to parse int and float
str = ["int", "float"]
3 changes: 2 additions & 1 deletion testData/mock/pydanticv1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .main import BaseModel, BaseConfig
from .class_validators import validator, root_validator
from .fields import Field, Schema
from .env_settings import BaseSettings
from .env_settings import BaseSettings
from .networks import *
6 changes: 6 additions & 0 deletions testData/mock/pydanticv1/networks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from builtins import str

class AnyUrl(str):
pass
class HttpUrl(AnyUrl):
pass
52 changes: 31 additions & 21 deletions testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt
Original file line number Diff line number Diff line change
@@ -1,44 +1,50 @@
package com.koxudaxi.pydantic

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry
import com.intellij.openapi.application.ApplicationManager
import com.intellij.testFramework.PsiTestUtil
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.testFramework.waitForProjectLeakingThreads
import java.util.concurrent.TimeUnit

open class PydanticInitializerTest : PydanticTestCase() {
lateinit var pydanticConfigService: PydanticConfigService
lateinit var testMethodName: String
override fun setUp() {
super.setUp()
pydanticConfigService = PydanticConfigService.getInstance(myFixture!!.project)
testMethodName = getTestName(true)
private fun setUpConfig() {
this.pydanticConfigService = PydanticConfigService.getInstance(myFixture!!.project)
this.testMethodName = getTestName(true)
}


private fun setUpPyProjectToml(runnable: () -> Unit) {
pydanticConfigService.pyprojectToml = "/src/${testMethodName}"
val pyProjectToml = myFixture!!.copyFileToProject("${testDataMethodPath}/pyproject.toml", testMethodName)
try {
runnable()
} finally {
PsiTestUtil.removeSourceRoot(myFixture!!.module, pyProjectToml)
ApplicationManager.getApplication().executeOnPooledThread {
setUpConfig()
pydanticConfigService.pyprojectToml = "/src/${testMethodName}"
val pyProjectToml = myFixture!!.copyFileToProject("${testDataMethodPath}/pyproject.toml", testMethodName)
try {
runnable()
} finally {
PsiTestUtil.removeSourceRoot(myFixture!!.module, pyProjectToml)
}
}
}

private fun setUpMypyIni(runnable: () -> Unit) {
pydanticConfigService.mypyIni = "/src/${testMethodName}"
val mypyIni = myFixture!!.copyFileToProject("${testDataMethodPath}/mypy.ini", testMethodName)
try {
runnable()
} finally {
PsiTestUtil.removeSourceRoot(myFixture!!.module, mypyIni)
ApplicationManager.getApplication().executeOnPooledThread {
setUpConfig()
pydanticConfigService.mypyIni = "/src/${testMethodName}"
val mypyIni = myFixture!!.copyFileToProject("${testDataMethodPath}/mypy.ini", testMethodName)
try {
runnable()
} finally {
PsiTestUtil.removeSourceRoot(myFixture!!.module, mypyIni)
}
}
}

fun testpyprojecttoml() {
setUpPyProjectToml {
assertEquals(this.pydanticConfigService.parsableTypeMap, mutableMapOf("datetime.datetime" to listOf("int")))
assertEquals(this.pydanticConfigService.parsableTypeMap, mutableMapOf(
"datetime.datetime" to listOf("int"),
"pydantic.networks.HttpUrl" to listOf("str")
))
assertEquals(this.pydanticConfigService.acceptableTypeMap, mutableMapOf("str" to listOf("int", "float")))
assertEquals(this.pydanticConfigService.parsableTypeHighlightType, ProblemHighlightType.WEAK_WARNING)
assertEquals(this.pydanticConfigService.acceptableTypeHighlightType, ProblemHighlightType.WARNING)
Expand All @@ -51,6 +57,7 @@ open class PydanticInitializerTest : PydanticTestCase() {
assertEquals(this.pydanticConfigService.acceptableTypeHighlightType, ProblemHighlightType.INFORMATION)
}
}

fun testpyprojecttomlempty() {
setUpPyProjectToml {
assertEquals(this.pydanticConfigService.parsableTypeMap, mutableMapOf<String, List<String>>())
Expand All @@ -61,6 +68,7 @@ open class PydanticInitializerTest : PydanticTestCase() {
}

fun testnothingpyprojecttoml() {
setUpConfig()
assertEquals(this.pydanticConfigService.parsableTypeMap, mutableMapOf<String, List<String>>())
assertEquals(this.pydanticConfigService.acceptableTypeMap, mutableMapOf<String, List<String>>())
assertEquals(this.pydanticConfigService.parsableTypeHighlightType, ProblemHighlightType.WARNING)
Expand Down Expand Up @@ -93,7 +101,9 @@ open class PydanticInitializerTest : PydanticTestCase() {
assertEquals(this.pydanticConfigService.currentWarnUntypedFields, false)
}
}

fun testnothingmypyini() {
setUpConfig()
assertEquals(this.pydanticConfigService.mypyWarnUntypedFields, null)
assertEquals(this.pydanticConfigService.mypyInitTyped, null)
assertEquals(this.pydanticConfigService.currentInitTyped, true)
Expand Down

0 comments on commit 2430460

Please sign in to comment.