multimethod icon indicating copy to clipboard operation
multimethod copied to clipboard

Trailing object arguments

Open KyleSJohnston opened this issue 3 years ago • 1 comments

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.

KyleSJohnston avatar Aug 12 '22 18:08 KyleSJohnston

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.

coady avatar Aug 13 '22 21:08 coady

Released in 1.9.

coady avatar Sep 21 '22 23:09 coady