diff --git a/CHANGELOG.md b/CHANGELOG.md index 9235ef8..818333b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## Unreleased +### Changed +* `multidispatch` supports variable keyword arguments ## [1.11.2](https://pypi.org/project/multimethod/1.11.2/) - 2024-02-27 ### Fixed diff --git a/multimethod/__init__.py b/multimethod/__init__.py index 3092425..132cd2d 100644 --- a/multimethod/__init__.py +++ b/multimethod/__init__.py @@ -397,7 +397,7 @@ class multidispatch(multimethod, dict[tuple[type, ...], Callable[..., RETURN]]): Allows dispatching on keyword arguments based on the first function signature. """ - signature: Optional[inspect.Signature] + signatures: dict[tuple, inspect.Signature] def __new__(cls, func: Callable[..., RETURN]) -> "multidispatch[RETURN]": return functools.update_wrapper(dict.__new__(cls), func) # type: ignore @@ -405,11 +405,8 @@ def __new__(cls, func: Callable[..., RETURN]) -> "multidispatch[RETURN]": def __init__(self, func: Callable[..., RETURN]) -> None: self.pending = set() self.generics = [] - try: - self.signature = inspect.signature(func) - except ValueError: - self.signature = None - msg = "base implementation will eventually ignore annotations as `singledispatch does`" + self.signatures = {} + msg = "base implementation will eventually ignore annotations as `singledispatch` does" with contextlib.suppress(NameError, AttributeError, TypeError): hints = signature.from_hints(func) if hints and all(map(issubclass, hints, hints)): @@ -419,9 +416,20 @@ def __init__(self, func: Callable[..., RETURN]) -> None: def __get__(self, instance, owner) -> Callable[..., RETURN]: return self if instance is None else types.MethodType(self, instance) # type: ignore + def __setitem__(self, types: tuple, func: Callable): + super().__setitem__(types, func) + with contextlib.suppress(ValueError): + signature = inspect.signature(func) + self.signatures.setdefault(tuple(signature.parameters), signature) + def __call__(self, *args: Any, **kwargs: Any) -> RETURN: """Resolve and dispatch to best method.""" - params = self.signature.bind(*args, **kwargs).args if (kwargs and self.signature) else args + params = args + if kwargs: + for signature in self.signatures.values(): # pragma: no branch + with contextlib.suppress(TypeError): + params = signature.bind(*args, **kwargs).args + break func = self.dispatch(*params) return func(*args, **kwargs) diff --git a/tests/test_dispatch.py b/tests/test_dispatch.py index f393220..39dfb27 100644 --- a/tests/test_dispatch.py +++ b/tests/test_dispatch.py @@ -114,9 +114,10 @@ def _(arg: int): @func.register def _(arg: int, extra: float): - return int + return float assert func(0) is func(arg=0) is int + assert func(0, 0.0) is func(arg=0, extra=0.0) is float assert multidispatch(bool)(1)