Skip to main content

Execute the first function that matches the given arguments.

Project description

signature_dispatch is a simple python library for overloading functions based on their call signature and type annotations.

Last release Python version Test status Test coverage GitHub last commit

Installation

Install from PyPI:

$ pip install signature_dispatch

Version numbers follow semantic versioning.

Usage

Use the module itself to decorate multiple functions (or methods) that all have the same name:

>>> import signature_dispatch
>>> @signature_dispatch
... def f1(x):
...    return x
...
>>> @signature_dispatch
... def f1(x, y):
...    return x, y
...

When called, all of the decorated functions will be tested in order to see if they match the given arguments. The first one that does will be invoked:

>>> f1(1)
1
>>> f1(1, 2)
(1, 2)

A TypeError will be raised if no matches are found:

>>> f1(1, 2, 3)
Traceback (most recent call last):
    ...
TypeError: can't dispatch the given arguments to any of the candidate functions:
arguments: 1, 2, 3
candidates:
(x): too many positional arguments
(x, y): too many positional arguments

Type annotations are taken into account when choosing which function to invoke:

>>> from typing import List
>>> @signature_dispatch
... def f2(x: int):
...    return 'int', x
...
>>> @signature_dispatch
... def f2(x: List[int]):
...    return 'list', x
...
>>> f2(1)
('int', 1)
>>> f2([1, 2])
('list', [1, 2])
>>> f2('a')
Traceback (most recent call last):
    ...
TypeError: can't dispatch the given arguments to any of the candidate functions:
arguments: 'a'
candidates:
(x: int): type of x must be int; got str instead
(x: List[int]): type of x must be a list; got str instead
>>> f2(['a'])
Traceback (most recent call last):
    ...
TypeError: can't dispatch the given arguments to any of the candidate functions:
arguments: ['a']
candidates:
(x: int): type of x must be int; got list instead
(x: List[int]): type of x[0] must be int; got str instead

Details

  • When using the module directly as a decorator, every decorated function must have the same name and must be defined in the same local scope. If this is not possible (e.g. the implementations are in different modules), every function decorated with @signature_dispatch provides an overload() method that can be used to add implementations defined elsewhere:

    >>> @signature_dispatch
    ... def f3(x):
    ...    return x
    ...
    >>> @f3.overload
    ... def _(x, y):
    ...    return x, y
    ...
    >>> f3(1)
    1
    >>> f3(1, 2)
    (1, 2)
  • By default, the decorated functions are tried in the order they were defined. If for some reason this order is undesirable, both @signature_dispatch and @*.overload accept an optional numeric priority argument that can be used to specify a custom order. Functions with higher priorities will be tried before those with lower priorities. Functions with the same priority will be tried in the order they were defined. The default priority is 0:

    >>> @signature_dispatch
    ... def f4():
    ...     return 'first'
    ...
    >>> @signature_dispatch(priority=1)
    ... def f4():
    ...     return 'second'
    ...
    >>> f4()
    'second'
  • The docstring will be taken from the first decorated function. All other docstrings will be ignored.

  • It’s possible to use @signature_dispatch with class/static methods, but doing so is a bit of a special case. Basically, the class/static method must be applied after all of the overloaded implementations have been defined:

    >>> class C:
    ...
    ...     @signature_dispatch
    ...     def m(cls, x):
    ...         return cls, x
    ...
    ...     @signature_dispatch
    ...     def m(cls, x, y):
    ...         return cls, x, y
    ...
    ...     m = classmethod(m)
    ...
    >>> obj = C()
    >>> obj.m(1)
    (<class '__main__.C'>, 1)
    >>> obj.m(1, 2)
    (<class '__main__.C'>, 1, 2)

    Let me know if you find this too annoying. It would probably be possible to special-case class/static methods so that you could just apply both decorators to all the same functions, but that could be complicated and this work-around seems fine for now.

  • Calling @signature_dispatch may be more expensive than you think, because it has to find the scope that it was called from. This is fast enough that it shouldn’t matter in most practical settings, but it does mean that you should take care to not write your code in such a way that, e.g., the @signature_dispatch decorator is called every time the function is invoked. Instead, decorate your functions once and then call the resulting function as often as you’d like.

  • You can get direct access to the core dispatching functionality provided by this library via the signature_dispatch.dispatch() function. This will allow you to call one of several functions based on a given set of arguments, without the need to use any decorators:

    >>> import signature_dispatch
    >>> candidates = [
    ...         lambda x: x,
    ...         lambda x, y: (x, y),
    ... ]
    >>> signature_dispatch.dispatch(candidates, args=(1,), kwargs={})
    1
    >>> signature_dispatch.dispatch(candidates, args=(1, 2), kwargs={})
    (1, 2)

Applications

Writing decorators that can optionally be given arguments is tricky to get right, but signature_dispatch makes it easy. For example, here is a decorator that prints a message to the terminal every time a function is called and optionally accepts an extra message to print:

>>> import signature_dispatch, functools
>>> from typing import Optional

>>> @signature_dispatch
... def log(msg: Optional[str]=None):
...     def decorator(f):
...         @functools.wraps(f)
...         def wrapper(*args, **kwargs):
...             print("Calling:", f.__name__)
...             if msg: print(msg)
...             return f(*args, **kwargs)
...         return wrapper
...     return decorator
...
>>> @signature_dispatch
... def log(f):
...     return log()(f)

Using @log without an argument:

>>> @log
... def foo():
...     pass
>>> foo()
Calling: foo

Using @log with an argument:

>>> @log("Hello world!")
... def bar():
...     pass
>>> bar()
Calling: bar
Hello world!

Alternatives

After having written this library, I subsequently found several existing libraries that (although it pains me to admit) do a better job of the same task.

For multiple dispatch in general:

For the specific task of making decorators:

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

signature_dispatch-1.0.1.tar.gz (15.3 kB view details)

Uploaded Source

Built Distribution

signature_dispatch-1.0.1-py3-none-any.whl (6.9 kB view details)

Uploaded Python 3

File details

Details for the file signature_dispatch-1.0.1.tar.gz.

File metadata

  • Download URL: signature_dispatch-1.0.1.tar.gz
  • Upload date:
  • Size: 15.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.8.0 pkginfo/1.9.6 readme-renderer/37.3 requests/2.28.2 requests-toolbelt/0.10.1 urllib3/1.26.15 tqdm/4.65.0 importlib-metadata/6.0.0 keyring/23.13.1 rfc3986/2.0.0 colorama/0.4.6 CPython/3.10.10

File hashes

Hashes for signature_dispatch-1.0.1.tar.gz
Algorithm Hash digest
SHA256 2daab258b1088b00d4cf288fd65403ce824a5a9b59e1164751e927e9197f1220
MD5 60ee3d8c031b6fdc9c48f0e8c329aaea
BLAKE2b-256 999817b9d428b002deadbe44993ddad60aecce6fc7527ddd9c0215ef3705d97f

See more details on using hashes here.

File details

Details for the file signature_dispatch-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: signature_dispatch-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 6.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.8.0 pkginfo/1.9.6 readme-renderer/37.3 requests/2.28.2 requests-toolbelt/0.10.1 urllib3/1.26.15 tqdm/4.65.0 importlib-metadata/6.0.0 keyring/23.13.1 rfc3986/2.0.0 colorama/0.4.6 CPython/3.10.10

File hashes

Hashes for signature_dispatch-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a6aa41954b8e45ccf64d399144bb5ce9d5b0f8fd4295f2a39361d1606eca442f
MD5 7001aea39f368f4d2016fd3aecf19788
BLAKE2b-256 0660feaf4e9bd8de884a540816849565687dc64c20ef5d1c70ddee15d6e2e9d9

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page