Trailing object arguments
While upgrading to 1.8, I discovered that dispatching on the number of object arguments no longer works. It looks like this behavior was introduced in https://github.com/coady/multimethod/pull/23 and became part of v1.5.
Here is an example contrasting the handling of int and object type annotations:
In [1]: from multimethod import multimethod
In [2]: @multimethod
...: def foo(bar: int):
...: return "one arg"
...:
In [3]: @multimethod
...: def foo(bar: int, baz: int):
...: return "two args"
...:
In [4]: assert foo(1) == "one arg"
In [5]: assert foo(1, 2) == "two args"
In [6]: @multimethod
...: def foo_object(bar: object):
...: return "one object arg"
...:
In [7]: @multimethod
...: def foo_object(bar: object, baz: object):
...: return "two object args"
...:
In [8]: assert foo_object(1.0) == "one object arg"
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
File ~/.local/lib/python3.10/site-packages/multimethod/__init__.py:312, in multimethod.__call__(self, *args, **kwargs)
311 try:
--> 312 return func(*args, **kwargs)
313 except TypeError as ex:
TypeError: foo_object() missing 1 required positional argument: 'baz'
The above exception was the direct cause of the following exception:
DispatchError Traceback (most recent call last)
Input In [8], in <cell line: 1>()
----> 1 assert foo_object(1.0) == "one object arg"
File ~/.local/lib/python3.10/site-packages/multimethod/__init__.py:314, in multimethod.__call__(self, *args, **kwargs)
312 return func(*args, **kwargs)
313 except TypeError as ex:
--> 314 raise DispatchError(f"Function {func.__code__}") from ex
DispatchError: Function <code object foo_object at 0x7f3c34df64a0, file "<ipython-input-7-0fe682abbfe2>", line 1>
In the object case, get_types returns an empty tuple for both functions, so the second function replaces the first.
My suggestion is to update https://github.com/coady/multimethod/blob/4681b069200c61d95650155200e61394a25b58f6/multimethod/init.py#L32 to be return tuple(annotations). Arguments without annotations will still be treated like objects, and objects will not be special types when relying on the number of arguments.
One alternative would be to drop trailing objects only if they were introduced implicitly. This would allow users to add : object where it would be needed to differentiate from another function. Something like this:
missing = object() # instance to denote an omitted type hint
annotations = [
type_hints.get(param.name, missing)
for param in inspect.signature(func).parameters.values()
if param.default is param.empty and param.kind in positionals
]
itr = itertools.dropwhile(lambda cls: cls is missing, reversed(annotations))
return tuple((c if c is not missing else object for c in itr))[::-1]
There are probably other considerations. I'd appreciate hearing about any workarounds—especially if this is desired behavior and unlikely to change.
The problem is the requirement that only positional args participate in dispatch, but keyword args are still allowed. Calling foo_object(object(), baz=None) should match def foo_object(bar: object, baz): but not def foo_object(bar: object, baz: object):.
So although I'd prefer the simpler option, I think we have to go with your second suggestion.
Released in 1.9.