From 2706fe3462429f5ff519c36bde358a484f04593f Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 8 Mar 2026 14:30:10 +0900 Subject: [PATCH 1/6] test: Expect `KeyError` for MSHTML default source interface Add a test to verify that calling `GetEvents` on an `htmlfile` object raises a `KeyError`. The `IProvideClassInfo2` for this object returns a null GUID for its default source interface. This test confirms that `comtypes` correctly fails when it cannot find this null interface in the registry. --- comtypes/test/test_eventinterface.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/comtypes/test/test_eventinterface.py b/comtypes/test/test_eventinterface.py index d30c0339..fd9f472a 100644 --- a/comtypes/test/test_eventinterface.py +++ b/comtypes/test/test_eventinterface.py @@ -111,5 +111,19 @@ def test_nondefault_eventinterface(self): del conn +class Test_MSHTML(ut.TestCase): + def test(self): + doc = CreateObject("htmlfile") + sink = object() + # MSHTML's HTMLDocument (which is what `CreateObject('htmlfile')` + # returns) does not expose a valid default source interface through + # `IProvideClassInfo2`. + # Specifically, it returns `GUID_NULL` as its default source interface, + # which results in a `KeyError` when `GetEvents` tries to look it up in + # the interface registry. + with self.assertRaises(KeyError, msg="{00000000-0000-0000-0000-000000000000}"): + GetEvents(doc, sink) + + if __name__ == "__main__": ut.main() From 181d249008adf5df34ffd8f618f61ab032ae6a69 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 8 Mar 2026 14:30:10 +0900 Subject: [PATCH 2/6] feat: Handle `GUID_NULL` for outgoing interface IID in `FindOutgoingInterface`. When `IProvideClassInfo2` returns `GUID_NULL` as the outgoing interface IID, `FindOutgoingInterface` now raises a `NotImplementedError`. This addresses cases where some COM servers (like `htmlfile`) return a null GUID instead of the default source interface's GUID. A new test case `test_retrieved_outgoing_iid_is_guid_null` has been added to `test_eventinterface.py` to verify this behavior. --- comtypes/client/_events.py | 2 ++ comtypes/test/test_eventinterface.py | 7 ++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/comtypes/client/_events.py b/comtypes/client/_events.py index e6b662df..a967820b 100644 --- a/comtypes/client/_events.py +++ b/comtypes/client/_events.py @@ -120,6 +120,8 @@ def FindOutgoingInterface(source: IUnknown) -> type[IUnknown]: except COMError: pass else: + if guid == comtypes.GUID(): + raise NotImplementedError("retrieved outgoing interface IID is GUID_NULL") # another try: block needed? try: interface = comtypes.com_interface_registry[str(guid)] diff --git a/comtypes/test/test_eventinterface.py b/comtypes/test/test_eventinterface.py index fd9f472a..a8aad44a 100644 --- a/comtypes/test/test_eventinterface.py +++ b/comtypes/test/test_eventinterface.py @@ -112,16 +112,13 @@ def test_nondefault_eventinterface(self): class Test_MSHTML(ut.TestCase): - def test(self): + def test_retrieved_outgoing_iid_is_guid_null(self): doc = CreateObject("htmlfile") sink = object() # MSHTML's HTMLDocument (which is what `CreateObject('htmlfile')` # returns) does not expose a valid default source interface through # `IProvideClassInfo2`. - # Specifically, it returns `GUID_NULL` as its default source interface, - # which results in a `KeyError` when `GetEvents` tries to look it up in - # the interface registry. - with self.assertRaises(KeyError, msg="{00000000-0000-0000-0000-000000000000}"): + with self.assertRaises(NotImplementedError): GetEvents(doc, sink) From 720cf7410d2f160e00fc60b26e35957557f3e0d8 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 8 Mar 2026 14:30:10 +0900 Subject: [PATCH 3/6] docs: Add comment on `GUID_NULL` from `IProvideClassInfo2` in `FindOutgoingInterface`. Adds a comment to `FindOutgoingInterface` to explain that some COM servers may return `GUID_NULL` from `IProvideClassInfo2.GetGUID` instead of a valid interface IID. This clarifies the subsequent error handling for this specific edge case. --- comtypes/client/_events.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/comtypes/client/_events.py b/comtypes/client/_events.py index a967820b..a6d193e6 100644 --- a/comtypes/client/_events.py +++ b/comtypes/client/_events.py @@ -121,6 +121,8 @@ def FindOutgoingInterface(source: IUnknown) -> type[IUnknown]: pass else: if guid == comtypes.GUID(): + # Some COM servers, even if they implement `IProvideClassInfo2`, + # may return GUID_NULL instead of the default source interface's GUID. raise NotImplementedError("retrieved outgoing interface IID is GUID_NULL") # another try: block needed? try: From 89e5b791789f363786d5f7adcf61296e7c50670d Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 8 Mar 2026 14:30:10 +0900 Subject: [PATCH 4/6] test: Expect `AssertionError` for IMAPI2 custom event interface. Add `Test_IMAPI2FS` to `test_eventinterface.py` to verify that `GetEvents` raises an `AssertionError` when used with the `MsftFileSystemImage` object. The default event interface `DFileSystemImageEvents` is a custom `TKIND_INTERFACE` that inherits from `IDispatch`, but is neither a dual nor pure dispatch interface. Its v-table methods, as generated from type info, lack the `dispid` attributes that `GetEvents` requires. This test confirms the expected failure. --- comtypes/test/test_eventinterface.py | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/comtypes/test/test_eventinterface.py b/comtypes/test/test_eventinterface.py index a8aad44a..5768a721 100644 --- a/comtypes/test/test_eventinterface.py +++ b/comtypes/test/test_eventinterface.py @@ -1,8 +1,10 @@ import gc +import tempfile import time import unittest as ut from ctypes import HRESULT, byref from ctypes.wintypes import MSG +from pathlib import Path from comtypes import COMMETHOD, GUID, IUnknown from comtypes.automation import DISPID @@ -122,5 +124,34 @@ def test_retrieved_outgoing_iid_is_guid_null(self): GetEvents(doc, sink) +class Test_IMAPI2FS(ut.TestCase): + def setUp(self): + CLSID_MsftFileSystemImage = GUID("{2C941FC5-975B-59BE-A960-9A2A262853A5}") + self.image = CreateObject(CLSID_MsftFileSystemImage) + self.image.FileSystemsToCreate = 1 # FsiFileSystemISO9660 + td = tempfile.TemporaryDirectory() + self.tmp_dir = Path(td.name) + self.addCleanup(td.cleanup) + + def tearDown(self): + del self.image + # Force garbage collection and wait slightly to ensure COM resources + # are released properly between tests. + gc.collect() + time.sleep(2) + + def test(self): + sink = object() + # The default event interface for IMAPI2's FileSystemImage is + # `DFileSystemImageEvents`. Although it inherits from `IDispatch`, + # it is a custom interface (`TKIND_INTERFACE`), not a dual or pure + # dispatch interface (`TKIND_DISPATCH`). + # Its methods are v-table bound, and the interface definition + # that comtypes generates from the type info lacks the `dispid` + # attributes that `GetEvents` requires. + with self.assertRaises(AssertionError): + GetEvents(self.image, sink) + + if __name__ == "__main__": ut.main() From 80f54cc338874688405e398c854aad515cdf04b3 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 8 Mar 2026 14:30:10 +0900 Subject: [PATCH 5/6] feat: Raise `NotImplementedError` for event interfaces lacking DISPIDs. Replace `assert` in `CreateEventReceiver` with an explicit check for `dispid`, raising `NotImplementedError` with a message when DISPIDs are missing. --- comtypes/client/_events.py | 10 +++++++++- comtypes/test/test_eventinterface.py | 5 +---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/comtypes/client/_events.py b/comtypes/client/_events.py index a6d193e6..603c5531 100644 --- a/comtypes/client/_events.py +++ b/comtypes/client/_events.py @@ -262,7 +262,15 @@ def _get_method_finder_(self, itf: type[IUnknown]) -> _MethodFinder: # Can dispid be at a different index? Should check code generator... # ...but hand-written code should also work... dispid = m.idlflags[0] - assert isinstance(dispid, comtypes.dispid) + if not isinstance(dispid, comtypes.dispid): + # The interface is a subclass of `IDispatch` but its methods do not + # have DISPIDs, indicating it's not an interface suitable for event + # handling. + raise NotImplementedError( + "Event receiver creation requires event methods to have DISPIDs " + f"for dispatching, but '{interface.__name__}' ({interface._iid_}) " + "lacks them, even though it inherits from 'IDispatch'." + ) impl = finder.get_impl(interface, m.name, m.paramflags, m.idlflags) # XXX Wouldn't work for 'propget', 'propput', 'propputref' # methods - are they allowed on event interfaces? diff --git a/comtypes/test/test_eventinterface.py b/comtypes/test/test_eventinterface.py index 5768a721..f15e5a6e 100644 --- a/comtypes/test/test_eventinterface.py +++ b/comtypes/test/test_eventinterface.py @@ -146,10 +146,7 @@ def test(self): # `DFileSystemImageEvents`. Although it inherits from `IDispatch`, # it is a custom interface (`TKIND_INTERFACE`), not a dual or pure # dispatch interface (`TKIND_DISPATCH`). - # Its methods are v-table bound, and the interface definition - # that comtypes generates from the type info lacks the `dispid` - # attributes that `GetEvents` requires. - with self.assertRaises(AssertionError): + with self.assertRaises(NotImplementedError): GetEvents(self.image, sink) From 1f5983bd9e0bdf9060d53ecf9b6b9eed7079737b Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 8 Mar 2026 14:30:10 +0900 Subject: [PATCH 6/6] test: Rename `Test_IMAPI2FS` test and clarify DISPID requirement. Rename `Test_IMAPI2FS.test` to `test_event_methods_lack_dispids` to better reflect the cause of the `NotImplementedError`. --- comtypes/test/test_eventinterface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_eventinterface.py b/comtypes/test/test_eventinterface.py index f15e5a6e..9fffc1a9 100644 --- a/comtypes/test/test_eventinterface.py +++ b/comtypes/test/test_eventinterface.py @@ -140,12 +140,13 @@ def tearDown(self): gc.collect() time.sleep(2) - def test(self): + def test_event_methods_lack_dispids(self): sink = object() # The default event interface for IMAPI2's FileSystemImage is # `DFileSystemImageEvents`. Although it inherits from `IDispatch`, # it is a custom interface (`TKIND_INTERFACE`), not a dual or pure - # dispatch interface (`TKIND_DISPATCH`). + # dispatch interface (`TKIND_DISPATCH`); therefore, its methods + # do not have DISPIDs. with self.assertRaises(NotImplementedError): GetEvents(self.image, sink)