Skip to content

Commit 68917e8

Browse files
authored
Merge pull request #42 from erezsh/dev
Added multidispatch singleton decorator, and multidispatch_final
2 parents fa15cd9 + e84de1a commit 68917e8

File tree

4 files changed

+125
-51
lines changed

4 files changed

+125
-51
lines changed

README.md

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,20 @@ print( Person("Bad", interests=['a', 1]) )
9797
Runtype dispatches according to the most specific type match -
9898

9999
```python
100-
from runtype import Dispatch
101-
dp = Dispatch()
100+
from runtype import multidispatch as md
102101

103-
@dp
104-
def mul(a: Any, b: Any):
105-
return a * b
106-
@dp
102+
@md
103+
def mul(a: list, b: list):
104+
return [mul(i, j) for i, j in zip(a, b, strict=True)]
105+
@md
107106
def mul(a: list, b: Any):
108107
return [ai*b for ai in a]
109-
@dp
108+
@md
110109
def mul(a: Any, b: list):
111110
return [bi*b for bi in b]
112-
@dp
113-
def mul(a: list, b: list):
114-
return [mul(i, j) for i, j in zip(a, b, strict=True)]
115-
111+
@md
112+
def mul(a: Any, b: Any):
113+
return a * b
116114

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

125123
```python
126-
dp = Dispatch()
127-
128124
@dataclass(frozen=False)
129125
class Point:
130126
x: int = 0
131127
y: int = 0
132128

133-
@dp
129+
@md
134130
def __init__(self, points: list | tuple):
135131
self.x, self.y = points
136132

137-
@dp
133+
@md
138134
def __init__(self, points: dict):
139135
self.x = points['x']
140136
self.y = points['y']

docs/dispatch.rst

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -62,34 +62,25 @@ Ideally, every project will instanciate Dispatch only once, in a module such as
6262
Basic Use
6363
---------
6464

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

67-
::
68-
69-
from runtype import Dispatch
70-
dp = Dispatch()
71-
72-
Then, the group can be used as a decorator for any number of functions.
73-
74-
Dispatch maintains the original name of every function. So, functions of different names will never collide with each other.
75-
76-
The order in which you define functions doesn't matter.
67+
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.
7768

7869
Example:
7970
::
8071

81-
dp = Dispatch()
72+
from runtype import multidispatch as md
8273

8374
@dataclass(frozen=False)
8475
class Point:
8576
x: int = 0
8677
y: int = 0
8778
88-
@dp
79+
@md
8980
def __init__(self, points: list | tuple):
9081
self.x, self.y = points
9182

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

10495

96+
A different dispatch object is created for each module, so collisions between different modules are impossible.
97+
98+
Users who want to define a dispatch across several modules, or to have more granular control, can use the Dispatch class:
99+
100+
::
101+
102+
from runtype import Dispatch
103+
dp = Dispatch()
104+
105+
Then, the group can be used as a decorator for any number of functions, in any module.
106+
107+
Functions will still be grouped by name.
108+
105109

106110
Specificity
107111
-----------
@@ -117,11 +121,11 @@ Example:
117121

118122
from typing import Union
119123

120-
@dp
124+
@md
121125
def f(a: int, b: int):
122126
return a + b
123127

124-
@dp
128+
@md
125129
def f(a: Union[int, str], b: int):
126130
return (a, b)
127131

@@ -147,9 +151,9 @@ Ambiguity can result from two situations:
147151
Example:
148152
::
149153

150-
>>> @dp
154+
>>> @md
151155
... def f(a, b: int): pass
152-
>>> @dp
156+
>>> @md
153157
... def f(a: int, b): pass
154158
>>> f(1, 1)
155159
Traceback (most recent call last):
@@ -161,14 +165,11 @@ Dispatch is designed to always throw an error when the right choice isn't obviou
161165
Another example:
162166
::
163167

164-
from runtype import Dispatch
165-
dp = Dispatch()
166-
167-
@dp
168+
@md
168169
def join(seq, sep: str = ''):
169170
return sep.join(str(s) for s in seq)
170171

171-
@dp
172+
@md
172173
def join(seq, sep: list):
173174
return join(join(sep, str(s)) for s in seq)
174175
...
@@ -191,39 +192,36 @@ Another example:
191192

192193
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`.
193194

194-
MyPy support (@overload)
195+
MyPy support
195196
------------------------
196197

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

199200
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.
200201

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

203-
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.
204+
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.
204205

205-
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.
206+
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.
206207

207208

208209
Example usage:
209210

210211
::
211212

212-
from runtype import Dispatch
213+
from runtype import multidispatch as md, multidispatch_final as md_final
213214
from typing import overload
214-
dp = Dispatch()
215215

216-
@overload
217-
@dp
216+
@md
218217
def join(seq, sep: str = ''):
219218
return sep.join(str(s) for s in seq)
220219

221-
@overload
222-
@dp
220+
@md
223221
def join(seq, sep: list):
224222
return join(join(sep, str(s)) for s in seq)
225223

226-
@dp
224+
@md_final
227225
def join(seq, sep):
228226
raise NotImplementedError()
229227

runtype/__init__.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Callable, TYPE_CHECKING
2+
13
from .dataclass import dataclass
24
from .dispatch import DispatchError, MultiDispatch
35
from .validation import (PythonTyping, TypeSystem, TypeMismatchError,
@@ -54,3 +56,64 @@ def Dispatch(typesystem: TypeSystem = PythonTyping()):
5456
return MultiDispatch(typesystem)
5557

5658

59+
60+
typesystem: TypeSystem = PythonTyping()
61+
62+
class PythonDispatch:
63+
def __init__(self):
64+
self.by_module = {}
65+
66+
def decorate(self, f: Callable) -> Callable:
67+
"""A decorator that enables multiple-dispatch for the given function.
68+
69+
The dispatch namespace is unique for each module, so there can be no name
70+
collisions for functions defined across different modules.
71+
Users that wish to share a dispatch across modules, should use the
72+
`Dispatch` class.
73+
74+
Parameters:
75+
f (Callable): Function to enable multiple-dispatch for
76+
77+
Returns:
78+
the decorated function
79+
80+
Example:
81+
::
82+
83+
>>> from runtype import multidispatch as md
84+
85+
>>> @md
86+
... def add1(i: Optional[int]):
87+
... return i + 1
88+
89+
>>> @md
90+
... def add1(s: Optional[str]):
91+
... return s + "1"
92+
93+
>>> @md
94+
... def add1(a): # accepts any type (least-specific)
95+
... return (a, 1)
96+
97+
>>> add1(1)
98+
2
99+
100+
>>> add1("1")
101+
11
102+
103+
>>> add1(1.0)
104+
(1.0, 1)
105+
106+
107+
"""
108+
module = f.__module__
109+
if module not in self.by_module:
110+
self.by_module[module] = MultiDispatch(typesystem)
111+
return self.by_module[module](f)
112+
113+
python_dispatch = PythonDispatch()
114+
115+
multidispatch_final = python_dispatch.decorate
116+
if TYPE_CHECKING:
117+
from typing import overload as multidispatch
118+
else:
119+
multidispatch = python_dispatch.decorate

tests/test_basic.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import logging
1212
logging.basicConfig(level=logging.INFO)
1313

14-
from runtype import Dispatch, DispatchError, dataclass, isa, is_subtype, issubclass, assert_isa, String, Int, validate_func, cv_type_checking
14+
from runtype import Dispatch, DispatchError, dataclass, isa, is_subtype, issubclass, assert_isa, String, Int, validate_func, cv_type_checking, multidispatch
1515
from runtype.dispatch import MultiDispatch
1616
from runtype.dataclass import Configuration
1717

@@ -626,7 +626,24 @@ def test_callable(self):
626626
def test_match(self):
627627
pass
628628

629+
def test_dispatch_singleton(self):
630+
def f(a: int):
631+
return 'a'
632+
f.__module__ = 'a'
633+
f1 = multidispatch(f)
629634

635+
def f(a: int):
636+
return 'a'
637+
f.__module__ = 'b'
638+
f2 = multidispatch(f)
639+
640+
assert f1(1) == 'a'
641+
assert f2(1) == 'a'
642+
643+
def f(a: int):
644+
return 'a'
645+
f.__module__ = 'a'
646+
self.assertRaises(ValueError, multidispatch, f)
630647

631648
class TestDataclass(TestCase):
632649
def setUp(self):

0 commit comments

Comments
 (0)