Skip to content

Commit 39af40d

Browse files
authored
add type annotations to client._generate and revise docstrings (#376)
* add typehints and revise docstrings * fix imports * fix imports
1 parent db0931d commit 39af40d

2 files changed

Lines changed: 82 additions & 58 deletions

File tree

comtypes/client/_generate.py

Lines changed: 73 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
11
from __future__ import print_function
2-
import types
3-
import os
4-
import sys
5-
6-
import comtypes
7-
from comtypes import GUID
8-
import comtypes.client
9-
from comtypes.tools import codegenerator, tlbparser
10-
from comtypes.typeinfo import LoadRegTypeLib, LoadTypeLibEx
2+
import ctypes
113
import importlib
12-
134
import logging
14-
logger = logging.getLogger(__name__)
15-
5+
import os
6+
import sys
7+
import types
168
if sys.version_info >= (3, 0):
179
base_text_type = str
1810
import winreg
1911
else:
2012
base_text_type = basestring
2113
import _winreg as winreg
2214

15+
from comtypes import GUID, TYPE_CHECKING, typeinfo
16+
import comtypes.client
17+
from comtypes.tools import codegenerator, tlbparser
18+
19+
if TYPE_CHECKING:
20+
from typing import Any, Tuple, List, Optional, Union as _UnionT
21+
22+
23+
logger = logging.getLogger(__name__)
2324

2425
PATH = os.environ["PATH"].split(os.pathsep)
2526

2627

2728
def _my_import(fullname):
29+
# type: (str) -> types.ModuleType
2830
"""helper function to import dotted modules"""
29-
import comtypes.gen
30-
if comtypes.client.gen_dir \
31-
and comtypes.client.gen_dir not in comtypes.gen.__path__:
32-
comtypes.gen.__path__.append(comtypes.client.gen_dir)
31+
import comtypes.gen as g
32+
if comtypes.client.gen_dir and comtypes.client.gen_dir not in g.__path__:
33+
g.__path__.append(comtypes.client.gen_dir) # type: ignore
3334
return importlib.import_module(fullname)
3435

3536

3637
def _resolve_filename(tlib_string, dirpath):
38+
# type: (str, str) -> Tuple[str, bool]
3739
"""Tries to make sense of a type library specified as a string.
38-
40+
3941
Args:
4042
tlib_string: type library designator
4143
dirpath: a directory to relativize the location
@@ -63,57 +65,59 @@ def _resolve_filename(tlib_string, dirpath):
6365

6466

6567
def GetModule(tlib):
68+
# type: (_UnionT[Any, typeinfo.ITypeLib]) -> types.ModuleType
6669
"""Create a module wrapping a COM typelibrary on demand.
6770
68-
'tlib' must be an ITypeLib COM pointer instance, the pathname of a
69-
type library, a COM CLSID GUID, or a tuple/list specifying the
70-
arguments to a comtypes.typeinfo.LoadRegTypeLib call:
71-
72-
(libid, wMajorVerNum, wMinorVerNum, lcid=0)
73-
74-
Or it can be an object with _reg_libid_ and _reg_version_
75-
attributes.
76-
77-
A relative pathname is interpreted as relative to the callers
78-
__file__, if this exists.
71+
'tlib' must be ...
72+
- an `ITypeLib` COM pointer instance
73+
- an absolute pathname of a type library
74+
- a relative pathname of a type library
75+
- interpreted as relative to the callers `__file__`, if this exists
76+
- a COM CLSID `GUID`
77+
- a `tuple`/`list` specifying the typelib
78+
- `List[_UnionT[str, int]]`
79+
- `(libid: str[, wMajorVerNum: int, wMinorVerNum: int[, lcid: int]])`
80+
- an object with `_reg_libid_: str` and `_reg_version_: Iterable[int]`
7981
8082
This function determines the module name from the typelib
8183
attributes, then tries to import it. If that fails because the
8284
module doesn't exist, the module is generated into the
83-
comtypes.gen package.
85+
`comtypes.gen` package.
8486
8587
It is possible to delete the whole `comtypes/gen` directory to
86-
remove all generated modules, the directory and the __init__.py
88+
remove all generated modules, the directory and the `__init__.py`
8789
file in it will be recreated when needed.
8890
89-
If comtypes.gen __path__ is not a directory (in a frozen
91+
If `comtypes.gen.__path__` is not a directory (in a frozen
9092
executable it lives in a zip archive), generated modules are only
9193
created in memory without writing them to the file system.
9294
9395
Example:
94-
95-
GetModule("shdocvw.dll")
96+
GetModule("UIAutomationCore.dll")
9697
9798
would create modules named
9899
99-
comtypes.gen._EAB22AC0_30C1_11CF_A7EB_0000C05BAE0B_0_1_1
100-
comtypes.gen.SHDocVw
100+
`comtypes.gen._944DE083_8FB8_45CF_BCB7_C477ACB2F897_L_M_m`
101+
- typelib wrapper module
102+
- where L, M, m are numbers of Lcid, Major-ver, minor-ver
103+
`comtypes.gen.UIAutomationClient`
104+
- friendly named module
101105
102106
containing the Python wrapper code for the type library used by
103-
Internet Explorer. The former module contains all the code, the
107+
UIAutomation. The former module contains all the code, the
104108
latter is a short stub loading the former.
105109
"""
106110
if isinstance(tlib, base_text_type):
107111
tlib_string = tlib
108-
# if a relative pathname is used, we try to interpret it relative to the
109-
# directory of the calling module (if not from command line)
112+
# if a relative pathname is used, we try to interpret it relative to
113+
# the directory of the calling module (if not from command line)
110114
frame = sys._getframe(1)
111-
_file_ = frame.f_globals.get("__file__", None)
115+
_file_ = frame.f_globals.get("__file__", None) # type: str
112116
pathname, is_abs = _resolve_filename(tlib_string, _file_ and os.path.dirname(_file_))
113117
logger.debug("GetModule(%s), resolved: %s", pathname, is_abs)
114118
tlib = _load_tlib(pathname) # don't register
115119
if not is_abs:
116-
# try to get path after loading, but this only works if already registered
120+
# try to get path after loading, but this only works if already registered
117121
pathname = tlbparser.get_tlib_filename(tlib)
118122
if pathname is None:
119123
logger.info("GetModule(%s): could not resolve to a filename", tlib)
@@ -137,36 +141,40 @@ def GetModule(tlib):
137141

138142

139143
def _load_tlib(obj):
144+
# type: (Any) -> typeinfo.ITypeLib
140145
"""Load a pointer of ITypeLib on demand."""
141146
# obj is a filepath or a ProgID
142147
if isinstance(obj, base_text_type):
143148
# in any case, attempt to load and if tlib_string is not valid, then raise
144149
# as "OSError: [WinError -2147312566] Error loading type library/DLL"
145-
return LoadTypeLibEx(obj)
150+
return typeinfo.LoadTypeLibEx(obj)
146151
# obj is a tlib GUID contain a clsid
147152
elif isinstance(obj, GUID):
148153
clsid = str(obj)
149154
# lookup associated typelib in registry
150155
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"CLSID\%s\TypeLib" % clsid) as key:
151156
libid = winreg.EnumValue(key, 0)[1]
152157
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"CLSID\%s\Version" % clsid) as key:
153-
version = winreg.EnumValue(key, 0)[1].split(".")
154-
return LoadRegTypeLib(GUID(libid), int(version[0]), int(version[1]), 0)
158+
ver = winreg.EnumValue(key, 0)[1].split(".")
159+
return typeinfo.LoadRegTypeLib(GUID(libid), int(ver[0]), int(ver[1]), 0)
155160
# obj is a sequence containing libid
156161
elif isinstance(obj, (tuple, list)):
157-
libid, version = obj[0], obj[1:]
158-
if not version: # case of version numbers are not containing
162+
libid, ver = obj[0], obj[1:]
163+
if not ver: # case of version numbers are not containing
159164
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"TypeLib\%s" % libid) as key:
160-
version = [int(v, base=16) for v in winreg.EnumKey(key, 0).split(".")]
161-
return LoadRegTypeLib(GUID(libid), *version)
165+
ver = [int(v, base=16) for v in winreg.EnumKey(key, 0).split(".")]
166+
return typeinfo.LoadRegTypeLib(GUID(libid), *ver)
162167
# obj is a COMObject implementation
163168
elif hasattr(obj, "_reg_libid_"):
164-
return LoadRegTypeLib(GUID(obj._reg_libid_), *obj._reg_version_)
165-
# perhaps obj is a pointer of ITypeLib
166-
return obj
169+
return typeinfo.LoadRegTypeLib(GUID(obj._reg_libid_), *obj._reg_version_)
170+
# obj is a pointer of ITypeLib
171+
elif isinstance(obj, ctypes.POINTER(typeinfo.ITypeLib)):
172+
return obj # type: ignore
173+
raise TypeError("'%r' is not supported type for loading typelib" % obj)
167174

168175

169176
def _create_module_in_file(modulename, code):
177+
# type: (str, str) -> types.ModuleType
170178
"""create module in file system, and import it"""
171179
# `modulename` is 'comtypes.gen.xxx'
172180
filename = "%s.py" % modulename.split(".")[-1]
@@ -179,18 +187,21 @@ def _create_module_in_file(modulename, code):
179187

180188

181189
def _create_module_in_memory(modulename, code):
190+
# type: (str, str) -> types.ModuleType
182191
"""create module in memory system, and import it"""
183192
# `modulename` is 'comtypes.gen.xxx'
193+
import comtypes.gen as g
184194
mod = types.ModuleType(modulename)
185-
abs_gen_path = os.path.abspath(comtypes.gen.__path__[0])
195+
abs_gen_path = os.path.abspath(g.__path__[0]) # type: ignore
186196
mod.__file__ = os.path.join(abs_gen_path, "<memory>")
187197
exec(code, mod.__dict__)
188198
sys.modules[modulename] = mod
189-
setattr(comtypes.gen, modulename.split(".")[-1], mod)
199+
setattr(g, modulename.split(".")[-1], mod)
190200
return mod
191201

192202

193203
def _create_friendly_module(tlib, modulename):
204+
# type: (typeinfo.ITypeLib, str) -> types.ModuleType
194205
"""helper which creates and imports the friendly-named module."""
195206
try:
196207
mod = _my_import(modulename)
@@ -211,6 +222,7 @@ def _create_friendly_module(tlib, modulename):
211222

212223

213224
def _create_wrapper_module(tlib, pathname):
225+
# type: (typeinfo.ITypeLib, Optional[str]) -> types.ModuleType
214226
"""helper which creates and imports the real typelib wrapper module."""
215227
modulename = codegenerator.name_wrapper_module(tlib)
216228
if modulename in sys.modules:
@@ -235,13 +247,16 @@ def _create_wrapper_module(tlib, pathname):
235247

236248

237249
def _get_known_symbols():
250+
# type: () -> dict[str, str]
238251
known_symbols = {} # type: dict[str, str]
239-
for mod_name in ("comtypes.persist",
240-
"comtypes.typeinfo",
241-
"comtypes.automation",
242-
"comtypes",
243-
"ctypes.wintypes",
244-
"ctypes"):
252+
for mod_name in (
253+
"comtypes.persist",
254+
"comtypes.typeinfo",
255+
"comtypes.automation",
256+
"comtypes",
257+
"ctypes.wintypes",
258+
"ctypes"
259+
):
245260
mod = importlib.import_module(mod_name)
246261
if hasattr(mod, "__known_symbols__"):
247262
names = mod.__known_symbols__ # type: list[str]

comtypes/test/test_client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ def test_clsid(self):
6969
mod = comtypes.client.GetModule(clsid)
7070
self.assertEqual(mod.MediaPlayer._reg_clsid_, clsid)
7171

72+
def test_ptr_itypelib(self):
73+
from comtypes import typeinfo
74+
mod = comtypes.client.GetModule(typeinfo.LoadTypeLibEx("scrrun.dll"))
75+
self.assertIs(mod, Scripting)
76+
7277
def test_imports_IEnumVARIANT_from_other_generated_modules(self):
7378
# NOTE: `codegenerator` generates code that contains unused imports,
7479
# but removing them are attracting wierd bugs in library-wrappers
@@ -84,6 +89,10 @@ def test_no_replacing_Patch_namespace(self):
8489
# NOTE: `WindowsInstaller`, which has `Patch` definition in dll.
8590
comtypes.client.GetModule("msi.dll")
8691

92+
def test_raises_typerror_if_takes_unsupported(self):
93+
with self.assertRaises(TypeError):
94+
comtypes.client.GetModule(object())
95+
8796

8897
class Test_KnownSymbols(ut.TestCase):
8998
# It is guaranteed that each element of `__known_symbols__` is in

0 commit comments

Comments
 (0)