From 2f293471a20ce9a0bd1b7d258cd8dad88aada493 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 13 Oct 2026 23:38:28 +0900 Subject: [PATCH 1/8] feat: Remove the interface code template comment generator. --- comtypes/tools/codegenerator/codegenerator.py | 3 - comtypes/tools/codegenerator/comments.py | 79 ------------------- 2 files changed, 82 deletions(-) delete mode 100644 comtypes/tools/codegenerator/comments.py diff --git a/comtypes/tools/codegenerator/codegenerator.py b/comtypes/tools/codegenerator/codegenerator.py index da389c7cd..53111d496 100644 --- a/comtypes/tools/codegenerator/codegenerator.py +++ b/comtypes/tools/codegenerator/codegenerator.py @@ -15,7 +15,6 @@ from comtypes import typeinfo from comtypes.tools import tlbparser, typedesc from comtypes.tools.codegenerator import heads, namespaces, packing -from comtypes.tools.codegenerator.comments import ComInterfaceBodyImplCommentWriter from comtypes.tools.codegenerator.helpers import ( ASSUME_STRINGS, ComMethodGenerator, @@ -662,8 +661,6 @@ def ComInterfaceBody(self, body: typedesc.ComInterfaceBody) -> None: raise TypeError("what's this?") print("]", file=ofi) - print(file=self.stream) - ComInterfaceBodyImplCommentWriter(self.stream).write(body) def DispInterface(self, itf: typedesc.DispInterface) -> None: self.generate(itf.get_head()) diff --git a/comtypes/tools/codegenerator/comments.py b/comtypes/tools/codegenerator/comments.py deleted file mode 100644 index 57f5b4a31..000000000 --- a/comtypes/tools/codegenerator/comments.py +++ /dev/null @@ -1,79 +0,0 @@ -import io - -from comtypes.tools import typedesc - - -class ComInterfaceBodyImplCommentWriter: - def __init__(self, stream: io.StringIO): - self.stream = stream - - def write(self, body: typedesc.ComInterfaceBody) -> None: - print( - "################################################################", - file=self.stream, - ) - print(f"# code template for {body.itf.name} implementation", file=self.stream) - print(f"# class {body.itf.name}_Impl(object):", file=self.stream) - - methods = {} - for m in body.itf.members: - if isinstance(m, typedesc.ComMethod): - # m.arguments is a sequence of tuples: - # (argtype, argname, idlflags, docstring) - # Some typelibs have unnamed method parameters! - inargs = [a[1] or "" for a in m.arguments if "out" not in a[2]] - outargs = [a[1] or "" for a in m.arguments if "out" in a[2]] - if "propget" in m.idlflags: - methods.setdefault(m.name, [0, inargs, outargs, m.doc])[0] |= 1 - elif "propput" in m.idlflags: - methods.setdefault(m.name, [0, inargs[:-1], inargs[-1:], m.doc])[ - 0 - ] |= 2 - else: - methods[m.name] = [0, inargs, outargs, m.doc] - - for name, (typ, inargs, outargs, doc) in methods.items(): - if typ == 0: # method - print( - f"# def {name}({', '.join(['self'] + inargs)}):", - file=self.stream, - ) - print(f"# {(doc or '-no docstring-')!r}", file=self.stream) - print(f"# #return {', '.join(outargs)}", file=self.stream) - elif typ == 1: # propget - print("# @property", file=self.stream) - print( - f"# def {name}({', '.join(['self'] + inargs)}):", - file=self.stream, - ) - print(f"# {(doc or '-no docstring-')!r}", file=self.stream) - print(f"# #return {', '.join(outargs)}", file=self.stream) - elif typ == 2: # propput - print( - f"# def _set({', '.join(['self'] + inargs + outargs)}):", - file=self.stream, - ) - print(f"# {(doc or '-no docstring-')!r}", file=self.stream) - print( - f"# {name} = property(fset = _set, doc = _set.__doc__)", - file=self.stream, - ) - elif typ == 3: # propget + propput - print( - f"# def _get({', '.join(['self'] + inargs)}):", - file=self.stream, - ) - print(f"# {(doc or '-no docstring-')!r}", file=self.stream) - print(f"# #return {', '.join(outargs)}", file=self.stream) - print( - f"# def _set({', '.join(['self'] + inargs + outargs)}):", - file=self.stream, - ) - print(f"# {(doc or '-no docstring-')!r}", file=self.stream) - print( - f"# {name} = property(_get, _set, doc = _set.__doc__)", - file=self.stream, - ) - else: - raise RuntimeError("BUG") - print("#", file=self.stream) From b0b98efe3bdc3c62f9fedbd3fc223c09acf1d8ca Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 13 Oct 2026 23:38:28 +0900 Subject: [PATCH 2/8] feat: Add docstrings to generated type annotations. When COM type info has a doc comment, the generator now adds it to the created method stub. This helps IDEs show better help text for generated methods and makes the type annotation output easier to understand. --- comtypes/test/test_typeannotator.py | 10 ++++++---- comtypes/tools/codegenerator/typeannotator.py | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 8b7578fce..493ebcd9a 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -29,7 +29,7 @@ def _create_typedesc_disp_interface(self) -> typedesc.DispInterface: itf = typedesc.DispInterface("IDispDerived", idispatch_type, guid, [], None) ham = typedesc.DispProperty(0, "ham", BSTR_type, ["readonly"], None) bacon = typedesc.DispMethod( - 472, 1, "bacon", VARIANT_BOOL_type, ["hidden"], None + 472, 1, "bacon", VARIANT_BOOL_type, ["hidden"], "bacon's \\ help" ) bacon.add_argument(VARIANT_type, "and", ["in"], None) bacon.add_argument(VARIANT_type, "foo", ["in"], None) @@ -71,7 +71,7 @@ def test_disp_interface(self): " def ham(self) -> hints.Incomplete: ...\n" " pass # @property # dispprop\n" " 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 bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Incomplete: "bacon\'s \\\\ help"; ...\n' # noqa " def _get_spam(self, arg1: hints.Incomplete = ..., /) -> 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" @@ -111,7 +111,9 @@ def _create_typedesc_com_interface(self) -> typedesc.ComInterface: 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 = typedesc.ComMethod( + 1, 1610678271, "bacon", HRESULT_type, [], "bacon's \\ help" + ) bacon.add_argument(VARIANT_type, "foo", ["in"], None) bacon.add_argument(VARIANT_type, "or", ["in"], None) global_ = typedesc.ComMethod( @@ -141,7 +143,7 @@ def test_com_interface(self): " def _get_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 bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Hresult: "bacon\'s \\\\ help"; ...\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, arg1: hints.Incomplete) -> hints.Hresult: ...\n" diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 20b37fd67..2b771e7f6 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -270,7 +270,10 @@ def getvalue(self, name: str) -> str: else: out = "tuple[" + ", ".join(outargs) + "]" in_ = ("self, " + ", ".join(inargs)) if inargs else "self" - content = f"def {name}({in_}) -> {out}: ..." + if self.method.doc is not None: + content = f"def {name}({in_}) -> {out}: {self.method.doc!r}; ..." + else: + content = f"def {name}({in_}) -> {out}: ..." if keyword.iskeyword(name): content = f"pass # avoid using a keyword for {content}" return content @@ -324,9 +327,15 @@ def getvalue(self, name: str) -> str: inargs.append("/") out = _to_outtype(self.method.returns) if inargs: - content = f"def {name}(self, {', '.join(inargs)}) -> {out}: ..." + if self.method.doc is not None: + content = f"def {name}(self, {', '.join(inargs)}) -> {out}: {self.method.doc!r}; ..." + else: + content = f"def {name}(self, {', '.join(inargs)}) -> {out}: ..." else: - content = f"def {name}(self) -> {out}: ..." + if self.method.doc is not None: + content = f"def {name}(self) -> {out}: {self.method.doc!r}; ..." + else: + content = f"def {name}(self) -> {out}: ..." if keyword.iskeyword(name): content = f"pass # avoid using a keyword for {content}" return content From 8682b1a364da66015c78c8b9425056c6547da63e Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 13 Oct 2026 23:38:28 +0900 Subject: [PATCH 3/8] fix: Fix overly permissive typing. --- comtypes/tools/typedesc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comtypes/tools/typedesc.py b/comtypes/tools/typedesc.py index 0b0bc49d1..755b6e02d 100644 --- a/comtypes/tools/typedesc.py +++ b/comtypes/tools/typedesc.py @@ -121,7 +121,7 @@ def add_argument( class DispProperty: # dispatchable COM property, parsed from typelib def __init__( - self, dispid: int, name: str, typ: Any, idlflags: list[str], doc: Optional[Any] + self, dispid: int, name: str, typ: Any, idlflags: list[str], doc: Optional[str] ) -> None: self.dispid = dispid self.name = name From f99341af3e2e60efc85d989748e9fde4f5e6e0a2 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 13 Oct 2026 23:38:28 +0900 Subject: [PATCH 4/8] feat: Attach docstrings to statically analyzed `DispProperty`. --- comtypes/test/test_typeannotator.py | 4 ++-- comtypes/tools/codegenerator/typeannotator.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 493ebcd9a..ba2179046 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -38,7 +38,7 @@ def _create_typedesc_disp_interface(self) -> typedesc.DispInterface: put_spam = typedesc.DispMethod(6, 4, "spam", void_type, ["propput"], None) put_spam.add_argument(VARIANT_type, "arg1", ["in", "optional"], None) put_spam.add_argument(VARIANT_type, "arg2", ["in"], None) - except_ = typedesc.DispProperty(1, "except", BSTR_type, ["readonly"], None) + except_ = typedesc.DispProperty(1, "except", BSTR_type, ["readonly"], "help") raise_ = typedesc.DispMethod( 474, 1, "raise", VARIANT_BOOL_type, ["hidden"], None ) @@ -70,7 +70,7 @@ def test_disp_interface(self): " @property # dispprop\n" " def ham(self) -> hints.Incomplete: ...\n" " pass # @property # dispprop\n" - " pass # avoid using a keyword for def except(self) -> hints.Incomplete: ...\n" # noqa + " pass # avoid using a keyword for def except(self) -> hints.Incomplete: 'help'; ...\n" # noqa ' def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Incomplete: "bacon\'s \\\\ help"; ...\n' # noqa " def _get_spam(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa " def _set_spam(self, arg1: hints.Incomplete = ..., /, *args: hints.Unpack[tuple[hints.Incomplete]]) -> hints.Incomplete: ...\n" # noqa diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 2b771e7f6..0ef12cb81 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -368,7 +368,10 @@ def generate(self) -> str: for mem in props: out = _to_outtype(mem.typ) decorator = "@property # dispprop" - content = f"def {mem.name}(self) -> {out}: ..." + if mem.doc is not None: + content = f"def {mem.name}(self) -> {out}: {mem.doc!r}; ..." + else: + content = f"def {mem.name}(self) -> {out}: ..." if keyword.iskeyword(mem.name): decorator = f"pass # {decorator}" content = f"pass # avoid using a keyword for {content}" From 6ec2a2af5fa79340196a745e03269b41caab6aee Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 13 Oct 2026 23:38:28 +0900 Subject: [PATCH 5/8] feat: Avoid attaching docstrings to setter-style methods. --- comtypes/test/test_typeannotator.py | 16 ++++++++++------ comtypes/tools/codegenerator/typeannotator.py | 9 ++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index ba2179046..c478c8d2b 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -33,9 +33,13 @@ def _create_typedesc_disp_interface(self) -> typedesc.DispInterface: ) bacon.add_argument(VARIANT_type, "and", ["in"], None) bacon.add_argument(VARIANT_type, "foo", ["in"], None) - get_spam = typedesc.DispMethod(6, 2, "spam", VARIANT_type, ["propget"], None) + get_spam = typedesc.DispMethod( + 6, 2, "spam", VARIANT_type, ["propget"], "spam help" + ) get_spam.add_argument(VARIANT_type, "arg1", ["in", "optional"], None) - put_spam = typedesc.DispMethod(6, 4, "spam", void_type, ["propput"], None) + put_spam = typedesc.DispMethod( + 6, 4, "spam", void_type, ["propput"], "spam setter" + ) put_spam.add_argument(VARIANT_type, "arg1", ["in", "optional"], None) put_spam.add_argument(VARIANT_type, "arg2", ["in"], None) except_ = typedesc.DispProperty(1, "except", BSTR_type, ["readonly"], "help") @@ -72,7 +76,7 @@ def test_disp_interface(self): " pass # @property # dispprop\n" " pass # avoid using a keyword for def except(self) -> hints.Incomplete: 'help'; ...\n" # noqa ' def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Incomplete: "bacon\'s \\\\ help"; ...\n' # noqa - " def _get_spam(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa + " def _get_spam(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: 'spam help'; ...\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 @@ -105,10 +109,10 @@ def _create_typedesc_com_interface(self) -> typedesc.ComInterface: 2, 1610678269, "spam", HRESULT_type, ["propget"], None ) get_ham = typedesc.ComMethod( - 2, 1610678270, "ham", HRESULT_type, ["propget"], None + 2, 1610678270, "ham", HRESULT_type, ["propget"], "ham help" ) put_ham = typedesc.ComMethod( - 4, 1610678270, "ham", HRESULT_type, ["propput"], None + 4, 1610678270, "ham", HRESULT_type, ["propput"], "ham setter" ) put_ham.add_argument(VARIANT_type, "arg1", ["in"], None) bacon = typedesc.ComMethod( @@ -140,7 +144,7 @@ def test_com_interface(self): expected = ( " def _get_spam(self) -> hints.Hresult: ...\n" " spam = hints.normal_property(_get_spam)\n" - " def _get_ham(self) -> hints.Hresult: ...\n" + " def _get_ham(self) -> hints.Hresult: 'ham help'; ...\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: "bacon\'s \\\\ help"; ...\n' # noqa diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 0ef12cb81..690ba1108 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -19,6 +19,9 @@ class _MethodAnnotator(abc.ABC, Generic[_T_MTD]): def __init__(self, method: _T_MTD) -> None: self.method = method + def is_put_or_putref(self) -> bool: + return "propput" in self.method.idlflags or "propputref" in self.method.idlflags + @property def inarg_specs(self) -> Sequence[tuple[Any, str, Optional[Any]]]: index = 0 @@ -270,7 +273,7 @@ def getvalue(self, name: str) -> str: else: out = "tuple[" + ", ".join(outargs) + "]" in_ = ("self, " + ", ".join(inargs)) if inargs else "self" - if self.method.doc is not None: + if self.method.doc is not None and not self.is_put_or_putref(): content = f"def {name}({in_}) -> {out}: {self.method.doc!r}; ..." else: content = f"def {name}({in_}) -> {out}: ..." @@ -327,12 +330,12 @@ def getvalue(self, name: str) -> str: inargs.append("/") out = _to_outtype(self.method.returns) if inargs: - if self.method.doc is not None: + if self.method.doc is not None and not self.is_put_or_putref(): content = f"def {name}(self, {', '.join(inargs)}) -> {out}: {self.method.doc!r}; ..." else: content = f"def {name}(self, {', '.join(inargs)}) -> {out}: ..." else: - if self.method.doc is not None: + if self.method.doc is not None and not self.is_put_or_putref(): content = f"def {name}(self) -> {out}: {self.method.doc!r}; ..." else: content = f"def {name}(self) -> {out}: ..." From e64b43251b5ca6131eb5d166013bacc785fb3da7 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 13 Oct 2026 23:38:28 +0900 Subject: [PATCH 6/8] feat: Use triple-quoted docstrings for generated `TYPE_CHECKING` annotations. --- comtypes/test/test_typeannotator.py | 10 ++++----- comtypes/tools/codegenerator/typeannotator.py | 21 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index c478c8d2b..955601492 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -74,9 +74,9 @@ def test_disp_interface(self): " @property # dispprop\n" " def ham(self) -> hints.Incomplete: ...\n" " pass # @property # dispprop\n" - " pass # avoid using a keyword for def except(self) -> hints.Incomplete: 'help'; ...\n" # noqa - ' def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Incomplete: "bacon\'s \\\\ help"; ...\n' # noqa - " def _get_spam(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: 'spam help'; ...\n" # noqa + ' pass # avoid using a keyword for def except(self) -> hints.Incomplete: """help"""; ...\n' # noqa + ' def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Incomplete: """bacon\'s \\\\ help"""; ...\n' # noqa + ' def _get_spam(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: """spam help"""; ...\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 @@ -144,10 +144,10 @@ def test_com_interface(self): expected = ( " def _get_spam(self) -> hints.Hresult: ...\n" " spam = hints.normal_property(_get_spam)\n" - " def _get_ham(self) -> hints.Hresult: 'ham help'; ...\n" + ' def _get_ham(self) -> hints.Hresult: """ham help"""; ...\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: "bacon\'s \\\\ help"; ...\n' # noqa + ' def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Hresult: """bacon\'s \\\\ help"""; ...\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, arg1: hints.Incomplete) -> hints.Hresult: ...\n" diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 690ba1108..051b70c75 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -237,6 +237,10 @@ def _generate_trailing_params(specs: Sequence[tuple[Any, str, Optional[Any]]]) - return f"*args: hints.Unpack[{params}]" +def _to_docstr(doc: str) -> str: + return '"""' + doc.replace("\\", "\\\\").replace('"', '\\"') + '"""' + + class ComMethodAnnotator(_MethodAnnotator[typedesc.ComMethod]): def _iter_outarg_specs(self) -> Iterator[tuple[Any, str]]: for typ, name, flags, _ in self.method.arguments: @@ -274,7 +278,7 @@ def getvalue(self, name: str) -> str: out = "tuple[" + ", ".join(outargs) + "]" in_ = ("self, " + ", ".join(inargs)) if inargs else "self" if self.method.doc is not None and not self.is_put_or_putref(): - content = f"def {name}({in_}) -> {out}: {self.method.doc!r}; ..." + content = f"def {name}({in_}) -> {out}: {_to_docstr(self.method.doc)}; ..." else: content = f"def {name}({in_}) -> {out}: ..." if keyword.iskeyword(name): @@ -329,16 +333,11 @@ def getvalue(self, name: str) -> str: if inargs: inargs.append("/") out = _to_outtype(self.method.returns) - if inargs: - if self.method.doc is not None and not self.is_put_or_putref(): - content = f"def {name}(self, {', '.join(inargs)}) -> {out}: {self.method.doc!r}; ..." - else: - content = f"def {name}(self, {', '.join(inargs)}) -> {out}: ..." + in_ = ("self, " + ", ".join(inargs)) if inargs else "self" + if self.method.doc is not None and not self.is_put_or_putref(): + content = f"def {name}({in_}) -> {out}: {_to_docstr(self.method.doc)}; ..." else: - if self.method.doc is not None and not self.is_put_or_putref(): - content = f"def {name}(self) -> {out}: {self.method.doc!r}; ..." - else: - content = f"def {name}(self) -> {out}: ..." + content = f"def {name}({in_}) -> {out}: ..." if keyword.iskeyword(name): content = f"pass # avoid using a keyword for {content}" return content @@ -372,7 +371,7 @@ def generate(self) -> str: out = _to_outtype(mem.typ) decorator = "@property # dispprop" if mem.doc is not None: - content = f"def {mem.name}(self) -> {out}: {mem.doc!r}; ..." + content = f"def {mem.name}(self) -> {out}: {_to_docstr(mem.doc)}; ..." else: content = f"def {mem.name}(self) -> {out}: ..." if keyword.iskeyword(mem.name): From 105b4dbf1d97dce236ef8afddf06fbb360dceb29 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 13 Oct 2026 23:38:28 +0900 Subject: [PATCH 7/8] feat: Apply getter docstrings to generated property assignments - attach the getter doc comment to generated property objects when available - keep setter-only properties untouched - preserve generated `TYPE_CHECKING` annotations as valid Python - update the related `typeannotator` regression expectations --- comtypes/test/test_typeannotator.py | 4 +-- comtypes/tools/codegenerator/typeannotator.py | 33 +++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 955601492..f5e7cdf17 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -78,7 +78,7 @@ def test_disp_interface(self): ' def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Incomplete: """bacon\'s \\\\ help"""; ...\n' # noqa ' def _get_spam(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: """spam help"""; ...\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" + ' spam = hints.named_property(\'spam\', _get_spam, _set_spam); """spam help"""\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 = ..., /, *args: hints.Unpack[tuple[hints.Incomplete]]) -> hints.Incomplete: ...\n" # noqa @@ -146,7 +146,7 @@ def test_com_interface(self): " spam = hints.normal_property(_get_spam)\n" ' def _get_ham(self) -> hints.Hresult: """ham help"""; ...\n' " def _set_ham(self, arg1: hints.Incomplete) -> hints.Hresult: ...\n" - " ham = hints.normal_property(_get_ham, _set_ham)\n" + ' ham = hints.normal_property(_get_ham, _set_ham); """ham help"""\n' ' def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Hresult: """bacon\'s \\\\ help"""; ...\n' # noqa " def _get_global(self) -> hints.Hresult: ...\n" " pass # avoid using a keyword for global = hints.normal_property(_get_global)\n" # noqa diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 051b70c75..0cb3d67b9 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -10,6 +10,7 @@ class _MethodTypeDesc(Protocol): arguments: list[tuple[Any, str, list[str], Optional[Any]]] idlflags: list[str] name: str + doc: Optional[str] _T_MTD = TypeVar("_T_MTD", bound=_MethodTypeDesc) @@ -102,7 +103,11 @@ def _patch_dunder(self, name: str) -> None: self._define_member(f"__iter__ = hints.to_dunder_iter({name})") def _define_named_prop( - self, mem_name: str, getter: Optional[str] = None, setter: Optional[str] = None + self, + mem_name: str, + getter: Optional[str] = None, + setter: Optional[str] = None, + doc: Optional[str] = None, ) -> None: if getter and setter: content = ( @@ -116,10 +121,16 @@ def _define_named_prop( return if keyword.iskeyword(mem_name): content = f"pass # avoid using a keyword for {content}" + if doc is not None: + content = f"{content}; {_to_docstr(doc)}" self._define_member(content) def _define_normal_prop( - self, mem_name: str, getter: Optional[str] = None, setter: Optional[str] = None + self, + mem_name: str, + getter: Optional[str] = None, + setter: Optional[str] = None, + doc: Optional[str] = None, ) -> None: if getter and setter: content = f"{mem_name} = hints.normal_property({getter}, {setter})" @@ -131,6 +142,8 @@ def _define_normal_prop( return if keyword.iskeyword(mem_name): content = f"pass # avoid using a keyword for {content}" + if doc is not None: + content = f"{content}; {_to_docstr(doc)}" self._define_member(content) def _define_member(self, content: str) -> None: @@ -143,9 +156,9 @@ def _gen_prop_get(self, name: str, fget: _T_MTD) -> None: getter_anno = self.to_method_annotator(fget) self._define_member(getter_anno.getvalue(f"_get_{name}")) if getter_anno.inarg_specs: - self._define_named_prop(name, f"_get_{name}") + self._define_named_prop(name, f"_get_{name}", doc=fget.doc) else: - self._define_normal_prop(name, f"_get_{name}") + self._define_normal_prop(name, f"_get_{name}", doc=fget.doc) def _gen_prop_get_put(self, name: str, fget: _T_MTD, fput: _T_MTD) -> None: getter_anno = self.to_method_annotator(fget) @@ -153,9 +166,9 @@ def _gen_prop_get_put(self, name: str, fget: _T_MTD, fput: _T_MTD) -> None: self._define_member(getter_anno.getvalue(f"_get_{name}")) self._define_member(setter_anno.getvalue(f"_set_{name}")) if getter_anno.inarg_specs: - self._define_named_prop(name, f"_get_{name}", f"_set_{name}") + self._define_named_prop(name, f"_get_{name}", f"_set_{name}", doc=fget.doc) else: - self._define_normal_prop(name, f"_get_{name}", f"_set_{name}") + self._define_normal_prop(name, f"_get_{name}", f"_set_{name}", doc=fget.doc) def _gen_prop_get_putref(self, name: str, fget: _T_MTD, fputref: _T_MTD) -> None: getter_anno = self.to_method_annotator(fget) @@ -163,9 +176,9 @@ def _gen_prop_get_putref(self, name: str, fget: _T_MTD, fputref: _T_MTD) -> None self._define_member(getter_anno.getvalue(f"_get_{name}")) self._define_member(setter_anno.getvalue(f"_setref_{name}")) if getter_anno.inarg_specs: - self._define_named_prop(name, f"_get_{name}", f"_setref_{name}") + self._define_named_prop(name, f"_get_{name}", f"_setref_{name}", doc=fget.doc) else: - self._define_normal_prop(name, f"_get_{name}", f"_setref_{name}") + self._define_normal_prop(name, f"_get_{name}", f"_setref_{name}", doc=fget.doc) def _gen_prop_get_put_putref( self, name: str, fget: _T_MTD, fput: _T_MTD, fputref: _T_MTD @@ -178,9 +191,9 @@ def _gen_prop_get_put_putref( self._define_member(putref_anno.getvalue(f"_setref_{name}")) setter = f"hints.put_or_putref(_set_{name}, _setref_{name})" if getter_anno.inarg_specs: - self._define_named_prop(name, f"_get_{name}", setter) + self._define_named_prop(name, f"_get_{name}", setter, doc=fget.doc) else: - self._define_normal_prop(name, f"_get_{name}", setter) + self._define_normal_prop(name, f"_get_{name}", setter, doc=fget.doc) def _gen_prop_put(self, name: str, fput: _T_MTD) -> None: setter_anno = self.to_method_annotator(fput) From 40b08b0eea095982fad285c4d31a9f913bd2cde0 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 13 Oct 2026 23:49:17 +0900 Subject: [PATCH 8/8] style: Apply formatter. --- comtypes/tools/codegenerator/typeannotator.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 0cb3d67b9..a9e7ca173 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -176,9 +176,13 @@ def _gen_prop_get_putref(self, name: str, fget: _T_MTD, fputref: _T_MTD) -> None self._define_member(getter_anno.getvalue(f"_get_{name}")) self._define_member(setter_anno.getvalue(f"_setref_{name}")) if getter_anno.inarg_specs: - self._define_named_prop(name, f"_get_{name}", f"_setref_{name}", doc=fget.doc) + self._define_named_prop( + name, f"_get_{name}", f"_setref_{name}", doc=fget.doc + ) else: - self._define_normal_prop(name, f"_get_{name}", f"_setref_{name}", doc=fget.doc) + self._define_normal_prop( + name, f"_get_{name}", f"_setref_{name}", doc=fget.doc + ) def _gen_prop_get_put_putref( self, name: str, fget: _T_MTD, fput: _T_MTD, fputref: _T_MTD