Skip to content

Commit

Permalink
add config panel (#92)
Browse files Browse the repository at this point in the history
* add config panel

* support init_typed
  • Loading branch information
koxudaxi committed Dec 15, 2019
1 parent 352aa2d commit 2457ea7
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 31 deletions.
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

0 comments on commit 2457ea7

Please sign in to comment.