Skip to content

Commit ce14575

Browse files
authored
Enhance dynamic dispatch and client testing. (#909)
* 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. * 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. * 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`. * 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. * test: Expand `test_client_dynamic.py` with named property access tests. * test: Verify `AttributeError` for non-existent attributes in `test_client_dynamic.py`. * 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. * 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. * 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.
1 parent 07ab1f4 commit ce14575

3 files changed

Lines changed: 140 additions & 26 deletions

File tree

comtypes/test/test_client.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
# create the typelib wrapper and import it
1111
comtypes.client.GetModule("scrrun.dll")
12-
from comtypes.gen import Scripting
12+
comtypes.client.GetModule("wbemdisp.tlb")
13+
from comtypes.gen import Scripting, WbemScripting
1314

1415

1516
class Test_GetModule(ut.TestCase):
@@ -218,6 +219,38 @@ def test_server_info(self):
218219
self.assertIsInstance(iuia.GetRootElement(), POINTER(IUIAutomationElement))
219220
self.assertIsInstance(iuia.GetRootElement(), IUIAutomationElement)
220221

222+
def test_raises_valueerror_if_takes_dynamic_true_and_interface(self):
223+
with self.assertRaises(ValueError):
224+
comtypes.client.CreateObject(
225+
"Scripting.Dictionary",
226+
interface=Scripting.IDictionary,
227+
dynamic=True, # type: ignore
228+
)
229+
230+
231+
class Test_CoGetObject(ut.TestCase):
232+
def test_returns_interface_pointer(self):
233+
wmi = comtypes.client.CoGetObject(
234+
"winmgmts:", interface=WbemScripting.ISWbemServices
235+
)
236+
self.assertIsInstance(wmi, WbemScripting.ISWbemServices)
237+
disks = wmi.InstancesOf("Win32_LogicalDisk")
238+
self.assertGreaterEqual(len(disks), 0)
239+
240+
def test_returns_dynamic_dispatch_object(self):
241+
wmi = comtypes.client.CoGetObject("winmgmts:", dynamic=True)
242+
self.assertIsInstance(wmi, comtypes.client.lazybind.Dispatch)
243+
disks = wmi.InstancesOf("Win32_LogicalDisk")
244+
self.assertGreaterEqual(disks.Count, 0)
245+
246+
def test_raises_valueerror_if_takes_dynamic_true_and_interface(self):
247+
with self.assertRaises(ValueError):
248+
comtypes.client.CoGetObject(
249+
"winmgmts:",
250+
interface=WbemScripting.ISWbemServices, # type: ignore
251+
dynamic=True, # type: ignore
252+
)
253+
221254

222255
class Test_Constants(ut.TestCase):
223256
def test_punk(self):

comtypes/test/test_client_dynamic.py

Lines changed: 96 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,39 @@
11
import ctypes
22
import unittest as ut
3-
from unittest import mock
43

5-
from comtypes import COMError, IUnknown, automation
4+
from comtypes import GUID, COMError, IUnknown, automation, hresult, typeinfo
65
from comtypes.client import CreateObject, GetModule, dynamic, lazybind
76

87

98
class Test_Dispatch_Function(ut.TestCase):
10-
# It is difficult to cause intentionally errors "in the regular way".
11-
# So `mock` is used to cover conditional branches.
12-
def test_returns_dynamic_Dispatch_if_takes_dynamic_Dispatch(self):
13-
obj = mock.MagicMock(spec=dynamic._Dispatch)
14-
self.assertIs(dynamic.Dispatch(obj), obj)
15-
16-
def test_returns_lazybind_Dispatch_if_takes_ptrIDispatch(self):
17-
# Conditional branches that return `lazybind.Dispatch` are also covered by
18-
# `test_dyndispatch` and others.
19-
obj = mock.MagicMock(spec=ctypes.POINTER(automation.IDispatch))
20-
self.assertIsInstance(dynamic.Dispatch(obj), lazybind.Dispatch)
9+
def test_returns_lazybind_Dispatch(self):
10+
# When `dynamic=True`, objects providing type information will return a
11+
# `lazybind.Dispatch` instance.
12+
orig = CreateObject("Scripting.Dictionary", interface=automation.IDispatch)
13+
disp = dynamic.Dispatch(orig)
14+
self.assertIsInstance(disp, lazybind.Dispatch)
15+
# Calling `dynamic.Dispatch` with an already dispatched object should
16+
# return the same instance.
17+
self.assertIs(disp, dynamic.Dispatch(disp))
2118

22-
def test_returns_dynamic_Dispatch_if_takes_ptrIDispatch_and_raised_comerr(self):
23-
obj = mock.MagicMock(spec=ctypes.POINTER(automation.IDispatch))
24-
obj.GetTypeInfo.side_effect = COMError(0, "test", ("", "", "", 0, 0))
25-
self.assertIsInstance(dynamic.Dispatch(obj), dynamic._Dispatch)
19+
def test_returns_dynamic_Dispatch(self):
20+
# When `dynamic=True`, objects that do NOT provide type information (or
21+
# fail to provide it) will return a `dynamic._Dispatch` instance.
22+
orig = CreateObject(
23+
"WindowsInstaller.Installer", interface=automation.IDispatch
24+
)
25+
disp = dynamic.Dispatch(orig)
26+
self.assertIsInstance(disp, dynamic._Dispatch)
27+
# Calling `dynamic.Dispatch` on an already dispatched object should
28+
# return the same instance.
29+
self.assertIs(disp, dynamic.Dispatch(disp))
2630

27-
def test_returns_dynamic_Dispatch_if_takes_ptrIDispatch_and_raised_winerr(self):
28-
obj = mock.MagicMock(spec=ctypes.POINTER(automation.IDispatch))
29-
obj.GetTypeInfo.side_effect = OSError()
30-
self.assertIsInstance(dynamic.Dispatch(obj), dynamic._Dispatch)
3131

32-
def test_returns_what_is_took_if_takes_other(self):
33-
obj = object()
34-
self.assertIs(dynamic.Dispatch(obj), obj)
32+
HKCU = 1 # HKEY_CURRENT_USER
33+
msiInstallStateUnknown = -1
3534

3635

37-
class Test_Dispatch_Class(ut.TestCase):
36+
class Test_dynamic_Dispatch(ut.TestCase):
3837
# `MethodCaller` and `_Collection` are indirectly covered in this.
3938
def test_dict(self):
4039
# The following conditional branches are not covered;
@@ -57,10 +56,82 @@ def test_dict(self):
5756
scr_dict = d.QueryInterface(scrrun.IDictionary)
5857
self.assertIsInstance(scr_dict, scrrun.IDictionary)
5958
d.Item["qux"] = scr_dict
59+
# `dynamic._Dispatch` reflects the underlying COM object's behavior.
60+
# For `Scripting.Dictionary`, out-of-bounds index access via `IDispatch`
61+
# typically results in a `COMError`, which is wrapped as `IndexError`.
6062
with self.assertRaises(IndexError):
6163
d[4]
6264
with self.assertRaises(AttributeError):
6365
d.__foo__
66+
with self.assertRaises(COMError) as cm:
67+
# Access a member that definitely does not exist.
68+
_ = d.DefinitelyNonExistentMember
69+
self.assertEqual(cm.exception.hresult, hresult.DISP_E_UNKNOWNNAME)
70+
71+
def test_installer(self):
72+
orig = CreateObject(
73+
"WindowsInstaller.Installer", interface=automation.IDispatch
74+
)
75+
installer = dynamic._Dispatch(orig)
76+
# Access a known property and method
77+
self.assertIsInstance(installer.Version, str)
78+
self.assertTrue(installer.RegistryValue(HKCU, r"Control Panel\Desktop"))
79+
# Test that calling `ProductState` as a method raises a `COMError`
80+
with self.assertRaises(COMError):
81+
installer.ProductState(str(GUID()))
82+
# Test `ProductState` as an item access
83+
self.assertEqual(msiInstallStateUnknown, installer.ProductState[str(GUID())])
84+
# Accessing a non-existent attribute should raise `AttributeError`
85+
with self.assertRaises(AttributeError):
86+
installer.__foo__
87+
88+
89+
class Test_lazybind_Dispatch(ut.TestCase):
90+
def test_dict(self):
91+
orig = CreateObject("Scripting.Dictionary", interface=automation.IDispatch)
92+
tinfo = orig.GetTypeInfo(0)
93+
d = lazybind.Dispatch(orig, tinfo)
94+
d.CompareMode = 42
95+
d.Item["foo"] = 1
96+
d.Item["bar"] = "spam foo"
97+
d.Item["baz"] = 3.14
98+
self.assertEqual(d.Item["foo"], 1)
99+
self.assertEqual([k for k in iter(d)], ["foo", "bar", "baz"])
100+
self.assertIsInstance(hash(d), int)
101+
# No `_FlagAsMethod` in `lazybind.Dispatch`
102+
self.assertIs(type(d._NewEnum()), ctypes.POINTER(IUnknown))
103+
scrrun = GetModule("scrrun.dll")
104+
scr_dict = d.QueryInterface(scrrun.IDictionary)
105+
self.assertIsInstance(scr_dict, scrrun.IDictionary)
106+
d.Item["qux"] = scr_dict
107+
# `lazybind.Dispatch`, using type information, might return `None` for
108+
# non-existent keys when accessed via direct index (`d[4]`),
109+
# as it doesn't directly map to the `Item` property's error handling.
110+
self.assertIsNone(d[4])
111+
with self.assertRaises(AttributeError):
112+
d.__foo__
113+
with self.assertRaises(NameError):
114+
# Access a member that definitely does not exist.
115+
_ = d.DefinitelyNonExistentMember
116+
117+
def test_installer(self):
118+
IID_Installer = GUID("{000C1090-0000-0000-C000-000000000046}")
119+
tlib = typeinfo.LoadTypeLibEx("msi.dll")
120+
tinfo = tlib.GetTypeInfoOfGuid(IID_Installer)
121+
orig = CreateObject(
122+
"WindowsInstaller.Installer", interface=automation.IDispatch
123+
)
124+
installer = lazybind.Dispatch(orig, tinfo)
125+
# Access a known property and method
126+
self.assertIsInstance(installer.Version, str)
127+
self.assertTrue(installer.RegistryValue(HKCU, r"Control Panel\Desktop"))
128+
# Test `ProductState` as a method call
129+
self.assertEqual(msiInstallStateUnknown, installer.ProductState(str(GUID())))
130+
# Test `ProductState` as an item access
131+
self.assertEqual(msiInstallStateUnknown, installer.ProductState[str(GUID())])
132+
# Accessing a non-existent attribute should raise `AttributeError`
133+
with self.assertRaises(AttributeError):
134+
installer.__foo__
64135

65136

66137
if __name__ == "__main__":

comtypes/test/test_getactiveobj.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,22 @@ def test(self):
7373

7474
class Test_MSVidCtlLib(unittest.TestCase):
7575
def test_register_and_revoke(self):
76+
CLSID_MSVidCtl = msvidctl.MSVidCtl._reg_clsid_
7677
vidctl = comtypes.client.CreateObject(msvidctl.MSVidCtl)
7778
with self.assertRaises(WindowsError):
7879
comtypes.client.GetActiveObject(msvidctl.MSVidCtl)
7980
handle = comtypes.client.RegisterActiveObject(vidctl, msvidctl.MSVidCtl)
81+
with self.assertRaises(ValueError):
82+
comtypes.client.GetActiveObject(
83+
CLSID_MSVidCtl,
84+
interface=msvidctl.IMSVidCtl,
85+
dynamic=True, # type: ignore
86+
)
8087
activeobj = comtypes.client.GetActiveObject(msvidctl.MSVidCtl)
8188
self.assertEqual(vidctl, activeobj)
89+
dynamicobj = comtypes.client.GetActiveObject(CLSID_MSVidCtl, dynamic=True)
90+
self.assertIsInstance(dynamicobj, comtypes.client.lazybind.Dispatch)
91+
self.assertEqual(hash(vidctl), hash(dynamicobj))
8292
comtypes.client.RevokeActiveObject(handle)
8393
with self.assertRaises(WindowsError):
8494
comtypes.client.GetActiveObject(msvidctl.MSVidCtl)

0 commit comments

Comments
 (0)