diff --git a/comtypes/test/test_dict.py b/comtypes/test/test_dict.py index 47fb025f..309b4ce1 100644 --- a/comtypes/test/test_dict.py +++ b/comtypes/test/test_dict.py @@ -1,23 +1,28 @@ -"""Use Scripting.Dictionary to test the lazybind module.""" +"""Use Scripting.Dictionary to test the lazybind and the generated modules.""" import unittest +from comtypes import typeinfo 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 + class Test(unittest.TestCase): - def test_dict(self): + def test_dynamic(self): d = CreateObject("Scripting.Dictionary", dynamic=True) self.assertEqual(type(d), Dispatch) # 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. ##d.HashVal # Add(Key, Item) -> None @@ -30,10 +35,11 @@ def test_dict(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) @@ -66,32 +72,29 @@ def test_dict(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 - # 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"] - 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) 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 @@ -100,6 +103,39 @@ def test_dict(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. + 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("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="spam") + with self.assertRaises(TypeError): + d(Key="egg") + if __name__ == "__main__": unittest.main() diff --git a/comtypes/test/test_msi.py b/comtypes/test/test_msi.py new file mode 100644 index 00000000..d941b043 --- /dev/null +++ b/comtypes/test/test_msi.py @@ -0,0 +1,95 @@ +import unittest as ut +import winreg + +import comtypes.client +from comtypes import GUID, 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 # HKEY_CLASSES_ROOT +HKCU = 1 # HKEY_CURRENT_USER + + +class Test_Installer(ut.TestCase): + 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 + ) + # Both methods below get the "Programmatic Identifier" used to handle + # ".txt" files. + with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".txt") as key: + 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, "\\")) + + 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="") # 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 diff --git a/comtypes/test/test_typeannotator.py b/comtypes/test/test_typeannotator.py index 280ab3ab..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 @@ -59,14 +70,15 @@ 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 = hints.named_property('def', _get_def, _set_def)" # 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)\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 10b68e76..c0dd6938 100644 --- a/comtypes/tools/codegenerator/typeannotator.py +++ b/comtypes/tools/codegenerator/typeannotator.py @@ -293,8 +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" - 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. + 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