From 022dd8efae1de796c25bb31ee6f2905e271a6dbc Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 01/15] test: Add `Test_IsEqual` for moniker comparison in `test_moniker.py` Introduces `Test_IsEqual` class to `test/test_moniker.py` to verify the correct functionality of `IsEqual` method for monikers. This test ensures that monikers with identical item IDs are correctly identified as equal, while those with different item IDs are not. --- comtypes/test/test_moniker.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index f4999c74..8bbc519d 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -51,6 +51,16 @@ def test_item(self): self.assertEqual(mon.Inverse().GetClassID(), CLSID_AntiMoniker) +class Test_IsEqual(unittest.TestCase): + def test_item(self): + item_id = str(GUID.create_new()) + mon1 = _create_item_moniker("!", item_id) + mon2 = _create_item_moniker("!", item_id) # Should be equal + mon3 = _create_item_moniker("!", str(GUID.create_new())) # Should not be equal + self.assertEqual(mon1.IsEqual(mon2), hresult.S_OK) + self.assertEqual(mon1.IsEqual(mon3), hresult.S_FALSE) + + class Test_IsRunning(unittest.TestCase): def test_item(self): vidctl = CreateObject(msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl) From f478e3d386ae32dae0efaecb239b724329296c5f Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 02/15] test: Add `Test_Hash` for moniker hash comparison in `test_moniker.py` Introduces `Test_Hash` class to `test/test_moniker.py` to verify the correct functionality of the `Hash` method for monikers. This test ensures that monikers with identical item IDs produce the same hash value, while those with different item IDs produce different hash values. --- comtypes/test/test_moniker.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index 8bbc519d..bfbef2fb 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -61,6 +61,17 @@ def test_item(self): self.assertEqual(mon1.IsEqual(mon3), hresult.S_FALSE) +class Test_Hash(unittest.TestCase): + def test_item(self): + item_id = str(GUID.create_new()) + mon1 = _create_item_moniker("!", item_id) + mon2 = _create_item_moniker("!", item_id) # Should be equal + mon3 = _create_item_moniker("!", str(GUID.create_new())) # Should not be equal + self.assertEqual(mon1.Hash(), mon2.Hash()) + self.assertNotEqual(mon1.Hash(), mon3.Hash()) + self.assertNotEqual(mon2.Hash(), mon3.Hash()) + + class Test_IsRunning(unittest.TestCase): def test_item(self): vidctl = CreateObject(msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl) From 7f8ebc0962eede4cbbae9449c4736bd7c2869852 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 03/15] test: Add `Test_ComposeWith` for moniker composition and error handling Introduces `Test_ComposeWith` to `test/test_moniker.py` to verify the `ComposeWith` method. This test ensures that composing item monikers results in a `CLSID_CompositeMoniker`. It also validates that when `fOnlyIfNotGeneric=True`, `ComposeWith` correctly raises a `COMError` with `MK_E_NEEDGENERIC`, indicating that the moniker cannot be composed generically. Constants `CLSID_CompositeMoniker` and `MK_E_NEEDGENERIC` were added to `test/monikers_helper.py` to support this test. --- comtypes/test/monikers_helper.py | 2 ++ comtypes/test/test_moniker.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/comtypes/test/monikers_helper.py b/comtypes/test/monikers_helper.py index 6ad3671b..d20daa97 100644 --- a/comtypes/test/monikers_helper.py +++ b/comtypes/test/monikers_helper.py @@ -6,6 +6,7 @@ # https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-mksys MKSYS_ITEMMONIKER = 4 +CLSID_CompositeMoniker = GUID("{00000309-0000-0000-c000-000000000046}") CLSID_AntiMoniker = GUID("{00000305-0000-0000-c000-000000000046}") CLSID_ItemMoniker = GUID("{00000304-0000-0000-c000-000000000046}") @@ -28,4 +29,5 @@ _GetRunningObjectTable.restype = HRESULT # Common COM Errors from Moniker/Binding Context operations +MK_E_NEEDGENERIC = -2147221022 # 0x800401E2 MK_E_UNAVAILABLE = -2147221021 # 0x800401E3 diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index bfbef2fb..916c0768 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -1,13 +1,16 @@ import contextlib import unittest +from _ctypes import COMError from ctypes import POINTER, byref from comtypes import GUID, hresult from comtypes.client import CreateObject, GetModule from comtypes.test.monikers_helper import ( + MK_E_NEEDGENERIC, MKSYS_ITEMMONIKER, ROTFLAGS_ALLOWANYCLIENT, CLSID_AntiMoniker, + CLSID_CompositeMoniker, CLSID_ItemMoniker, _CreateBindCtx, _CreateItemMoniker, @@ -51,6 +54,20 @@ def test_item(self): self.assertEqual(mon.Inverse().GetClassID(), CLSID_AntiMoniker) +class Test_ComposeWith(unittest.TestCase): + def test_item(self): + item_id = str(GUID.create_new()) + mon = _create_item_moniker("!", item_id) + item_mon2 = _create_item_moniker("!", str(GUID.create_new())) + self.assertEqual( + mon.ComposeWith(item_mon2, False).GetClassID(), + CLSID_CompositeMoniker, + ) + with self.assertRaises(COMError) as cm: + mon.ComposeWith(item_mon2, True) + self.assertEqual(cm.exception.hresult, MK_E_NEEDGENERIC) + + class Test_IsEqual(unittest.TestCase): def test_item(self): item_id = str(GUID.create_new()) From 6c17c7dba0c8abd9e7dcd961ca166d999ec44032 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 04/15] test: Add `Test_Enum` for composite moniker enumeration Introduces `Test_Enum` class with `test_generic_composite` method to `test/test_moniker.py`. This test verifies the `Enum` method of composite monikers, ensuring it correctly returns an `IEnumMoniker` instance. It uses the newly added `_CreateGenericComposite` helper in `test/monikers_helper.py`. --- comtypes/test/monikers_helper.py | 8 ++++++++ comtypes/test/test_moniker.py | 26 +++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/comtypes/test/monikers_helper.py b/comtypes/test/monikers_helper.py index d20daa97..f9f56d3b 100644 --- a/comtypes/test/monikers_helper.py +++ b/comtypes/test/monikers_helper.py @@ -16,6 +16,14 @@ _ole32 = OleDLL("ole32") +_CreateGenericComposite = _ole32.CreateGenericComposite +_CreateGenericComposite.argtypes = [ + POINTER(IUnknown), # pmkFirst + POINTER(IUnknown), # pmkRest + POINTER(POINTER(IUnknown)), # ppmkComposite +] +_CreateGenericComposite.restype = HRESULT + _CreateItemMoniker = _ole32.CreateItemMoniker _CreateItemMoniker.argtypes = [LPCOLESTR, LPCOLESTR, POINTER(POINTER(IUnknown))] _CreateItemMoniker.restype = HRESULT diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index 916c0768..2c2334e2 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -13,6 +13,7 @@ CLSID_CompositeMoniker, CLSID_ItemMoniker, _CreateBindCtx, + _CreateGenericComposite, _CreateItemMoniker, _GetRunningObjectTable, ) @@ -20,7 +21,18 @@ with contextlib.redirect_stdout(None): # supress warnings GetModule("msvidctl.dll") from comtypes.gen import MSVidCtlLib as msvidctl -from comtypes.gen.MSVidCtlLib import IBindCtx, IMoniker, IRunningObjectTable +from comtypes.gen.MSVidCtlLib import ( + IBindCtx, + IEnumMoniker, + IMoniker, + IRunningObjectTable, +) + + +def _create_generic_composite(mk_first: IMoniker, mk_rest: IMoniker) -> IMoniker: + mon = POINTER(IMoniker)() + _CreateGenericComposite(mk_first, mk_rest, byref(mon)) + return mon # type: ignore def _create_item_moniker(delim: str, item: str) -> IMoniker: @@ -104,3 +116,15 @@ def test_item(self): rot.Revoke(dw_reg) # After revoking: should NOT be running again self.assertEqual(mon.IsRunning(bctx, None, None), hresult.S_FALSE) + + +class Test_Enum(unittest.TestCase): + def test_generic_composite(self): + item_id1 = str(GUID.create_new()) + item_id2 = str(GUID.create_new()) + item_mon1 = _create_item_moniker("!", item_id1) + item_mon2 = _create_item_moniker("!", item_id2) + # Create a composite moniker to ensure multiple elements for enumeration + comp_mon = _create_generic_composite(item_mon1, item_mon2) + enum_moniker = comp_mon.Enum(True) # True for forward enumeration + self.assertIsInstance(enum_moniker, IEnumMoniker) From 9d36eb1a5ffc16a9c36e7fec6e3281de740b4dac Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 05/15] test: Add `test_generic_composite` to `test_moniker.py` This test verifies the correct behavior of `IsSystemMoniker`, `GetDisplayName`, `GetClassID`, and `Inverse` methods for generic composite monikers. The `MKSYS_GENERICCOMPOSITE` constant was added to `monikers_helper.py` to support this test. --- comtypes/test/monikers_helper.py | 1 + comtypes/test/test_moniker.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/comtypes/test/monikers_helper.py b/comtypes/test/monikers_helper.py index f9f56d3b..5ef6eb7f 100644 --- a/comtypes/test/monikers_helper.py +++ b/comtypes/test/monikers_helper.py @@ -4,6 +4,7 @@ from comtypes import GUID, IUnknown # https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-mksys +MKSYS_GENERICCOMPOSITE = 1 MKSYS_ITEMMONIKER = 4 CLSID_CompositeMoniker = GUID("{00000309-0000-0000-c000-000000000046}") diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index 2c2334e2..b2ed19f5 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -7,6 +7,7 @@ from comtypes.client import CreateObject, GetModule from comtypes.test.monikers_helper import ( MK_E_NEEDGENERIC, + MKSYS_GENERICCOMPOSITE, MKSYS_ITEMMONIKER, ROTFLAGS_ALLOWANYCLIENT, CLSID_AntiMoniker, @@ -56,6 +57,19 @@ def _create_rot() -> IRunningObjectTable: class Test_IsSystemMoniker_GetDisplayName_Inverse(unittest.TestCase): + def test_generic_composite(self): + item_id1 = str(GUID.create_new()) + item_id2 = str(GUID.create_new()) + mon = _create_generic_composite( + _create_item_moniker("!", item_id1), + _create_item_moniker("!", item_id2), + ) + self.assertEqual(mon.IsSystemMoniker(), MKSYS_GENERICCOMPOSITE) + bctx = _create_bctx() + self.assertEqual(mon.GetDisplayName(bctx, None), f"!{item_id1}!{item_id2}") + self.assertEqual(mon.GetClassID(), CLSID_CompositeMoniker) + self.assertEqual(mon.Inverse().GetClassID(), CLSID_CompositeMoniker) + def test_item(self): item_id = str(GUID.create_new()) mon = _create_item_moniker("!", item_id) From 4c2a8ed15431ae5f86949296a9357f60d08b6629 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 06/15] test: Add `Test_RemoteBindToObject` for file moniker binding Introduces `Test_RemoteBindToObject` class with `test_file` method to `test/test_moniker.py`. This test verifies the `RemoteBindToObject` method for file monikers, ensuring correct binding to `IPersistFile`. The `_CreateFileMoniker` helper function was added to `monikers_helper.py` to support this test. --- comtypes/test/monikers_helper.py | 4 ++++ comtypes/test/test_moniker.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/comtypes/test/monikers_helper.py b/comtypes/test/monikers_helper.py index 5ef6eb7f..6203cd24 100644 --- a/comtypes/test/monikers_helper.py +++ b/comtypes/test/monikers_helper.py @@ -25,6 +25,10 @@ ] _CreateGenericComposite.restype = HRESULT +_CreateFileMoniker = _ole32.CreateFileMoniker +_CreateFileMoniker.argtypes = [LPCOLESTR, POINTER(POINTER(IUnknown))] +_CreateFileMoniker.restype = HRESULT + _CreateItemMoniker = _ole32.CreateItemMoniker _CreateItemMoniker.argtypes = [LPCOLESTR, LPCOLESTR, POINTER(POINTER(IUnknown))] _CreateItemMoniker.restype = HRESULT diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index b2ed19f5..643f32f8 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -1,10 +1,14 @@ import contextlib +import os +import tempfile import unittest from _ctypes import COMError from ctypes import POINTER, byref +from pathlib import Path from comtypes import GUID, hresult from comtypes.client import CreateObject, GetModule +from comtypes.persist import IPersistFile from comtypes.test.monikers_helper import ( MK_E_NEEDGENERIC, MKSYS_GENERICCOMPOSITE, @@ -14,6 +18,7 @@ CLSID_CompositeMoniker, CLSID_ItemMoniker, _CreateBindCtx, + _CreateFileMoniker, _CreateGenericComposite, _CreateItemMoniker, _GetRunningObjectTable, @@ -36,6 +41,12 @@ def _create_generic_composite(mk_first: IMoniker, mk_rest: IMoniker) -> IMoniker return mon # type: ignore +def _create_file_moniker(path: str) -> IMoniker: + mon = POINTER(IMoniker)() + _CreateFileMoniker(path, byref(mon)) + return mon # type: ignore + + def _create_item_moniker(delim: str, item: str) -> IMoniker: mon = POINTER(IMoniker)() _CreateItemMoniker(delim, item, byref(mon)) @@ -142,3 +153,19 @@ def test_generic_composite(self): comp_mon = _create_generic_composite(item_mon1, item_mon2) enum_moniker = comp_mon.Enum(True) # True for forward enumeration self.assertIsInstance(enum_moniker, IEnumMoniker) + + +class Test_RemoteBindToObject(unittest.TestCase): + def test_file(self): + bctx = _create_bctx() + with tempfile.TemporaryDirectory() as t: + tmpdir = Path(t) + tmpfile = tmpdir / "tmp.lnk" + tmpfile.touch() + mon = _create_file_moniker(str(tmpfile)) + bound_obj = mon.RemoteBindToObject(bctx, None, IPersistFile._iid_) + pf = bound_obj.QueryInterface(IPersistFile) + self.assertEqual( + os.path.normcase(os.path.normpath(tmpfile)), + os.path.normcase(os.path.normpath(pf.GetCurFile())), + ) From 6b2078400de0ad019572e1f753cc54491c49ee70 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 07/15] test: Add `test_file` for `IsSystemMoniker` to `test_moniker.py` Introduces `test_file` method to `Test_IsSystemMoniker_GetDisplayName_Inverse` in `test/test_moniker.py`. This test verifies the correct behavior of `IsSystemMoniker`, `GetDisplayName`, `GetClassID`, and `Inverse` methods for file monikers. The `MKSYS_FILEMONIKER` and `CLSID_FileMoniker` constants were added to `monikers_helper.py` to support this test. --- comtypes/test/monikers_helper.py | 2 ++ comtypes/test/test_moniker.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/comtypes/test/monikers_helper.py b/comtypes/test/monikers_helper.py index 6203cd24..52a8e4ac 100644 --- a/comtypes/test/monikers_helper.py +++ b/comtypes/test/monikers_helper.py @@ -5,9 +5,11 @@ # https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-mksys MKSYS_GENERICCOMPOSITE = 1 +MKSYS_FILEMONIKER = 2 MKSYS_ITEMMONIKER = 4 CLSID_CompositeMoniker = GUID("{00000309-0000-0000-c000-000000000046}") +CLSID_FileMoniker = GUID("{00000303-0000-0000-C000-000000000046}") CLSID_AntiMoniker = GUID("{00000305-0000-0000-c000-000000000046}") CLSID_ItemMoniker = GUID("{00000304-0000-0000-c000-000000000046}") diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index 643f32f8..adc316bd 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -11,11 +11,13 @@ from comtypes.persist import IPersistFile from comtypes.test.monikers_helper import ( MK_E_NEEDGENERIC, + MKSYS_FILEMONIKER, MKSYS_GENERICCOMPOSITE, MKSYS_ITEMMONIKER, ROTFLAGS_ALLOWANYCLIENT, CLSID_AntiMoniker, CLSID_CompositeMoniker, + CLSID_FileMoniker, CLSID_ItemMoniker, _CreateBindCtx, _CreateFileMoniker, @@ -81,6 +83,15 @@ def test_generic_composite(self): self.assertEqual(mon.GetClassID(), CLSID_CompositeMoniker) self.assertEqual(mon.Inverse().GetClassID(), CLSID_CompositeMoniker) + def test_file(self): + with tempfile.NamedTemporaryFile() as f: + mon = _create_file_moniker(f.name) + self.assertEqual(mon.IsSystemMoniker(), MKSYS_FILEMONIKER) + bctx = _create_bctx() + self.assertEqual(mon.GetDisplayName(bctx, None), f.name) + self.assertEqual(mon.GetClassID(), CLSID_FileMoniker) + self.assertEqual(mon.Inverse().GetClassID(), CLSID_AntiMoniker) + def test_item(self): item_id = str(GUID.create_new()) mon = _create_item_moniker("!", item_id) From 13f65f35fcddbbc0a0f409ff97ede5a68a389c84 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 08/15] test: Add `Test_CommonPrefixWith` for moniker common prefix detection Introduces `Test_CommonPrefixWith` class with `test_file` method to `test/test_moniker.py`. This test verifies the `CommonPrefixWith` method for file monikers, ensuring it correctly identifies the common directory path between different file monikers. --- comtypes/test/test_moniker.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index adc316bd..0492f77d 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -154,6 +154,41 @@ def test_item(self): self.assertEqual(mon.IsRunning(bctx, None, None), hresult.S_FALSE) +class Test_CommonPrefixWith(unittest.TestCase): + def test_file(self): + bctx = _create_bctx() + # Create temporary directories and files for realistic File Monikers + with tempfile.TemporaryDirectory() as t: + tmpdir = Path(t) + dir_a = tmpdir / "dir_a" + dir_b = tmpdir / "dir_a" / "dir_b" + dir_b.mkdir(parents=True) + file1 = dir_a / "file1.txt" + file2 = dir_b / "file2.txt" + file3 = tmpdir / "file3.txt" + mon1 = _create_file_moniker(str(file1)) # tmpdir/dir_a/file1.txt + mon2 = _create_file_moniker(str(file2)) # tmpdir/dir_a/dir_b/file2.txt + mon3 = _create_file_moniker(str(file3)) # tmpdir/file3.txt + # Common prefix between mon1 and mon2 (tmpdir/dir_a) + self.assertEqual( + os.path.normcase( + os.path.normpath( + mon1.CommonPrefixWith(mon2).GetDisplayName(bctx, None) + ) + ), + os.path.normcase(os.path.normpath(dir_a)), + ) + # Common prefix between mon1 and mon3 (tmpdir) + self.assertEqual( + os.path.normcase( + os.path.normpath( + mon1.CommonPrefixWith(mon3).GetDisplayName(bctx, None) + ) + ), + os.path.normcase(os.path.normpath(tmpdir)), + ) + + class Test_Enum(unittest.TestCase): def test_generic_composite(self): item_id1 = str(GUID.create_new()) From b3ee4d60f80e2b48360cd374e4c4d1491d955b05 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 09/15] test: Add `Test_RelativePathTo` for moniker relative path calculation Introduces `Test_RelativePathTo` class with `test_file` method to `/test/test_moniker.py`. This test verifies the `RelativePathTo` method for file monikers, ensuring it correctly calculates the relative path between two file monikers, mirroring `pathlib.Path.relative_to`. --- comtypes/test/test_moniker.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index 0492f77d..07f60c69 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -189,6 +189,36 @@ def test_file(self): ) +class Test_RelativePathTo(unittest.TestCase): + def test_file(self): + bctx = _create_bctx() + with tempfile.TemporaryDirectory() as t: + tmpdir = Path(t) + dir_a = tmpdir / "dir_a" + dir_b = tmpdir / "dir_b" + dir_a.mkdir() + dir_b.mkdir() + file1 = dir_a / "file1.txt" + file2 = dir_b / "file2.txt" + mon_from = _create_file_moniker(str(file1)) # tmpdir/dir_a/file1.txt + mon_to = _create_file_moniker(str(file2)) # tmpdir/dir_b/file2.txt + # The COM API returns paths with backslashes on Windows, so we normalize. + self.assertEqual( + # Check the display name of the relative moniker + # The moniker's `RelativePathTo` method calculates the path from + # the base of the `mon_from` to the target `mon_to`. + os.path.normcase( + os.path.normpath( + mon_from.RelativePathTo(mon_to).GetDisplayName(bctx, None) + ) + ), + # Calculate the relative path from the directory of file1 to file2 + os.path.normcase( + os.path.normpath(file2.relative_to(file1, walk_up=True)) + ), + ) + + class Test_Enum(unittest.TestCase): def test_generic_composite(self): item_id1 = str(GUID.create_new()) From 1b889848002dc0a286732228d88b7d444add3b85 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 10/15] test: Add tests for `IBindCtx.RemoteSetBindOptions` and `RemoteGetBindOptions`. This commit introduces a new test class `Test_Set_Get_BindOptions` to `test/test_bctx.py`. It verifies the correct functionality of `SetBindOptions` and `GetBindOptions` by setting and retrieving `tagBIND_OPTS2` values and asserting their correctness. --- comtypes/test/test_bctx.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_bctx.py b/comtypes/test/test_bctx.py index adb88ba8..3ca6f382 100644 --- a/comtypes/test/test_bctx.py +++ b/comtypes/test/test_bctx.py @@ -1,9 +1,9 @@ import contextlib import unittest from _ctypes import COMError -from ctypes import POINTER, byref +from ctypes import POINTER, byref, sizeof -from comtypes import GUID, hresult +from comtypes import GUID, hresult, tagBIND_OPTS2 from comtypes.client import CreateObject, GetModule from comtypes.test.monikers_helper import ( ROTFLAGS_ALLOWANYCLIENT, @@ -64,3 +64,33 @@ def test_returns_rot(self): rot_from_func.Revoke(dw_reg) # After revoking: should NOT be running again self.assertEqual(rot_from_bctx, rot_from_func) + + +class Test_Set_Get_BindOptions(unittest.TestCase): + def test_set_get_bind_options(self): + bctx = _create_bctx() + # Create an instance of `BIND_OPTS2` and set some values. + # In comtypes, instances of Structure subclasses like `tagBIND_OPTS2` + # can be passed directly as arguments where COM methods expect a + # pointer to the structure. + hr = bctx.RemoteSetBindOptions( + tagBIND_OPTS2( + cbStruct=sizeof(tagBIND_OPTS2), + grfFlags=0x11223344, + grfMode=0x55667788, + dwTickCountDeadline=12345, + ) + ) + self.assertEqual(hr, hresult.S_OK) + # Create a new instance for retrieval. + # The `cbStruct` field is crucial in COM as it indicates the size of + # the structure to the COM component, allowing it to handle different + # versions of the structure (for backward and forward compatibility). + # https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ibindctx-getbindoptions#notes-to-callers + bind_opts = tagBIND_OPTS2(cbStruct=sizeof(tagBIND_OPTS2)) + ret = bctx.RemoteGetBindOptions(bind_opts) + self.assertIsInstance(ret, tagBIND_OPTS2) + self.assertEqual(bind_opts.cbStruct, sizeof(tagBIND_OPTS2)) + self.assertEqual(bind_opts.grfFlags, 0x11223344) + self.assertEqual(bind_opts.grfMode, 0x55667788) + self.assertEqual(bind_opts.dwTickCountDeadline, 12345) From 78f54f7ba59284359b1a20765de4445f4cb8ffde Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 11/15] test: Add tests for `IBindCtx` object parameter (`...ObjectParam`) methods. This commit introduces a new test class `Test_Get_Register_Revoke_ObjectParam` to `test/test_bctx.py`. It verifies the functionality of `GetObjectParam`, `RegisterObjectParam`, and `RevokeObjectParam` by registering and revoking a COM object and asserting its presence or absence. --- comtypes/test/test_bctx.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/comtypes/test/test_bctx.py b/comtypes/test/test_bctx.py index 3ca6f382..3dd2d3f0 100644 --- a/comtypes/test/test_bctx.py +++ b/comtypes/test/test_bctx.py @@ -66,6 +66,30 @@ def test_returns_rot(self): self.assertEqual(rot_from_bctx, rot_from_func) +class Test_Get_Register_Revoke_ObjectParam(unittest.TestCase): + def test_get_and_register_and_revoke(self): + bctx = _create_bctx() + key = str(GUID.create_new()) + vidctl = CreateObject(msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl) + # `GetObjectParam` should fail as it's NOT registered yet + with self.assertRaises(COMError) as cm: + bctx.GetObjectParam(key) + self.assertEqual(cm.exception.hresult, hresult.E_FAIL) + # Register object + hr = bctx.RegisterObjectParam(key, vidctl) + self.assertEqual(hr, hresult.S_OK) + # `GetObjectParam` should succeed now + ret_obj = bctx.GetObjectParam(key) + self.assertEqual(ret_obj.QueryInterface(msvidctl.IMSVidCtl), vidctl) + # Revoke object + hr = bctx.RevokeObjectParam(key) + self.assertEqual(hr, hresult.S_OK) + # `GetObjectParam` should fail again after revoke + with self.assertRaises(COMError) as cm: + bctx.GetObjectParam(key) + self.assertEqual(cm.exception.hresult, hresult.E_FAIL) + + class Test_Set_Get_BindOptions(unittest.TestCase): def test_set_get_bind_options(self): bctx = _create_bctx() From de61c35e6b12387a0f56006e3201b0aefbaf8b56 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 12/15] test: Add tests for `IBindCtx` bound object (`...ObjectBound`) methods. This commit introduces a new test class to `test/test_bctx.py`. It verifies the functionality of `RegisterObjectBound`, `RevokeObjectBound`, and `ReleaseBoundObjects` by registering and unregistering a COM object within the bind context. --- comtypes/test/test_bctx.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/comtypes/test/test_bctx.py b/comtypes/test/test_bctx.py index 3dd2d3f0..7f0b364d 100644 --- a/comtypes/test/test_bctx.py +++ b/comtypes/test/test_bctx.py @@ -66,6 +66,29 @@ def test_returns_rot(self): self.assertEqual(rot_from_bctx, rot_from_func) +class Test_Register_Revoke_Release_ObjectBound(unittest.TestCase): + def test_register_and_revoke(self): + bctx = _create_bctx() + vidctl = CreateObject(msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl) + # Binds the object to the bind context, ensuring it stays alive during + # the binding operation. + hr = bctx.RegisterObjectBound(vidctl) + self.assertEqual(hr, hresult.S_OK) + # At this point, `bctx` holds a reference to `vidctl`. + # Unlike `RegisterObjectParam`, there is no public API to retrieve + # objects registered via `RegisterObjectBound` from `IBindCtx`. + # Therefore, direct testing of `vidctl`'s accessibility via `bctx` + # after binding (similar to `GetObjectParam`) is not possible. + # Releases the reference to the object previously registered. + hr = bctx.RevokeObjectBound(vidctl) + self.assertEqual(hr, hresult.S_OK) + # `bctx` holds a reference to `vidctl` again. + # Releases all object references currently held by the bind context. + bctx.RegisterObjectBound(vidctl) + hr = bctx.ReleaseBoundObjects() + self.assertEqual(hr, hresult.S_OK) + + class Test_Get_Register_Revoke_ObjectParam(unittest.TestCase): def test_get_and_register_and_revoke(self): bctx = _create_bctx() From 172f8836175219615a6cca233e0733f936960971 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 12:00:02 +0900 Subject: [PATCH 13/15] test: Add test for `IRunningObjectTable.EnumRunning` method. --- comtypes/test/test_rot.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/comtypes/test/test_rot.py b/comtypes/test/test_rot.py index 88813ad1..e230f6b5 100644 --- a/comtypes/test/test_rot.py +++ b/comtypes/test/test_rot.py @@ -16,7 +16,12 @@ with contextlib.redirect_stdout(None): # supress warnings GetModule("msvidctl.dll") from comtypes.gen import MSVidCtlLib as msvidctl -from comtypes.gen.MSVidCtlLib import IBindCtx, IMoniker, IRunningObjectTable +from comtypes.gen.MSVidCtlLib import ( + IBindCtx, + IEnumMoniker, + IMoniker, + IRunningObjectTable, +) def _create_item_moniker(delim: str, item: str) -> IMoniker: @@ -62,3 +67,10 @@ def test_item(self): with self.assertRaises(COMError) as cm: rot.GetObject(mon) self.assertEqual(cm.exception.hresult, MK_E_UNAVAILABLE) + + +class Test_EnumRunning(unittest.TestCase): + def test_returns_enum_moniker(self): + rot = _create_rot() + enum_moniker = rot.EnumRunning() + self.assertIsInstance(enum_moniker, IEnumMoniker) From 10469d2a9cc1d6f79247810d715c51c2cf1e774e Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 13:18:06 +0900 Subject: [PATCH 14/15] fix: Resolve path comparison issues in `test_moniker.py` Add `_get_long_path_name` to `test_moniker.py`. This change ensures consistent path normalization across test environments, preventing failures due to short path vs. long path discrepancies. --- comtypes/test/test_moniker.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index 07f60c69..a6563a73 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -1,9 +1,11 @@ import contextlib +import ctypes import os import tempfile import unittest from _ctypes import COMError -from ctypes import POINTER, byref +from ctypes import POINTER, WinDLL, byref +from ctypes.wintypes import DWORD, LPCWSTR, LPWSTR, MAX_PATH from pathlib import Path from comtypes import GUID, hresult @@ -36,6 +38,19 @@ IRunningObjectTable, ) +_kernel32 = WinDLL("kernel32") + +_GetLongPathNameW = _kernel32.GetLongPathNameW +_GetLongPathNameW.argtypes = [LPCWSTR, LPWSTR, DWORD] +_GetLongPathNameW.restype = DWORD + + +def _get_long_path_name(path: str) -> str: + """Converts a path to its long form using GetLongPathNameW.""" + buffer = ctypes.create_unicode_buffer(MAX_PATH) + length = _GetLongPathNameW(path, buffer, MAX_PATH) + return buffer.value[:length] + def _create_generic_composite(mk_first: IMoniker, mk_rest: IMoniker) -> IMoniker: mon = POINTER(IMoniker)() @@ -88,7 +103,14 @@ def test_file(self): mon = _create_file_moniker(f.name) self.assertEqual(mon.IsSystemMoniker(), MKSYS_FILEMONIKER) bctx = _create_bctx() - self.assertEqual(mon.GetDisplayName(bctx, None), f.name) + self.assertEqual( + os.path.normcase( + os.path.normpath( + _get_long_path_name(mon.GetDisplayName(bctx, None)) + ) + ), + os.path.normcase(os.path.normpath(_get_long_path_name(f.name))), + ) self.assertEqual(mon.GetClassID(), CLSID_FileMoniker) self.assertEqual(mon.Inverse().GetClassID(), CLSID_AntiMoniker) @@ -242,6 +264,8 @@ def test_file(self): bound_obj = mon.RemoteBindToObject(bctx, None, IPersistFile._iid_) pf = bound_obj.QueryInterface(IPersistFile) self.assertEqual( - os.path.normcase(os.path.normpath(tmpfile)), - os.path.normcase(os.path.normpath(pf.GetCurFile())), + os.path.normcase(os.path.normpath(_get_long_path_name(str(tmpfile)))), + os.path.normcase( + os.path.normpath(_get_long_path_name(pf.GetCurFile())) + ), ) From 2e53d6a494f20779b973b4417f19480faa33706f Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 24 Jan 2026 13:33:30 +0900 Subject: [PATCH 15/15] fix: Simplify relative path comparison in `test_moniker.py` Replaced the dynamic calculation of relative paths using `Path.relative_to` with a literal string in `Test_RelativePathTo.test_file`. This simplifies the test logic and resolves the `TypeError` caused by `walk_up=True` in older Python environments, aligning with the goal of reducing complexity. --- comtypes/test/test_moniker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index a6563a73..2709b7db 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -235,9 +235,7 @@ def test_file(self): ) ), # Calculate the relative path from the directory of file1 to file2 - os.path.normcase( - os.path.normpath(file2.relative_to(file1, walk_up=True)) - ), + os.path.normcase(os.path.normpath("..\\..\\dir_b\\file2.txt")), )