From 76ee022bad88abca9f2359ed0fb8ad056eba5924 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:00 +0900 Subject: [PATCH 1/9] test: Remove mocks and use live COM objects in `test_client_dynamic.py`. Refactor `dynamic.Dispatch` tests in `test_client_dynamic.py` by removing `mock` usage. Tests now directly create COM objects with `CreateObject` to validate `lazybind.Dispatch` and `dynamic._Dispatch` instances more accurately. --- comtypes/test/test_client_dynamic.py | 49 +++++++++++++--------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/comtypes/test/test_client_dynamic.py b/comtypes/test/test_client_dynamic.py index 1825c0dc..283624bf 100644 --- a/comtypes/test/test_client_dynamic.py +++ b/comtypes/test/test_client_dynamic.py @@ -1,37 +1,32 @@ import ctypes import unittest as ut -from unittest import mock -from comtypes import COMError, IUnknown, automation +from comtypes import IUnknown, automation from comtypes.client import CreateObject, GetModule, dynamic, lazybind class Test_Dispatch_Function(ut.TestCase): - # It is difficult to cause intentionally errors "in the regular way". - # So `mock` is used to cover conditional branches. - def test_returns_dynamic_Dispatch_if_takes_dynamic_Dispatch(self): - obj = mock.MagicMock(spec=dynamic._Dispatch) - self.assertIs(dynamic.Dispatch(obj), obj) - - def test_returns_lazybind_Dispatch_if_takes_ptrIDispatch(self): - # Conditional branches that return `lazybind.Dispatch` are also covered by - # `test_dyndispatch` and others. - obj = mock.MagicMock(spec=ctypes.POINTER(automation.IDispatch)) - self.assertIsInstance(dynamic.Dispatch(obj), lazybind.Dispatch) - - def test_returns_dynamic_Dispatch_if_takes_ptrIDispatch_and_raised_comerr(self): - obj = mock.MagicMock(spec=ctypes.POINTER(automation.IDispatch)) - obj.GetTypeInfo.side_effect = COMError(0, "test", ("", "", "", 0, 0)) - self.assertIsInstance(dynamic.Dispatch(obj), dynamic._Dispatch) - - def test_returns_dynamic_Dispatch_if_takes_ptrIDispatch_and_raised_winerr(self): - obj = mock.MagicMock(spec=ctypes.POINTER(automation.IDispatch)) - obj.GetTypeInfo.side_effect = OSError() - self.assertIsInstance(dynamic.Dispatch(obj), dynamic._Dispatch) - - def test_returns_what_is_took_if_takes_other(self): - obj = object() - self.assertIs(dynamic.Dispatch(obj), obj) + def test_returns_lazybind_Dispatch(self): + # When `dynamic=True`, objects providing type information will return a + # `lazybind.Dispatch` instance. + orig = CreateObject("Scripting.Dictionary", interface=automation.IDispatch) + disp = dynamic.Dispatch(orig) + self.assertIsInstance(disp, lazybind.Dispatch) + # Calling `dynamic.Dispatch` with an already dispatched object should + # return the same instance. + self.assertIs(disp, dynamic.Dispatch(disp)) + + def test_returns_dynamic_Dispatch(self): + # When `dynamic=True`, objects that do NOT provide type information (or + # fail to provide it) will return a `dynamic._Dispatch` instance. + orig = CreateObject( + "WindowsInstaller.Installer", interface=automation.IDispatch + ) + disp = dynamic.Dispatch(orig) + self.assertIsInstance(disp, dynamic._Dispatch) + # Calling `dynamic.Dispatch` on an already dispatched object should + # return the same instance. + self.assertIs(disp, dynamic.Dispatch(disp)) class Test_Dispatch_Class(ut.TestCase): From 4bc5cc5c4e44fd80b71b5d5a16c734f71a479528 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:00 +0900 Subject: [PATCH 2/9] test: Rename `Test_Dispatch_Class` to `Test_dynamic_Dispatch` and introduce `Test_lazybind_Dispatch`. Adjust index access assertion in `lazybind.Dispatch` tests to reflect its `None` return for non-existent keys, contrasting with `dynamic._Dispatch`'s `IndexError`. Add comments explaining these behaviors. --- comtypes/test/test_client_dynamic.py | 31 +++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/comtypes/test/test_client_dynamic.py b/comtypes/test/test_client_dynamic.py index 283624bf..84d0c9af 100644 --- a/comtypes/test/test_client_dynamic.py +++ b/comtypes/test/test_client_dynamic.py @@ -29,7 +29,7 @@ def test_returns_dynamic_Dispatch(self): self.assertIs(disp, dynamic.Dispatch(disp)) -class Test_Dispatch_Class(ut.TestCase): +class Test_dynamic_Dispatch(ut.TestCase): # `MethodCaller` and `_Collection` are indirectly covered in this. def test_dict(self): # The following conditional branches are not covered; @@ -52,11 +52,40 @@ def test_dict(self): scr_dict = d.QueryInterface(scrrun.IDictionary) self.assertIsInstance(scr_dict, scrrun.IDictionary) d.Item["qux"] = scr_dict + # `dynamic._Dispatch` reflects the underlying COM object's behavior. + # For `Scripting.Dictionary`, out-of-bounds index access via `IDispatch` + # typically results in a `COMError`, which is wrapped as `IndexError`. with self.assertRaises(IndexError): d[4] with self.assertRaises(AttributeError): d.__foo__ +class Test_lazybind_Dispatch(ut.TestCase): + def test_dict(self): + orig = CreateObject("Scripting.Dictionary", interface=automation.IDispatch) + tinfo = orig.GetTypeInfo(0) + d = lazybind.Dispatch(orig, tinfo) + d.CompareMode = 42 + d.Item["foo"] = 1 + d.Item["bar"] = "spam foo" + d.Item["baz"] = 3.14 + self.assertEqual(d.Item["foo"], 1) + self.assertEqual([k for k in iter(d)], ["foo", "bar", "baz"]) + self.assertIsInstance(hash(d), int) + # No `_FlagAsMethod` in `lazybind.Dispatch` + self.assertIs(type(d._NewEnum()), ctypes.POINTER(IUnknown)) + scrrun = GetModule("scrrun.dll") + scr_dict = d.QueryInterface(scrrun.IDictionary) + self.assertIsInstance(scr_dict, scrrun.IDictionary) + d.Item["qux"] = scr_dict + # `lazybind.Dispatch`, using type information, might return `None` for + # non-existent keys when accessed via direct index (`d[4]`), + # as it doesn't directly map to the `Item` property's error handling. + self.assertIsNone(d[4]) + with self.assertRaises(AttributeError): + d.__foo__ + + if __name__ == "__main__": ut.main() From abf30a7ca7561c2b28c13c5e216f8bf818fd070b Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:00 +0900 Subject: [PATCH 3/9] test: Enhance `test_client_dynamic.py` with non-existent member access tests. Add specific tests for `dynamic.Dispatch` and `lazybind.Dispatch` to verify behavior when attempting to access non-existent members. `dynamic.Dispatch` is expected to raise `COMError` with `hresult.DISP_E_UNKNOWNNAME`, while `lazybind.Dispatch` should raise `NameError`. --- comtypes/test/test_client_dynamic.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/comtypes/test/test_client_dynamic.py b/comtypes/test/test_client_dynamic.py index 84d0c9af..2229a525 100644 --- a/comtypes/test/test_client_dynamic.py +++ b/comtypes/test/test_client_dynamic.py @@ -1,7 +1,7 @@ import ctypes import unittest as ut -from comtypes import IUnknown, automation +from comtypes import COMError, IUnknown, automation, hresult from comtypes.client import CreateObject, GetModule, dynamic, lazybind @@ -59,6 +59,10 @@ def test_dict(self): d[4] with self.assertRaises(AttributeError): d.__foo__ + with self.assertRaises(COMError) as cm: + # Access a member that definitely does not exist. + _ = d.DefinitelyNonExistentMember + self.assertEqual(cm.exception.hresult, hresult.DISP_E_UNKNOWNNAME) class Test_lazybind_Dispatch(ut.TestCase): @@ -85,6 +89,9 @@ def test_dict(self): self.assertIsNone(d[4]) with self.assertRaises(AttributeError): d.__foo__ + with self.assertRaises(NameError): + # Access a member that definitely does not exist. + _ = d.DefinitelyNonExistentMember if __name__ == "__main__": From 04173310a25c527e7caae3838829ea78c625d571 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:00 +0900 Subject: [PATCH 4/9] test: Introduce `test_installer` methods for both `dynamic._Dispatch` and `lazybind.Dispatch`. These tests verify property and method access on the `WindowsInstaller.Installer` COM object, ensuring proper interaction with real-world COM components. --- comtypes/test/test_client_dynamic.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/comtypes/test/test_client_dynamic.py b/comtypes/test/test_client_dynamic.py index 2229a525..6b05cfba 100644 --- a/comtypes/test/test_client_dynamic.py +++ b/comtypes/test/test_client_dynamic.py @@ -1,7 +1,7 @@ import ctypes import unittest as ut -from comtypes import COMError, IUnknown, automation, hresult +from comtypes import GUID, COMError, IUnknown, automation, hresult, typeinfo from comtypes.client import CreateObject, GetModule, dynamic, lazybind @@ -29,6 +29,9 @@ def test_returns_dynamic_Dispatch(self): self.assertIs(disp, dynamic.Dispatch(disp)) +HKCU = 1 # HKEY_CURRENT_USER + + class Test_dynamic_Dispatch(ut.TestCase): # `MethodCaller` and `_Collection` are indirectly covered in this. def test_dict(self): @@ -64,6 +67,15 @@ def test_dict(self): _ = d.DefinitelyNonExistentMember self.assertEqual(cm.exception.hresult, hresult.DISP_E_UNKNOWNNAME) + def test_installer(self): + orig = CreateObject( + "WindowsInstaller.Installer", interface=automation.IDispatch + ) + installer = dynamic._Dispatch(orig) + # Access a known property and method + self.assertIsInstance(installer.Version, str) + self.assertTrue(installer.RegistryValue(HKCU, r"Control Panel\Desktop")) + class Test_lazybind_Dispatch(ut.TestCase): def test_dict(self): @@ -93,6 +105,18 @@ def test_dict(self): # Access a member that definitely does not exist. _ = d.DefinitelyNonExistentMember + def test_installer(self): + IID_Installer = GUID("{000C1090-0000-0000-C000-000000000046}") + tlib = typeinfo.LoadTypeLibEx("msi.dll") + tinfo = tlib.GetTypeInfoOfGuid(IID_Installer) + orig = CreateObject( + "WindowsInstaller.Installer", interface=automation.IDispatch + ) + installer = lazybind.Dispatch(orig, tinfo) + # Access a known property and method + self.assertIsInstance(installer.Version, str) + self.assertTrue(installer.RegistryValue(HKCU, r"Control Panel\Desktop")) + if __name__ == "__main__": ut.main() From bce1061e3ca7771d4b07bf007c3f081452109689 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:00 +0900 Subject: [PATCH 5/9] test: Expand `test_client_dynamic.py` with named property access tests. --- comtypes/test/test_client_dynamic.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/comtypes/test/test_client_dynamic.py b/comtypes/test/test_client_dynamic.py index 6b05cfba..c498540a 100644 --- a/comtypes/test/test_client_dynamic.py +++ b/comtypes/test/test_client_dynamic.py @@ -30,6 +30,7 @@ def test_returns_dynamic_Dispatch(self): HKCU = 1 # HKEY_CURRENT_USER +msiInstallStateUnknown = -1 class Test_dynamic_Dispatch(ut.TestCase): @@ -75,6 +76,11 @@ def test_installer(self): # Access a known property and method self.assertIsInstance(installer.Version, str) self.assertTrue(installer.RegistryValue(HKCU, r"Control Panel\Desktop")) + # Test that calling `ProductState` as a method raises a `COMError` + with self.assertRaises(COMError): + installer.ProductState(str(GUID())) + # Test `ProductState` as an item access + self.assertEqual(msiInstallStateUnknown, installer.ProductState[str(GUID())]) class Test_lazybind_Dispatch(ut.TestCase): @@ -116,6 +122,10 @@ def test_installer(self): # Access a known property and method self.assertIsInstance(installer.Version, str) self.assertTrue(installer.RegistryValue(HKCU, r"Control Panel\Desktop")) + # Test `ProductState` as a method call + self.assertEqual(msiInstallStateUnknown, installer.ProductState(str(GUID()))) + # Test `ProductState` as an item access + self.assertEqual(msiInstallStateUnknown, installer.ProductState[str(GUID())]) if __name__ == "__main__": From 8b9b8ad396accbfbacdd32334fad449245362560 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:00 +0900 Subject: [PATCH 6/9] test: Verify `AttributeError` for non-existent attributes in `test_client_dynamic.py`. --- comtypes/test/test_client_dynamic.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/comtypes/test/test_client_dynamic.py b/comtypes/test/test_client_dynamic.py index c498540a..a9e48bf9 100644 --- a/comtypes/test/test_client_dynamic.py +++ b/comtypes/test/test_client_dynamic.py @@ -81,6 +81,9 @@ def test_installer(self): installer.ProductState(str(GUID())) # Test `ProductState` as an item access self.assertEqual(msiInstallStateUnknown, installer.ProductState[str(GUID())]) + # Accessing a non-existent attribute should raise `AttributeError` + with self.assertRaises(AttributeError): + installer.__foo__ class Test_lazybind_Dispatch(ut.TestCase): @@ -126,6 +129,9 @@ def test_installer(self): self.assertEqual(msiInstallStateUnknown, installer.ProductState(str(GUID()))) # Test `ProductState` as an item access self.assertEqual(msiInstallStateUnknown, installer.ProductState[str(GUID())]) + # Accessing a non-existent attribute should raise `AttributeError` + with self.assertRaises(AttributeError): + installer.__foo__ if __name__ == "__main__": From 4bd015292f5afe7ff5a2b99979bd79ec79c23216 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:00 +0900 Subject: [PATCH 7/9] test: Enhance `test_getactiveobj.py` to verify `GetActiveObject` with `dynamic=True`. This change adds a test case to `Test_MSVidCtlLib` in `test_getactiveobj.py` to confirm that `comtypes.client.GetActiveObject` raises a `ValueError` when both `dynamic=True` and an `interface` are provided simultaneously. Additionally, it verifies that `GetActiveObject` correctly returns a `comtypes.client.lazybind.Dispatch` object with an identical hash to the original object when `dynamic=True` is specified. --- comtypes/test/test_getactiveobj.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/comtypes/test/test_getactiveobj.py b/comtypes/test/test_getactiveobj.py index 2dcee256..495e2c12 100644 --- a/comtypes/test/test_getactiveobj.py +++ b/comtypes/test/test_getactiveobj.py @@ -73,12 +73,22 @@ def test(self): class Test_MSVidCtlLib(unittest.TestCase): def test_register_and_revoke(self): + CLSID_MSVidCtl = msvidctl.MSVidCtl._reg_clsid_ vidctl = comtypes.client.CreateObject(msvidctl.MSVidCtl) with self.assertRaises(WindowsError): comtypes.client.GetActiveObject(msvidctl.MSVidCtl) handle = comtypes.client.RegisterActiveObject(vidctl, msvidctl.MSVidCtl) + with self.assertRaises(ValueError): + comtypes.client.GetActiveObject( + CLSID_MSVidCtl, + interface=msvidctl.IMSVidCtl, + dynamic=True, # type: ignore + ) activeobj = comtypes.client.GetActiveObject(msvidctl.MSVidCtl) self.assertEqual(vidctl, activeobj) + dynamicobj = comtypes.client.GetActiveObject(CLSID_MSVidCtl, dynamic=True) + self.assertIsInstance(dynamicobj, comtypes.client.lazybind.Dispatch) + self.assertEqual(hash(vidctl), hash(dynamicobj)) comtypes.client.RevokeActiveObject(handle) with self.assertRaises(WindowsError): comtypes.client.GetActiveObject(msvidctl.MSVidCtl) From eae0b8d374f527636888e4b5d85f45e768c84421 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:01 +0900 Subject: [PATCH 8/9] test: Verify `CreateObject` raises `ValueError` for `dynamic=True` with `interface`. This test case confirms that `comtypes.client.CreateObject` correctly raises a `ValueError` when an `interface` is provided alongside `dynamic=True`, preventing a potential misuse of the API. --- comtypes/test/test_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/comtypes/test/test_client.py b/comtypes/test/test_client.py index 3a75252c..6d4b366b 100644 --- a/comtypes/test/test_client.py +++ b/comtypes/test/test_client.py @@ -218,6 +218,14 @@ def test_server_info(self): self.assertIsInstance(iuia.GetRootElement(), POINTER(IUIAutomationElement)) self.assertIsInstance(iuia.GetRootElement(), IUIAutomationElement) + def test_raises_valueerror_if_takes_dynamic_true_and_interface(self): + with self.assertRaises(ValueError): + comtypes.client.CreateObject( + "Scripting.Dictionary", + interface=Scripting.IDictionary, + dynamic=True, # type: ignore + ) + class Test_Constants(ut.TestCase): def test_punk(self): From a590e430710b4dbacf51636f4791afc9ca5e7a94 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 20 Jan 2026 23:06:01 +0900 Subject: [PATCH 9/9] test: Add comprehensive tests for `comtypes.client.CoGetObject`. Introduces a new test class `Test_CoGetObject` to thoroughly verify the functionality of `comtypes.client.CoGetObject`. It includes test cases for successfully returning an interface pointer and a dynamic dispatch object, as well as confirming that a `ValueError` is raised when both `dynamic=True` and an `interface` are simultaneously provided. --- comtypes/test/test_client.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/comtypes/test/test_client.py b/comtypes/test/test_client.py index 6d4b366b..9191d7f4 100644 --- a/comtypes/test/test_client.py +++ b/comtypes/test/test_client.py @@ -9,7 +9,8 @@ # create the typelib wrapper and import it comtypes.client.GetModule("scrrun.dll") -from comtypes.gen import Scripting +comtypes.client.GetModule("wbemdisp.tlb") +from comtypes.gen import Scripting, WbemScripting class Test_GetModule(ut.TestCase): @@ -227,6 +228,30 @@ def test_raises_valueerror_if_takes_dynamic_true_and_interface(self): ) +class Test_CoGetObject(ut.TestCase): + def test_returns_interface_pointer(self): + wmi = comtypes.client.CoGetObject( + "winmgmts:", interface=WbemScripting.ISWbemServices + ) + self.assertIsInstance(wmi, WbemScripting.ISWbemServices) + disks = wmi.InstancesOf("Win32_LogicalDisk") + self.assertGreaterEqual(len(disks), 0) + + def test_returns_dynamic_dispatch_object(self): + wmi = comtypes.client.CoGetObject("winmgmts:", dynamic=True) + self.assertIsInstance(wmi, comtypes.client.lazybind.Dispatch) + disks = wmi.InstancesOf("Win32_LogicalDisk") + self.assertGreaterEqual(disks.Count, 0) + + def test_raises_valueerror_if_takes_dynamic_true_and_interface(self): + with self.assertRaises(ValueError): + comtypes.client.CoGetObject( + "winmgmts:", + interface=WbemScripting.ISWbemServices, # type: ignore + dynamic=True, # type: ignore + ) + + class Test_Constants(ut.TestCase): def test_punk(self): obj = comtypes.client.CreateObject(Scripting.Dictionary)