diff --git a/comtypes/_memberspec.py b/comtypes/_memberspec.py index 5ba366ec..1ffcefaf 100644 --- a/comtypes/_memberspec.py +++ b/comtypes/_memberspec.py @@ -2,7 +2,7 @@ from typing import Any, NamedTuple from typing import Dict, List, Tuple, Type from typing import Optional, Union as _UnionT -from typing import Callable, Iterator +from typing import Callable, Iterator, Sequence from comtypes import _CData import comtypes @@ -43,7 +43,7 @@ def _unpack_argspec( def _resolve_argspec( - items: Tuple[_ArgSpecElmType, ...] + items: Sequence[_ArgSpecElmType], ) -> Tuple[Tuple[_ParamFlagType, ...], Tuple[Type[_CData], ...]]: """Unpacks and converts from argspec to paramflags and argtypes. @@ -445,12 +445,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 @@ -458,7 +457,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 @@ -466,17 +465,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) @@ -484,7 +484,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 @@ -517,10 +519,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 58e31f8c..6d4dcc78 100644 --- a/comtypes/automation.py +++ b/comtypes/automation.py @@ -2,16 +2,20 @@ import array import datetime import decimal +import itertools import sys from ctypes import * from ctypes import _Pointer from _ctypes import CopyComPointer from ctypes.wintypes import DWORD, LONG, UINT, VARIANT_BOOL, WCHAR, WORD -from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Type +from typing import Any, ClassVar, overload, TYPE_CHECKING +from typing import Optional, Union as _UnionT +from typing import Dict, List, Tuple, Type +from typing import Callable, Container, Mapping, Sequence from comtypes import _CData, BSTR, COMError, COMMETHOD, GUID, IID, IUnknown, STDMETHOD from comtypes.hresult import * -from comtypes._memberspec import _DispMemberSpec +from comtypes._memberspec import _DispMemberSpec, _resolve_argspec import comtypes.patcher import comtypes @@ -856,25 +860,29 @@ def _invoke(self, memid: int, invkind: int, lcid: int, *args: Any) -> Any: ) return var._get_value(dynamic=True) - def __make_dp(self, _invkind: int, *args: Any) -> DISPPARAMS: - array = (VARIANT * len(args))() - for i, a in enumerate(args[::-1]): - array[i].value = a - dp = DISPPARAMS() - dp.cArgs = len(args) - dp.rgvarg = array - if _invkind in (DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYPUTREF): # propput - dp.cNamedArgs = 1 - dp.rgdispidNamedArgs = pointer(DISPID(DISPID_PROPERTYPUT)) - else: - dp.cNamedArgs = 0 - return dp + @overload + def Invoke( + self, dispid: int, *args: Any, _invkind: int = ..., _lcid: int = ... + ) -> Any: + ... # noqa + + @overload + def Invoke( + self, + dispid: int, + *args: Any, + _argspec: Sequence["hints._ArgSpecElmType"], + _invkind: int = ..., + _lcid: int = ..., + **kw: Any, + ) -> Any: + ... # noqa def Invoke(self, dispid: int, *args: Any, **kw: Any) -> Any: """Invoke a method or property.""" # Memory management in Dispatch::Invoke calls: - # http://msdn.microsoft.com/library/en-us/automat/htm/chap5_4x2q.asp + # https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/passing-parameters # Quote: # The *CALLING* code is responsible for releasing all strings and # objects referred to by rgvarg[ ] or placed in *pVarResult. @@ -882,9 +890,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 = self.__make_dp(_invkind, *args) + _argspec = kw.pop("_argspec", ()) + dp = DispParamsGenerator(_invkind, _argspec).generate(*args, **kw) result = VARIANT() excepinfo = EXCEPINFO() argerr = c_uint() @@ -934,6 +941,140 @@ def Invoke(self, dispid: int, *args: Any, **kw: Any) -> Any: # XXX Would separate methods for _METHOD, _PROPERTYGET and _PROPERTYPUT be better? +class DispParamsGenerator(object): + __slots__ = ("invkind", "argspec") + + def __init__( + self, invkind: int, argspec: Sequence["hints._ArgSpecElmType"] + ) -> None: + self.invkind = invkind + self.argspec = argspec + + def generate(self, *args: Any, **kw: Any) -> DISPPARAMS: + """Generate `DISPPARAMS` for passing to `IDispatch::Invoke`. + + 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`. + """ + 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(new_args) + if self.invkind in (DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYPUTREF): # propput + dp.cNamedArgs = 1 + dp.rgvarg = array + dp.rgdispidNamedArgs = pointer(DISPID(DISPID_PROPERTYPUT)) + else: + dp.cNamedArgs = 0 + 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 # XXX Only one-dimensional arrays are currently implemented diff --git a/comtypes/hints.pyi b/comtypes/hints.pyi index 9a47671a..d6430983 100644 --- a/comtypes/hints.pyi +++ b/comtypes/hints.pyi @@ -32,6 +32,10 @@ from comtypes import IUnknown as IUnknown, GUID as GUID 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, + _ParamFlagType as _ParamFlagType, +) from comtypes._safearray import tagSAFEARRAY as tagSAFEARRAY Incomplete: TypeAlias = Any diff --git a/comtypes/test/test_DISPPARAMS.py b/comtypes/test/test_DISPPARAMS.py index ea89f79e..7a416d62 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() diff --git a/comtypes/test/test_namedargs.py b/comtypes/test/test_namedargs.py new file mode 100644 index 00000000..2481f2fc --- /dev/null +++ b/comtypes/test/test_namedargs.py @@ -0,0 +1,227 @@ +from _ctypes import COMError +from pathlib import Path +import tempfile +import unittest as ut + +from comtypes import client +from comtypes.automation import VARIANT + +# TODO: Add TestCase using non-env-specific typelib. + + +class Test_Excel(ut.TestCase): + """for DispMethods""" + + def setUp(self): + try: + client.GetModule(("{00020813-0000-0000-C000-000000000046}",)) + from comtypes.gen import Excel + + self.Excel = Excel + self.xl = client.CreateObject( + Excel.Application, interface=Excel._Application + ) + except (ImportError, OSError): + self.skipTest("This depends on Excel.") + + def tearDown(self): + # Close all open workbooks without saving, then quit excel. + for wb in self.xl.Workbooks: + wb.Close(0) + self.xl.Quit() + del self.xl + + def test_range_value(self): + xl = self.xl + xl.Workbooks.Add() + xl.Range["A1:C1"].Value[()] = (10, "20", 31.4) + xl.Range["A2:C2"].Value[()] = ("x", "y", "z") + xl.Range["A3:C3"].Value[:] = ("3", "2", "1") + rng = xl.Range("A1:C3") + expected_values = ((10.0, 20.0, 31.4), ("x", "y", "z"), (3.0, 2.0, 1.0)) + self.assertTrue( + rng.Value[self.Excel.xlRangeValueDefault] + == rng.Value(RangeValueDataType=self.Excel.xlRangeValueDefault) + == rng.Value(VARIANT.missing) + == rng.Value() + == expected_values + ) + with self.assertRaises(TypeError): + rng.Value( + self.Excel.xlRangeValueDefault, + RangeValueDataType=self.Excel.xlRangeValueDefault, + ) + with self.assertRaises(TypeError): + rng.Value(self.Excel.xlRangeValueDefault, Foo="Ham") + with self.assertRaises(TypeError): + rng.Value(Foo="Ham") + + def test_range_address(self): + Excel = self.Excel + self.xl.Workbooks.Add() + rng = self.xl.Range("A1:C3") + self.assertTrue( + rng.Address() + == rng.Address(None) + == rng.Address[None, None] + == rng.Address(RowAbsolute=True) + == rng.Address(ColumnAbsolute=True) + == rng.Address(RowAbsolute=True, ColumnAbsolute=True) + == rng.Address(ColumnAbsolute=True, RowAbsolute=True) + == "$A$1:$C$3" + ) + self.assertTrue( + rng.Address(RowAbsolute=False) + == rng.Address[False] + == rng.Address(ColumnAbsolute=True, RowAbsolute=False) + == "$A1:$C3" + ) + self.assertTrue( + rng.Address[None, False] + == rng.Address(ColumnAbsolute=False) + == rng.Address(ColumnAbsolute=False, RowAbsolute=True) + == "A$1:C$3" + ) + self.assertTrue( + rng.Address[None, None, self.Excel.xlR1C1] + == rng.Address(None, ReferenceStyle=self.Excel.xlR1C1) + == rng.Address(ReferenceStyle=self.Excel.xlR1C1) + == "R1C1:R3C3" + ) + with self.assertRaises(TypeError): + rng.Address(Foo="Ham") + with self.assertRaises(COMError): + rng.Address(False, False, Excel.xlR1C1, False, self.xl.Range("D4"), False) + with self.assertRaises(TypeError): + rng.Address( + False, False, Excel.xlR1C1, False, self.xl.Range("D4"), Foo="Ham" + ) + with self.assertRaises(TypeError): + rng.Address( + RowAbsolute=False, + ColumnAbsolute=False, + ReferenceStyle=Excel.xlR1C1, + External=False, + RelativeTo=self.xl.Range("D4"), + Foo="Ham", + ) + with self.assertRaises(TypeError): + rng.Address( + RowAbsolute=False, + ColumnAbsolute=False, + ReferenceStyle=Excel.xlR1C1, + External=False, + Foo="Ham", + ) + + def test_range_autofill(self): + Excel = self.Excel + xl = self.xl + xl.Workbooks.Add() + xl.Range["A1:E1"].Value[()] = (1, 1, 1, 1, 1) + xl.Range["A2:E2"].Value[()] = (2, 2, 2, 2, 2) + xl.Range("A1").AutoFill(xl.Range("A1:A4")) + xl.Range("B1").AutoFill(xl.Range("B1:B4"), Excel.xlFillSeries) + xl.Range("C1:C2").AutoFill(xl.Range("C1:C4"), Excel.xlFillCopy) + xl.Range("C1:C2").AutoFill(xl.Range("C1:C4"), Type=Excel.xlFillCopy) + xl.Range("D1:D2").AutoFill(Destination=xl.Range("D1:D4"), Type=Excel.xlFillCopy) + xl.Range("E1:E3").AutoFill(Type=Excel.xlFillCopy, Destination=xl.Range("E1:E4")) + self.assertEqual( + xl.Range("A1:E4").Value(), + ( + (1.0, 1.0, 1.0, 1.0, 1.0), + (1.0, 2.0, 2.0, 2.0, 2.0), + (1.0, 3.0, 1.0, 1.0, None), + (1.0, 4.0, 2.0, 2.0, 1.0), + ), + ) + with self.assertRaises(COMError): + xl.Range("A1").AutoFill() + with self.assertRaises(TypeError): + xl.Range("B1").AutoFill(xl.Range("B1:B4"), Destination=xl.Range("B1:B4")) + with self.assertRaises(TypeError): + xl.Range("B1").AutoFill(Type=Excel.xlFillCopy) + with self.assertRaises(TypeError): + xl.Range("B1").AutoFill( + xl.Range("B1:B4"), Type=Excel.xlFillCopy, Foo="spam" + ) + + +class Test_IDictionary(ut.TestCase): + """for ComMethods""" + + def setUp(self): + client.GetModule("scrrun.dll") + from comtypes.gen import Scripting + + self.dic = client.CreateObject( + Scripting.Dictionary, interface=Scripting.IDictionary + ) + + def tearDown(self): + del self.dic + + def test_takes_valid_args(self): + self.dic.Add(Key="foo", Item="spam") + self.dic.Add("bar", Item="ham") + self.dic.Add(Item="bacon", Key="baz") + self.dic.Add("qux", "egg") + self.assertEqual(set(self.dic.Keys()), {"foo", "bar", "baz", "qux"}) + self.assertEqual(set(self.dic.Items()), {"spam", "ham", "bacon", "egg"}) + + def test_takes_invalid_args(self): + with self.assertRaises(TypeError): + self.dic.Add(Key="foo", Item="spam", Eric="Idle") + with self.assertRaises(TypeError): + self.dic.Add("foo", "spam", Eric="Idle") + with self.assertRaises(TypeError): + self.dic.Add("foo") + + +class Test_FSO(ut.TestCase): + """for ComMethods""" + + def setUp(self): + client.GetModule("scrrun.dll") + from comtypes.gen import Scripting + + self.fso = client.CreateObject( + Scripting.FileSystemObject, interface=Scripting.IFileSystem + ) + + def tearDown(self): + del self.fso + + def test_takes_valid_args(self): + with tempfile.TemporaryDirectory() as t: + tmp_dir = Path(t) + tmp_file = tmp_dir / "tmp.txt" + for args, kwargs in [ + ((tmp_file.__fspath__(),), {}), + ((tmp_file.__fspath__(), True), {}), + ((tmp_file.__fspath__(),), {"Force": True}), + ((), {"FileSpec": tmp_file.__fspath__(), "Force": True}), + ((), {"Force": True, "FileSpec": tmp_file.__fspath__()}), + ]: + tmp_file.touch() + with self.subTest(args=args, kwargs=kwargs): + self.fso.DeleteFile(*args, **kwargs) + self.assertFalse(tmp_file.exists()) + + def test_takes_invalid_args(self): + with tempfile.TemporaryDirectory() as t: + tmp_dir = Path(t) + tmp_file = tmp_dir / "tmp.txt" + tmp_file.touch() + with self.assertRaises(TypeError): + self.fso.DeleteFile() + with self.assertRaises(TypeError): + self.fso.DeleteFile(Force=True) + with self.assertRaises(TypeError): + self.fso.DeleteFile( + tmp_file.__fspath__(), FileSpec=tmp_file.__fspath__() + ) + + +if __name__ == "__main__": + ut.main()