From cebe263f6a96ecc23b6e77c4f81741ab3ddc1f5c Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sun, 10 Sep 2023 23:04:08 +0900 Subject: [PATCH 1/2] Fix incorrect init interpretation --- .../koxudaxi/pydantic/PydanticTypeProvider.kt | 12 ++++++++ .../keywordArgumentInitArgsKwargs.py | 16 +++++++++++ .../completion/keywordArgumentInitKwargs.py | 16 +++++++++++ .../completion/keywordArgumentInitPosition.py | 16 +++++++++++ .../pydantic/PydanticCompletionTest.kt | 28 +++++++++++++++++++ 5 files changed, 88 insertions(+) create mode 100644 testData/completion/keywordArgumentInitArgsKwargs.py create mode 100644 testData/completion/keywordArgumentInitKwargs.py create mode 100644 testData/completion/keywordArgumentInitPosition.py diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index d7ed09d4..506ca206 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -479,6 +479,18 @@ class PydanticTypeProvider : PyTypeProviderBase() { getPydanticModelInit(pyClass, context)?.let { val callParameters = it.getParameters(context) .filterNot { parameter -> parameter.isSelf } + val callParametersWithoutKeywordContainer = callParameters.filterNot { + parameter -> parameter.isKeywordContainer + } + val hasKeywordContainer = callParametersWithoutKeywordContainer.size != callParameters.size + if (hasKeywordContainer) { + val hasNonPositionalContainer = callParametersWithoutKeywordContainer.any { + parameter -> !parameter.isPositionalContainer + } + if (!hasNonPositionalContainer) { + return@let null + } + } return PyCallableTypeImpl(callParameters, clsType.toInstance()) } diff --git a/testData/completion/keywordArgumentInitArgsKwargs.py b/testData/completion/keywordArgumentInitArgsKwargs.py new file mode 100644 index 00000000..7ced5b9c --- /dev/null +++ b/testData/completion/keywordArgumentInitArgsKwargs.py @@ -0,0 +1,16 @@ + + +from pydantic import BaseModel + +class Z(BaseModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) +class A(Z): + abc: str + cde: str + efg: str + +class B(A): + hij: str + +A() \ No newline at end of file diff --git a/testData/completion/keywordArgumentInitKwargs.py b/testData/completion/keywordArgumentInitKwargs.py new file mode 100644 index 00000000..e92affbe --- /dev/null +++ b/testData/completion/keywordArgumentInitKwargs.py @@ -0,0 +1,16 @@ + + +from pydantic import BaseModel + +class Z(BaseModel): + def __init__(self, **kwargs): + super().__init__(**kwargs) +class A(Z): + abc: str + cde: str + efg: str + +class B(A): + hij: str + +A() \ No newline at end of file diff --git a/testData/completion/keywordArgumentInitPosition.py b/testData/completion/keywordArgumentInitPosition.py new file mode 100644 index 00000000..28367e13 --- /dev/null +++ b/testData/completion/keywordArgumentInitPosition.py @@ -0,0 +1,16 @@ + + +from pydantic import BaseModel + +class Z(BaseModel): + def __init__(self, xyz: str): + super().__init__() +class A(Z): + abc: str + cde: str + efg: str + +class B(A): + hij: str + +A() \ No newline at end of file diff --git a/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt b/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt index 2b4b7eeb..00d1d764 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt @@ -712,6 +712,34 @@ open class PydanticCompletionTest : PydanticTestCase() { ) } + fun testKeywordArgumentInitArgsKwargs() { + doFieldTest( + listOf( + Pair("abc=", "str A"), + Pair("cde=", "str A"), + Pair("efg=", "str A") + ) + ) + } + + fun testKeywordArgumentInitKwargs() { + doFieldTest( + listOf( + Pair("abc=", "str A"), + Pair("cde=", "str A"), + Pair("efg=", "str A") + ) + ) + } + + fun testKeywordArgumentInitPosition() { + doFieldTest( + listOf( + Pair("xyz=", "str A") + ) + ) + } + fun testdataclassKeywordArgument() { doFieldTest( listOf( From 422bc013353b77ef76cefd29be4a3f09c4d732ab Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 11 Sep 2023 01:47:26 +0900 Subject: [PATCH 2/2] Fix unittest --- docs/ignore-init-arguments.md | 18 ++++++++++++- docs/ignore-init-keyword-arguments.md | 26 +++++++++++++++++++ src/com/koxudaxi/pydantic/Pydantic.kt | 19 +++++++++++++- .../pydantic/PydanticConfigService.kt | 1 + .../koxudaxi/pydantic/PydanticInitializer.kt | 2 ++ .../koxudaxi/pydantic/PydanticTypeProvider.kt | 12 --------- .../keywordArgumentInitArgsKwargsDisable.py | 16 ++++++++++++ .../initializer/pyprojecttoml/pyproject.toml | 2 ++ .../pydantic/PydanticCompletionTest.kt | 17 +++++++++--- .../pydantic/PydanticInitializerTest.kt | 2 ++ 10 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 docs/ignore-init-keyword-arguments.md create mode 100644 testData/completion/keywordArgumentInitArgsKwargsDisable.py diff --git a/docs/ignore-init-arguments.md b/docs/ignore-init-arguments.md index fef9de99..f756301a 100644 --- a/docs/ignore-init-arguments.md +++ b/docs/ignore-init-arguments.md @@ -21,5 +21,21 @@ The option has to be defined in pyproject.toml ```toml [tool.pydantic-pycharm-plugin] ignore-init-method-arguments = true - ``` + +!!! info +**This feature is in version 0.4.9 or later** + +If a third-party library provides a model that extends BaseModel, it may override the `__init__` method, as in `__init__(self, **kwargs)`. +If this is the case, the plugin user should set `ignore-init-method-arguments = true` to ignore the `__init__` method argument. +But it is difficult to tell if the library is using BaseModel or not. + +The plugin ignore the `__init__` method if argument is only `**kwargs`. the option is provided as `ignore-init-method-keyword-arguments`. +This option is enabled by default, so if you create a model that inherits from BaseModel with a method like `__init__(self, **kwargs)` defined, ignore this `init` argument. + +If you want to disable this option, please put the following setting in `pyproject.toml`. + +```toml +[tool.pydantic-pycharm-plugin]. +ignore-init-method-keyword-arguments = true +``` \ No newline at end of file diff --git a/docs/ignore-init-keyword-arguments.md b/docs/ignore-init-keyword-arguments.md new file mode 100644 index 00000000..d7ab24c4 --- /dev/null +++ b/docs/ignore-init-keyword-arguments.md @@ -0,0 +1,26 @@ +# Ignore `__init__` method arguments + + +!!! info + **This feature is in version 0.4.8 or later** + +You can write `__init__` method on a model for adding some logic. + +However, default arguments on `__init__` method will be overridden, And you will lose autocompletion for `__init__` methods by the plugin. + +![options_init](init_arguments.png) + +`ignore-init-method-arguments` option resolves this problem. +The option ignore arguments on `__init__` method. + +![options_ignore_init](ignore_init_arguments.png) + + +The option has to be defined in pyproject.toml + +```toml +[tool.pydantic-pycharm-plugin] +ignore-init-method-keyword-arguments = false + +``` + diff --git a/src/com/koxudaxi/pydantic/Pydantic.kt b/src/com/koxudaxi/pydantic/Pydantic.kt index 3ff660fe..ddcad874 100644 --- a/src/com/koxudaxi/pydantic/Pydantic.kt +++ b/src/com/koxudaxi/pydantic/Pydantic.kt @@ -783,11 +783,28 @@ internal fun getQualifiedName(pyExpression: PyExpression, context: TypeEvalConte } fun getPydanticModelInit(pyClass: PyClass, context: TypeEvalContext): PyFunction? { - if (PydanticConfigService.getInstance(pyClass.project).ignoreInitMethodArguments) return null + val pydanticConfigService = PydanticConfigService.getInstance(pyClass.project) + if (pydanticConfigService.ignoreInitMethodArguments) return null val pyFunction = pyClass.findInitOrNew(true, context) ?: return null if (pyFunction.name != PyNames.INIT) return null val containingClass = pyFunction.containingClass ?: return null if (!isPydanticModel(containingClass, false, context)) return null + + if (!pydanticConfigService.ignoreInitMethodKeywordArguments) return pyFunction + val callParameters = pyFunction.getParameters(context) + .filterNot { parameter -> parameter.isSelf } + val callParametersWithoutKeywordContainer = callParameters.filterNot { + parameter -> parameter.isKeywordContainer + } + val hasKeywordContainer = callParametersWithoutKeywordContainer.size != callParameters.size + if (hasKeywordContainer) { + val hasNonPositionalContainer = callParametersWithoutKeywordContainer.any { + parameter -> !parameter.isPositionalContainer + } + if (!hasNonPositionalContainer) { + return null + } + } return pyFunction } diff --git a/src/com/koxudaxi/pydantic/PydanticConfigService.kt b/src/com/koxudaxi/pydantic/PydanticConfigService.kt index 35ad9d09..f7329314 100644 --- a/src/com/koxudaxi/pydantic/PydanticConfigService.kt +++ b/src/com/koxudaxi/pydantic/PydanticConfigService.kt @@ -20,6 +20,7 @@ class PydanticConfigService : PersistentStateComponent { var acceptableTypeMap = mapOf>() var acceptableTypeHighlightType: ProblemHighlightType = ProblemHighlightType.WEAK_WARNING var ignoreInitMethodArguments: Boolean = false + var ignoreInitMethodKeywordArguments: Boolean = true val currentInitTyped: Boolean get() = this.mypyInitTyped ?: this.initTyped val currentWarnUntypedFields: Boolean diff --git a/src/com/koxudaxi/pydantic/PydanticInitializer.kt b/src/com/koxudaxi/pydantic/PydanticInitializer.kt index 7fbd06c6..4820b30b 100644 --- a/src/com/koxudaxi/pydantic/PydanticInitializer.kt +++ b/src/com/koxudaxi/pydantic/PydanticInitializer.kt @@ -142,6 +142,8 @@ class PydanticInitializer : ProjectActivity { getHighlightLevel(table, "acceptable-type-highlight", ProblemHighlightType.WEAK_WARNING) configService.ignoreInitMethodArguments = table.getBoolean("ignore-init-method-arguments") ?: false + configService.ignoreInitMethodKeywordArguments = + table.getBoolean("ignore-init-method-keyword-arguments") ?: true } private fun getHighlightLevel(table: TomlTable, path: String, default: ProblemHighlightType): ProblemHighlightType { diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index 506ca206..d7ed09d4 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -479,18 +479,6 @@ class PydanticTypeProvider : PyTypeProviderBase() { getPydanticModelInit(pyClass, context)?.let { val callParameters = it.getParameters(context) .filterNot { parameter -> parameter.isSelf } - val callParametersWithoutKeywordContainer = callParameters.filterNot { - parameter -> parameter.isKeywordContainer - } - val hasKeywordContainer = callParametersWithoutKeywordContainer.size != callParameters.size - if (hasKeywordContainer) { - val hasNonPositionalContainer = callParametersWithoutKeywordContainer.any { - parameter -> !parameter.isPositionalContainer - } - if (!hasNonPositionalContainer) { - return@let null - } - } return PyCallableTypeImpl(callParameters, clsType.toInstance()) } diff --git a/testData/completion/keywordArgumentInitArgsKwargsDisable.py b/testData/completion/keywordArgumentInitArgsKwargsDisable.py new file mode 100644 index 00000000..7ced5b9c --- /dev/null +++ b/testData/completion/keywordArgumentInitArgsKwargsDisable.py @@ -0,0 +1,16 @@ + + +from pydantic import BaseModel + +class Z(BaseModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) +class A(Z): + abc: str + cde: str + efg: str + +class B(A): + hij: str + +A() \ No newline at end of file diff --git a/testData/initializer/pyprojecttoml/pyproject.toml b/testData/initializer/pyprojecttoml/pyproject.toml index 9e576ad6..e8a704bd 100644 --- a/testData/initializer/pyprojecttoml/pyproject.toml +++ b/testData/initializer/pyprojecttoml/pyproject.toml @@ -4,6 +4,8 @@ parsable-type-highlight = "weak_warning" acceptable-type-highlight = "warning" ignore-init-method-arguments = true +ignore-init-method-keyword-arguments = false + [tool.pydantic-pycharm-plugin.parsable-types] ## datetime.datetime field may parse int diff --git a/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt b/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt index 00d1d764..17f6228b 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt @@ -721,7 +721,18 @@ open class PydanticCompletionTest : PydanticTestCase() { ) ) } - + fun testKeywordArgumentInitArgsKwargsDisable() { + val config = PydanticConfigService.getInstance(myFixture!!.project) + config.ignoreInitMethodKeywordArguments = false + try { + doFieldTest( + emptyList( + ) + )} + finally { + config.ignoreInitMethodKeywordArguments = true + } + } fun testKeywordArgumentInitKwargs() { doFieldTest( listOf( @@ -734,9 +745,7 @@ open class PydanticCompletionTest : PydanticTestCase() { fun testKeywordArgumentInitPosition() { doFieldTest( - listOf( - Pair("xyz=", "str A") - ) + emptyList() ) } diff --git a/testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt b/testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt index 51b142fb..74cc1173 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticInitializerTest.kt @@ -60,6 +60,7 @@ open class PydanticInitializerTest : PydanticTestCase() { assertEquals(this.pydanticConfigService.parsableTypeHighlightType, ProblemHighlightType.WEAK_WARNING) assertEquals(this.pydanticConfigService.acceptableTypeHighlightType, ProblemHighlightType.WARNING) assertEquals(this.pydanticConfigService.ignoreInitMethodArguments, true) + assertEquals(this.pydanticConfigService.ignoreInitMethodKeywordArguments, false) } } } @@ -96,6 +97,7 @@ open class PydanticInitializerTest : PydanticTestCase() { assertEquals(this.pydanticConfigService.parsableTypeHighlightType, ProblemHighlightType.WARNING) assertEquals(this.pydanticConfigService.acceptableTypeHighlightType, ProblemHighlightType.WEAK_WARNING) assertEquals(this.pydanticConfigService.ignoreInitMethodArguments, false) + assertEquals(this.pydanticConfigService.ignoreInitMethodKeywordArguments, true) } } }