From b90b88b08789dcd049adb9f56b0099c63d70009a Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Wed, 13 Sep 2023 00:33:08 +0900 Subject: [PATCH] Ignore `__init__` method if parameter is only only `**kwargs` arguments. (#798) * Fix incorrect init interpretation * 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 + .../keywordArgumentInitArgsKwargs.py | 16 ++++++++ .../keywordArgumentInitArgsKwargsDisable.py | 16 ++++++++ .../completion/keywordArgumentInitKwargs.py | 16 ++++++++ .../completion/keywordArgumentInitPosition.py | 16 ++++++++ .../initializer/pyprojecttoml/pyproject.toml | 2 + .../pydantic/PydanticCompletionTest.kt | 37 +++++++++++++++++++ .../pydantic/PydanticInitializerTest.kt | 2 + 12 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 docs/ignore-init-keyword-arguments.md create mode 100644 testData/completion/keywordArgumentInitArgsKwargs.py create mode 100644 testData/completion/keywordArgumentInitArgsKwargsDisable.py create mode 100644 testData/completion/keywordArgumentInitKwargs.py create mode 100644 testData/completion/keywordArgumentInitPosition.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/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/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/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/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 2b4b7eeb..17f6228b 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticCompletionTest.kt @@ -712,6 +712,43 @@ open class PydanticCompletionTest : PydanticTestCase() { ) } + fun testKeywordArgumentInitArgsKwargs() { + doFieldTest( + listOf( + Pair("abc=", "str A"), + Pair("cde=", "str A"), + Pair("efg=", "str A") + ) + ) + } + fun testKeywordArgumentInitArgsKwargsDisable() { + val config = PydanticConfigService.getInstance(myFixture!!.project) + config.ignoreInitMethodKeywordArguments = false + try { + doFieldTest( + emptyList( + ) + )} + finally { + config.ignoreInitMethodKeywordArguments = true + } + } + fun testKeywordArgumentInitKwargs() { + doFieldTest( + listOf( + Pair("abc=", "str A"), + Pair("cde=", "str A"), + Pair("efg=", "str A") + ) + ) + } + + fun testKeywordArgumentInitPosition() { + doFieldTest( + emptyList() + ) + } + fun testdataclassKeywordArgument() { doFieldTest( listOf( 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) } } }