Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support acceptable type #104

Merged
merged 12 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* Support same features as `pydantic.BaseModel`
* (After PyCharm 2020.1 and this plugin version 0.1.0, PyCharm treats `pydantic.dataclasses.dataclass` as third-party dataclass.)

### Inspection for type-checking
### Inspection for type-checking (Experimental)
**In version 0.1.1, This feature is broken. Please use it in [0.1.2](https://github.com/koxudaxi/pydantic-pycharm-plugin/releases/tag/0.1.2) or later.**

This plugin provides an inspection for type-checking, which is compatible with pydantic.
Expand All @@ -36,7 +36,7 @@ Don't use this type checker with a builtin type checker same time.

![inspection 1](https://raw.githubusercontent.com/koxudaxi/pydantic-pycharm-plugin/master/docs/inspection1.png)

### Parsable Type
### Parsable Type (Experimental)
Pydantic has lots of support for coercing types. However, PyCharm gives a message saying only `Expected type "x," got "y" instead:`
When you set parsable-type on a type, then the message will be changed to `Field is of type "x", "y" may not be parsable to "x"`

Expand All @@ -56,6 +56,48 @@ str = ["int", "float"]

# datetime.datetime field may parse int
"datetime.datetime" = [ "int" ]

# your_module.your_type field may parse str
"your_module.your_type" = [ "str" ]

[tool.pydantic-pycharm-plugin]
# You can set higlith level (default is "warning")
# You can select it from "warning", "weak_warning", "disable"
parsable-type-highlight = "warning"

## If you set acceptable-type-highlight then, you have to set it at same depth.
acceptable-type-highlight = "disable"
```

### Acceptable Type (Experimental)
**This feature is in version [0.1.3](https://github.com/koxudaxi/pydantic-pycharm-plugin/releases/tag/0.1.3) or later.**

Pydantic can always parse a few types to other types. For example, `int` to `str`. It always succeeds.
You can set it as an acceptable type. The message is `Field is of type 'x', 'y' is set as an acceptable type in pyproject.toml`.
Also,You may want to disable the message.You can do it, by setting "disable" on `acceptable-type-highlight`.

#### Set acceptable-type in pyproject.toml
You should create `pyproject.toml` in your project root.
And, you define acceptable-type like a example.

exapmle:

```toml
[tool.pydantic-pycharm-plugin.acceptable-types]

# str field accepts to parse int and float
str = ["int", "float"]

# datetime.datetime field may parse int
"datetime.datetime" = [ "int" ]

[tool.pydantic-pycharm-plugin]
# You can set higlith level (default is "weak_warning")
# You can select it from "warning", "weak_warning", "disable"
acceptable-type-highlight = "disable"

# If you set parsable-type-highlight then, you have to set it at same depth.
parsable-type-highlight = "warning"
```

#### Related issues
Expand Down
1 change: 1 addition & 0 deletions resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<h2>version 0.1.3</h2>
<p>Features</p>
<ul>
<li>Support acceptable type [#104] </li>
<li>Support parsable type highlight level [#103] </li>
</ul>
<h2>version 0.1.2</h2>
Expand Down
3 changes: 3 additions & 0 deletions src/com/koxudaxi/pydantic/PydanticConfigService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class PydanticConfigService : PersistentStateComponent<PydanticConfigService> {
var pyprojectToml: String? = null
var parsableTypeMap = mutableMapOf<String, List<String>>()
var parsableTypeHighlightType: ProblemHighlightType = ProblemHighlightType.WARNING
var acceptableTypeMap = mutableMapOf<String, List<String>>()
var acceptableTypeHighlightType: ProblemHighlightType = ProblemHighlightType.WEAK_WARNING

override fun getState(): PydanticConfigService {
return this
}
Expand Down
32 changes: 26 additions & 6 deletions src/com/koxudaxi/pydantic/PydanticInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,33 +50,53 @@ class PydanticInitializer : StartupActivity {
if (configFile is VirtualFile) {
loadPyprojecToml(configFile, configService)
} else {
configService.parsableTypeMap.clear()
configService.parsableTypeHighlightType = ProblemHighlightType.WARNING
clear(configService)
}
}

private fun clear(configService: PydanticConfigService) {
configService.parsableTypeMap.clear()
configService.acceptableTypeMap.clear()
configService.parsableTypeHighlightType = ProblemHighlightType.WARNING
configService.acceptableTypeHighlightType = ProblemHighlightType.WEAK_WARNING
}

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

val table = result.getTableOrEmpty("tool.pydantic-pycharm-plugin") ?: return
val table = result.getTableOrEmpty("tool.pydantic-pycharm-plugin")
if (table.isEmpty) {
clear(configService)
return
}

val temporaryParsableTypeMap = getTypeMap("parsable-types", table)
if (configService.parsableTypeMap != temporaryParsableTypeMap) {
configService.parsableTypeMap = temporaryParsableTypeMap
}

configService.parsableTypeHighlightType = getHighlightLevel(table, "parsable-type-highlight")
val temporaryAcceptableTypeMap = getTypeMap("acceptable-types", table)
if (configService.acceptableTypeMap != temporaryAcceptableTypeMap) {
configService.acceptableTypeMap = temporaryAcceptableTypeMap
}

configService.parsableTypeHighlightType = getHighlightLevel(table, "parsable-type-highlight", ProblemHighlightType.WARNING)
configService.acceptableTypeHighlightType = getHighlightLevel(table, "acceptable-type-highlight", ProblemHighlightType.WEAK_WARNING)
}

private fun getHighlightLevel(table: TomlTable, path: String): ProblemHighlightType {
private fun getHighlightLevel(table: TomlTable, path: String, default: ProblemHighlightType): ProblemHighlightType {
return when (table.get(path) as? String) {
"warning" -> {
ProblemHighlightType.WARNING
}
"weak_warning" -> {
ProblemHighlightType.WEAK_WARNING
}
"disable" -> {
ProblemHighlightType.INFORMATION
}
else -> {
ProblemHighlightType.WARNING
default
}
}
}
Expand Down
96 changes: 68 additions & 28 deletions src/com/koxudaxi/pydantic/PydanticTypeCheckerInspection.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.koxudaxi.pydantic

import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.python.codeInsight.typing.matchingProtocolDefinitions
Expand Down Expand Up @@ -45,52 +45,92 @@ class PydanticTypeCheckerInspection : PyTypeCheckerInspection() {
.forEach { mapping: PyArgumentsMapping -> analyzeCallee(callSite, mapping) }
}

private fun getParsableTypeFromTypeMap(typeForParameter: PyType, cache: MutableMap<PyType, PyType?>): PyType? {
return getTypeFromTypeMap(
{ project: Project -> PydanticConfigService.getInstance(project).parsableTypeMap },
typeForParameter,
cache
)
}

private fun getParsableType(typeForParameter: PyType): PyType? {
val pyTypes: MutableSet<PyType> = mutableSetOf(typeForParameter)
val project = holder!!.project
getPyClassTypeByPyTypes(typeForParameter).toSet().forEach { type ->
val classQName: String? = type.classQName
private fun getAcceptableTypeFromTypeMap(typeForParameter: PyType, cache: MutableMap<PyType, PyType?>): PyType? {
return getTypeFromTypeMap(
{ project: Project -> PydanticConfigService.getInstance(project).acceptableTypeMap },
typeForParameter,
cache
)
}

PydanticConfigService.getInstance(project).parsableTypeMap[classQName]?.mapNotNull {
createPyClassTypeImpl(it, project, myTypeEvalContext)
?: createPyClassTypeImpl(it, project, myTypeEvalContext)
}?.toCollection(pyTypes)
private fun getTypeFromTypeMap(getTypeMap: (project: Project) -> (MutableMap<String, List<String>>), typeForParameter: PyType, cache: MutableMap<PyType, PyType?>): PyType? {
return when {
cache.containsKey(typeForParameter) -> {
cache[typeForParameter]
}
else -> {
val project = holder!!.project
val typeMap = getTypeMap(project)
val unionType = PyUnionType.union(getPyClassTypeByPyTypes(typeForParameter).toSet().flatMap { type ->
typeMap[type.classQName]?.mapNotNull {
createPyClassTypeImpl(it, project, myTypeEvalContext)
} as? List<PyType> ?: listOf()
})
cache[typeForParameter] = unionType
unionType
}
}
return PyUnionType.union(pyTypes)
}

private fun analyzeCallee(callSite: PyCallSiteExpression, mapping: PyArgumentsMapping) {
val callableType = mapping.callableType ?: return
val receiver = callSite.getReceiver(callableType.callable)
val substitutions = PyTypeChecker.unifyReceiver(receiver, myTypeEvalContext)
val mappedParameters = mapping.mappedParameters
val cachedParsableTypeMap = mutableMapOf<PyType, PyType?>()
val cachedAcceptableTypeMap = mutableMapOf<PyType, PyType?>()
for ((argument, parameter) in PyCallExpressionHelper.getRegularMappedParameters(mappedParameters)) {
val expected = parameter.getArgumentType(myTypeEvalContext)
val parsableType = expected?.let { getParsableType(it) }
val actual = promoteToLiteral(argument, expected, myTypeEvalContext)
val strictMatched = matchParameterAndArgument(expected, actual, argument, substitutions)
val strictResult = AnalyzeArgumentResult(argument, expected, substituteGenerics(expected, substitutions), actual, strictMatched)
if (!strictResult.isMatched) {
val expectedType = PythonDocumentationProvider.getTypeName(strictResult.expectedType, myTypeEvalContext)
val actualType = PythonDocumentationProvider.getTypeName(strictResult.actualType, myTypeEvalContext)
val parsableMatched = matchParameterAndArgument(parsableType, actual, argument, substitutions)
val parsableResult = AnalyzeArgumentResult(argument, parsableType, substituteGenerics(parsableType, substitutions), actual, parsableMatched)
if (parsableResult.isMatched) {
registerProblem(
argument,
String.format("Field is of type '%s', '%s' may not be parsable to '%s'",
expectedType,
actualType,
expectedType),
pydanticConfigService.parsableTypeHighlightType
)
} else {
registerProblem(argument, String.format("Expected type '%s', got '%s' instead",
expectedType,
actualType)
)
if (expected is PyType) {
val parsableType = getParsableTypeFromTypeMap(expected, cachedParsableTypeMap)
if (parsableType != null) {
val parsableMatched = matchParameterAndArgument(parsableType, actual, argument, substitutions)
if (AnalyzeArgumentResult(argument, parsableType, substituteGenerics(parsableType, substitutions), actual, parsableMatched).isMatched) {
registerProblem(
argument,
String.format("Field is of type '%s', '%s' may not be parsable to '%s'",
expectedType,
actualType,
expectedType),
pydanticConfigService.parsableTypeHighlightType
)
continue
}
}
val acceptableType = getAcceptableTypeFromTypeMap(expected, cachedAcceptableTypeMap)
if (acceptableType != null) {
val acceptableMatched = matchParameterAndArgument(acceptableType, actual, argument, substitutions)
if (AnalyzeArgumentResult(argument, acceptableType, substituteGenerics(acceptableType, substitutions), actual, acceptableMatched).isMatched) {
registerProblem(
argument,
String.format("Field is of type '%s', '%s' is set as an acceptable type in pyproject.toml",
expectedType,
actualType,
expectedType),
pydanticConfigService.acceptableTypeHighlightType
)
continue
}
}
}
registerProblem(argument, String.format("Expected type '%s', got '%s' instead",
expectedType,
actualType)
)
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions testData/typecheckerinspection/acceptableType.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from builtins import *
from typing import Union

from pydantic import BaseModel


class A(BaseModel):
a: str


A(a=str('123'))
A(<weak_warning descr="Field is of type 'str', 'int' is set as an acceptable type in pyproject.toml">a=int(123)</weak_warning>)
12 changes: 12 additions & 0 deletions testData/typecheckerinspection/acceptableTypeDisable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from builtins import *
from typing import Union

from pydantic import BaseModel


class A(BaseModel):
a: str
b: str

A(a=str('123'), b=str('123'))
A(a=int(123), b=int(123))
11 changes: 11 additions & 0 deletions testData/typecheckerinspection/acceptableTypeInvalid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from builtins import *
from typing import Union

from pydantic import BaseModel


class A(BaseModel):
a: str

A(a=str('123'))
A(<warning descr="Expected type 'str', got 'bytes' instead">a=bytes(123)</warning>)
12 changes: 12 additions & 0 deletions testData/typecheckerinspection/acceptableTypeWarning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from builtins import *
from typing import Union

from pydantic import BaseModel


class A(BaseModel):
a: str


A(a=str('123'))
A(<warning descr="Field is of type 'str', 'int' is set as an acceptable type in pyproject.toml">a=int(123)</warning>)
6 changes: 3 additions & 3 deletions testData/typecheckerinspection/parsableTypeDisable.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

class A(BaseModel):
a: str
b: str


A(a=str('123'))
A(a=int(123))
A(a=str('123'), b=str('123'))
A(a=int(123), b=int(123))
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,33 @@ open class PydanticTypeCheckerInspectionTest : PydanticInspectionBase() {

fun testParsableTypeInvalid() {
val pydanticConfigService = PydanticConfigService.getInstance(myFixture!!.project)
pydanticConfigService.parsableTypeMap["builtins.str"] = arrayListOf("int")
pydanticConfigService.parsableTypeMap["builtins.str"] = arrayListOf("builtins.int")
doTest()
}

fun testAcceptableType() {
val pydanticConfigService = PydanticConfigService.getInstance(myFixture!!.project)
pydanticConfigService.acceptableTypeMap["builtins.str"] = arrayListOf("builtins.int")
doTest()
}
fun testAcceptableTypeWarning() {
val pydanticConfigService = PydanticConfigService.getInstance(myFixture!!.project)
pydanticConfigService.acceptableTypeMap["builtins.str"] = arrayListOf("builtins.int")
pydanticConfigService.acceptableTypeHighlightType = ProblemHighlightType.WARNING
doTest()
}
fun testAcceptableTypeDisable() {
val pydanticConfigService = PydanticConfigService.getInstance(myFixture!!.project)
pydanticConfigService.acceptableTypeMap["builtins.str"] = arrayListOf("builtins.int")
pydanticConfigService.acceptableTypeHighlightType = ProblemHighlightType.INFORMATION
doTest()
}

fun testAcceptableTypeInvalid() {
val pydanticConfigService = PydanticConfigService.getInstance(myFixture!!.project)
pydanticConfigService.acceptableTypeMap["builtins.str"] = arrayListOf("builtins.int")
doTest()
}
fun testField() {
doTest()
}
Expand Down