diff --git a/comtypes/server/register.py b/comtypes/server/register.py index 41c6ffc7..9e982e06 100644 --- a/comtypes/server/register.py +++ b/comtypes/server/register.py @@ -36,17 +36,18 @@ python mycomobj.py /nodebug """ -import sys +import logging import os +import sys import winreg -import logging +from ctypes import WinError, c_ulong, c_wchar_p, create_string_buffer, sizeof, windll +from typing import Iterator, Tuple import comtypes -from comtypes.typeinfo import LoadTypeLibEx, UnRegisterTypeLib, REGKIND_REGISTER +import comtypes.server.inprocserver from comtypes.hresult import * from comtypes.server import w_getopt -import comtypes.server.inprocserver -from ctypes import windll, c_ulong, c_wchar_p, WinError, sizeof, create_string_buffer +from comtypes.typeinfo import REGKIND_REGISTER, LoadTypeLibEx, UnRegisterTypeLib _debug = logging.getLogger(__name__).debug @@ -147,8 +148,7 @@ def register(self, cls, executable=None): self._register(cls, executable) def _register(self, cls, executable=None): - table = self._registry_entries(cls) - table.sort() + table = sorted(RegistryEntries(cls)) _debug("Registering %s", cls) for hkey, subkey, valuename, value in table: _debug("[%s\\%s]", _explain(hkey), subkey) @@ -159,7 +159,7 @@ def _register(self, cls, executable=None): tlib = getattr(cls, "_reg_typelib_", None) if tlib is not None: if hasattr(sys, "frozendllhandle"): - dll = self._get_serverdll() + dll = _get_serverdll() _debug("LoadTypeLibEx(%s, REGKIND_REGISTER)", dll) LoadTypeLibEx(dll, REGKIND_REGISTER) else: @@ -184,7 +184,7 @@ def unregister(self, cls, force=False): def _unregister(self, cls, force=False): # If force==False, we only remove those entries that we # actually would have written. It seems ATL does the same. - table = [t[:2] for t in self._registry_entries(cls)] + table = [t[:2] for t in RegistryEntries(cls)] # only unique entries table = list(set(table)) table.sort() @@ -214,16 +214,26 @@ def _unregister(self, cls, force=False): raise _debug("Done") - def _get_serverdll(self): - """Return the pathname of the dll hosting the COM object.""" - handle = getattr(sys, "frozendllhandle", None) - if handle is not None: - buf = create_string_buffer(260) - windll.kernel32.GetModuleFileNameA(handle, buf, sizeof(buf)) - return buf[:] - import _ctypes - return _ctypes.__file__ +def _get_serverdll(): + """Return the pathname of the dll hosting the COM object.""" + handle = getattr(sys, "frozendllhandle", None) + if handle is not None: + buf = create_string_buffer(260) + windll.kernel32.GetModuleFileNameA(handle, buf, sizeof(buf)) + return buf[:] + import _ctypes + + return _ctypes.__file__ + + +class RegistryEntries(object): + def __init__(self, cls): + self._cls = cls + self._table = [] + + def _add(self, rootkey: int, subkey: str, name: str, value: str) -> None: + self._table.append((rootkey, subkey, name, value)) def _get_full_classname(self, cls): """Return . for 'cls'.""" @@ -238,8 +248,8 @@ def _get_pythonpath(self, cls): dirname = os.path.dirname(sys.modules[modname].__file__) return os.path.abspath(dirname) - def _registry_entries(self, cls): - """Return a sequence of tuples containing registry entries. + def __iter__(self) -> Iterator[Tuple[int, str, str, str]]: + """Return a iterator of tuples containing registry entries. The tuples must be (key, subkey, name, value). @@ -259,16 +269,13 @@ def _registry_entries(self, cls): Note that the first part of the progid string is typically the IDL library name of the type library containing the coclass. """ + cls = self._cls HKCR = winreg.HKEY_CLASSES_ROOT - # table format: rootkey, subkey, valuename, value - table = [] - append = lambda *args: table.append(args) - # basic entry - names the comobject - reg_clsid = str( - cls._reg_clsid_ - ) # that's the only required attribute for registration + + # that's the only required attribute for registration + reg_clsid = str(cls._reg_clsid_) reg_desc = getattr(cls, "_reg_desc_", "") if not reg_desc: # Simple minded algorithm to construct a description from @@ -278,30 +285,30 @@ def _registry_entries(self, cls): ) if reg_desc: reg_desc = reg_desc.replace(".", " ") - append(HKCR, f"CLSID\\{reg_clsid}", "", reg_desc) + self._add(HKCR, f"CLSID\\{reg_clsid}", "", reg_desc) reg_progid = getattr(cls, "_reg_progid_", None) if reg_progid: # for ProgIDFromCLSID: - append(HKCR, f"CLSID\\{reg_clsid}\\ProgID", "", reg_progid) # 1 + self._add(HKCR, f"CLSID\\{reg_clsid}\\ProgID", "", reg_progid) # 1 # for CLSIDFromProgID if reg_desc: - append(HKCR, reg_progid, "", reg_desc) # 2 - append(HKCR, f"{reg_progid}\\CLSID", "", reg_clsid) # 3 + self._add(HKCR, reg_progid, "", reg_desc) # 2 + self._add(HKCR, f"{reg_progid}\\CLSID", "", reg_clsid) # 3 reg_novers_progid = getattr(cls, "_reg_novers_progid_", None) if reg_novers_progid: - append( + self._add( HKCR, f"CLSID\\{reg_clsid}\\VersionIndependentProgID", # 1a "", reg_novers_progid, ) if reg_desc: - append(HKCR, reg_novers_progid, "", reg_desc) # 2a - append(HKCR, f"{reg_novers_progid}\\CurVer", "", reg_progid) # - append(HKCR, f"{reg_novers_progid}\\CLSID", "", reg_clsid) # 3a + self._add(HKCR, reg_novers_progid, "", reg_desc) # 2a + self._add(HKCR, f"{reg_novers_progid}\\CurVer", "", reg_progid) # + self._add(HKCR, f"{reg_novers_progid}\\CLSID", "", reg_clsid) # 3a clsctx = getattr(cls, "_reg_clsctx_", 0) @@ -317,9 +324,11 @@ def _registry_entries(self, cls): script = os.path.abspath(sys.modules[cls.__module__].__file__) if " " in script: script = f'"{script}"' - append(HKCR, rf"CLSID\{reg_clsid}\LocalServer32", "", f"{exe} {script}") + self._add( + HKCR, rf"CLSID\{reg_clsid}\LocalServer32", "", f"{exe} {script}" + ) else: - append(HKCR, rf"CLSID\{reg_clsid}\LocalServer32", "", f"{exe}") + self._add(HKCR, rf"CLSID\{reg_clsid}\LocalServer32", "", f"{exe}") # Register InprocServer32 only when run from script or from # py2exe dll server, not from py2exe exe server. @@ -327,21 +336,19 @@ def _registry_entries(self, cls): None, "dll", ): - append( - HKCR, rf"CLSID\{reg_clsid}\InprocServer32", "", self._get_serverdll() - ) + self._add(HKCR, rf"CLSID\{reg_clsid}\InprocServer32", "", _get_serverdll()) # only for non-frozen inproc servers the PythonPath/PythonClass is needed. if ( not hasattr(sys, "frozendllhandle") or not comtypes.server.inprocserver._clsid_to_class ): - append( + self._add( HKCR, rf"CLSID\{reg_clsid}\InprocServer32", "PythonClass", self._get_full_classname(cls), ) - append( + self._add( HKCR, rf"CLSID\{reg_clsid}\InprocServer32", "PythonPath", @@ -350,7 +357,7 @@ def _registry_entries(self, cls): reg_threading = getattr(cls, "_reg_threading_", None) if reg_threading is not None: - append( + self._add( HKCR, rf"CLSID\{reg_clsid}\InprocServer32", "ThreadingModel", @@ -359,9 +366,9 @@ def _registry_entries(self, cls): reg_tlib = getattr(cls, "_reg_typelib_", None) if reg_tlib is not None: - append(HKCR, rf"CLSID\{reg_clsid}\Typelib", "", reg_tlib[0]) + self._add(HKCR, rf"CLSID\{reg_clsid}\Typelib", "", reg_tlib[0]) - return table + yield from self._table ################################################################ diff --git a/comtypes/test/test_server_register.py b/comtypes/test/test_server_register.py new file mode 100644 index 00000000..4c5fbfbb --- /dev/null +++ b/comtypes/test/test_server_register.py @@ -0,0 +1,235 @@ +import _ctypes +import ctypes +import os +import sys +import unittest as ut +import winreg +from unittest import mock + +import comtypes +from comtypes import GUID +from comtypes.server import register +from comtypes.server.register import RegistryEntries, _get_serverdll + +HKCR = winreg.HKEY_CLASSES_ROOT + + +class Test_get_serverdll(ut.TestCase): + def test_nonfrozen(self): + self.assertEqual(_ctypes.__file__, _get_serverdll()) + + def test_frozen(self): + with mock.patch.object(register, "sys") as _sys: + with mock.patch.object(register, "windll") as _windll: + handle = 1234 + _sys.frozendllhandle = handle + self.assertEqual(b"\x00" * 260, _get_serverdll()) + GetModuleFileName = _windll.kernel32.GetModuleFileNameA + (((hModule, lpFilename, nSize), _),) = GetModuleFileName.call_args_list + self.assertEqual(handle, hModule) + buf_type = type(ctypes.create_string_buffer(260)) + self.assertIsInstance(lpFilename, buf_type) + self.assertEqual(260, nSize) + + +class Test_RegistryEntries_NonFrozen(ut.TestCase): + def test_reg_clsid(self): + reg_clsid = GUID.create_new() + + class Cls: + _reg_clsid_ = reg_clsid + + expected = [(HKCR, rf"CLSID\{reg_clsid}", "", "")] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_reg_desc(self): + reg_clsid = GUID.create_new() + reg_desc = "description for testing" + + class Cls: + _reg_clsid_ = reg_clsid + _reg_desc_ = reg_desc + + expected = [(HKCR, rf"CLSID\{reg_clsid}", "", reg_desc)] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_reg_novers_progid(self): + reg_clsid = GUID.create_new() + reg_novers_progid = "Lib.Server" + + class Cls: + _reg_clsid_ = reg_clsid + _reg_novers_progid_ = reg_novers_progid + + expected = [(HKCR, rf"CLSID\{reg_clsid}", "", "Lib Server")] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_progid(self): + reg_clsid = GUID.create_new() + reg_progid = "Lib.Server.1" + + class Cls: + _reg_clsid_ = reg_clsid + _reg_progid_ = reg_progid + + expected = [ + (HKCR, rf"CLSID\{reg_clsid}", "", "Lib Server 1"), + (HKCR, rf"CLSID\{reg_clsid}\ProgID", "", reg_progid), + (HKCR, reg_progid, "", "Lib Server 1"), + (HKCR, rf"{reg_progid}\CLSID", "", str(reg_clsid)), + ] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_reg_progid_reg_desc(self): + reg_clsid = GUID.create_new() + reg_progid = "Lib.Server.1" + reg_desc = "description for testing" + + class Cls: + _reg_clsid_ = reg_clsid + _reg_progid_ = reg_progid + _reg_desc_ = reg_desc + + expected = [ + (HKCR, rf"CLSID\{reg_clsid}", "", reg_desc), + (HKCR, rf"CLSID\{reg_clsid}\ProgID", "", reg_progid), + (HKCR, reg_progid, "", "description for testing"), + (HKCR, rf"{reg_progid}\CLSID", "", str(reg_clsid)), + ] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_reg_progid_reg_novers_progid(self): + reg_clsid = GUID.create_new() + reg_progid = "Lib.Server.1" + reg_novers_progid = "Lib.Server" + + class Cls: + _reg_clsid_ = reg_clsid + _reg_progid_ = reg_progid + _reg_novers_progid_ = reg_novers_progid + + clsid_sub = rf"CLSID\{reg_clsid}" + expected = [ + (HKCR, clsid_sub, "", "Lib Server"), + (HKCR, rf"{clsid_sub}\ProgID", "", reg_progid), + (HKCR, reg_progid, "", "Lib Server"), + (HKCR, rf"{reg_progid}\CLSID", "", str(reg_clsid)), + (HKCR, rf"{clsid_sub}\VersionIndependentProgID", "", reg_novers_progid), + (HKCR, reg_novers_progid, "", "Lib Server"), + (HKCR, rf"{reg_novers_progid}\CurVer", "", "Lib.Server.1"), + (HKCR, rf"{reg_novers_progid}\CLSID", "", str(reg_clsid)), + ] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_local_server(self): + reg_clsid = GUID.create_new() + reg_clsctx = comtypes.CLSCTX_LOCAL_SERVER + + class Cls: + _reg_clsid_ = reg_clsid + _reg_clsctx_ = reg_clsctx + + clsid_sub = rf"CLSID\{reg_clsid}" + local_srv_sub = rf"{clsid_sub}\LocalServer32" + expected = [ + (HKCR, clsid_sub, "", ""), + (HKCR, local_srv_sub, "", f"{sys.executable} {__file__}"), + ] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_inproc_server(self): + reg_clsid = GUID.create_new() + reg_clsctx = comtypes.CLSCTX_INPROC_SERVER + + class Cls: + _reg_clsid_ = reg_clsid + _reg_clsctx_ = reg_clsctx + + clsid_sub = rf"CLSID\{reg_clsid}" + inproc_srv_sub = rf"{clsid_sub}\InprocServer32" + full_classname = f"{__name__}.Cls" + expected = [ + (HKCR, clsid_sub, "", ""), + (HKCR, inproc_srv_sub, "", _ctypes.__file__), + (HKCR, inproc_srv_sub, "PythonClass", full_classname), + (HKCR, inproc_srv_sub, "PythonPath", os.path.dirname(__file__)), + ] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_inproc_server_reg_threading(self): + reg_clsid = GUID.create_new() + reg_threading = "Both" + reg_clsctx = comtypes.CLSCTX_INPROC_SERVER + + class Cls: + _reg_clsid_ = reg_clsid + _reg_threading_ = reg_threading + _reg_clsctx_ = reg_clsctx + + clsid_sub = rf"CLSID\{reg_clsid}" + inproc_srv_sub = rf"{clsid_sub}\InprocServer32" + full_classname = f"{__name__}.Cls" + expected = [ + (HKCR, clsid_sub, "", ""), + (HKCR, inproc_srv_sub, "", _ctypes.__file__), + (HKCR, inproc_srv_sub, "PythonClass", full_classname), + (HKCR, inproc_srv_sub, "PythonPath", os.path.dirname(__file__)), + (HKCR, inproc_srv_sub, "ThreadingModel", reg_threading), + ] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_reg_typelib(self): + reg_clsid = GUID.create_new() + libid = str(GUID.create_new()) + reg_typelib = (libid, 1, 0) + + class Cls: + _reg_clsid_ = reg_clsid + _reg_typelib_ = reg_typelib + + expected = [ + (HKCR, rf"CLSID\{reg_clsid}", "", ""), + (HKCR, rf"CLSID\{reg_clsid}\Typelib", "", libid), + ] + self.assertEqual(expected, list(RegistryEntries(Cls))) + + def test_all_entries(self): + reg_clsid = GUID.create_new() + libid = str(GUID.create_new()) + reg_typelib = (libid, 1, 0) + reg_threading = "Both" + reg_progid = "Lib.Server.1" + reg_novers_progid = "Lib.Server" + reg_desc = "description for testing" + reg_clsctx = comtypes.CLSCTX_INPROC_SERVER | comtypes.CLSCTX_LOCAL_SERVER + + class Cls: + _reg_clsid_ = reg_clsid + _reg_typelib_ = reg_typelib + _reg_threading_ = reg_threading + _reg_progid_ = reg_progid + _reg_novers_progid_ = reg_novers_progid + _reg_desc_ = reg_desc + _reg_clsctx_ = reg_clsctx + + clsid_sub = rf"CLSID\{reg_clsid}" + inproc_srv_sub = rf"{clsid_sub}\InprocServer32" + local_srv_sub = rf"{clsid_sub}\LocalServer32" + full_classname = f"{__name__}.Cls" + expected = [ + (HKCR, clsid_sub, "", reg_desc), + (HKCR, rf"{clsid_sub}\ProgID", "", reg_progid), + (HKCR, reg_progid, "", reg_desc), + (HKCR, rf"{reg_progid}\CLSID", "", str(reg_clsid)), + (HKCR, rf"{clsid_sub}\VersionIndependentProgID", "", reg_novers_progid), + (HKCR, reg_novers_progid, "", reg_desc), + (HKCR, rf"{reg_novers_progid}\CurVer", "", reg_progid), + (HKCR, rf"{reg_novers_progid}\CLSID", "", str(reg_clsid)), + (HKCR, local_srv_sub, "", f"{sys.executable} {__file__}"), + (HKCR, inproc_srv_sub, "", _ctypes.__file__), + (HKCR, inproc_srv_sub, "PythonClass", full_classname), + (HKCR, inproc_srv_sub, "PythonPath", os.path.dirname(__file__)), + (HKCR, inproc_srv_sub, "ThreadingModel", reg_threading), + (HKCR, rf"{clsid_sub}\Typelib", "", libid), + ] + self.assertEqual(expected, list(RegistryEntries(Cls)))