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

add config panel #92

Merged
merged 6 commits into from
Dec 15, 2019
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
9 changes: 8 additions & 1 deletion resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
<vendor email="koaxudai@gmail.com">Koudai Aono @koxudaxi</vendor>
<change-notes><![CDATA[
<h2>version 0.0.29</h2>
<p>Features, BugFixes</p>
<p>Features</p>
<ul>
<li>Inspect untyped fields [#93] </li>
<li>Add config panel [#92] </li>
</ul>
<h2>version 0.0.28</h2>
<p>Features, BugFixes</p>
Expand Down Expand Up @@ -152,10 +153,16 @@
implementationClass="com.koxudaxi.pydantic.PydanticCompletionContributor"/>
<typedHandler implementation="com.koxudaxi.pydantic.PydanticTypedValidatorMethodHandler"
id="pydanticTypedValidatorMethodHandler" order="before pyMethodNameTypedHandler"/>
<projectService
serviceImplementation="com.koxudaxi.pydantic.PydanticConfigService"/>

<projectConfigurable groupId="tools" instance="com.koxudaxi.pydantic.PydanticConfigurable"/>

</extensions>
<extensions defaultExtensionNs="Pythonid">
<typeProvider implementation="com.koxudaxi.pydantic.PydanticTypeProvider"/>
<inspectionExtension implementation="com.koxudaxi.pydantic.PydanticIgnoreInspection"/>

</extensions>

</idea-plugin>
Expand Down
74 changes: 74 additions & 0 deletions src/com/koxudaxi/pydantic/PydanticConfigPanel.form
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.koxudaxi.pydantic.PydanticConfigPanel">
<grid id="27dc6" binding="configPanel" default-binding="true" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="360" y="153" width="2880" height="5535"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="da542" layout-manager="GridLayoutManager" row-count="5" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="1a909" class="javax.swing.JCheckBox" binding="initTypedCheckBox" default-binding="true">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Type-checking for init [init_typed]"/>
</properties>
</component>
<hspacer id="18091">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<component id="2573b" class="javax.swing.JTextPane" binding="ifEnabledIncludeTheTextPane" default-binding="true">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="7" hsize-policy="6" anchor="1" fill="1" indent="3" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<editable value="false"/>
<opaque value="false"/>
<text value="If enabled, include the field types as type hints in the generated signature for the __init__ method. This means that you'll get errors if you pass an argument that is not already the right type to __init__, even if parsing could safely convert the type."/>
</properties>
</component>
<component id="59fe" class="javax.swing.JCheckBox" binding="warnUntypedFieldsCheckBox">
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<borderPaintedFlat value="true"/>
<text value="Warning untyped fields [warn_untyped_fields]"/>
</properties>
</component>
<component id="d96bd" class="javax.swing.JTextPane" binding="ifEnabledRaiseATextPane" default-binding="true">
<constraints>
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="7" hsize-policy="6" anchor="0" fill="3" indent="3" use-parent-layout="false">
<preferred-size width="150" height="50"/>
</grid>
</constraints>
<properties>
<editable value="false"/>
<opaque value="false"/>
<text value="If enabled, raise a error whenever a field is declared on a model without explicitly specifying its type."/>
</properties>
</component>
</children>
</grid>
<vspacer id="a33f9">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>
35 changes: 35 additions & 0 deletions src/com/koxudaxi/pydantic/PydanticConfigPanel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.koxudaxi.pydantic;

import com.intellij.openapi.project.Project;

import javax.swing.*;

public class PydanticConfigPanel {

PydanticConfigPanel(Project project) {
PydanticConfigService pydanticConfigService = PydanticConfigService.Companion.getInstance(project);

this.initTypedCheckBox.setSelected(pydanticConfigService.getInitTyped());
this.warnUntypedFieldsCheckBox.setSelected(pydanticConfigService.getWarnUntypedFields());

}

private JPanel configPanel;
private JCheckBox initTypedCheckBox;
private JTextPane ifEnabledIncludeTheTextPane;
private JCheckBox warnUntypedFieldsCheckBox;
private JTextPane ifEnabledRaiseATextPane;

public Boolean getInitTyped() {
return initTypedCheckBox.isSelected();
}

public Boolean getWarnUntypedFields() {
return warnUntypedFieldsCheckBox.isSelected();
}

public JPanel getConfigPanel() {
return configPanel;
}

}
29 changes: 29 additions & 0 deletions src/com/koxudaxi/pydantic/PydanticConfigService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.koxudaxi.pydantic

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.XmlSerializerUtil

@State(name = "PydanticConfigService", storages = [Storage("pydantic.xml")])
class PydanticConfigService : PersistentStateComponent<PydanticConfigService> {
var initTyped = true
var warnUntypedFields = false

override fun getState(): PydanticConfigService {
return this
}

override fun loadState(config: PydanticConfigService) {
XmlSerializerUtil.copyBean(config, this)
}

companion object {
fun getInstance(project: Project?): PydanticConfigService {
return ServiceManager.getService(project!!, PydanticConfigService::class.java)
}
}

}
39 changes: 39 additions & 0 deletions src/com/koxudaxi/pydantic/PydanticConfigurable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.koxudaxi.pydantic

import com.intellij.openapi.options.Configurable
import com.intellij.openapi.project.Project
import javax.swing.JComponent


class PydanticConfigurable internal constructor(project: Project) : Configurable {
private val pydanticConfigService: PydanticConfigService = PydanticConfigService.getInstance(project)
private val configPanel: PydanticConfigPanel = PydanticConfigPanel(project)
override fun getDisplayName(): String {
return "Pydantic"
}

override fun getHelpTopic(): String? {
return null
}

override fun createComponent(): JComponent? {
reset()
return configPanel.configPanel
}

override fun reset() {}

override fun isModified(): Boolean {
if (configPanel.initTyped == null || configPanel.warnUntypedFields == null) return false
return (pydanticConfigService.initTyped != configPanel.initTyped) ||
(pydanticConfigService.warnUntypedFields != configPanel.warnUntypedFields)
}

override fun apply() {
pydanticConfigService.initTyped = configPanel.initTyped
pydanticConfigService.warnUntypedFields = configPanel.warnUntypedFields
}

override fun disposeUIResources() {
}
}
18 changes: 8 additions & 10 deletions src/com/koxudaxi/pydantic/PydanticInspection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,16 @@ import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.PyClassTypeImpl
import javax.swing.JComponent

var defaultWarnUntypedFields = false

class PydanticInspection : PyInspection() {
var warnUntypedFields = defaultWarnUntypedFields

override fun buildVisitor(holder: ProblemsHolder,
isOnTheFly: Boolean,
session: LocalInspectionToolSession): PsiElementVisitor = Visitor(holder, session)

inner class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) {

val pydanticConfigService = PydanticConfigService.getInstance(holder.project)

override fun visitPyFunction(node: PyFunction?) {
super.visitPyFunction(node)

Expand Down Expand Up @@ -64,7 +63,7 @@ class PydanticInspection : PyInspection() {
super.visitPyAssignmentStatement(node)

if (node == null) return
if (this@PydanticInspection.warnUntypedFields) {
if (pydanticConfigService.warnUntypedFields) {
inspectWarnUntypedFields(node)
}
inspectReadOnlyProperty(node)
Expand Down Expand Up @@ -121,16 +120,15 @@ class PydanticInspection : PyInspection() {
val pyClass = getPyClassByAttribute(node) ?: return
if (!isPydanticModel(pyClass, myTypeEvalContext)) return
if (node.annotation != null) return

registerProblem(node,
"Untyped fields disallowed", ProblemHighlightType.WARNING)

}
}

override fun createOptionsPanel(): JComponent? {
val panel = MultipleCheckboxOptionsPanel(this)
panel.addCheckbox( "Warning untyped fields", "warnUntypedFields")
return panel
}
// override fun createOptionsPanel(): JComponent? {
// val panel = MultipleCheckboxOptionsPanel(this)
// panel.addCheckbox( "Warning untyped fields", "warnUntypedFields")
// return panel
// }
}
32 changes: 19 additions & 13 deletions src/com/koxudaxi/pydantic/PydanticTypeProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import com.jetbrains.python.psi.impl.PyCallExpressionImpl
import com.jetbrains.python.psi.impl.PyCallExpressionNavigator
import com.jetbrains.python.psi.impl.PySubscriptionExpressionImpl
import com.jetbrains.python.psi.types.*
import com.koxudaxi.pydantic.PydanticConfigService.Companion.getInstance
import one.util.streamex.StreamEx

class PydanticTypeProvider : PyTypeProviderBase() {


override fun getReferenceExpressionType(referenceExpression: PyReferenceExpression, context: TypeEvalContext): PyType? {
return getPydanticTypeForCallee(referenceExpression, context)
}
Expand Down Expand Up @@ -77,7 +77,7 @@ class PydanticTypeProvider : PyTypeProviderBase() {
.asSequence()
.map {
when {
it is PyClass -> getPydanticTypeForClass(it, context)
it is PyClass -> getPydanticTypeForClass(it, context, true)
it is PyParameter && it.isSelf -> {
PsiTreeUtil.getParentOfType(it, PyFunction::class.java)
?.takeIf { it.modifier == PyFunction.Modifier.CLASSMETHOD }
Expand All @@ -86,14 +86,14 @@ class PydanticTypeProvider : PyTypeProviderBase() {
it is PyNamedParameter -> it.getArgumentType(context)?.let { pyType ->
getPyClassTypeByPyTypes(pyType).filter { pyClassType ->
pyClassType.isDefinition
}.map { filteredPyClassType -> getPydanticTypeForClass(filteredPyClassType.pyClass, context) }.firstOrNull()
}.map { filteredPyClassType -> getPydanticTypeForClass(filteredPyClassType.pyClass, context, true) }.firstOrNull()
}
it is PyTargetExpression -> (it as? PyTypedElement)?.let { pyTypedElement ->
context.getType(pyTypedElement)
?.let { pyType -> getPyClassTypeByPyTypes(pyType) }
?.filter { pyClassType -> pyClassType.isDefinition }
?.map { filteredPyClassType ->
getPydanticTypeForClass(filteredPyClassType.pyClass, context)
getPydanticTypeForClass(filteredPyClassType.pyClass, context, true)
}?.firstOrNull()
}
else -> null
Expand All @@ -102,11 +102,12 @@ class PydanticTypeProvider : PyTypeProviderBase() {
.firstOrNull { it != null }
}

private fun getPydanticTypeForClass(pyClass: PyClass, context: TypeEvalContext): PyCallableType? {
private fun getPydanticTypeForClass(pyClass: PyClass, context: TypeEvalContext, init: Boolean = false): PyCallableType? {
if (!isPydanticModel(pyClass, context)) return null
val clsType = (context.getType(pyClass) as? PyClassLikeType) ?: return null
val ellipsis = PyElementGenerator.getInstance(pyClass.project).createEllipsis()

val typed = !init || getInstance(pyClass.project).initTyped
val collected = linkedMapOf<String, PyCallableParameter>()
val pydanticVersion = getPydanticVersion(pyClass.project, context)
val config = getConfig(pyClass, context, true)
Expand All @@ -117,7 +118,7 @@ class PydanticTypeProvider : PyTypeProviderBase() {
if (!isPydanticModel(current, context)) continue

getClassVariables(current, context)
.mapNotNull { fieldToParameter(it, ellipsis, context, current, pydanticVersion, config) }
.mapNotNull { fieldToParameter(it, ellipsis, context, current, pydanticVersion, config, typed) }
.filter { parameter -> parameter.name?.let { !collected.containsKey(it) } ?: false }
.forEach { parameter -> collected[parameter.name!!] = parameter }
}
Expand All @@ -133,7 +134,8 @@ class PydanticTypeProvider : PyTypeProviderBase() {
context: TypeEvalContext,
pyClass: PyClass,
pydanticVersion: KotlinVersion?,
config: HashMap<String, Any?>): PyCallableParameter? {
config: HashMap<String, Any?>,
typed: Boolean = true): PyCallableParameter? {
if (field.name == null || ! isValidFieldName(field.name!!)) return null
if (!hasAnnotationValue(field) && !field.hasAssignedValue()) return null // skip fields that are invalid syntax

Expand All @@ -143,12 +145,16 @@ class PydanticTypeProvider : PyTypeProviderBase() {
else -> defaultValueFromField
}

val typeForParameter = if (!hasAnnotationValue(field) && defaultValueFromField is PyTypedElement) {
// get type from default value
context.getType(defaultValueFromField)
} else {
// get type from annotation
getTypeForParameter(field, context)
val typeForParameter = when {
!typed -> null
!hasAnnotationValue(field) && defaultValueFromField is PyTypedElement -> {
// get type from default value
context.getType(defaultValueFromField)
}
else -> {
// get type from annotation
getTypeForParameter(field, context)
}
}

return PyCallableParameterImpl.nonPsi(
Expand Down
9 changes: 3 additions & 6 deletions testSrc/com/koxudaxi/pydantic/PydanticInspectionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,8 @@ open class PydanticInspectionTest : PydanticInspectionBase() {
}

fun testWarnUntypedFields() {
try {
defaultWarnUntypedFields = true
doTest()
} finally {
defaultWarnUntypedFields = false
}
val pydanticConfigService = PydanticConfigService.getInstance(myFixture!!.project)
pydanticConfigService.warnUntypedFields = true
doTest()
}
}
1 change: 0 additions & 1 deletion testSrc/com/koxudaxi/pydantic/PydanticTestCase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ abstract class PydanticTestCase(version: String = "v1") : UsefulTestCase() {
packageDir = myFixture!!.findFileInTempDir("package")
addSourceRoot(myFixture!!.module, packageDir!!)


PythonDialectsTokenSetProvider.reset()
setLanguageLevel(LanguageLevel.PYTHON37)
}
Expand Down