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

Fix type-map edge case for parsable-type and acceptable-type #118

Merged
merged 6 commits into from
Apr 29, 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
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