From 2a47b9e7e0dc16e4833b6f157d4269091b7db0b5 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 5 Feb 2024 02:24:04 +0000 Subject: [PATCH] allow passing `**kw` to dispmethods --- comtypes/_memberspec.py | 22 +++-- comtypes/automation.py | 155 ++++++++++++++++++++++++------- comtypes/hints.pyi | 5 +- comtypes/test/test_DISPPARAMS.py | 155 +++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 45 deletions(-) diff --git a/comtypes/_memberspec.py b/comtypes/_memberspec.py index d80352b67..6a2fe89bc 100644 --- a/comtypes/_memberspec.py +++ b/comtypes/_memberspec.py @@ -454,12 +454,11 @@ def fset(obj, value): # Should the funcs/mths we create have restype and/or argtypes attributes? def _make_disp_method(self, m: _DispMemberSpec) -> Callable[..., Any]: - memid = m.memid if "propget" in m.idlflags: def getfunc(obj, *args, **kw): return obj.Invoke( - memid, _invkind=2, *args, **kw + m.memid, _invkind=2, _argspec=m.argspec, *args, **kw ) # DISPATCH_PROPERTYGET return getfunc @@ -467,7 +466,7 @@ def getfunc(obj, *args, **kw): def putfunc(obj, *args, **kw): return obj.Invoke( - memid, _invkind=4, *args, **kw + m.memid, _invkind=4, _argspec=m.argspec, *args, **kw ) # DISPATCH_PROPERTYPUT return putfunc @@ -475,17 +474,18 @@ def putfunc(obj, *args, **kw): def putreffunc(obj, *args, **kw): return obj.Invoke( - memid, _invkind=8, *args, **kw + m.memid, _invkind=8, _argspec=m.argspec, *args, **kw ) # DISPATCH_PROPERTYPUTREF return putreffunc - # a first attempt to make use of the restype. Still, support for - # named arguments and default argument values should be added. + # a first attempt to make use of the restype. if hasattr(m.restype, "__com_interface__"): interface = m.restype.__com_interface__ # type: ignore def comitffunc(obj, *args, **kw): - result = obj.Invoke(memid, _invkind=1, *args, **kw) + result = obj.Invoke( + m.memid, _invkind=1, _argspec=m.argspec, *args, **kw + ) if result is None: return return result.QueryInterface(interface) @@ -493,7 +493,9 @@ def comitffunc(obj, *args, **kw): return comitffunc def func(obj, *args, **kw): - return obj.Invoke(memid, _invkind=1, *args, **kw) # DISPATCH_METHOD + return obj.Invoke( + m.memid, _invkind=1, _argspec=m.argspec, *args, **kw + ) # DISPATCH_METHOD return func @@ -526,10 +528,10 @@ def __getitem__(self, index): else: return self.fget(self.instance, index) - def __call__(self, *args): + def __call__(self, *args, **kw): if self.fget is None: raise TypeError("object is not callable") - return self.fget(self.instance, *args) + return self.fget(self.instance, *args, **kw) def __setitem__(self, index, value): if self.fset is None: diff --git a/comtypes/automation.py b/comtypes/automation.py index a9c14fc9c..2a0d97267 100644 --- a/comtypes/automation.py +++ b/comtypes/automation.py @@ -2,6 +2,7 @@ import array import datetime import decimal +import itertools import sys from ctypes import * from ctypes import _Pointer @@ -11,7 +12,9 @@ Any, Callable, ClassVar, + Container, List, + Mapping, Optional, overload, Sequence, @@ -21,6 +24,7 @@ ) from comtypes import BSTR, COMError, COMMETHOD, GUID, IID, IUnknown, STDMETHOD +from comtypes._memberspec import _resolve_argspec from comtypes.hresult import * import comtypes.patcher import comtypes @@ -875,9 +879,8 @@ def Invoke(self, dispid: int, *args: Any, **kw: Any) -> Any: # For comtypes this is handled in DISPPARAMS.__del__ and VARIANT.__del__. _invkind = kw.pop("_invkind", 1) # DISPATCH_METHOD _lcid = kw.pop("_lcid", 0) - if kw: - raise ValueError("named parameters not yet implemented") - dp = DispParamsGenerator(_invkind).generate(*args) + _argspec = kw.pop("_argspec", ()) + dp = DispParamsGenerator(_invkind, _argspec).generate(*args, **kw) result = VARIANT() excepinfo = EXCEPINFO() argerr = c_uint() @@ -928,45 +931,38 @@ def Invoke(self, dispid: int, *args: Any, **kw: Any) -> Any: class DispParamsGenerator(object): - __slots__ = ("invkind",) + __slots__ = ("invkind", "argspec") - def __init__(self, invkind: int) -> None: + def __init__( + self, invkind: int, argspec: Sequence["hints._ArgSpecElmType"] + ) -> None: self.invkind = invkind + self.argspec = argspec - def generate(self, *args: Any) -> DISPPARAMS: + def generate(self, *args: Any, **kw: Any) -> DISPPARAMS: """Generate `DISPPARAMS` for passing to `IDispatch::Invoke`. - Examples: - >>> _get_rgvarg = lambda dp: [dp.rgvarg[i] for i in range(dp.cArgs)] - - >>> dp = DispParamsGenerator(DISPATCH_METHOD).generate(9) - >>> _get_rgvarg(dp), bool(dp.rgdispidNamedArgs), dp.cArgs, dp.cNamedArgs - ([VARIANT(vt=0x3, 9)], False, 1, 0) - >>> dp = DispParamsGenerator(DISPATCH_PROPERTYGET).generate('foo', 3.14) - >>> _get_rgvarg(dp), bool(dp.rgdispidNamedArgs), dp.cArgs, dp.cNamedArgs - ([VARIANT(vt=0x5, 3.14), VARIANT(vt=0x8, 'foo')], False, 2, 0) - >>> dp = DispParamsGenerator(DISPATCH_PROPERTYPUT).generate(8) - >>> _get_rgvarg(dp), dp.rgdispidNamedArgs.contents, dp.cArgs, dp.cNamedArgs - ([VARIANT(vt=0x3, 8)], c_long(-3), 1, 1) - >>> dp = DispParamsGenerator(DISPATCH_PROPERTYPUTREF).generate(7, 'bar') - >>> _get_rgvarg(dp), dp.rgdispidNamedArgs.contents, dp.cArgs, dp.cNamedArgs - ([VARIANT(vt=0x8, 'bar'), VARIANT(vt=0x3, 7)], c_long(-3), 2, 1) - - >>> gen = DispParamsGenerator(DISPATCH_METHOD) - >>> _get_rgvarg(gen.generate()) - [] - >>> _get_rgvarg(gen.generate(4)) - [VARIANT(vt=0x3, 4)] - >>> _get_rgvarg(gen.generate(4, 3.14)) - [VARIANT(vt=0x5, 3.14), VARIANT(vt=0x3, 4)] - >>> _get_rgvarg(gen.generate(4, 3.14, 'foo')) - [VARIANT(vt=0x8, 'foo'), VARIANT(vt=0x5, 3.14), VARIANT(vt=0x3, 4)] + Notes: + The following would be occured only when `**kw` is passed. + - Check the required arguments specified by the `argspec` are satisfied. + - Complement non-passed optional arguments with their default values + from the `argspec`. """ - array = (VARIANT * len(args))() - for i, a in enumerate(args[::-1]): + if kw: + new_args = self._resolve_kwargs(*args, **kw) + else: + # Argument validation based on `argspec` is not triggered unless `**kw` + # is passed, because... + # - for backward compatibility with `1.2.0` and earlier. + # - there might be unexpected `argspec` in the real world. + # - `IDispatch.Invoke` might be called as a public method and `_argspec` + # is not passed. + new_args = args + array = (VARIANT * len(new_args))() + for i, a in enumerate(new_args[::-1]): array[i].value = a dp = DISPPARAMS() - dp.cArgs = len(args) + dp.cArgs = len(new_args) if self.invkind in (DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYPUTREF): # propput dp.cNamedArgs = 1 dp.rgvarg = array @@ -976,6 +972,97 @@ def generate(self, *args: Any) -> DISPPARAMS: dp.rgvarg = array return dp + def _resolve_kwargs(self, *args: Any, **kw: Any) -> Sequence[Any]: + pfs, _ = _resolve_argspec(self.argspec) + arg_names, arg_defaults = self._resolve_paramflags(pfs) + self._validate_unexpected(kw, arg_names, arg_defaults) + new_args, used_names = [], set() + for name in itertools.chain(arg_names, arg_defaults): + if not args and not kw: + break + if name in kw: + if args or name in used_names: + raise TypeError(f"got multiple values for argument {name!r}") + new_args.append(kw.pop(name)) + used_names.add(name) + elif args: + new_args.append(args[0]) + used_names.add(name) + args = args[1:] + elif name in arg_defaults: + new_args.append(arg_defaults[name]) + used_names.add(name) + else: + continue + self._validate_missings(arg_names, used_names) + if args or kw: + # messages should be... + # - takes 0 positional arguments but 1 was given + # - takes 1 positional argument but N were given + # - takes L to M positional arguments but N were given + # + # `kw` resolution is only called when `**kw` is passed to `generate`. + # And `TypeError: got multiple values` is raised when there are + # multiple arguments. + # This conditional branch is for edge cases that may arise in the future. + raise TypeError # too many arguments + return new_args + + def _resolve_paramflags( + self, pfs: Sequence["hints._ParamFlagType"] + ) -> Tuple[Sequence[str], Mapping[str, Any]]: + arg_names, arg_defaults = [], {} + for p in pfs: + if len(p) == 2: + if arg_defaults: + raise ValueError("unexpected ordered params") + _, name = p + if name is None: + raise ValueError("unnamed argument") + arg_names.append(name) + else: + _, name, defval = p + if name is None: + raise ValueError("unnamed argument") + arg_defaults[name] = defval + return arg_names, arg_defaults + + def _validate_unexpected( + self, + kw: Mapping[str, Any], + arg_names: Sequence[str], + arg_defaults: Mapping[str, Any], + ) -> None: + for name in kw: + if name not in arg_names and name not in arg_defaults: + raise TypeError(f"got an unexpected keyword argument {name!r}") + + def _validate_excessive( + self, + args: Sequence[Any], + kw: Mapping[str, Any], + arg_names: Sequence[str], + ) -> None: + len_required_positionals = len(set(arg_names) - set(kw.keys())) + print(arg_names, kw.keys(), set(arg_names) - set(kw.keys())) + len_args = len(args) + if len_args > len_required_positionals: + raise TypeError + + def _validate_missings( + self, arg_names: Sequence[str], used_names: Container[str] + ) -> None: + mis = [n for n in arg_names if n not in used_names] + if not mis: + return + if len(mis) == 1: + head = "missing 1 required positional argument" + tail = repr(mis[0]) + else: + head = f"missing {len(mis)} required positional arguments" + tail = ", ".join(map(repr, mis[:-1])) + f" and {mis[-1]!r}" + raise TypeError(f"{head}: {tail}") + ################################################################ # safearrays diff --git a/comtypes/hints.pyi b/comtypes/hints.pyi index 8e87e3ce1..ad49d6fd8 100644 --- a/comtypes/hints.pyi +++ b/comtypes/hints.pyi @@ -2,4 +2,7 @@ from comtypes.automation import IDispatch as IDispatch, VARIANT as VARIANT from comtypes.server import IClassFactory as IClassFactory from comtypes.typeinfo import ITypeInfo as ITypeInfo -from comtypes._memberspec import _ArgSpecElmType as _ArgSpecElmType +from comtypes._memberspec import ( + _ArgSpecElmType as _ArgSpecElmType, + _ParamFlagType as _ParamFlagType, +) diff --git a/comtypes/test/test_DISPPARAMS.py b/comtypes/test/test_DISPPARAMS.py index ea89f79e1..7a416d62b 100644 --- a/comtypes/test/test_DISPPARAMS.py +++ b/comtypes/test/test_DISPPARAMS.py @@ -40,5 +40,160 @@ def X_test_2(self): self.assertEqual(dp.rgvarg[2].value, "foo") +class Test_DispParamsGenerator(ut.TestCase): + def _get_rgvargs(self, dp): + return [dp.rgvarg[i].value for i in range(dp.cArgs)] + + def test_invkind(self): + from comtypes.automation import ( + DispParamsGenerator, + DISPATCH_METHOD, + DISPATCH_PROPERTYGET, + DISPATCH_PROPERTYPUT, + DISPATCH_PROPERTYPUTREF, + DISPID_PROPERTYPUT, + ) + + def _is_null(d): + return not bool(d) + + def _is_dispid_propput(d): + return d.contents.value == DISPID_PROPERTYPUT + + for invkind, id_validator, c_namedargs in [ + (DISPATCH_METHOD, lambda d: not bool(d), 0), + (DISPATCH_PROPERTYGET, _is_null, 0), + (DISPATCH_PROPERTYPUT, _is_dispid_propput, 1), + (DISPATCH_PROPERTYPUTREF, _is_dispid_propput, 1), + ]: + with self.subTest( + invkind=invkind, id_validator=id_validator, c_namedargs=c_namedargs + ): + dp = DispParamsGenerator(invkind, ()).generate(9) + self.assertEqual(self._get_rgvargs(dp), [9]) + self.assertTrue(id_validator(dp.rgdispidNamedArgs)) + self.assertEqual(dp.cArgs, 1) + self.assertEqual(dp.cNamedArgs, c_namedargs) + + def test_c_args(self): + from comtypes.automation import DispParamsGenerator, DISPATCH_METHOD + + for args, c_args in [ + ((), 0), + ((9,), 1), + (("foo", 3.14), 2), + ((2, "bar", 1.41), 3), + ]: + with self.subTest(args=args, c_args=c_args): + dp = DispParamsGenerator(DISPATCH_METHOD, ()).generate(*args) + self.assertEqual(dp.cArgs, c_args) + + def test_no_argspec(self): + from comtypes.automation import DispParamsGenerator, DISPATCH_METHOD + + gen = DispParamsGenerator(DISPATCH_METHOD, ()) + for args, rgvargs in [ + ((), []), + ((9,), [9]), + (("foo", 3.14), [3.14, "foo"]), + ((2, "bar", 1.41), [1.41, "bar", 2]), + ]: + with self.subTest(args=args, rgvargs=rgvargs): + dp = gen.generate(*args) + self.assertEqual(self._get_rgvargs(dp), rgvargs) + with self.assertRaises(TypeError) as ce: + gen.generate(4, 3.14, "foo", a="spam") + self.assertEqual(ce.exception.args, ("got an unexpected keyword argument 'a'",)) + + def test_argspec_in_x2(self): + from comtypes.automation import DispParamsGenerator, DISPATCH_METHOD + + IN = ["in"] + spec = ((IN, ..., "a"), (IN, ..., "b")) + gen = DispParamsGenerator(DISPATCH_METHOD, spec) # type: ignore + self.assertEqual(self._get_rgvargs(gen.generate(3, 2.2)), [2.2, 3]) + self.assertEqual(self._get_rgvargs(gen.generate(b=1.4, a=2)), [1.4, 2]) + self.assertEqual(self._get_rgvargs(gen.generate(5, b=3.1)), [3.1, 5]) + with self.assertRaises(TypeError) as ce: + gen.generate(1, a=2) + self.assertEqual(ce.exception.args, ("got multiple values for argument 'a'",)) + with self.assertRaises(TypeError) as ce: + gen.generate(a=3) + self.assertEqual( + ce.exception.args, ("missing 1 required positional argument: 'b'",) + ) + with self.assertRaises(TypeError) as ce: + gen.generate(a=1, b=2, c=3) + self.assertEqual(ce.exception.args, ("got an unexpected keyword argument 'c'",)) + # THOSE MIGHT RAISE `COMError` IN CALLER. + self.assertEqual(self._get_rgvargs(gen.generate()), []) + self.assertEqual(self._get_rgvargs(gen.generate(1, 2, 3)), [3, 2, 1]) + + def test_argspec_in_x1_and_optin_x1(self): + from comtypes.automation import DispParamsGenerator, DISPATCH_METHOD + + IN, OPT_IN = ["in"], ["in", "optional"] + spec = ((IN, ..., "a"), (OPT_IN, ..., "b", "foo")) + gen = DispParamsGenerator(DISPATCH_METHOD, spec) # type: ignore + self.assertEqual(self._get_rgvargs(gen.generate(2)), [2]) + self.assertEqual(self._get_rgvargs(gen.generate(4, "bar")), ["bar", 4]) + with self.assertRaises(TypeError) as ce: + gen.generate(b="baz") + self.assertEqual( + ce.exception.args, ("missing 1 required positional argument: 'a'",) + ) + with self.assertRaises(TypeError) as ce: + gen.generate(4, "bar", b="baz") + self.assertEqual(ce.exception.args, ("got multiple values for argument 'b'",)) + + def test_argspec_in_x3(self): + from comtypes.automation import DispParamsGenerator, DISPATCH_METHOD + + IN = ["in"] + spec = ((IN, ..., "a"), (IN, ..., "b"), (IN, ..., "c"), (IN, ..., "d")) + gen = DispParamsGenerator(DISPATCH_METHOD, spec) # type: ignore + self.assertEqual(self._get_rgvargs(gen.generate(1, 2, 3, 4)), [4, 3, 2, 1]) + with self.assertRaises(TypeError) as ce: + gen.generate(d=4) + self.assertEqual( + ce.exception.args, + ("missing 3 required positional arguments: 'a', 'b' and 'c'",), + ) + with self.assertRaises(TypeError) as ce: + gen.generate(a=1, c=3) + self.assertEqual( + ce.exception.args, + ("missing 2 required positional arguments: 'b' and 'd'",), + ) + with self.assertRaises(TypeError) as ce: + gen.generate(1, b=2, d=4) + self.assertEqual( + ce.exception.args, + ("missing 1 required positional argument: 'c'",), + ) + + def test_argspec_optin_x3(self): + from comtypes.automation import DispParamsGenerator, DISPATCH_METHOD + + OPT_IN = ["in", "optional"] + spec = ( + (OPT_IN, ..., "a", 1), + (OPT_IN, ..., "b", 3.1), + (OPT_IN, ..., "c", "foo"), + ) + gen = DispParamsGenerator(DISPATCH_METHOD, spec) # type: ignore + self.assertEqual(self._get_rgvargs(gen.generate()), []) + self.assertEqual(self._get_rgvargs(gen.generate(a=2)), [2]) + self.assertEqual(self._get_rgvargs(gen.generate(b=1.7)), [1.7, 1]) + self.assertEqual(self._get_rgvargs(gen.generate(c="bar")), ["bar", 3.1, 1]) + self.assertEqual(self._get_rgvargs(gen.generate(2, c="bar")), ["bar", 3.1, 2]) + with self.assertRaises(TypeError) as ce: + gen.generate(d=5) + self.assertEqual(ce.exception.args, ("got an unexpected keyword argument 'd'",)) + with self.assertRaises(TypeError) as ce: + gen.generate(3, a=5) + self.assertEqual(ce.exception.args, ("got multiple values for argument 'a'",)) + + if __name__ == "__main__": ut.main()