ovld 0.3.9__py3-none-any.whl → 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ovld/__init__.py +41 -15
- ovld/core.py +404 -589
- ovld/dependent.py +275 -0
- ovld/mro.py +193 -161
- ovld/recode.py +518 -16
- ovld/typemap.py +383 -0
- ovld/types.py +219 -0
- ovld/utils.py +9 -110
- ovld/version.py +1 -1
- ovld-0.4.1.dist-info/METADATA +216 -0
- ovld-0.4.1.dist-info/RECORD +13 -0
- ovld-0.3.9.dist-info/METADATA +0 -305
- ovld-0.3.9.dist-info/RECORD +0 -10
- {ovld-0.3.9.dist-info → ovld-0.4.1.dist-info}/WHEEL +0 -0
- {ovld-0.3.9.dist-info → ovld-0.4.1.dist-info}/licenses/LICENSE +0 -0
ovld/utils.py
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
"""Miscellaneous utilities."""
|
2
2
|
|
3
3
|
import functools
|
4
|
-
import sys
|
5
|
-
from dataclasses import dataclass
|
6
|
-
from typing import Protocol, runtime_checkable
|
7
4
|
|
8
5
|
|
9
6
|
class Named:
|
@@ -48,122 +45,24 @@ def keyword_decorator(deco):
|
|
48
45
|
return new_deco
|
49
46
|
|
50
47
|
|
51
|
-
class
|
52
|
-
|
53
|
-
return cls.chk(sub)
|
48
|
+
class UsageError(Exception):
|
49
|
+
pass
|
54
50
|
|
55
51
|
|
56
|
-
|
57
|
-
|
52
|
+
class Unusable:
|
53
|
+
def __init__(self, message):
|
54
|
+
self.__message = message
|
58
55
|
|
59
|
-
|
60
|
-
|
61
|
-
`meta(lambda cls: cls.__name__.startswith("X"))`.
|
56
|
+
def __call__(self, *args, **kwargs):
|
57
|
+
raise UsageError(self.__message)
|
62
58
|
|
63
|
-
|
64
|
-
|
65
|
-
True or False depending on whether it matches some condition.
|
66
|
-
"""
|
67
|
-
|
68
|
-
class M(metaclass=_MetaMC):
|
69
|
-
@classmethod
|
70
|
-
def chk(cls, sub):
|
71
|
-
return condition(sub)
|
72
|
-
|
73
|
-
return M
|
74
|
-
|
75
|
-
|
76
|
-
def _getcls(ref):
|
77
|
-
module, *parts = ref.split(".")
|
78
|
-
curr = __import__(module)
|
79
|
-
for part in parts:
|
80
|
-
curr = getattr(curr, part)
|
81
|
-
return curr
|
82
|
-
|
83
|
-
|
84
|
-
def deferred(ref):
|
85
|
-
"""Represent a class from an external module without importing it.
|
86
|
-
|
87
|
-
For instance, `deferred("numpy.ndarray")` matches instances of
|
88
|
-
numpy.ndarray, but it does not import numpy. When tested against a
|
89
|
-
class, if the first part of class's `__module__` is `numpy`, then
|
90
|
-
we do get the class and perform a normal issubclass check.
|
91
|
-
|
92
|
-
If the module is already loaded, `deferred` returns the class directly.
|
93
|
-
|
94
|
-
Arguments:
|
95
|
-
ref: A string starting with a module name representing the path
|
96
|
-
to import a class.
|
97
|
-
"""
|
98
|
-
module, _ = ref.split(".", 1)
|
99
|
-
if module in sys.modules:
|
100
|
-
return _getcls(ref)
|
101
|
-
|
102
|
-
@meta
|
103
|
-
def check(cls):
|
104
|
-
full_cls_mod = getattr(cls, "__module__", None)
|
105
|
-
cls_module = full_cls_mod.split(".", 1)[0] if full_cls_mod else None
|
106
|
-
if cls_module == module:
|
107
|
-
return issubclass(cls, _getcls(ref))
|
108
|
-
else:
|
109
|
-
return False
|
110
|
-
|
111
|
-
return check
|
112
|
-
|
113
|
-
|
114
|
-
def exactly(base_cls):
|
115
|
-
"""Match the class but not its subclasses."""
|
116
|
-
|
117
|
-
@meta
|
118
|
-
def check(cls):
|
119
|
-
return cls is base_cls
|
120
|
-
|
121
|
-
return check
|
122
|
-
|
123
|
-
|
124
|
-
def strict_subclass(base_cls):
|
125
|
-
"""Match subclasses but not the base class."""
|
126
|
-
|
127
|
-
@meta
|
128
|
-
def check(cls):
|
129
|
-
return (
|
130
|
-
isinstance(cls, type)
|
131
|
-
and issubclass(cls, base_cls)
|
132
|
-
and cls is not base_cls
|
133
|
-
)
|
134
|
-
|
135
|
-
return check
|
136
|
-
|
137
|
-
|
138
|
-
def has_attribute(*attrs):
|
139
|
-
"""Match classes with the given attributes."""
|
140
|
-
|
141
|
-
@meta
|
142
|
-
def check(cls):
|
143
|
-
return all(hasattr(cls, a) for a in attrs)
|
144
|
-
|
145
|
-
return check
|
146
|
-
|
147
|
-
|
148
|
-
@runtime_checkable
|
149
|
-
@dataclass
|
150
|
-
class Dataclass(Protocol):
|
151
|
-
@classmethod
|
152
|
-
def __subclasshook__(cls, subclass):
|
153
|
-
return hasattr(subclass, "__dataclass_fields__") and hasattr(
|
154
|
-
subclass, "__dataclass_params__"
|
155
|
-
)
|
59
|
+
def __getattr__(self, attr):
|
60
|
+
raise UsageError(self.__message)
|
156
61
|
|
157
62
|
|
158
63
|
__all__ = [
|
159
64
|
"BOOTSTRAP",
|
160
65
|
"MISSING",
|
161
|
-
"Dataclass",
|
162
66
|
"Named",
|
163
|
-
"deferred",
|
164
|
-
"exactly",
|
165
|
-
"has_attribute",
|
166
|
-
"meta",
|
167
67
|
"keyword_decorator",
|
168
|
-
"strict_subclass",
|
169
68
|
]
|
ovld/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
version = "0.
|
1
|
+
version = "0.4.1"
|
@@ -0,0 +1,216 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: ovld
|
3
|
+
Version: 0.4.1
|
4
|
+
Summary: Overloading Python functions
|
5
|
+
Project-URL: Homepage, https://ovld.readthedocs.io/en/latest/
|
6
|
+
Project-URL: Documentation, https://ovld.readthedocs.io/en/latest/
|
7
|
+
Project-URL: Repository, https://github.com/breuleux/ovld
|
8
|
+
Author-email: Olivier Breuleux <breuleux@gmail.com>
|
9
|
+
License-Expression: MIT
|
10
|
+
License-File: LICENSE
|
11
|
+
Requires-Python: >=3.9
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
|
14
|
+
|
15
|
+
# Ovld
|
16
|
+
|
17
|
+
Fast multiple dispatch in Python, with many extra features.
|
18
|
+
|
19
|
+
[📋 Documentation](https://ovld.readthedocs.io/en/latest/)
|
20
|
+
|
21
|
+
With ovld, you can write a version of the same function for every type signature using annotations instead of writing an awkward sequence of `isinstance` statements. Unlike Python's `singledispatch`, it works for multiple arguments.
|
22
|
+
|
23
|
+
* ⚡️ **[Fast](https://ovld.readthedocs.io/en/latest/compare/#results):** `ovld` is the fastest multiple dispatch library around, by some margin.
|
24
|
+
* 🚀 [**Variants**](https://ovld.readthedocs.io/en/latest/usage/#variants) and [**mixins**](https://ovld.readthedocs.io/en/latest/usage/#mixins) of functions and methods.
|
25
|
+
* 🦄 **[Dependent types](https://ovld.readthedocs.io/en/latest/dependent/):** Overloaded functions can depend on more than argument types: they can depend on actual values.
|
26
|
+
* 🔑 **Extensive:** Dispatch on functions, methods, positional arguments and even [keyword arguments](https://ovld.readthedocs.io/en/latest/usage/#keyword-arguments) (with some restrictions).
|
27
|
+
|
28
|
+
## Example
|
29
|
+
|
30
|
+
Here's a function that recursively adds lists, tuples and dictionaries:
|
31
|
+
|
32
|
+
```python
|
33
|
+
from ovld import ovld, recurse
|
34
|
+
|
35
|
+
@ovld
|
36
|
+
def add(x: list, y: list):
|
37
|
+
return [recurse(a, b) for a, b in zip(x, y)]
|
38
|
+
|
39
|
+
@ovld
|
40
|
+
def add(x: tuple, y: tuple):
|
41
|
+
return tuple(recurse(a, b) for a, b in zip(x, y))
|
42
|
+
|
43
|
+
@ovld
|
44
|
+
def add(x: dict, y: dict):
|
45
|
+
return {k: recurse(v, y[k]) for k, v in x.items()}
|
46
|
+
|
47
|
+
@ovld
|
48
|
+
def add(x: object, y: object):
|
49
|
+
return x + y
|
50
|
+
|
51
|
+
assert add([1, 2], [3, 4]) == [4, 6]
|
52
|
+
```
|
53
|
+
|
54
|
+
The `recurse` function is special: it will recursively call the current ovld object. You may ask: how is it different from simply calling `add`? The difference is that if you create a *variant* of `add`, `recurse` will automatically call the variant.
|
55
|
+
|
56
|
+
For example:
|
57
|
+
|
58
|
+
|
59
|
+
## Variants
|
60
|
+
|
61
|
+
A *variant* of an `ovld` is a copy of the `ovld`, with some methods added or changed. For example, let's take the definition of `add` above and make a variant that multiplies numbers instead:
|
62
|
+
|
63
|
+
```python
|
64
|
+
@add.variant
|
65
|
+
def mul(self, x: object, y: object):
|
66
|
+
return x * y
|
67
|
+
|
68
|
+
assert mul([1, 2], [3, 4]) == [3, 8]
|
69
|
+
```
|
70
|
+
|
71
|
+
Simple! This means you can define one `ovld` that recursively walks generic data structures, and then specialize it in various ways.
|
72
|
+
|
73
|
+
|
74
|
+
## Priority and call_next
|
75
|
+
|
76
|
+
You can define a numeric priority for each method (the default priority is 0):
|
77
|
+
|
78
|
+
```python
|
79
|
+
from ovld import call_next
|
80
|
+
|
81
|
+
@ovld(priority=1000)
|
82
|
+
def f(x: int):
|
83
|
+
return call_next(x + 1)
|
84
|
+
|
85
|
+
@ovld
|
86
|
+
def f(x: int):
|
87
|
+
return x * x
|
88
|
+
|
89
|
+
assert f(10) == 121
|
90
|
+
```
|
91
|
+
|
92
|
+
Both definitions above have the same type signature, but since the first has higher priority, that is the one that will be called.
|
93
|
+
|
94
|
+
However, that does not mean there is no way to call the second one. Indeed, when the first function calls the special function `call_next(x + 1)`, it will call the next function in the list below itself.
|
95
|
+
|
96
|
+
The pattern you see above is how you may wrap each call with some generic behavior. For instance, if you did something like that:
|
97
|
+
|
98
|
+
```python
|
99
|
+
@f.variant(priority=1000)
|
100
|
+
def f2(x: object)
|
101
|
+
print(f"f({x!r})")
|
102
|
+
return call_next(x)
|
103
|
+
```
|
104
|
+
|
105
|
+
You would effectively be creating a clone of `f` that traces every call.
|
106
|
+
|
107
|
+
|
108
|
+
## Dependent types
|
109
|
+
|
110
|
+
A dependent type is a type that depends on a value. `ovld` supports this, either through `Literal[value]` or `Dependent[bound, check]`. For example, this definition of factorial:
|
111
|
+
|
112
|
+
```python
|
113
|
+
from typing import Literal
|
114
|
+
from ovld import ovld, recurse, Dependent
|
115
|
+
|
116
|
+
@ovld
|
117
|
+
def fact(n: Literal[0]):
|
118
|
+
return 1
|
119
|
+
|
120
|
+
@ovld
|
121
|
+
def fact(n: Dependent[int, lambda n: n > 0]):
|
122
|
+
return n * recurse(n - 1)
|
123
|
+
|
124
|
+
assert fact(5) == 120
|
125
|
+
fact(-1) # Error!
|
126
|
+
```
|
127
|
+
|
128
|
+
The first argument to `Dependent` must be a type bound. The bound must match before the logic is called, which also ensures we don't get a performance hit for unrelated types. For type checking purposes, `Dependent[T, A]` is equivalent to `Annotated[T, A]`.
|
129
|
+
|
130
|
+
### dependent_check
|
131
|
+
|
132
|
+
Define your own types with the `@dependent_check` decorator:
|
133
|
+
|
134
|
+
```python
|
135
|
+
import torch
|
136
|
+
from ovld import ovld, dependent_check
|
137
|
+
|
138
|
+
@dependent_check
|
139
|
+
def Shape(tensor: torch.Tensor, *shape):
|
140
|
+
return (
|
141
|
+
len(tensor.shape) == len(shape)
|
142
|
+
and all(s2 is Any or s1 == s2 for s1, s2 in zip(tensor.shape, shape))
|
143
|
+
)
|
144
|
+
|
145
|
+
@dependent_check
|
146
|
+
def Dtype(tensor: torch.Tensor, dtype):
|
147
|
+
return tensor.dtype == dtype
|
148
|
+
|
149
|
+
@ovld
|
150
|
+
def f(tensor: Shape[3, Any]):
|
151
|
+
# Matches 3xN tensors
|
152
|
+
...
|
153
|
+
|
154
|
+
@ovld
|
155
|
+
def f(tensor: Shape[2, 2] & Dtype[torch.float32]):
|
156
|
+
# Only matches 2x2 tensors that also have the float32 dtype
|
157
|
+
...
|
158
|
+
```
|
159
|
+
|
160
|
+
The first parameter is the value to check. The type annotation (e.g. `value: torch.Tensor` above) is interpreted by `ovld` to be the bound for this type, so `Shape` will only be called on parameters of type `torch.Tensor`.
|
161
|
+
|
162
|
+
## Methods
|
163
|
+
|
164
|
+
Either inherit from `OvldBase` or use the `OvldMC` metaclass to use multiple dispatch on methods.
|
165
|
+
|
166
|
+
```python
|
167
|
+
from ovld import OvldBase, OvldMC
|
168
|
+
|
169
|
+
# class Cat(OvldBase): <= Also an option
|
170
|
+
class Cat(metaclass=OvldMC):
|
171
|
+
def interact(self, x: Mouse):
|
172
|
+
return "catch"
|
173
|
+
|
174
|
+
def interact(self, x: Food):
|
175
|
+
return "devour"
|
176
|
+
|
177
|
+
def interact(self, x: PricelessVase):
|
178
|
+
return "destroy"
|
179
|
+
```
|
180
|
+
|
181
|
+
### Subclasses
|
182
|
+
|
183
|
+
Subclasses inherit overloaded methods. They may define additional overloads for these methods which will only be valid for the subclass, but they need to use the `@extend_super` decorator (this is required for clarity):
|
184
|
+
|
185
|
+
|
186
|
+
```python
|
187
|
+
from ovld import OvldMC, extend_super
|
188
|
+
|
189
|
+
class One(metaclass=OvldMC):
|
190
|
+
def f(self, x: int):
|
191
|
+
return "an integer"
|
192
|
+
|
193
|
+
class Two(One):
|
194
|
+
@extend_super
|
195
|
+
def f(self, x: str):
|
196
|
+
return "a string"
|
197
|
+
|
198
|
+
assert Two().f(1) == "an integer"
|
199
|
+
assert Two().f("s") == "a string"
|
200
|
+
```
|
201
|
+
|
202
|
+
# Benchmarks
|
203
|
+
|
204
|
+
`ovld` is pretty fast: the overhead is comparable to `isinstance` or `match`, and only 2-3x slower when dispatching on `Literal` types. Compared to other multiple dispatch libraries, it is 1.5x to 100x faster.
|
205
|
+
|
206
|
+
Time relative to the fastest implementation (1.00) (lower is better).
|
207
|
+
|
208
|
+
| Bench | custom | [ovld](https://github.com/breuleux/ovld) | [plum](https://github.com/beartype/plum) | [multim](https://github.com/coady/multimethod) | [multid](https://github.com/mrocklin/multipledispatch/) | [runtype](https://github.com/erezsh/runtype) | [fastcore](https://github.com/fastai/fastcore) | [singled](https://docs.python.org/3/library/functools.html#functools.singledispatch) |
|
209
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
210
|
+
|[trivial](https://github.com/breuleux/ovld/tree/master/benchmarks/test_trivial.py)|1.14|1.00|2.53|3.64|1.61|1.86|41.31|1.54|
|
211
|
+
|[add](https://github.com/breuleux/ovld/tree/master/benchmarks/test_add.py)|1.01|1.00|3.46|4.83|2.21|2.66|56.08|x|
|
212
|
+
|[multer](https://github.com/breuleux/ovld/tree/master/benchmarks/test_multer.py)|1.00|1.06|9.79|4.11|7.19|1.89|40.37|6.34|
|
213
|
+
|[ast](https://github.com/breuleux/ovld/tree/master/benchmarks/test_ast.py)|1.00|1.06|23.07|3.04|1.68|1.87|29.11|1.63|
|
214
|
+
|[calc](https://github.com/breuleux/ovld/tree/master/benchmarks/test_calc.py)|1.00|1.96|80.00|43.21|x|x|x|x|
|
215
|
+
|[fib](https://github.com/breuleux/ovld/tree/master/benchmarks/test_fib.py)|1.00|3.58|438.97|123.58|x|x|x|x|
|
216
|
+
|[tweak](https://github.com/breuleux/ovld/tree/master/benchmarks/test_tweaknum.py)|1.00|2.59|x|x|x|x|x|x|
|
@@ -0,0 +1,13 @@
|
|
1
|
+
ovld/__init__.py,sha256=Vp9wbIy_opFmJx-vCcToQcIP3cWFIvgbfYHITyiwPLs,1305
|
2
|
+
ovld/core.py,sha256=S6YSbZk5ajKvdVqn5NyuduDAU_moBylZGmpNskQE7XI,25397
|
3
|
+
ovld/dependent.py,sha256=QITsWu2uCwqkHE0tunETy8Jqwc272uoG5YM0I5yy0m4,7303
|
4
|
+
ovld/mro.py,sha256=rS0pCLLWwLUI0NdthpoJSrdvWR4-XgRfx1pLAvJde14,5586
|
5
|
+
ovld/recode.py,sha256=Vc97Nv1j2GWuitLOzIuIhpscZ4gaOJQP3hLNp8SGTQ8,17890
|
6
|
+
ovld/typemap.py,sha256=U_BmXtts1oYVa6gI3cgGHMX5kFcCJY_mt_cjWvDo3jQ,12979
|
7
|
+
ovld/types.py,sha256=Zeb7xhHbL4T7qIRHI-I_cjG61UIWrfZv_EwjwqhB-rY,6381
|
8
|
+
ovld/utils.py,sha256=V6Y8oZ6ojq8JaODL1rMZbU5L9QG0YSqHNYmpIFiwy3M,1294
|
9
|
+
ovld/version.py,sha256=FBeN5tbWV0BpBmjJ3d3pzaNRlEg_Blp421Tbg8X3cAI,18
|
10
|
+
ovld-0.4.1.dist-info/METADATA,sha256=OOhqZjiT-T6jb-c0iATIElVxfZbZ62qDcuJQcxvHMQM,7713
|
11
|
+
ovld-0.4.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
12
|
+
ovld-0.4.1.dist-info/licenses/LICENSE,sha256=cSwNTIzd1cbI89xt3PeZZYJP2y3j8Zus4bXgo4svpX8,1066
|
13
|
+
ovld-0.4.1.dist-info/RECORD,,
|
ovld-0.3.9.dist-info/METADATA
DELETED
@@ -1,305 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.3
|
2
|
-
Name: ovld
|
3
|
-
Version: 0.3.9
|
4
|
-
Summary: Overloading Python functions
|
5
|
-
Author-email: Olivier Breuleux <breuleux@gmail.com>
|
6
|
-
License-Expression: MIT
|
7
|
-
License-File: LICENSE
|
8
|
-
Requires-Python: >=3.8
|
9
|
-
Description-Content-Type: text/markdown
|
10
|
-
|
11
|
-
|
12
|
-
# Ovld
|
13
|
-
|
14
|
-
Multiple dispatch in Python, with some extra features.
|
15
|
-
|
16
|
-
With ovld, you can write a version of the same function for every type signature using annotations instead of writing an awkward sequence of `isinstance` statements. Unlike Python `singledispatch`, it works for multiple arguments.
|
17
|
-
|
18
|
-
Other features of `ovld`:
|
19
|
-
|
20
|
-
* Multiple dispatch for methods (with `metaclass=ovld.OvldMC`)
|
21
|
-
* Create variants of functions
|
22
|
-
* Built-in support for extensible recursion
|
23
|
-
* Function wrappers
|
24
|
-
* Function postprocessors
|
25
|
-
* Nice stack traces
|
26
|
-
|
27
|
-
## Example
|
28
|
-
|
29
|
-
Here's a function that adds lists, tuples and dictionaries:
|
30
|
-
|
31
|
-
```python
|
32
|
-
from ovld import ovld
|
33
|
-
|
34
|
-
@ovld
|
35
|
-
def add(x: list, y: list):
|
36
|
-
return [add(a, b) for a, b in zip(x, y)]
|
37
|
-
|
38
|
-
@ovld
|
39
|
-
def add(x: tuple, y: tuple):
|
40
|
-
return tuple(add(a, b) for a, b in zip(x, y))
|
41
|
-
|
42
|
-
@ovld
|
43
|
-
def add(x: dict, y: dict):
|
44
|
-
return {k: add(v, y[k]) for k, v in x.items()}
|
45
|
-
|
46
|
-
@ovld
|
47
|
-
def add(x: object, y: object):
|
48
|
-
return x + y
|
49
|
-
```
|
50
|
-
|
51
|
-
## Bootstrapping and variants
|
52
|
-
|
53
|
-
Now, there is another way to do this using ovld's *auto-bootstrapping*. Simply list `self` as the first argument to the function, and `self` will be bound to the function itself, so you can call `self(x, y)` for the recursion instead of `add(x, y)`:
|
54
|
-
|
55
|
-
|
56
|
-
```python
|
57
|
-
@ovld
|
58
|
-
def add(self, x: list, y: list):
|
59
|
-
return [self(a, b) for a, b in zip(x, y)]
|
60
|
-
|
61
|
-
@ovld
|
62
|
-
def add(self, x: tuple, y: tuple):
|
63
|
-
return tuple(self(a, b) for a, b in zip(x, y))
|
64
|
-
|
65
|
-
@ovld
|
66
|
-
def add(self, x: dict, y: dict):
|
67
|
-
return {k: self(v, y[k]) for k, v in x.items()}
|
68
|
-
|
69
|
-
@ovld
|
70
|
-
def add(self, x: object, y: object):
|
71
|
-
return x + y
|
72
|
-
```
|
73
|
-
|
74
|
-
Why is this useful, though? Observe:
|
75
|
-
|
76
|
-
```python
|
77
|
-
@add.variant
|
78
|
-
def mul(self, x: object, y: object):
|
79
|
-
return x * y
|
80
|
-
|
81
|
-
assert add([1, 2], [3, 4]) == [4, 6]
|
82
|
-
assert mul([1, 2], [3, 4]) == [3, 8]
|
83
|
-
```
|
84
|
-
|
85
|
-
A `variant` of a function is a copy which inherits all of the original's implementations but may define new ones. And because `self` is bound to the function that's called at the top level, the implementations for `list`, `tuple` and `dict` will bind `self` to `add` or `mul` depending on which one was called. You may also call `self.super(*args)` to invoke the parent implementation for that type.
|
86
|
-
|
87
|
-
|
88
|
-
## Custom dispatch
|
89
|
-
|
90
|
-
You can define your own dispatching function. The dispatcher's first argument is always `self`.
|
91
|
-
|
92
|
-
* `self.resolve(x, y)` to get the right function for the types of x and y
|
93
|
-
* `self[type(x), type(y)]` will also return the right function for these types, but it works directly with the types.
|
94
|
-
|
95
|
-
For example, here is how you might define a function such that f(x) <=> f(x, x):
|
96
|
-
|
97
|
-
```python
|
98
|
-
@ovld.dispatch
|
99
|
-
def add_default(self, x, y=None):
|
100
|
-
if y is None:
|
101
|
-
y = x
|
102
|
-
return self.resolve(x, y)(x, y)
|
103
|
-
|
104
|
-
@ovld
|
105
|
-
def add_default(x: int, y: int):
|
106
|
-
return x + y
|
107
|
-
|
108
|
-
@ovld
|
109
|
-
def add_default(x: str, y: str):
|
110
|
-
return x + y
|
111
|
-
|
112
|
-
@ovld
|
113
|
-
def add_default(xs: list, ys: list):
|
114
|
-
return [add_default(x, y) for x, y in zip(xs, ys)]
|
115
|
-
|
116
|
-
assert add_default([1, 2, "alouette"]) == [2, 4, "alouettealouette"]
|
117
|
-
```
|
118
|
-
|
119
|
-
There are other uses for this feature, e.g. memoization.
|
120
|
-
|
121
|
-
The normal functions may also have a `self`, which works the same as bootstrapping.
|
122
|
-
|
123
|
-
## Postprocess
|
124
|
-
|
125
|
-
`@ovld`, `@ovld.dispatch`, etc. take a `postprocess` argument which should be a function of one argument. That function will be called with the result of the call and must return the final result of the call.
|
126
|
-
|
127
|
-
Note that intermediate, bootstrapped recursive calls (recursive calls using `self()`) will **not** be postprocessed (if you want to wrap these calls, you can do so otherwise, like defining a custom dispatch). Only the result of the top level call is postprocessed.
|
128
|
-
|
129
|
-
## Methods
|
130
|
-
|
131
|
-
Use the `OvldMC` metaclass to use multiple dispatch on methods. In this case there is no bootstrapping as described above and `self` is simply bound to the class instance.
|
132
|
-
|
133
|
-
```python
|
134
|
-
from ovld import OvldMC
|
135
|
-
|
136
|
-
class Cat(metaclass=OvldMC):
|
137
|
-
def interact(self, x: Mouse):
|
138
|
-
return "catch"
|
139
|
-
|
140
|
-
def interact(self, x: Food):
|
141
|
-
return "devour"
|
142
|
-
|
143
|
-
def interact(self, x: PricelessVase):
|
144
|
-
return "destroy"
|
145
|
-
```
|
146
|
-
|
147
|
-
Subclasses of `Cat` will inherit the overloaded `interact` and it may define additional overloaded methods which will only be valid for the subclass.
|
148
|
-
|
149
|
-
**Note:** It is possible to use `ovld.dispatch` on methods, but in this case be aware that the first argument for the dispatch method will not be the usual `self` but an `OvldCall` object. The `self` can be retrived as `ovldcall.obj`. Here's an example to make it all clear:
|
150
|
-
|
151
|
-
```python
|
152
|
-
class Stuff(metaclass=OvldMC):
|
153
|
-
def __init__(self, mul):
|
154
|
-
self.mul = mul
|
155
|
-
|
156
|
-
@ovld.dispatch
|
157
|
-
def calc(ovldcall, x):
|
158
|
-
# Wraps every call to self.calc, but we receive ovldcall instead of self
|
159
|
-
# ovldcall[type(x)] returns the right method to call
|
160
|
-
# ovldcall.obj is the self (the actual instance of Stuff)
|
161
|
-
return ovldcall[type(x)](x) * ovldcall.obj.mul
|
162
|
-
|
163
|
-
def calc(self, x: int):
|
164
|
-
return x + 1
|
165
|
-
|
166
|
-
def calc(self, xs: list):
|
167
|
-
return [self.calc(x) for x in xs]
|
168
|
-
|
169
|
-
print(Stuff(2).calc([1, 2, 3])) # [4, 6, 8, 4, 6, 8]
|
170
|
-
```
|
171
|
-
|
172
|
-
### Mixins in subclasses
|
173
|
-
|
174
|
-
The `@extend_super` decorator on a method will combine the method with the definition on the superclass:
|
175
|
-
|
176
|
-
```python
|
177
|
-
from ovld import OvldMC, extend_super
|
178
|
-
|
179
|
-
class One(metaclass=OvldMC):
|
180
|
-
def f(self, x: int):
|
181
|
-
return "an integer"
|
182
|
-
|
183
|
-
class Two(One):
|
184
|
-
@extend_super
|
185
|
-
def f(self, x: str):
|
186
|
-
return "a string"
|
187
|
-
|
188
|
-
assert Two().f(1) == "an integer"
|
189
|
-
assert Two().f("s") == "a string"
|
190
|
-
```
|
191
|
-
|
192
|
-
## Ambiguous calls
|
193
|
-
|
194
|
-
The following definitions will cause a TypeError at runtime when called with two ints, because it is unclear which function is the right match:
|
195
|
-
|
196
|
-
```python
|
197
|
-
@ovld
|
198
|
-
def ambig(x: int, y: object):
|
199
|
-
print("io")
|
200
|
-
|
201
|
-
@ovld
|
202
|
-
def ambig(x: object, y: int):
|
203
|
-
print("oi")
|
204
|
-
|
205
|
-
ambig(8, 8) # ???
|
206
|
-
```
|
207
|
-
|
208
|
-
You may define an additional function with signature (int, int) to disambiguate:
|
209
|
-
|
210
|
-
```python
|
211
|
-
@ovld
|
212
|
-
def ambig(x: int, y: int):
|
213
|
-
print("ii")
|
214
|
-
```
|
215
|
-
|
216
|
-
## Other features
|
217
|
-
|
218
|
-
### meta
|
219
|
-
|
220
|
-
To test arbitrary conditions, you can use `meta`:
|
221
|
-
|
222
|
-
```python
|
223
|
-
from ovld import ovld, meta
|
224
|
-
|
225
|
-
@meta
|
226
|
-
def StartsWithT(cls):
|
227
|
-
return cls.__name__.startswith("T")
|
228
|
-
|
229
|
-
@ovld
|
230
|
-
def f(x: StartsWithT):
|
231
|
-
return "T"
|
232
|
-
|
233
|
-
assert f(TypeError("xyz")) == "T"
|
234
|
-
|
235
|
-
|
236
|
-
# Or: a useful example, since dataclasses have no common superclass:
|
237
|
-
|
238
|
-
from dataclasses import dataclass, is_dataclass
|
239
|
-
|
240
|
-
@dataclass
|
241
|
-
class Point:
|
242
|
-
x: int
|
243
|
-
y: int
|
244
|
-
|
245
|
-
@ovld
|
246
|
-
def f(x: meta(is_dataclass)):
|
247
|
-
return "dataclass"
|
248
|
-
|
249
|
-
assert f(Point(1, 2)) == "dataclass"
|
250
|
-
```
|
251
|
-
|
252
|
-
|
253
|
-
### deferred
|
254
|
-
|
255
|
-
You may define overloads for certain classes from external packages without
|
256
|
-
having to import them:
|
257
|
-
|
258
|
-
|
259
|
-
```python
|
260
|
-
from ovld import ovld, deferred
|
261
|
-
|
262
|
-
@ovld
|
263
|
-
def f(x: deferred("numpy.ndarray")):
|
264
|
-
return "ndarray"
|
265
|
-
|
266
|
-
# numpy is not imported
|
267
|
-
assert "numpy" not in sys.modules
|
268
|
-
|
269
|
-
# But once we import it, the ovld works:
|
270
|
-
import numpy
|
271
|
-
assert f(numpy.arange(10)) == "ndarray"
|
272
|
-
```
|
273
|
-
|
274
|
-
|
275
|
-
### Tracebacks
|
276
|
-
|
277
|
-
`ovld` automagically renames functions so that the stack trace is more informative:
|
278
|
-
|
279
|
-
```python
|
280
|
-
@add.variant
|
281
|
-
def bad(self, x: object, y: object):
|
282
|
-
raise Exception("Bad.")
|
283
|
-
|
284
|
-
bad([1], [2])
|
285
|
-
|
286
|
-
"""
|
287
|
-
File "/Users/breuleuo/code/ovld/ovld/core.py", line 148, in bad.entry
|
288
|
-
res = ovc(*args, **kwargs)
|
289
|
-
File "/Users/breuleuo/code/ovld/ovld/core.py", line 182, in bad.dispatch
|
290
|
-
return method(self.bind_to, *args, **kwargs)
|
291
|
-
File "example.py", line 6, in bad[list, list]
|
292
|
-
return [self(a, b) for a, b in zip(x, y)]
|
293
|
-
File "example.py", line 6, in <listcomp>
|
294
|
-
return [self(a, b) for a, b in zip(x, y)]
|
295
|
-
File "/Users/breuleuo/code/ovld/ovld/core.py", line 182, in bad.dispatch
|
296
|
-
return method(self.bind_to, *args, **kwargs)
|
297
|
-
File "example.py", line 26, in bad[*, *]
|
298
|
-
raise Exception("Bad.")
|
299
|
-
Exception: Bad.
|
300
|
-
"""
|
301
|
-
```
|
302
|
-
|
303
|
-
The functions on the stack have names like `bad.entry`, `bad.dispatch`, `bad[list, list]` and `bad[*, *]` (`*` stands for `object`), which lets you better understand what happened just from the stack trace.
|
304
|
-
|
305
|
-
This also means profilers will be able to differentiate between these paths and between variants, even if they share code paths.
|
ovld-0.3.9.dist-info/RECORD
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
ovld/__init__.py,sha256=jqq7r0RhfvkqmQa8Wc7wqX3Qh5NbwL_6Nfr1sp88Kh0,734
|
2
|
-
ovld/core.py,sha256=3JmnfLF6puVtWkc01Hnxuss4gVdWzX9mrLVMfysQqJY,32895
|
3
|
-
ovld/mro.py,sha256=W6T5IPec6YQIvv8oSwg-xGHTzZP1zOuTZuaRCegbl-I,6296
|
4
|
-
ovld/recode.py,sha256=45VmTUmk2GmWRfTLANMHhyzVcgBit2RvwULzyWpOMhI,2234
|
5
|
-
ovld/utils.py,sha256=8kINtHrWdC0bNVccxYIkHBaGFk_YFpjtnjAa9GyXNmA,3861
|
6
|
-
ovld/version.py,sha256=8WhSz7JLKtk-44zKbF-sqJmexEzvpM-d0cPHqr6cE5k,18
|
7
|
-
ovld-0.3.9.dist-info/METADATA,sha256=wbN2H6NflCIkt-OZJvzrVXWNIz8bH0FTow83Ir0C8NM,8282
|
8
|
-
ovld-0.3.9.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
9
|
-
ovld-0.3.9.dist-info/licenses/LICENSE,sha256=cSwNTIzd1cbI89xt3PeZZYJP2y3j8Zus4bXgo4svpX8,1066
|
10
|
-
ovld-0.3.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|