From c13d6680a69551c7f90842ae65adee7c2305f36e Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 01/11] add `Library` to generated modules' `__all__`. because that symbol is public but not included. --- comtypes/tools/codegenerator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index 48a34e42..bf87db21 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -876,6 +876,7 @@ def TypeLib(self, lib: typedesc.TypeLib) -> None: ) print(file=self.stream) print(file=self.stream) + self.names.add("Library") def External(self, ext: typedesc.External) -> None: modname = name_wrapper_module(ext.tlib) From 9171e080b2deddbc693d0c3a9b4fe3b6e9ab63ff Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 02/11] add `typelib_path` to generated modules' `__all__`. because that symbol is public but not included. --- comtypes/tools/codegenerator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index bf87db21..db42e351 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -495,6 +495,7 @@ def _generate_typelib_path(self, filename): os.path.abspath(os.path.join(comtypes.gen.__path__[0], path)) ) assert os.path.isfile(p) + self.names.add("typelib_path") def generate_code(self, items, filename): From 93e67ad0a961b939cf22ab97ce0141ef860a60da Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 03/11] make `ModuleGenerator` class that encapsulates `CodeGenerator` instance. --- comtypes/client/_generate.py | 115 +++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 54 deletions(-) diff --git a/comtypes/client/_generate.py b/comtypes/client/_generate.py index 0fa9aca9..cbeef2b5 100644 --- a/comtypes/client/_generate.py +++ b/comtypes/client/_generate.py @@ -121,14 +121,7 @@ def GetModule(tlib: _UnionT[Any, typeinfo.ITypeLib]) -> types.ModuleType: pathname = None tlib = _load_tlib(tlib) logger.debug("GetModule(%s)", tlib.GetLibAttr()) - # create and import the real typelib wrapper module - mod = _create_wrapper_module(tlib, pathname) - # try to get the friendly-name, if not, returns the real typelib wrapper module - modulename = codegenerator.name_friendly_module(tlib) - if modulename is None: - return mod - # create and import the friendly-named module - return _create_friendly_module(tlib, modulename) + return ModuleGenerator().generate(tlib, pathname) def _load_tlib(obj: Any) -> typeinfo.ITypeLib: @@ -193,52 +186,66 @@ def _create_module_in_memory(modulename: str, code: str) -> types.ModuleType: return mod -def _create_friendly_module( - tlib: typeinfo.ITypeLib, modulename: str -) -> types.ModuleType: - """helper which creates and imports the friendly-named module.""" - try: - mod = _my_import(modulename) - except Exception as details: - logger.info("Could not import %s: %s", modulename, details) - else: - return mod - # the module is always regenerated if the import fails - logger.info("# Generating %s", modulename) - # determine the Python module name - modname = codegenerator.name_wrapper_module(tlib).split(".")[-1] - code = "from comtypes.gen import %s\n" % modname - code += "globals().update(%s.__dict__)\n" % modname - code += "__name__ = '%s'" % modulename - if comtypes.client.gen_dir is None: - return _create_module_in_memory(modulename, code) - return _create_module_in_file(modulename, code) - - -def _create_wrapper_module( - tlib: typeinfo.ITypeLib, pathname: Optional[str] -) -> types.ModuleType: - """helper which creates and imports the real typelib wrapper module.""" - modulename = codegenerator.name_wrapper_module(tlib) - if modulename in sys.modules: - return sys.modules[modulename] - try: - return _my_import(modulename) - except Exception as details: - logger.info("Could not import %s: %s", modulename, details) - # generate the module since it doesn't exist or is out of date - logger.info("# Generating %s", modulename) - p = tlbparser.TypeLibParser(tlib) - if pathname is None: - pathname = tlbparser.get_tlib_filename(tlib) - items = list(p.parse().values()) - codegen = codegenerator.CodeGenerator(_get_known_symbols()) - code = codegen.generate_code(items, filename=pathname) - for ext_tlib in codegen.externals: # generates dependency COM-lib modules - GetModule(ext_tlib) - if comtypes.client.gen_dir is None: - return _create_module_in_memory(modulename, code) - return _create_module_in_file(modulename, code) +class ModuleGenerator(object): + def __init__(self): + self.codegen = codegenerator.CodeGenerator(_get_known_symbols()) + + def generate( + self, tlib: typeinfo.ITypeLib, pathname: Optional[str] + ) -> types.ModuleType: + # create and import the real typelib wrapper module + mod = self._create_wrapper_module(tlib, pathname) + # try to get the friendly-name, if not, returns the real typelib wrapper module + modulename = codegenerator.name_friendly_module(tlib) + if modulename is None: + return mod + # create and import the friendly-named module + return self._create_friendly_module(tlib, modulename) + + def _create_friendly_module( + self, tlib: typeinfo.ITypeLib, modulename: str + ) -> types.ModuleType: + """helper which creates and imports the friendly-named module.""" + try: + mod = _my_import(modulename) + except Exception as details: + logger.info("Could not import %s: %s", modulename, details) + else: + return mod + # the module is always regenerated if the import fails + logger.info("# Generating %s", modulename) + # determine the Python module name + modname = codegenerator.name_wrapper_module(tlib).split(".")[-1] + code = "from comtypes.gen import %s\n" % modname + code += "globals().update(%s.__dict__)\n" % modname + code += "__name__ = '%s'" % modulename + if comtypes.client.gen_dir is None: + return _create_module_in_memory(modulename, code) + return _create_module_in_file(modulename, code) + + def _create_wrapper_module( + self, tlib: typeinfo.ITypeLib, pathname: Optional[str] + ) -> types.ModuleType: + """helper which creates and imports the real typelib wrapper module.""" + modulename = codegenerator.name_wrapper_module(tlib) + if modulename in sys.modules: + return sys.modules[modulename] + try: + return _my_import(modulename) + except Exception as details: + logger.info("Could not import %s: %s", modulename, details) + # generate the module since it doesn't exist or is out of date + logger.info("# Generating %s", modulename) + p = tlbparser.TypeLibParser(tlib) + if pathname is None: + pathname = tlbparser.get_tlib_filename(tlib) + items = list(p.parse().values()) + code = self.codegen.generate_code(items, filename=pathname) + for ext_tlib in self.codegen.externals: # generates dependency COM-lib modules + GetModule(ext_tlib) + if comtypes.client.gen_dir is None: + return _create_module_in_memory(modulename, code) + return _create_module_in_file(modulename, code) def _get_known_symbols() -> Dict[str, str]: From 9c517118fdbe785494fd9e05aeaae89dd66094c8 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 04/11] rename to `generate_wrapper_code` from `generate_code` --- comtypes/client/_generate.py | 2 +- comtypes/tools/codegenerator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comtypes/client/_generate.py b/comtypes/client/_generate.py index cbeef2b5..a9a71b95 100644 --- a/comtypes/client/_generate.py +++ b/comtypes/client/_generate.py @@ -240,7 +240,7 @@ def _create_wrapper_module( if pathname is None: pathname = tlbparser.get_tlib_filename(tlib) items = list(p.parse().values()) - code = self.codegen.generate_code(items, filename=pathname) + code = self.codegen.generate_wrapper_code(items, filename=pathname) for ext_tlib in self.codegen.externals: # generates dependency COM-lib modules GetModule(ext_tlib) if comtypes.client.gen_dir is None: diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index db42e351..052ed13b 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -497,7 +497,7 @@ def _generate_typelib_path(self, filename): assert os.path.isfile(p) self.names.add("typelib_path") - def generate_code(self, items, filename): + def generate_wrapper_code(self, items, filename): tlib_mtime = None From 2c662b8e2d4045af20e1a3d0e7aa0e3bfa4d9b2a Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 05/11] add `generate_friendly_code` --- comtypes/client/_generate.py | 6 ++---- comtypes/tools/codegenerator.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/comtypes/client/_generate.py b/comtypes/client/_generate.py index a9a71b95..9b8d7654 100644 --- a/comtypes/client/_generate.py +++ b/comtypes/client/_generate.py @@ -215,10 +215,8 @@ def _create_friendly_module( # the module is always regenerated if the import fails logger.info("# Generating %s", modulename) # determine the Python module name - modname = codegenerator.name_wrapper_module(tlib).split(".")[-1] - code = "from comtypes.gen import %s\n" % modname - code += "globals().update(%s.__dict__)\n" % modname - code += "__name__ = '%s'" % modulename + modname = codegenerator.name_wrapper_module(tlib) + code = self.codegen.generate_friendly_code(modname) if comtypes.client.gen_dir is None: return _create_module_in_memory(modulename, code) return _create_module_in_file(modulename, code) diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index 052ed13b..97acc3de 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -558,6 +558,28 @@ def generate_wrapper_code(self, items, filename): print("_check_version(%r, %f)" % (version, tlib_mtime), file=output) return output.getvalue() + def generate_friendly_code(self, modname: str) -> str: + # `modname` is wrapper module name like `comtypes.gen._xxxx..._x_x_x` + output = io.StringIO() + txtwrapper = textwrap.TextWrapper( + subsequent_indent=" ", initial_indent=" ", break_long_words=False + ) + joined_names = ", ".join(str(n) for n in self.names) + symbols = f"from {modname} import {joined_names}" + if len(symbols) > 80: + wrapped_names = "\n".join(txtwrapper.wrap(joined_names)) + symbols = f"from {modname} import (\n{wrapped_names}\n)" + print(symbols, file=output) + print(file=output) + print(file=output) + quoted_names = ", ".join(repr(str(n)) for n in self.names) + dunder_all = "__all__ = [%s]" % quoted_names + if len(dunder_all) > 80: + wrapped_quoted_names = "\n".join(txtwrapper.wrap(quoted_names)) + dunder_all = "__all__ = [\n%s\n]" % wrapped_quoted_names + print(dunder_all, file=output) + return output.getvalue() + def need_VARIANT_imports(self, value): text = repr(value) if "Decimal(" in text: From 5af032aa6bbdd199771d4e8e644789fe03ce89b7 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 06/11] add type annotations to `generate_wrapper_code` --- comtypes/tools/codegenerator.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index 97acc3de..bcdd8f80 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -7,7 +7,16 @@ import os import sys import textwrap -from typing import Any, Dict, Iterator, List, Optional, Tuple, Union as _UnionT +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union as _UnionT, +) import io import comtypes @@ -497,7 +506,9 @@ def _generate_typelib_path(self, filename): assert os.path.isfile(p) self.names.add("typelib_path") - def generate_wrapper_code(self, items, filename): + def generate_wrapper_code( + self, tdescs: Sequence[Any], filename: Optional[str] + ) -> str: tlib_mtime = None @@ -521,7 +532,7 @@ def generate_wrapper_code(self, items, filename): self.declarations.add("_lcid", "0", "change this if required") self._generate_typelib_path(filename) - items = set(items) + items = set(tdescs) loops = 0 while items: loops += 1 From 9cf530527cc0647a7a6cd1c5c3ffbde48d25d677 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 07/11] add docstring --- comtypes/tools/codegenerator.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index bcdd8f80..bbffea5c 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -509,7 +509,15 @@ def _generate_typelib_path(self, filename): def generate_wrapper_code( self, tdescs: Sequence[Any], filename: Optional[str] ) -> str: + """Returns the code for the COM type library wrapper module. + The returned `Python` code string is containing definitions of interfaces, + coclasses, constants, and structures. + + The module will have long name that is derived from the type library guid, lcid + and version numbers. + Such as `comtypes.gen._xxxxxxxx_xxxx_xxxx_xxxx_xxxxxxxxxxxx_l_M_m`. + """ tlib_mtime = None if filename is not None: @@ -570,7 +578,15 @@ def generate_wrapper_code( return output.getvalue() def generate_friendly_code(self, modname: str) -> str: - # `modname` is wrapper module name like `comtypes.gen._xxxx..._x_x_x` + """Returns the code for the COM type library friendly module. + + The returned `Python` code string is containing `from {modname} import + DefinedInWrapper, ...` and `__all__ = ['DefinedInWrapper', ...]` + The `modname` is the wrapper module name like `comtypes.gen._xxxx..._x_x_x`. + + The module will have shorter name that is derived from the type library name. + Such as "comtypes.gen.stdole" and "comtypes.gen.Excel". + """ output = io.StringIO() txtwrapper = textwrap.TextWrapper( subsequent_indent=" ", initial_indent=" ", break_long_words=False From 433e42c6b11228355e5192927fc382f4c9dece7f Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 08/11] add `get_symbols` methods to `DeclaredNamespaces` and `ImportedNamespaces` --- comtypes/tools/codegenerator.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index bbffea5c..32e4aa7d 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -14,6 +14,7 @@ List, Optional, Sequence, + Set, Tuple, Union as _UnionT, ) @@ -1380,6 +1381,10 @@ def add(self, name1, name2=None, symbols=None): IUnknown ) import ctypes.wintypes + >>> assert imports.get_symbols() == { + ... 'Decimal', 'GUID', 'COMMETHOD', 'DISPMETHOD', 'IUnknown', + ... 'dispid', 'CoClass', 'BSTR', 'DISPPROPERTY' + ... } >>> print(imports.getvalue(for_stub=True)) from ctypes import * import datetime @@ -1432,6 +1437,14 @@ def __contains__(self, item): return self.data[import_] == from_ return False + def get_symbols(self) -> Set[str]: + names = set() + for key, val in self.data.items(): + if val is None or key == "*": + continue + names.add(key) + return names + def _make_line(self, from_, imports, for_stub): if for_stub: import_ = ", ".join("%s as %s" % (n, n) for n in imports) @@ -1483,9 +1496,18 @@ def add(self, alias, definition, comment=None): >>> print(declarations.getvalue()) STRING = c_char_p _lcid = 0 # change this if required + >>> assert declarations.get_symbols() == { + ... 'STRING', '_lcid' + ... } """ self.data[(alias, definition)] = comment + def get_symbols(self) -> Set[str]: + names = set() + for alias, _ in self.data.keys(): + names.add(alias) + return names + def getvalue(self): lines = [] for (alias, definition), comment in self.data.items(): From 4d01edc6a8b960a9612318a6792c5162e7878f7e Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 09/11] update imporing symbols --- comtypes/tools/codegenerator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index 32e4aa7d..0ead4dbb 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -592,7 +592,10 @@ def generate_friendly_code(self, modname: str) -> str: txtwrapper = textwrap.TextWrapper( subsequent_indent=" ", initial_indent=" ", break_long_words=False ) - joined_names = ", ".join(str(n) for n in self.names) + importing_symbols = set(self.names) + importing_symbols.update(self.imports.get_symbols()) + importing_symbols.update(self.declarations.get_symbols()) + joined_names = ", ".join(str(n) for n in importing_symbols) symbols = f"from {modname} import {joined_names}" if len(symbols) > 80: wrapped_names = "\n".join(txtwrapper.wrap(joined_names)) From 226912cff51661627a67fb4d58d6dbe74b02c08f Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 10/11] add type annotation to return value for `__init__` --- comtypes/client/_generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comtypes/client/_generate.py b/comtypes/client/_generate.py index 9b8d7654..9468293e 100644 --- a/comtypes/client/_generate.py +++ b/comtypes/client/_generate.py @@ -187,7 +187,7 @@ def _create_module_in_memory(modulename: str, code: str) -> types.ModuleType: class ModuleGenerator(object): - def __init__(self): + def __init__(self) -> None: self.codegen = codegenerator.CodeGenerator(_get_known_symbols()) def generate( From 38e5a9c7c73b705dfebeb7716d3eb428670d6d20 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 17 Jan 2023 08:34:47 +0900 Subject: [PATCH 11/11] change to using f-string in `generate_friendly_code` --- comtypes/tools/codegenerator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/comtypes/tools/codegenerator.py b/comtypes/tools/codegenerator.py index 0ead4dbb..8bc98686 100644 --- a/comtypes/tools/codegenerator.py +++ b/comtypes/tools/codegenerator.py @@ -604,10 +604,10 @@ def generate_friendly_code(self, modname: str) -> str: print(file=output) print(file=output) quoted_names = ", ".join(repr(str(n)) for n in self.names) - dunder_all = "__all__ = [%s]" % quoted_names + dunder_all = f"__all__ = [{quoted_names}]" if len(dunder_all) > 80: wrapped_quoted_names = "\n".join(txtwrapper.wrap(quoted_names)) - dunder_all = "__all__ = [\n%s\n]" % wrapped_quoted_names + dunder_all = f"__all__ = [\n{wrapped_quoted_names}\n]" print(dunder_all, file=output) return output.getvalue()