From 85ae317904f531a359a26f2bad0a04c753ab98f3 Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 01/14] test: Add test for pure dispatch WindowsInstaller object and dispmethod calls. Verifies the `WindowsInstaller.Installer` COM object's creation, its pure dispatch nature (non-dual), and its dispmethod calls. --- comtypes/test/test_msi.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 comtypes/test/test_msi.py diff --git a/comtypes/test/test_msi.py b/comtypes/test/test_msi.py new file mode 100644 index 00000000..02320322 --- /dev/null +++ b/comtypes/test/test_msi.py @@ -0,0 +1,34 @@ +import unittest as ut +import winreg + +import comtypes.client +from comtypes import typeinfo +from comtypes.automation import IDispatch + +MSI_TLIB = typeinfo.LoadTypeLibEx("msi.dll") +comtypes.client.GetModule(MSI_TLIB) +import comtypes.gen.WindowsInstaller as msi + +HKCR = 0 + + +class Test_Installer(ut.TestCase): + def test_hkcr_registry_value(self): + # `WindowsInstaller.Installer` provides access to Windows configuration. + installer = comtypes.client.CreateObject( + "WindowsInstaller.Installer", interface=msi.Installer + ) + IID_Installer = msi.Installer._iid_ + # This confirms that the Installer is a pure dispatch interface. + self.assertIsInstance(installer, IDispatch) + ti = MSI_TLIB.GetTypeInfoOfGuid(IID_Installer) + ta = ti.GetTypeAttr() + self.assertEqual(IID_Installer, ta.guid) + self.assertFalse(ta.wTypeFlags & typeinfo.TYPEFLAG_FDUAL) + # Both methods below get the "Programmatic Identifier" used to handle + # ".txt" files. + with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".txt") as key: + winreg_val, _ = winreg.QueryValueEx(key, "") + msi_val = installer.RegistryValue(HKCR, ".txt", "") + # This confirms that the Installer can correctly read system information. + self.assertEqual(winreg_val, msi_val) From db0ee2c498714b749660c75f8fdb9817f6d9ef7c Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 02/14] refactor: Improve readability and conciseness. --- comtypes/test/test_msi.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/comtypes/test/test_msi.py b/comtypes/test/test_msi.py index 02320322..aeaf5e17 100644 --- a/comtypes/test/test_msi.py +++ b/comtypes/test/test_msi.py @@ -15,12 +15,12 @@ class Test_Installer(ut.TestCase): def test_hkcr_registry_value(self): # `WindowsInstaller.Installer` provides access to Windows configuration. - installer = comtypes.client.CreateObject( + inst = comtypes.client.CreateObject( "WindowsInstaller.Installer", interface=msi.Installer ) IID_Installer = msi.Installer._iid_ # This confirms that the Installer is a pure dispatch interface. - self.assertIsInstance(installer, IDispatch) + self.assertIsInstance(inst, IDispatch) ti = MSI_TLIB.GetTypeInfoOfGuid(IID_Installer) ta = ti.GetTypeAttr() self.assertEqual(IID_Installer, ta.guid) @@ -28,7 +28,6 @@ def test_hkcr_registry_value(self): # Both methods below get the "Programmatic Identifier" used to handle # ".txt" files. with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".txt") as key: - winreg_val, _ = winreg.QueryValueEx(key, "") - msi_val = installer.RegistryValue(HKCR, ".txt", "") + progid, _ = winreg.QueryValueEx(key, "") # This confirms that the Installer can correctly read system information. - self.assertEqual(winreg_val, msi_val) + self.assertEqual(progid, inst.RegistryValue(HKCR, ".txt", "")) From 7f85f3a38fe775e59a7e314e2fc425a7ead94111 Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 03/14] test: Verify `RegistryValue` behavior without optional argument Adds a test for `Installer.RegistryValue`'s key existence check. Also renames a test method for clarity and updates comments. --- comtypes/test/test_msi.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_msi.py b/comtypes/test/test_msi.py index aeaf5e17..7de47be9 100644 --- a/comtypes/test/test_msi.py +++ b/comtypes/test/test_msi.py @@ -9,11 +9,12 @@ comtypes.client.GetModule(MSI_TLIB) import comtypes.gen.WindowsInstaller as msi -HKCR = 0 +HKCR = 0 # HKEY_CLASSES_ROOT +HKCU = 1 # HKEY_CURRENT_USER class Test_Installer(ut.TestCase): - def test_hkcr_registry_value(self): + def test_registry_value_with_root_key_value(self): # `WindowsInstaller.Installer` provides access to Windows configuration. inst = comtypes.client.CreateObject( "WindowsInstaller.Installer", interface=msi.Installer @@ -31,3 +32,17 @@ def test_hkcr_registry_value(self): progid, _ = winreg.QueryValueEx(key, "") # This confirms that the Installer can correctly read system information. self.assertEqual(progid, inst.RegistryValue(HKCR, ".txt", "")) + + def test_registry_value_with_root_key(self): + inst = comtypes.client.CreateObject( + "WindowsInstaller.Installer", interface=msi.Installer + ) + # If the third arg is missing, `Installer.RegistryValue` returns a Boolean + # designating whether the key exists. + # https://learn.microsoft.com/en-us/windows/win32/msi/installer-registryvalue + # The `HKEY_CURRENT_USER\\Control Panel\\Desktop` registry key is a standard + # registry key that exists across all versions of the Windows. + self.assertTrue(inst.RegistryValue(HKCU, r"Control Panel\Desktop")) + # Since a single backslash is reserved as a path separator and cannot be used + # in a key name itself. Therefore, such a key exists in no version of Windows. + self.assertFalse(inst.RegistryValue(HKCU, "\\")) From 9fd2b7e3720f022f1b52f3ec6373751402c60059 Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 04/14] test: Add safeguard test for named parameters in dispmethods. Adds a test to ensure a `ValueError` is raised when calling a dispmethod with named parameters, which is not yet supported. This acts as a safeguard against invalid calls. --- comtypes/test/test_msi.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/comtypes/test/test_msi.py b/comtypes/test/test_msi.py index 7de47be9..ec47cd72 100644 --- a/comtypes/test/test_msi.py +++ b/comtypes/test/test_msi.py @@ -19,13 +19,6 @@ def test_registry_value_with_root_key_value(self): inst = comtypes.client.CreateObject( "WindowsInstaller.Installer", interface=msi.Installer ) - IID_Installer = msi.Installer._iid_ - # This confirms that the Installer is a pure dispatch interface. - self.assertIsInstance(inst, IDispatch) - ti = MSI_TLIB.GetTypeInfoOfGuid(IID_Installer) - ta = ti.GetTypeAttr() - self.assertEqual(IID_Installer, ta.guid) - self.assertFalse(ta.wTypeFlags & typeinfo.TYPEFLAG_FDUAL) # Both methods below get the "Programmatic Identifier" used to handle # ".txt" files. with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".txt") as key: @@ -46,3 +39,25 @@ def test_registry_value_with_root_key(self): # Since a single backslash is reserved as a path separator and cannot be used # in a key name itself. Therefore, such a key exists in no version of Windows. self.assertFalse(inst.RegistryValue(HKCU, "\\")) + + def test_registry_value_with_named_params(self): + inst = comtypes.client.CreateObject( + "WindowsInstaller.Installer", interface=msi.Installer + ) + IID_Installer = msi.Installer._iid_ + # This confirms that the Installer is a pure dispatch interface. + self.assertIsInstance(inst, IDispatch) + ti = MSI_TLIB.GetTypeInfoOfGuid(IID_Installer) + ta = ti.GetTypeAttr() + self.assertEqual(IID_Installer, ta.guid) + self.assertFalse(ta.wTypeFlags & typeinfo.TYPEFLAG_FDUAL) + # NOTE: Named parameters are not yet implemented for the dispmethod called + # via the `Invoke` method. + # See https://github.com/enthought/comtypes/issues/371 + # As a safeguard until implementation is complete, an error will be raised + # if named arguments are passed to prevent invalid calls. + # TODO: After named parameters are supported, this will become a test to + # assert the return value. + ERRMSG = "named parameters not yet implemented" + with self.assertRaises(ValueError, msg=ERRMSG): + inst.RegistryValue(Root=HKCR, Key=".txt", Value="") From 7dd4f952ad7cd6d217bd3a1fe544226e03579e6b Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 05/14] test: Add test for `Installer.ProductState` property. Adds a test for the `Installer.ProductState` property to verify both correct value retrieval and error handling. --- comtypes/test/test_msi.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_msi.py b/comtypes/test/test_msi.py index ec47cd72..d941b043 100644 --- a/comtypes/test/test_msi.py +++ b/comtypes/test/test_msi.py @@ -2,7 +2,7 @@ import winreg import comtypes.client -from comtypes import typeinfo +from comtypes import GUID, typeinfo from comtypes.automation import IDispatch MSI_TLIB = typeinfo.LoadTypeLibEx("msi.dll") @@ -60,4 +60,36 @@ def test_registry_value_with_named_params(self): # assert the return value. ERRMSG = "named parameters not yet implemented" with self.assertRaises(ValueError, msg=ERRMSG): - inst.RegistryValue(Root=HKCR, Key=".txt", Value="") + inst.RegistryValue(Root=HKCR, Key=".txt", Value="") # type: ignore + with self.assertRaises(ValueError, msg=ERRMSG): + inst.RegistryValue(Value="", Root=HKCR, Key=".txt") # type: ignore + with self.assertRaises(ValueError, msg=ERRMSG): + inst.RegistryValue(HKCR, Key=".txt", Value="") # type: ignore + with self.assertRaises(ValueError, msg=ERRMSG): + inst.RegistryValue(HKCR, ".txt", Value="") # type: ignore + with self.assertRaises(ValueError, msg=ERRMSG): + inst.RegistryValue(Root=HKCU, Key=r"Control Panel\Desktop") # type: ignore + with self.assertRaises(ValueError, msg=ERRMSG): + inst.RegistryValue(Key=r"Control Panel\Desktop", Root=HKCR) # type: ignore + with self.assertRaises(ValueError, msg=ERRMSG): + inst.RegistryValue(HKCR, Key=r"Control Panel\Desktop") # type: ignore + + def test_product_state(self): + inst = comtypes.client.CreateObject( + "WindowsInstaller.Installer", interface=msi.Installer + ) + # There is no product associated with the Null GUID. + pdcode = str(GUID()) + expected = msi.MsiInstallState.msiInstallStateUnknown + self.assertEqual(expected, inst.ProductState(pdcode)) + self.assertEqual(expected, inst.ProductState[pdcode]) + # The `ProductState` property is a read-only property. + # https://learn.microsoft.com/en-us/windows/win32/msi/installer-productstate-property + with self.assertRaises(TypeError): + inst.ProductState[pdcode] = msi.MsiInstallState.msiInstallStateDefault # type: ignore + # NOTE: Named parameters are not yet implemented for the named property. + # See https://github.com/enthought/comtypes/issues/371 + # TODO: After named parameters are supported, this will become a test to + # assert the return value. + with self.assertRaises(TypeError): + inst.ProductState(Product=pdcode) # type: ignore From 9d86b16c6ca78011e095fae14f67b58b13202b0d Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 06/14] test: Correct typo and rename test method in `test_dict.py`. Corrected a typo in a comment from "assing" to "assign". Renamed the test method `test_dict` to `test_dynamic` to more accurately reflect that it tests the dynamic dispatch capabilities of `comtypes`. --- comtypes/test/test_dict.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_dict.py b/comtypes/test/test_dict.py index 47fb025f..2e35b75b 100644 --- a/comtypes/test/test_dict.py +++ b/comtypes/test/test_dict.py @@ -8,7 +8,7 @@ class Test(unittest.TestCase): - def test_dict(self): + def test_dynamic(self): d = CreateObject("Scripting.Dictionary", dynamic=True) self.assertEqual(type(d), Dispatch) @@ -70,7 +70,7 @@ def test_dict(self): # This calls propputref, since we assign an Object d.Item["object"] = s - # This calls propput, since we assing a Value + # This calls propput, since we assign a Value d.Item["value"] = s.CompareMode a = d.Item["object"] From 6f9750ec986590b9b97b9ddd9ccc7361b2a261b5 Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 07/14] test: Clarify comment for `HashVal` in `test_dict.py` Updated the comment for the `HashVal` property to clarify that it is a 'hidden' member used internally by `Scripting.Dictionary`, not intended for external use. --- comtypes/test/test_dict.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comtypes/test/test_dict.py b/comtypes/test/test_dict.py index 2e35b75b..128cd94e 100644 --- a/comtypes/test/test_dict.py +++ b/comtypes/test/test_dict.py @@ -18,6 +18,7 @@ def test_dynamic(self): setattr(d, "Count", -1) # HashVal is a 'named' propget, no propput + # HashVal is a 'hidden' member and used internally. ##d.HashVal # Add(Key, Item) -> None From bd9333065d11a2d142fde26cc67acf15221bf40d Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 08/14] test: Refactor `test_dynamic` in `test_dict.py` to use `Scripting` constants. Replaced magic numbers with `comtypes.gen.Scripting` constants in `comtypes/test/test_dict.py`. This improves readability and maintainability by using named constants from the `scrrun.dll` type library, aligning with official documentation for `Scripting.Dictionary.CompareMode`. --- comtypes/test/test_dict.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/comtypes/test/test_dict.py b/comtypes/test/test_dict.py index 128cd94e..24a747af 100644 --- a/comtypes/test/test_dict.py +++ b/comtypes/test/test_dict.py @@ -3,9 +3,12 @@ import unittest from comtypes.automation import VARIANT -from comtypes.client import CreateObject +from comtypes.client import CreateObject, GetModule from comtypes.client.lazybind import Dispatch +GetModule("scrrun.dll") +import comtypes.gen.Scripting as scrrun # noqa + class Test(unittest.TestCase): def test_dynamic(self): @@ -31,10 +34,11 @@ def test_dynamic(self): # CompareMode: propget, propput # (Can only be set when dict is empty!) - self.assertEqual(d.CompareMode, 0) - d.CompareMode = 1 - self.assertEqual(d.CompareMode, 1) - d.CompareMode = 0 + # Verify that the default is BinaryCompare. + self.assertEqual(d.CompareMode, scrrun.BinaryCompare) + d.CompareMode = scrrun.TextCompare + self.assertEqual(d.CompareMode, scrrun.TextCompare) + d.CompareMode = scrrun.BinaryCompare # Exists(key) -> bool self.assertEqual(d.Exists(42), False) @@ -67,7 +71,7 @@ def test_dynamic(self): # part 2, testing propput and propputref s = CreateObject("Scripting.Dictionary", dynamic=True) - s.CompareMode = 42 + s.CompareMode = scrrun.DatabaseCompare # This calls propputref, since we assign an Object d.Item["object"] = s @@ -77,14 +81,14 @@ def test_dynamic(self): a = d.Item["object"] self.assertEqual(d.Item["object"], s) - self.assertEqual(d.Item["object"].CompareMode, 42) - self.assertEqual(d.Item["value"], 42) + self.assertEqual(d.Item["object"].CompareMode, scrrun.DatabaseCompare) + self.assertEqual(d.Item["value"], scrrun.DatabaseCompare) # Changing a property of the object - s.CompareMode = 5 + s.CompareMode = scrrun.BinaryCompare self.assertEqual(d.Item["object"], s) - self.assertEqual(d.Item["object"].CompareMode, 5) - self.assertEqual(d.Item["value"], 42) + self.assertEqual(d.Item["object"].CompareMode, scrrun.BinaryCompare) + self.assertEqual(d.Item["value"], scrrun.DatabaseCompare) # This also calls propputref since we assign an Object d.Item["var"] = VARIANT(s) From a47fe6ffa8c7be2b8d4c8eca8ea6a31e6cb499e5 Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 09/14] test: Add dual interface tests for `Scripting.Dictionary`. --- comtypes/test/test_dict.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/comtypes/test/test_dict.py b/comtypes/test/test_dict.py index 24a747af..29ba06c8 100644 --- a/comtypes/test/test_dict.py +++ b/comtypes/test/test_dict.py @@ -2,6 +2,7 @@ import unittest +from comtypes import typeinfo from comtypes.automation import VARIANT from comtypes.client import CreateObject, GetModule from comtypes.client.lazybind import Dispatch @@ -105,6 +106,36 @@ def test_dynamic(self): # d(key) -> value self.assertEqual(d("blah"), "blarg") + def test_static(self): + d = CreateObject(scrrun.Dictionary, interface=scrrun.IDictionary) + # This confirms that the Dictionary is a dual interface. + self.assertTrue( + d.GetTypeInfo(0).GetTypeAttr().wTypeFlags & typeinfo.TYPEFLAG_FDUAL + ) + # Dual interfaces call COM methods that support named arguments. + d.Add("one", 1) + d.Add("two", Item=2) + d.Add(Key="three", Item=3) + d.Add(Item=4, Key="four") + d.Item["five"] = 5 + d["six"] = 6 + self.assertEqual(d.Count, 6) + self.assertEqual(len(d), 6) + self.assertEqual(d("six"), 6) + self.assertEqual(d.Item("five"), 5) + self.assertEqual(d("four"), 4) + self.assertEqual(d["three"], 3) + self.assertEqual(d.Item["two"], 2) + self.assertEqual(d("one"), 1) + # NOTE: Named parameters are not yet implemented for the named property. + # See https://github.com/enthought/comtypes/issues/371 + # TODO: After named parameters are supported, this will become a test to + # assert the return value. + with self.assertRaises(TypeError): + d.Item(Key="two") + with self.assertRaises(TypeError): + d(Key="one") + if __name__ == "__main__": unittest.main() From 046983724d1b6d8eef24648f5c37016222093824 Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 10/14] refactor: Improve code quality in `test_dict.py`. --- comtypes/test/test_dict.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/comtypes/test/test_dict.py b/comtypes/test/test_dict.py index 29ba06c8..1ec048cc 100644 --- a/comtypes/test/test_dict.py +++ b/comtypes/test/test_dict.py @@ -8,7 +8,7 @@ from comtypes.client.lazybind import Dispatch GetModule("scrrun.dll") -import comtypes.gen.Scripting as scrrun # noqa +import comtypes.gen.Scripting as scrrun class Test(unittest.TestCase): @@ -79,8 +79,6 @@ def test_dynamic(self): # This calls propput, since we assign a Value d.Item["value"] = s.CompareMode - a = d.Item["object"] - self.assertEqual(d.Item["object"], s) self.assertEqual(d.Item["object"].CompareMode, scrrun.DatabaseCompare) self.assertEqual(d.Item["value"], scrrun.DatabaseCompare) @@ -96,8 +94,7 @@ def test_dynamic(self): self.assertEqual(d.Item["var"], s) # iter(d) - keys = [x for x in d] - self.assertEqual(d.Keys(), tuple([x for x in d])) + self.assertEqual(d.Keys(), tuple(x for x in d)) # d[key] = value # d[key] -> value @@ -109,9 +106,8 @@ def test_dynamic(self): def test_static(self): d = CreateObject(scrrun.Dictionary, interface=scrrun.IDictionary) # This confirms that the Dictionary is a dual interface. - self.assertTrue( - d.GetTypeInfo(0).GetTypeAttr().wTypeFlags & typeinfo.TYPEFLAG_FDUAL - ) + ti = d.GetTypeInfo(0) + self.assertTrue(ti.GetTypeAttr().wTypeFlags & typeinfo.TYPEFLAG_FDUAL) # Dual interfaces call COM methods that support named arguments. d.Add("one", 1) d.Add("two", Item=2) From 943f77709d15a7057d2d087fb2503b20f0ee86bd Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 11/14] test: Refactor `test_dynamic` and `test_static` in `test_dict.py`. --- comtypes/test/test_dict.py | 40 +++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/comtypes/test/test_dict.py b/comtypes/test/test_dict.py index 1ec048cc..309b4ce1 100644 --- a/comtypes/test/test_dict.py +++ b/comtypes/test/test_dict.py @@ -1,4 +1,4 @@ -"""Use Scripting.Dictionary to test the lazybind module.""" +"""Use Scripting.Dictionary to test the lazybind and the generated modules.""" import unittest @@ -19,7 +19,7 @@ def test_dynamic(self): # Count is a normal propget, no propput self.assertEqual(d.Count, 0) with self.assertRaises(AttributeError): - setattr(d, "Count", -1) + d.Count = -1 # HashVal is a 'named' propget, no propput # HashVal is a 'hidden' member and used internally. @@ -108,29 +108,33 @@ def test_static(self): # This confirms that the Dictionary is a dual interface. ti = d.GetTypeInfo(0) self.assertTrue(ti.GetTypeAttr().wTypeFlags & typeinfo.TYPEFLAG_FDUAL) + # Count is a normal propget, no propput + self.assertEqual(d.Count, 0) + with self.assertRaises(AttributeError): + d.Count = -1 # type: ignore # Dual interfaces call COM methods that support named arguments. - d.Add("one", 1) - d.Add("two", Item=2) - d.Add(Key="three", Item=3) - d.Add(Item=4, Key="four") - d.Item["five"] = 5 - d["six"] = 6 - self.assertEqual(d.Count, 6) - self.assertEqual(len(d), 6) - self.assertEqual(d("six"), 6) - self.assertEqual(d.Item("five"), 5) - self.assertEqual(d("four"), 4) - self.assertEqual(d["three"], 3) - self.assertEqual(d.Item["two"], 2) - self.assertEqual(d("one"), 1) + d.Add("spam", "foo") + d.Add("egg", Item="bar") + self.assertEqual(d.Count, 2) + d.Add(Key="ham", Item="baz") + self.assertEqual(len(d), 3) + d.Add(Item="qux", Key="toast") + d.Item["beans"] = "quux" + d["bacon"] = "corge" + self.assertEqual(d("spam"), "foo") + self.assertEqual(d.Item["egg"], "bar") + self.assertEqual(d["ham"], "baz") + self.assertEqual(d("toast"), "qux") + self.assertEqual(d.Item("beans"), "quux") + self.assertEqual(d("bacon"), "corge") # NOTE: Named parameters are not yet implemented for the named property. # See https://github.com/enthought/comtypes/issues/371 # TODO: After named parameters are supported, this will become a test to # assert the return value. with self.assertRaises(TypeError): - d.Item(Key="two") + d.Item(Key="spam") with self.assertRaises(TypeError): - d(Key="one") + d(Key="egg") if __name__ == "__main__": From 3abca85db3eb54062986e984aab4fffb2a7125df Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 12/14] refactor: Enforce positional-only arguments for dispmethods. To improve type safety, this change marks all arguments in generated dispmethod stubs as positional-only. This allows static type checkers to catch invalid calls using named arguments, which are not supported. --- comtypes/tools/codegenerator/typeannotator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 10b68e76..7f7851e0 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -294,7 +294,13 @@ def getvalue(self, name: str) -> str: has_optional = True out = _to_outtype(self.method.returns) in_ = ("self, " + ", ".join(inargs)) if inargs else "self" - content = f"def {name}({in_}) -> {out}: ..." + # NOTE: Since named parameters are not yet implemented, all arguments + # for the dispmethod (called via `Invoke`) are marked as positional-only + # parameters, introduced in PEP570. See also `automation.IDispatch.Invoke`. + # See https://github.com/enthought/comtypes/issues/371 + # TODO: After named parameters are supported, the positional-only parameter + # markers will be removed. + content = f"def {name}({in_}, /) -> {out}: ..." if keyword.iskeyword(name): content = f"pass # avoid using a keyword for {content}" return content From f55f0f82fcb61e944164d286c99fee958e3d549a Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 13/14] test: Align `test_disp_interface` with actual annotator output. --- comtypes/test/test_typeannotator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 280ab3ab..20a9d2ba 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -59,13 +59,13 @@ def test_disp_interface(self): " def ham(self) -> hints.Incomplete: ...\n" " pass # @property # dispprop\n" " pass # avoid using a keyword for def except(self) -> hints.Incomplete: ...\n" # noqa - " def bacon(self, *args: hints.Any, **kwargs: hints.Any) -> hints.Incomplete: ...\n" # noqa - " def _get_spam(self, arg1: hints.Incomplete = ...) -> hints.Incomplete: ...\n" # noqa - " def _set_spam(self, arg1: hints.Incomplete = ..., **kwargs: hints.Any) -> hints.Incomplete: ...\n" # noqa + " def bacon(self, *args: hints.Any, **kwargs: hints.Any, /) -> hints.Incomplete: ...\n" # noqa + " def _get_spam(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa + " def _set_spam(self, arg1: hints.Incomplete = ..., **kwargs: hints.Any, /) -> hints.Incomplete: ...\n" # noqa " spam = hints.named_property('spam', _get_spam, _set_spam)\n" - " pass # avoid using a keyword for def raise(self, foo: hints.Incomplete, bar: hints.Incomplete = ...) -> hints.Incomplete: ...\n" # noqa - " def _get_def(self, arg1: hints.Incomplete = ...) -> hints.Incomplete: ...\n" # noqa - " def _set_def(self, arg1: hints.Incomplete = ..., **kwargs: hints.Any) -> hints.Incomplete: ...\n" # noqa + " pass # avoid using a keyword for def raise(self, foo: hints.Incomplete, bar: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa + " def _get_def(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa + " def _set_def(self, arg1: hints.Incomplete = ..., **kwargs: hints.Any, /) -> hints.Incomplete: ...\n" # noqa " pass # avoid using a keyword for def = hints.named_property('def', _get_def, _set_def)" # noqa ) self.assertEqual( From 8dc6b016052bfb5c1a6ccc3f31643cb297e9ebe0 Mon Sep 17 00:00:00 2001 From: junkmd Date: Fri, 19 Dec 2025 07:19:30 +0900 Subject: [PATCH 14/14] fix: Avoid inappropriate signature for dispmethods with no arguments. Modified `DispMethodAnnotator` to prevent adding a positional-only parameter marker ('/') to method signatures that have no arguments. This fixes a bug where it would generate inappropriate type hints like `def method(self, /)`. An additional test case with a no-argument dispmethod (`egg`) was also added to `test_disp_interface` to verify the fix. --- comtypes/test/test_typeannotator.py | 16 ++++++++++++++-- comtypes/tools/codegenerator/typeannotator.py | 6 ++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 20a9d2ba..1417aa37 100644 --- a/comtypes/test/test_typeannotator.py +++ b/comtypes/test/test_typeannotator.py @@ -48,7 +48,18 @@ def _create_typedesc_disp_interface(self) -> typedesc.DispInterface: put_def = typedesc.DispMethod(8, 4, "def", void_type, ["propput"], None) put_def.add_argument(VARIANT_type, "arg1", ["in", "optional"], None) put_def.add_argument(VARIANT_type, "arg2", ["in"], None) - for m in [ham, bacon, get_spam, put_spam, except_, raise_, get_def, put_def]: + egg = typedesc.DispMethod(643, 1, "egg", VARIANT_BOOL_type, [], None) + for m in [ + ham, + bacon, + get_spam, + put_spam, + except_, + raise_, + get_def, + put_def, + egg, + ]: itf.add_member(m) return itf @@ -66,7 +77,8 @@ def test_disp_interface(self): " pass # avoid using a keyword for def raise(self, foo: hints.Incomplete, bar: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa " def _get_def(self, arg1: hints.Incomplete = ..., /) -> hints.Incomplete: ...\n" # noqa " def _set_def(self, arg1: hints.Incomplete = ..., **kwargs: hints.Any, /) -> hints.Incomplete: ...\n" # noqa - " pass # avoid using a keyword for def = hints.named_property('def', _get_def, _set_def)" # noqa + " pass # avoid using a keyword for def = hints.named_property('def', _get_def, _set_def)\n" # noqa + " def egg(self) -> hints.Incomplete: ..." # noqa ) self.assertEqual( expected, typeannotator.DispInterfaceMembersAnnotator(itf).generate() diff --git a/comtypes/tools/codegenerator/typeannotator.py b/comtypes/tools/codegenerator/typeannotator.py index 7f7851e0..c0dd6938 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -293,14 +293,16 @@ def getvalue(self, name: str) -> str: inargs.append(f"{argname}: hints.Incomplete = ...") has_optional = True out = _to_outtype(self.method.returns) - in_ = ("self, " + ", ".join(inargs)) if inargs else "self" # NOTE: Since named parameters are not yet implemented, all arguments # for the dispmethod (called via `Invoke`) are marked as positional-only # parameters, introduced in PEP570. See also `automation.IDispatch.Invoke`. # See https://github.com/enthought/comtypes/issues/371 # TODO: After named parameters are supported, the positional-only parameter # markers will be removed. - content = f"def {name}({in_}, /) -> {out}: ..." + if inargs: + content = f"def {name}(self, {', '.join(inargs)}, /) -> {out}: ..." + else: + content = f"def {name}(self) -> {out}: ..." if keyword.iskeyword(name): content = f"pass # avoid using a keyword for {content}" return content