From 8f450083b975f58182dde0229b34abc66e244521 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 22 Dec 2025 20:27:48 +0900 Subject: [PATCH 1/6] test: Add syntax validation for generated interface members. Adds two new tests, `test_valid_syntax_dispmethods` and `test_valid_syntax_commethods`, to `test_typeannotator.py`. These tests verify that the code generated by `DispInterfaceMembersAnnotator` and `ComInterfaceMembersAnnotator` is syntactically valid Python. They do this by using `ast.parse` on the generated code, which will raise an exception if the syntax is incorrect. --- comtypes/test/test_typeannotator.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 33ec7b4a..09ddf86f 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -1,3 +1,4 @@ +import ast import unittest from comtypes.tools import typedesc @@ -84,6 +85,17 @@ def test_disp_interface(self): expected, typeannotator.DispInterfaceMembersAnnotator(itf).generate() ) + def test_valid_syntax_dispmethods(self): + itf = self._create_typedesc_disp_interface() + definition = "\n".join( + ( + "class ISomeInterface(IDispatch):", + " if TYPE_CHECKING:", + f"{typeannotator.DispInterfaceMembersAnnotator(itf).generate()}", + ) + ) + ast.parse(definition, mode="exec") + def _create_typedesc_com_interface(self) -> typedesc.ComInterface: guid = "{00000000-0000-0000-0000-000000000000}" itf = typedesc.ComInterface( @@ -136,3 +148,14 @@ def test_com_interface(self): self.assertEqual( expected, typeannotator.ComInterfaceMembersAnnotator(itf).generate() ) + + def test_valid_syntax_commethods(self): + itf = self._create_typedesc_com_interface() + definition = "\n".join( + ( + "class ISomeInterface(IUnknown):", + " if TYPE_CHECKING:", + f"{typeannotator.ComInterfaceMembersAnnotator(itf).generate()}", + ) + ) + ast.parse(definition, mode="exec") From 0afdb027b583af5d80b516a2f8f49dfd8490bb04 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 22 Dec 2025 20:27:48 +0900 Subject: [PATCH 2/6] feat: Improve signature generation for named properties. The previous logic in `ComMethodAnnotator` and `DispMethodAnnotator` generated a generic `**kwargs: hints.Any` signature for methods where a required parameter followed an optional one. This pattern is typical for named property assignments (e.g., `obj.prop[key] = value`), but the resulting signature was imprecise for static analysis. This commit refines the signature generation for these cases. It now produces a more specific signature using a positional-only marker and variadic arguments, like `/, *args: hints.Unpack[tuple[...]]`. This change provides more accurate type hints for multi-argument `propput` and `propputref` operations, enhancing type safety for both COM and dispatch interfaces. --- comtypes/tools/codegenerator/typeannotator.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 9dc54f7a..d2f9526d 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -222,6 +222,11 @@ def _to_outtype(typ: Any) -> str: return "hints.Incomplete" +def _generate_trailing_params(specs: Sequence[tuple[Any, str, Optional[Any]]]) -> str: + params = f"tuple[{', '.join(('hints.Incomplete',) * len(specs))}]" + return f"*args: hints.Unpack[{params}]" + + class ComMethodAnnotator(_MethodAnnotator[typedesc.ComMethod]): def _iter_outarg_specs(self) -> Iterator[tuple[Any, str]]: for typ, name, flags, _ in self.method.arguments: @@ -229,18 +234,22 @@ def _iter_outarg_specs(self) -> Iterator[tuple[Any, str]]: yield typ, name def getvalue(self, name: str) -> str: + specs = self.inarg_specs inargs = [] has_optional = False - for _, argname, default in self.inarg_specs: + for i, (_, argname, default) in enumerate(specs): if keyword.iskeyword(argname): inargs = ["*args: hints.Any", "**kwargs: hints.Any"] break if default is None: if has_optional: - # probably propput or propputref + # Required parameters are positioned after optional ones. + # This likely indicates a named propput or named propputref + # assignment in the form of `obj.prop[...] = ...`. # HACK: Something that goes into this conditional branch # should be a special callback. - inargs.append("**kwargs: hints.Any") + inargs.append("/") + inargs.append(_generate_trailing_params(specs[i:])) break inargs.append(f"{argname}: hints.Incomplete") else: @@ -275,6 +284,7 @@ def generate(self) -> str: class DispMethodAnnotator(_MethodAnnotator[typedesc.DispMethod]): def getvalue(self, name: str) -> str: + specs = self.inarg_specs inargs = [] has_optional = False # NOTE: Since named parameters are not yet implemented, all arguments @@ -282,21 +292,19 @@ def getvalue(self, name: str) -> str: # positional-only parameters, introduced in PEP570. # See also `automation.IDispatch.Invoke`. # See https://github.com/enthought/comtypes/issues/371 - for _, argname, default in self.inarg_specs: + for i, (_, argname, default) in enumerate(specs): if keyword.iskeyword(argname): inargs = ["*args: hints.Any", "**kwargs: hints.Any"] break if default is None: if has_optional: - # Required parameter follows an optional one. - # probably propput or propputref - # TODO: After named parameters are supported, - # the positional-only parameter markers - # will be removed. + # Required parameters are positioned after optional ones. + # This likely indicates a named propput or named propputref + # assignment in the form of `obj.prop[...] = ...`. inargs.append("/") # HACK: Something that goes into this conditional branch # should be a special callback. - inargs.append("**kwargs: hints.Any") + inargs.append(_generate_trailing_params(specs[i:])) break inargs.append(f"{argname}: hints.Incomplete") else: From f2ca38ee2dbfb389006cd6807fcf78a5994db3cd Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 22 Dec 2025 20:27:48 +0900 Subject: [PATCH 3/6] docs: Add docstring to `_generate_trailing_params`. The `_generate_trailing_params` function handles a specific edge case in COM interface generation: `propput` or `propputref` methods with multiple arguments. --- comtypes/tools/codegenerator/typeannotator.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index d2f9526d..20b37fd6 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -223,6 +223,13 @@ def _to_outtype(typ: Any) -> str: def _generate_trailing_params(specs: Sequence[tuple[Any, str, Optional[Any]]]) -> str: + """Generates a type hint for variadic positional arguments. + + This is for cases where required parameters follow optional ones, which is + not directly representable in Python's syntax. This pattern typically + occurs in COM `propput` or `propputref` methods that take multiple + arguments, corresponding to assignments like `obj.prop[a, b] = value`. + """ params = f"tuple[{', '.join(('hints.Incomplete',) * len(specs))}]" return f"*args: hints.Unpack[{params}]" From 32d0133af3b170598df35536409b5990c2198eb9 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 22 Dec 2025 20:27:48 +0900 Subject: [PATCH 4/6] test: Enhance tests for method signature generation. This commit significantly enhances the tests for `ComMethodAnnotator` and `DispMethodAnnotator` to cover more complex argument patterns and improves the underlying signature generation logic. --- comtypes/test/test_typeannotator.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 09ddf86f..0ecb060f 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -73,11 +73,11 @@ def test_disp_interface(self): " pass # avoid using a keyword for def except(self) -> hints.Incomplete: ...\n" # noqa " def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Incomplete: ...\n" # noqa " def _get_spam(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa - " def _set_spam(self, arg1: hints.Incomplete = ..., /, **kwargs: hints.Any) -> hints.Incomplete: ...\n" # noqa + " def _set_spam(self, arg1: hints.Incomplete = ..., /, *args: hints.Unpack[tuple[hints.Incomplete]]) -> hints.Incomplete: ...\n" # noqa " spam = hints.named_property('spam', _get_spam, _set_spam)\n" " pass # avoid using a keyword for def raise(self, foo: hints.Incomplete, bar: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa " def _get_def(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa - " def _set_def(self, arg1: hints.Incomplete = ..., /, **kwargs: hints.Any) -> hints.Incomplete: ...\n" # noqa + " def _set_def(self, arg1: hints.Incomplete = ..., /, *args: hints.Unpack[tuple[hints.Incomplete]]) -> hints.Incomplete: ...\n" # noqa " pass # avoid using a keyword for def = hints.named_property('def', _get_def, _set_def)\n" # noqa " def egg(self) -> hints.Incomplete: ..." # noqa ) @@ -119,9 +119,12 @@ def _create_typedesc_com_interface(self) -> typedesc.ComInterface: get_class = typedesc.ComMethod( 2, 1610678273, "class", HRESULT_type, ["propget"], None ) + get_class.add_argument(VARIANT_type, "arg1", ["in"], None) put_class = typedesc.ComMethod( 4, 1610678273, "class", HRESULT_type, ["propput"], None ) + put_class.add_argument(VARIANT_type, "arg1", ["in", "optional"], None) + put_class.add_argument(VARIANT_type, "arg2", ["in"], None) pass_ = typedesc.ComMethod(1, 1610678274, "pass", HRESULT_type, [], None) pass_.add_argument(VARIANT_type, "foo", ["in"], None) pass_.add_argument(VARIANT_type, "bar", ["in", "optional"], None) @@ -140,9 +143,9 @@ def test_com_interface(self): " def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Hresult: ...\n" # noqa " def _get_global(self) -> hints.Hresult: ...\n" " pass # avoid using a keyword for global = hints.normal_property(_get_global)\n" # noqa - " def _get_class(self) -> hints.Hresult: ...\n" - " def _set_class(self) -> hints.Hresult: ...\n" - " pass # avoid using a keyword for class = hints.normal_property(_get_class, _set_class)\n" # noqa + " def _get_class(self, arg1: hints.Incomplete) -> hints.Hresult: ...\n" + " def _set_class(self, arg1: hints.Incomplete = ..., /, *args: hints.Unpack[tuple[hints.Incomplete]]) -> hints.Hresult: ...\n" # noqa + " pass # avoid using a keyword for class = hints.named_property('class', _get_class, _set_class)\n" # noqa " pass # avoid using a keyword for def pass(self, foo: hints.Incomplete, bar: hints.Incomplete = ...) -> hints.Hresult: ..." # noqa ) self.assertEqual( From 243a16b3254689dc9a6708b47f261f85d0c4fb83 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 22 Dec 2025 20:27:48 +0900 Subject: [PATCH 5/6] test: Add test for property setters with arguments. This commit expands the test suite in `test_typeannotator.py` to cover `propput` properties that accept arguments. The test for `ComInterfaceMembersAnnotator` now includes a `ham` property with a setter that takes an argument. This ensures that the generated type hint correctly reflects the method signature for such setters, which was not previously tested. --- comtypes/test/test_typeannotator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 0ecb060f..8b7578fc 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -110,6 +110,7 @@ def _create_typedesc_com_interface(self) -> typedesc.ComInterface: put_ham = typedesc.ComMethod( 4, 1610678270, "ham", HRESULT_type, ["propput"], None ) + put_ham.add_argument(VARIANT_type, "arg1", ["in"], None) bacon = typedesc.ComMethod(1, 1610678271, "bacon", HRESULT_type, [], None) bacon.add_argument(VARIANT_type, "foo", ["in"], None) bacon.add_argument(VARIANT_type, "or", ["in"], None) @@ -138,7 +139,7 @@ def test_com_interface(self): " def _get_spam(self) -> hints.Hresult: ...\n" " spam = hints.normal_property(_get_spam)\n" " def _get_ham(self) -> hints.Hresult: ...\n" - " def _set_ham(self) -> hints.Hresult: ...\n" + " def _set_ham(self, arg1: hints.Incomplete) -> hints.Hresult: ...\n" " ham = hints.normal_property(_get_ham, _set_ham)\n" " def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Hresult: ...\n" # noqa " def _get_global(self) -> hints.Hresult: ...\n" From 0c95ef44765cd2a0fd0591613f992d892a8632b9 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 22 Dec 2025 20:27:48 +0900 Subject: [PATCH 6/6] feat: Conform to PEP 570 for descriptor protocols and dunder methods in `hints.pyi`. Apply '/' marker to descriptor protocol and dunder methods, improving clarity and compliance with PEP 570. --- comtypes/hints.pyi | 58 +++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/comtypes/hints.pyi b/comtypes/hints.pyi index 74f7011d..0659023e 100644 --- a/comtypes/hints.pyi +++ b/comtypes/hints.pyi @@ -105,31 +105,35 @@ class _GetSetNormalProperty(Generic[_T_Inst, _R_Get, _T_SetVal]): fset: Callable[[_T_Inst, _T_SetVal], Any] @overload - def __get__(self, instance: None, owner: type[_T_Inst]) -> Self: ... + def __get__(self, instance: None, owner: type[_T_Inst], /) -> Self: ... @overload - def __get__(self, instance: _T_Inst, owner: Optional[type[_T_Inst]]) -> _R_Get: ... - def __set__(self, instance: _T_Inst, value: _T_SetVal) -> None: ... + def __get__( + self, instance: _T_Inst, owner: Optional[type[_T_Inst]], / + ) -> _R_Get: ... + def __set__(self, instance: _T_Inst, value: _T_SetVal, /) -> None: ... class _GetOnlyNormalProperty(Generic[_T_Inst, _R_Get]): fget: Callable[[_T_Inst], Any] @overload - def __get__(self, instance: None, owner: type[_T_Inst]) -> Self: ... + def __get__(self, instance: None, owner: type[_T_Inst], /) -> Self: ... @overload - def __get__(self, instance: _T_Inst, owner: Optional[type[_T_Inst]]) -> _R_Get: ... - def __set__(self, instance: _T_Inst, value: Any) -> NoReturn: ... + def __get__( + self, instance: _T_Inst, owner: Optional[type[_T_Inst]], / + ) -> _R_Get: ... + def __set__(self, instance: _T_Inst, value: Any, /) -> NoReturn: ... class _SetOnlyNormalProperty(Generic[_T_Inst, _T_SetVal]): fget: Callable[[_T_Inst], Any] fset: Callable[[_T_Inst, _T_SetVal], Any] @overload - def __get__(self, instance: None, owner: type[_T_Inst]) -> Self: ... + def __get__(self, instance: None, owner: type[_T_Inst], /) -> Self: ... @overload def __get__( - self, instance: _T_Inst, owner: Optional[type[_T_Inst]] + self, instance: _T_Inst, owner: Optional[type[_T_Inst]], / ) -> NoReturn: ... - def __set__(self, instance: _T_Inst, value: _T_SetVal) -> None: ... + def __set__(self, instance: _T_Inst, value: _T_SetVal, /) -> None: ... @overload def normal_property( @@ -149,9 +153,9 @@ class _GetSetBoundNamedProperty(Generic[_T_Inst, _P_Get, _R_Get, _P_Set]): fget: Callable[Concatenate[_T_Inst, _P_Get], _R_Get] fset: Callable[Concatenate[_T_Inst, _P_Set], Any] __doc__: Optional[str] - def __getitem__(self, index: Any) -> _R_Get: ... + def __getitem__(self, index: Any, /) -> _R_Get: ... def __call__(self, *args: _P_Get.args, **kwargs: _P_Get.kwargs) -> _R_Get: ... - def __setitem__(self, index: Any, value: Any) -> None: ... + def __setitem__(self, index: Any, value: Any, /) -> None: ... def __iter__(self) -> NoReturn: ... class _GetSetNamedProperty(Generic[_T_Inst, _P_Get, _R_Get, _P_Set]): @@ -161,20 +165,20 @@ class _GetSetNamedProperty(Generic[_T_Inst, _P_Get, _R_Get, _P_Set]): __doc__: Optional[str] @overload - def __get__(self, instance: None, owner: type[_T_Inst]) -> Self: ... + def __get__(self, instance: None, owner: type[_T_Inst], /) -> Self: ... @overload def __get__( - self, instance: _T_Inst, owner: Optional[type[_T_Inst]] + self, instance: _T_Inst, owner: Optional[type[_T_Inst]], / ) -> _GetSetBoundNamedProperty[_T_Inst, _P_Get, _R_Get, _P_Set]: ... - def __set__(self, instance: _T_Inst, value: Any) -> NoReturn: ... + def __set__(self, instance: _T_Inst, value: Any, /) -> NoReturn: ... class _GetOnlyBoundNamedProperty(Generic[_T_Inst, _P_Get, _R_Get]): name: str fget: Callable[Concatenate[_T_Inst, _P_Get], _R_Get] __doc__: Optional[str] - def __getitem__(self, index: Any) -> _R_Get: ... + def __getitem__(self, index: Any, /) -> _R_Get: ... def __call__(self, *args: _P_Get.args, **kwargs: _P_Get.kwargs) -> _R_Get: ... - def __setitem__(self, index: Any, value: Any) -> NoReturn: ... + def __setitem__(self, index: Any, value: Any, /) -> NoReturn: ... def __iter__(self) -> NoReturn: ... class _GetOnlyNamedProperty(Generic[_T_Inst, _P_Get, _R_Get]): @@ -183,20 +187,20 @@ class _GetOnlyNamedProperty(Generic[_T_Inst, _P_Get, _R_Get]): __doc__: Optional[str] @overload - def __get__(self, instance: None, owner: type[_T_Inst]) -> Self: ... + def __get__(self, instance: None, owner: type[_T_Inst], /) -> Self: ... @overload def __get__( - self, instance: _T_Inst, owner: Optional[type[_T_Inst]] + self, instance: _T_Inst, owner: Optional[type[_T_Inst]], / ) -> _GetOnlyBoundNamedProperty[_T_Inst, _P_Get, _R_Get]: ... - def __set__(self, instance: _T_Inst, value: Any) -> NoReturn: ... + def __set__(self, instance: _T_Inst, value: Any, /) -> NoReturn: ... class _SetOnlyBoundNamedProperty(Generic[_T_Inst, _P_Set]): name: str fset: Callable[Concatenate[_T_Inst, _P_Set], Any] __doc__: Optional[str] - def __getitem__(self, index: Any) -> NoReturn: ... + def __getitem__(self, index: Any, /) -> NoReturn: ... def __call__(self, *args: Any, **kwargs: Any) -> NoReturn: ... - def __setitem__(self, index: Any, value: Any) -> None: ... + def __setitem__(self, index: Any, value: Any, /) -> None: ... def __iter__(self) -> NoReturn: ... class _SetOnlyNamedProperty(Generic[_T_Inst, _P_Set]): @@ -205,12 +209,12 @@ class _SetOnlyNamedProperty(Generic[_T_Inst, _P_Set]): __doc__: Optional[str] @overload - def __get__(self, instance: None, owner: type[_T_Inst]) -> Self: ... + def __get__(self, instance: None, owner: type[_T_Inst], /) -> Self: ... @overload def __get__( - self, instance: _T_Inst, owner: Optional[type[_T_Inst]] + self, instance: _T_Inst, owner: Optional[type[_T_Inst]], / ) -> _SetOnlyBoundNamedProperty[_T_Inst, _P_Set]: ... - def __set__(self, instance: _T_Inst, value: Any) -> NoReturn: ... + def __set__(self, instance: _T_Inst, value: Any, /) -> NoReturn: ... @overload def named_property( @@ -231,9 +235,11 @@ def named_property( class _Descriptor(Protocol[_T_Inst, _R_Get]): @overload - def __get__(self, instance: None, owner: type[_T_Inst]) -> Self: ... + def __get__(self, instance: None, owner: type[_T_Inst], /) -> Self: ... @overload - def __get__(self, instance: _T_Inst, owner: Optional[type[_T_Inst]]) -> _R_Get: ... + def __get__( + self, instance: _T_Inst, owner: Optional[type[_T_Inst]], / + ) -> _R_Get: ... # `__len__` for objects with `Count` @overload