Skip to content

Commit

Permalink
Merge pull request #42 from erezsh/dev
Browse files Browse the repository at this point in the history
Added multidispatch singleton decorator, and multidispatch_final
  • Loading branch information
erezsh authored Sep 22, 2023
2 parents fa15cd9 + e84de1a commit 68917e8
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 51 deletions.
26 changes: 11 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,20 @@ print( Person("Bad", interests=['a', 1]) )
Runtype dispatches according to the most specific type match -

```python
from runtype import Dispatch
dp = Dispatch()
from runtype import multidispatch as md

@dp
def mul(a: Any, b: Any):
return a * b
@dp
@md
def mul(a: list, b: list):
return [mul(i, j) for i, j in zip(a, b, strict=True)]
@md
def mul(a: list, b: Any):
return [ai*b for ai in a]
@dp
@md
def mul(a: Any, b: list):
return [bi*b for bi in b]
@dp
def mul(a: list, b: list):
return [mul(i, j) for i, j in zip(a, b, strict=True)]

@md
def mul(a: Any, b: Any):
return a * b

assert mul("a", 4) == "aaaa" # Any, Any
assert mul([1, 2, 3], 2) == [2, 4, 6] # list, Any
Expand All @@ -123,18 +121,16 @@ assert mul([1, 2], [3, 4]) == [3, 8] # list, list
Dispatch can also be used for extending the dataclass builtin `__init__`:

```python
dp = Dispatch()

@dataclass(frozen=False)
class Point:
x: int = 0
y: int = 0

@dp
@md
def __init__(self, points: list | tuple):
self.x, self.y = points

@dp
@md
def __init__(self, points: dict):
self.x = points['x']
self.y = points['y']
Expand Down
68 changes: 33 additions & 35 deletions docs/dispatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,34 +62,25 @@ Ideally, every project will instanciate Dispatch only once, in a module such as
Basic Use
---------

First, users must instanciate the `Dispatch` class, to create a dispatch group:
Multidispatch groups functions by their name. Functions of different names will never collide with each other.

::

from runtype import Dispatch
dp = Dispatch()

Then, the group can be used as a decorator for any number of functions.

Dispatch maintains the original name of every function. So, functions of different names will never collide with each other.

The order in which you define functions doesn't matter.
The order in which you define functions doesn't matter to runtype, but it's recommended to order functions from most specific to least specific.

Example:
::

dp = Dispatch()
from runtype import multidispatch as md

@dataclass(frozen=False)
class Point:
x: int = 0
y: int = 0
@dp
@md
def __init__(self, points: list | tuple):
self.x, self.y = points

@dp
@md
def __init__(self, points: dict):
self.x = points['x']
self.y = points['y']
Expand All @@ -102,6 +93,19 @@ Example:
assert p0 == Point({"x": 0, "y": 0}) # User constructor


A different dispatch object is created for each module, so collisions between different modules are impossible.

Users who want to define a dispatch across several modules, or to have more granular control, can use the Dispatch class:

::

from runtype import Dispatch
dp = Dispatch()

Then, the group can be used as a decorator for any number of functions, in any module.

Functions will still be grouped by name.


Specificity
-----------
Expand All @@ -117,11 +121,11 @@ Example:

from typing import Union

@dp
@md
def f(a: int, b: int):
return a + b

@dp
@md
def f(a: Union[int, str], b: int):
return (a, b)

Expand All @@ -147,9 +151,9 @@ Ambiguity can result from two situations:
Example:
::

>>> @dp
>>> @md
... def f(a, b: int): pass
>>> @dp
>>> @md
... def f(a: int, b): pass
>>> f(1, 1)
Traceback (most recent call last):
Expand All @@ -161,14 +165,11 @@ Dispatch is designed to always throw an error when the right choice isn't obviou
Another example:
::

from runtype import Dispatch
dp = Dispatch()

@dp
@md
def join(seq, sep: str = ''):
return sep.join(str(s) for s in seq)

@dp
@md
def join(seq, sep: list):
return join(join(sep, str(s)) for s in seq)
...
Expand All @@ -191,39 +192,36 @@ Another example:

Dispatch chooses the right function based on the idea specificity, which means that `class MyStr(str)` is more specific than `str`, and so on: `MyStr(str) < str < Union[int, str] < object`.

MyPy support (@overload)
MyPy support
------------------------

Dispatch can be made to work with the overload decorator, aiding in granular type resolution.
multidispatch works with mypy by employing the typing.overload decorator, aiding in granular type resolution.

However, due to the limited design of the overload decorator, there are several rules that need to be followed, and limitations that should be considered.

1. The overload decorator must be placed above the dispatch decorator.
1. For MyPy's benefit, more specific functions should be placed above less specific functions.

1. The last dispatched function of each function group, must be written without type declarations, and without the overload decorator. It is recommended to use this function for error handling.
2. The last dispatched function of each function group, must be written without type declarations (making it the least specific), and use the multidispatch_final decorator. It is recommended to use this function for error handling and default functionality.

3. Mypy doesn't support all of the functionality of Runtype's dispatch, such as full specificity resolution. Therefore, some valid dispatch constructs will produce an error in mypy.
Note: Mypy doesn't support all of the functionality of Runtype's dispatch, such as full specificity resolution. Therefore, some valid dispatch constructs will produce an error in mypy.


Example usage:

::

from runtype import Dispatch
from runtype import multidispatch as md, multidispatch_final as md_final
from typing import overload
dp = Dispatch()

@overload
@dp
@md
def join(seq, sep: str = ''):
return sep.join(str(s) for s in seq)

@overload
@dp
@md
def join(seq, sep: list):
return join(join(sep, str(s)) for s in seq)

@dp
@md_final
def join(seq, sep):
raise NotImplementedError()

Expand Down
63 changes: 63 additions & 0 deletions runtype/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Callable, TYPE_CHECKING

from .dataclass import dataclass
from .dispatch import DispatchError, MultiDispatch
from .validation import (PythonTyping, TypeSystem, TypeMismatchError,
Expand Down Expand Up @@ -54,3 +56,64 @@ def Dispatch(typesystem: TypeSystem = PythonTyping()):
return MultiDispatch(typesystem)



typesystem: TypeSystem = PythonTyping()

class PythonDispatch:
def __init__(self):
self.by_module = {}

def decorate(self, f: Callable) -> Callable:
"""A decorator that enables multiple-dispatch for the given function.
The dispatch namespace is unique for each module, so there can be no name
collisions for functions defined across different modules.
Users that wish to share a dispatch across modules, should use the
`Dispatch` class.
Parameters:
f (Callable): Function to enable multiple-dispatch for
Returns:
the decorated function
Example:
::
>>> from runtype import multidispatch as md
>>> @md
... def add1(i: Optional[int]):
... return i + 1
>>> @md
... def add1(s: Optional[str]):
... return s + "1"
>>> @md
... def add1(a): # accepts any type (least-specific)
... return (a, 1)
>>> add1(1)
2
>>> add1("1")
11
>>> add1(1.0)
(1.0, 1)
"""
module = f.__module__
if module not in self.by_module:
self.by_module[module] = MultiDispatch(typesystem)
return self.by_module[module](f)

python_dispatch = PythonDispatch()

multidispatch_final = python_dispatch.decorate
if TYPE_CHECKING:
from typing import overload as multidispatch
else:
multidispatch = python_dispatch.decorate
19 changes: 18 additions & 1 deletion tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import logging
logging.basicConfig(level=logging.INFO)

from runtype import Dispatch, DispatchError, dataclass, isa, is_subtype, issubclass, assert_isa, String, Int, validate_func, cv_type_checking
from runtype import Dispatch, DispatchError, dataclass, isa, is_subtype, issubclass, assert_isa, String, Int, validate_func, cv_type_checking, multidispatch
from runtype.dispatch import MultiDispatch
from runtype.dataclass import Configuration

Expand Down Expand Up @@ -626,7 +626,24 @@ def test_callable(self):
def test_match(self):
pass

def test_dispatch_singleton(self):
def f(a: int):
return 'a'
f.__module__ = 'a'
f1 = multidispatch(f)

def f(a: int):
return 'a'
f.__module__ = 'b'
f2 = multidispatch(f)

assert f1(1) == 'a'
assert f2(1) == 'a'

def f(a: int):
return 'a'
f.__module__ = 'a'
self.assertRaises(ValueError, multidispatch, f)

class TestDataclass(TestCase):
def setUp(self):
Expand Down

0 comments on commit 68917e8

Please sign in to comment.