alternative 0.1.0__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.
- alternative-0.1.0.dist-info/METADATA +50 -0
- alternative-0.1.0.dist-info/RECORD +4 -0
- alternative-0.1.0.dist-info/WHEEL +4 -0
- alternative.py +404 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alternative
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A library to help with the management of alternative implementations of functions
|
|
5
|
+
Author-email: Oliver Bristow <evilumbrella+github@gmail.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: pytest>=8.3.5
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
[](https://pypi.org/project/alternative/) [](https://github.com/Code0x58/alternative/actions/workflows/ci.yml) [](https://codecov.io/gh/Code0x58/alternative)
|
|
11
|
+
|
|
12
|
+
This is a library to help with the management of alternative implementations of functions, as you may have when
|
|
13
|
+
optimising.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import alternative
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@alternative.reference
|
|
20
|
+
def constant_number():
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@constant_number.add(default=True)
|
|
25
|
+
def alternative_constant_number():
|
|
26
|
+
return 2
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@constant_number.add
|
|
30
|
+
def unused_alternative_constant_number():
|
|
31
|
+
return 3
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# the reference implementation is substituted with the default implementation - if no default was specified, the reference would have been used
|
|
35
|
+
assert constant_number() == 2
|
|
36
|
+
# alternative implementations still act like themselves
|
|
37
|
+
assert unused_alternative_constant_number() == 3
|
|
38
|
+
|
|
39
|
+
# TODO: mash in measure + benchmark + equality tests from examples
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The library tries to avoid unpleasant surprises that may come from import orders or maintenance issues:
|
|
43
|
+
* the implementation that is used cannot be changed once it has been used
|
|
44
|
+
* implementations cannot be added once they have been inspected: reduces the changes of tests only covering a subset of
|
|
45
|
+
|
|
46
|
+
Running with `ALTERNATIVE_DEBUG=1` as an environment variable will result in the library recording which lines were
|
|
47
|
+
responsible for critical state changes like the point that a default implementation was selected or the implementations
|
|
48
|
+
were inspected. These are then used in error messages which can be used to resolve stateful issues.
|
|
49
|
+
|
|
50
|
+
For how this can tie in with equivalence checks and benchmarking in pytest, see the examples directory.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
alternative.py,sha256=VHJJV4wGLWztgTU3SC1gtHTY0KlPsvfYTAstyuPLQgY,14965
|
|
2
|
+
alternative-0.1.0.dist-info/METADATA,sha256=pJSIQaq5rzFZVFfr8mHeEU1e7ipaMG1hOd29r6rnrkI,2188
|
|
3
|
+
alternative-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
+
alternative-0.1.0.dist-info/RECORD,,
|
alternative.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import enum
|
|
5
|
+
import inspect
|
|
6
|
+
import os
|
|
7
|
+
from functools import wraps, lru_cache
|
|
8
|
+
from typing import Callable
|
|
9
|
+
from typing import cast, overload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEBUG = os.environ.get("ALTERNATIVE_DEBUG", "0").lower() in (
|
|
13
|
+
"1",
|
|
14
|
+
"yes",
|
|
15
|
+
"y",
|
|
16
|
+
"true",
|
|
17
|
+
"t",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _UNDEFINED: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
UNDEFINED = _UNDEFINED()
|
|
25
|
+
|
|
26
|
+
type ImplementationSig[**P, R] = Callable[P, R] | Implementation[P, R]
|
|
27
|
+
type AlternativesWrapper[**P, R] = Callable[[ImplementationSig], Alternatives[P, R]]
|
|
28
|
+
type ImplementationWrapper[**P, R] = Callable[[ImplementationSig], Implementation[P, R]]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AlternativeError(Exception):
|
|
32
|
+
"""Base class for all alternative errors."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AddTooLate(AlternativeError):
|
|
36
|
+
"""Cannot add implementations after the alternatives have been invoked."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MultipleDefaults(AlternativeError):
|
|
40
|
+
"""Cannot set the default implementation more than once."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_caller_path() -> str | None:
|
|
44
|
+
"""
|
|
45
|
+
Return 'module.QualName (file.py:line)' pointing to the line
|
|
46
|
+
that invoked the caller of this function.
|
|
47
|
+
Falls back to '<unknown module>.<unknown> (<unknown location>)'
|
|
48
|
+
if there is no two-up Python frame (e.g. called directly from C embedding).
|
|
49
|
+
"""
|
|
50
|
+
frame = inspect.currentframe()
|
|
51
|
+
# Walk back two frames: 0=this, 1=caller, 2=caller of caller
|
|
52
|
+
if not frame or not frame.f_back:
|
|
53
|
+
caller = None # no two-up frame
|
|
54
|
+
else:
|
|
55
|
+
caller = frame.f_back.f_back
|
|
56
|
+
|
|
57
|
+
# walk though any frames that are in the current file as they will not be helpful
|
|
58
|
+
while caller is None or caller.f_code.co_filename == __file__:
|
|
59
|
+
# a bit of a jiggly approach of handling caller being None to make type checking easier and help coverage
|
|
60
|
+
if caller:
|
|
61
|
+
caller = caller.f_back
|
|
62
|
+
if caller is None:
|
|
63
|
+
return "<unknown module>.<unknown> (<unknown location>)"
|
|
64
|
+
code = caller.f_code
|
|
65
|
+
module = caller.f_globals.get("__name__", "<unknown module>")
|
|
66
|
+
qualname = getattr(code, "co_qualname", code.co_name)
|
|
67
|
+
|
|
68
|
+
filename = code.co_filename or "<unknown file>"
|
|
69
|
+
lineno = caller.f_lineno or "?"
|
|
70
|
+
location = f"{filename}:{lineno}"
|
|
71
|
+
|
|
72
|
+
return f"{module}.{qualname} ({location})"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def maybe_get_caller_path() -> str | None:
|
|
76
|
+
"""Return the call site if DEBUG is True, otherwise None."""
|
|
77
|
+
if DEBUG:
|
|
78
|
+
return get_caller_path()
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Mutability(enum.IntEnum):
|
|
83
|
+
"""The mutability of an Alternatives object, for providing consistency.
|
|
84
|
+
|
|
85
|
+
The general assumption is that all of these levels should be used for good practice/avoiding surprises. Reducing the
|
|
86
|
+
maximum effective level allows some flexibility, but probably shouldn't be used and is likely to be removed.
|
|
87
|
+
|
|
88
|
+
TODO: deprecate the setting of the maximum effective level
|
|
89
|
+
TODO: on freezing, self __call__ to underlying callable to speed things up
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
MUTABLE = 0
|
|
93
|
+
"""Implementations can be added and a default can be set - the initial state"""
|
|
94
|
+
FROZEN_IMPLEMENTATION = 1
|
|
95
|
+
"""Default implementations cannot be added or set - state after the default is set"""
|
|
96
|
+
FROZEN = 2
|
|
97
|
+
"""No new implementations can be added - state after the first invocation."""
|
|
98
|
+
# TODO: consider having a flag for "always allow new implementations" or MAX_EFFECTIVE_LEVEL with default that can
|
|
99
|
+
# be set on construction
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Alternatives[**P, R]:
|
|
103
|
+
def __init__(self, implementation: Callable[P, R], *, default: bool = False):
|
|
104
|
+
imp = Implementation(self, implementation)
|
|
105
|
+
self.reference = imp
|
|
106
|
+
# tracks the active implementation
|
|
107
|
+
self._default: Implementation[P, R] | None = None
|
|
108
|
+
self._debug_default: str | None = None
|
|
109
|
+
self._invoked = False
|
|
110
|
+
self._debug_invoked_site: str | None = None
|
|
111
|
+
# tracks the use of the set should be
|
|
112
|
+
self._enumerated = False
|
|
113
|
+
self._debug_invoked_site: str | None = None
|
|
114
|
+
|
|
115
|
+
self._callable: Callable[P, R] | None = None
|
|
116
|
+
self._debug_callable_used: str | None = None
|
|
117
|
+
|
|
118
|
+
# beware the order of this depends on the sequence of imports, so may vary between entrypoints
|
|
119
|
+
self._implementations: list[Implementation[P, R]] = []
|
|
120
|
+
self._implementations_used: bool = False
|
|
121
|
+
"""indicates if the list of implementations has been used though the external API"""
|
|
122
|
+
self._debug_implementations_used: str | None = None
|
|
123
|
+
|
|
124
|
+
self.add(imp, default=default)
|
|
125
|
+
|
|
126
|
+
@overload
|
|
127
|
+
def add(
|
|
128
|
+
self, implementation: _UNDEFINED = UNDEFINED, *, default: bool = False
|
|
129
|
+
) -> ImplementationWrapper[P, R]: ...
|
|
130
|
+
@overload
|
|
131
|
+
def add(
|
|
132
|
+
self, implementation: ImplementationSig[P, R], *, default: bool = False
|
|
133
|
+
) -> Implementation[P, R]: ...
|
|
134
|
+
|
|
135
|
+
def add(
|
|
136
|
+
self,
|
|
137
|
+
implementation=UNDEFINED,
|
|
138
|
+
*,
|
|
139
|
+
default=False,
|
|
140
|
+
) -> Implementation[P, R] | ImplementationWrapper[P, R]:
|
|
141
|
+
if self._implementations_used:
|
|
142
|
+
# avoid surprises from the implementation changing, which should be unexpected
|
|
143
|
+
if DEBUG:
|
|
144
|
+
msg = f"added implementation after first invocation at {self._debug_implementations_used}"
|
|
145
|
+
else:
|
|
146
|
+
msg = None
|
|
147
|
+
raise AddTooLate(msg)
|
|
148
|
+
|
|
149
|
+
if isinstance(implementation, _UNDEFINED):
|
|
150
|
+
# FIXME: handle when implementation is for a different set of alternatives
|
|
151
|
+
def wrapper(
|
|
152
|
+
implementation: ImplementationSig[P, R],
|
|
153
|
+
) -> Implementation[P, R]:
|
|
154
|
+
return self.add(implementation, default=default)
|
|
155
|
+
|
|
156
|
+
return cast(ImplementationWrapper[P, R], wrapper)
|
|
157
|
+
|
|
158
|
+
if not isinstance(implementation, Implementation):
|
|
159
|
+
imp = Implementation(self, implementation)
|
|
160
|
+
else:
|
|
161
|
+
imp = Implementation(self, implementation.implementation)
|
|
162
|
+
|
|
163
|
+
if default:
|
|
164
|
+
if self._default is not None:
|
|
165
|
+
# only allow explicitly setting the default implementation once
|
|
166
|
+
if DEBUG:
|
|
167
|
+
msg = f"first default was specified at {self._debug_default}"
|
|
168
|
+
else:
|
|
169
|
+
msg = None
|
|
170
|
+
raise MultipleDefaults(msg)
|
|
171
|
+
# there is the AddTooLate guard above which stops setting of a default after invocation - see test_default_after_invocation
|
|
172
|
+
self._default = imp
|
|
173
|
+
self._debug_default = maybe_get_caller_path()
|
|
174
|
+
|
|
175
|
+
self._implementations.append(imp)
|
|
176
|
+
return imp
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def callable(self) -> Callable[P, R]:
|
|
180
|
+
"""Return the active implementation.
|
|
181
|
+
|
|
182
|
+
Setting the default implementation is disabled after this is accessed."""
|
|
183
|
+
if self._callable is None:
|
|
184
|
+
# finalise the callable
|
|
185
|
+
if self._default:
|
|
186
|
+
self._callable = self._default.implementation
|
|
187
|
+
else:
|
|
188
|
+
self._callable = self.reference
|
|
189
|
+
self._debug_callable_used = maybe_get_caller_path()
|
|
190
|
+
self.__call__ = self._callable
|
|
191
|
+
# access the list of implementations to freeze them
|
|
192
|
+
assert self.implementations
|
|
193
|
+
return self._callable
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def implementations(self) -> list[Implementation[P, R]]:
|
|
197
|
+
if not self._implementations_used:
|
|
198
|
+
self._implementations_used = True
|
|
199
|
+
self._debug_implementations_used = maybe_get_caller_path()
|
|
200
|
+
return self._implementations
|
|
201
|
+
|
|
202
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
203
|
+
# this method will only be called at most once as self.callable overwrites self.__call__
|
|
204
|
+
return self.callable(*args, **kwargs)
|
|
205
|
+
|
|
206
|
+
def measure[M](
|
|
207
|
+
self, /, operator: Callable[[R], M], *args: P.args, **kwargs: P.kwargs
|
|
208
|
+
) -> dict[Implementation[P, R], M]:
|
|
209
|
+
"""Invoke each implementation with the given parameters, then evaluate their results with the operator.
|
|
210
|
+
|
|
211
|
+
This is useful when comparing implementations that have different results, which can be compared by some cost.
|
|
212
|
+
For example, this can be used to compare the complexity of quantum circuits that implement the same function.
|
|
213
|
+
|
|
214
|
+
The results are sorted by the operator's result if the result is sortable (i.e. does not raise TypeError when
|
|
215
|
+
__lt__(a,b) is called); otherwise they are returned in the order of the implementations.
|
|
216
|
+
"""
|
|
217
|
+
result = {
|
|
218
|
+
i: operator(i.implementation(*args, **kwargs)) for i in self.implementations
|
|
219
|
+
}
|
|
220
|
+
try:
|
|
221
|
+
# try to sort the dictionary by the measurements
|
|
222
|
+
return dict(sorted(result.items(), key=lambda x: x[1]))
|
|
223
|
+
except TypeError:
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
@overload
|
|
227
|
+
def pytest_parametrize(
|
|
228
|
+
self,
|
|
229
|
+
test: _UNDEFINED = UNDEFINED,
|
|
230
|
+
*,
|
|
231
|
+
only_default: bool = False,
|
|
232
|
+
): ...
|
|
233
|
+
@overload
|
|
234
|
+
def pytest_parametrize(
|
|
235
|
+
self,
|
|
236
|
+
test: Callable,
|
|
237
|
+
*,
|
|
238
|
+
only_default: bool = False,
|
|
239
|
+
): ...
|
|
240
|
+
def pytest_parametrize(
|
|
241
|
+
self,
|
|
242
|
+
test=UNDEFINED,
|
|
243
|
+
*,
|
|
244
|
+
only_default: bool = False,
|
|
245
|
+
):
|
|
246
|
+
"""Decorator to parametrise a test function with implementations - always includes the reference implementation.
|
|
247
|
+
|
|
248
|
+
:param test: Test function to wrap - this is elided if using the decorator syntax.
|
|
249
|
+
:parameter only_default: Only include the reference and default implementations. If False, include all implementations.
|
|
250
|
+
"""
|
|
251
|
+
import pytest
|
|
252
|
+
|
|
253
|
+
if isinstance(test, _UNDEFINED):
|
|
254
|
+
|
|
255
|
+
def inner(f: Callable):
|
|
256
|
+
return self.pytest_parametrize(f, only_default=only_default)
|
|
257
|
+
|
|
258
|
+
return inner
|
|
259
|
+
|
|
260
|
+
# FIXME: pull out this block into a function to test that implementations is made correctly
|
|
261
|
+
# + make sure that all arguments are passed through correctly
|
|
262
|
+
if only_default:
|
|
263
|
+
reference_implementation = self.reference.implementation
|
|
264
|
+
default_implementation = self.callable
|
|
265
|
+
implementations = [reference_implementation]
|
|
266
|
+
if default_implementation is not reference_implementation:
|
|
267
|
+
implementations.append(default_implementation)
|
|
268
|
+
else:
|
|
269
|
+
implementations = [i.implementation for i in self.implementations]
|
|
270
|
+
|
|
271
|
+
@pytest.mark.parametrize("implementation", implementations)
|
|
272
|
+
@wraps(test)
|
|
273
|
+
def inner(*args, **kwargs):
|
|
274
|
+
return test(*args, **kwargs)
|
|
275
|
+
|
|
276
|
+
return inner
|
|
277
|
+
|
|
278
|
+
@overload
|
|
279
|
+
def pytest_parametrize_pairs(
|
|
280
|
+
self,
|
|
281
|
+
test: _UNDEFINED = UNDEFINED,
|
|
282
|
+
*,
|
|
283
|
+
n_cache: int | None = 0,
|
|
284
|
+
double_reference: bool = False,
|
|
285
|
+
only_default: bool = False,
|
|
286
|
+
): ...
|
|
287
|
+
@overload
|
|
288
|
+
def pytest_parametrize_pairs(
|
|
289
|
+
self,
|
|
290
|
+
test: Callable,
|
|
291
|
+
*,
|
|
292
|
+
n_cache: int | None = 0,
|
|
293
|
+
double_reference: bool = False,
|
|
294
|
+
only_default: bool = False,
|
|
295
|
+
): ...
|
|
296
|
+
|
|
297
|
+
def pytest_parametrize_pairs(
|
|
298
|
+
self,
|
|
299
|
+
test=UNDEFINED,
|
|
300
|
+
*,
|
|
301
|
+
n_cache=0,
|
|
302
|
+
double_reference=False,
|
|
303
|
+
only_default=False,
|
|
304
|
+
):
|
|
305
|
+
"""Decorator to parametrise a test function with the reference and alternative implementations.
|
|
306
|
+
|
|
307
|
+
:parameter test: Inner pytest function to parameterise with reference and alternative implementations - this is elided if using the decorator syntax.
|
|
308
|
+
:parameter n_cache: Passed to lru_cache which wraps the reference implementation. Set to non-0 values for
|
|
309
|
+
it to actually cache.
|
|
310
|
+
:parameter double_reference: If True, the reference implementation will be included in the implementations. This
|
|
311
|
+
can be used for sanity-checking equivalence tests.
|
|
312
|
+
:parameter only_default: If True, only the default implementation will be used as the other implementation. If
|
|
313
|
+
double_reference is True, the reference implementation will be included as well.
|
|
314
|
+
"""
|
|
315
|
+
import pytest
|
|
316
|
+
|
|
317
|
+
if isinstance(test, _UNDEFINED):
|
|
318
|
+
|
|
319
|
+
def inner(f: Callable):
|
|
320
|
+
return self.pytest_parametrize_pairs(
|
|
321
|
+
f,
|
|
322
|
+
n_cache=n_cache,
|
|
323
|
+
double_reference=double_reference,
|
|
324
|
+
only_default=only_default,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return inner
|
|
328
|
+
|
|
329
|
+
# FIXME: pull out this block into a function to test that reference+implementations is made correctly
|
|
330
|
+
# + make sure that all arguments are passed through correctly
|
|
331
|
+
reference_implementation = lru_cache(maxsize=n_cache)(
|
|
332
|
+
self.reference.implementation
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# use the underlying functions instead of the implementation objects so pytest can generate helpful names
|
|
336
|
+
if only_default:
|
|
337
|
+
implementations = [self.callable]
|
|
338
|
+
if double_reference and self.callable is not self.reference.implementation:
|
|
339
|
+
implementations[:0] = [reference_implementation]
|
|
340
|
+
else:
|
|
341
|
+
implementations = [i.implementation for i in self.implementations[1:]]
|
|
342
|
+
if double_reference:
|
|
343
|
+
implementations[:0] = [reference_implementation]
|
|
344
|
+
|
|
345
|
+
@pytest.mark.parametrize("reference", [reference_implementation])
|
|
346
|
+
@pytest.mark.parametrize("implementation", implementations)
|
|
347
|
+
@wraps(test)
|
|
348
|
+
def inner(*args, **kwargs):
|
|
349
|
+
return test(*args, **kwargs)
|
|
350
|
+
|
|
351
|
+
return inner
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclasses.dataclass(unsafe_hash=True)
|
|
355
|
+
class Implementation[**P, R]:
|
|
356
|
+
alternatives: Alternatives[P, R]
|
|
357
|
+
implementation: Callable[P, R]
|
|
358
|
+
|
|
359
|
+
# TODO: add something like "label" which can be printed, e.g. "examples/test_measure.py:36" which would be a hyperlink in PyCharm
|
|
360
|
+
|
|
361
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
362
|
+
self.__call__ = self.implementation
|
|
363
|
+
return self.__call__(*args, **kwargs)
|
|
364
|
+
|
|
365
|
+
@overload
|
|
366
|
+
def add(
|
|
367
|
+
self, implementation: _UNDEFINED = UNDEFINED, *, default: bool = False
|
|
368
|
+
) -> ImplementationWrapper[P, R]: ...
|
|
369
|
+
@overload
|
|
370
|
+
def add(
|
|
371
|
+
self, implementation: ImplementationSig[P, R], *, default: bool = False
|
|
372
|
+
) -> Implementation[P, R]: ...
|
|
373
|
+
|
|
374
|
+
def add(
|
|
375
|
+
self, implementation=UNDEFINED, *, default=False
|
|
376
|
+
) -> Implementation[P, R] | ImplementationWrapper[P, R]:
|
|
377
|
+
"""Add an alternative implementation."""
|
|
378
|
+
return self.alternatives.add(implementation, default=default)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@overload
|
|
382
|
+
def reference[**P, R](
|
|
383
|
+
implementation: _UNDEFINED = UNDEFINED, *, default: bool = False
|
|
384
|
+
) -> AlternativesWrapper[P, R]: ...
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@overload
|
|
388
|
+
def reference[**P, R](
|
|
389
|
+
implementation: ImplementationSig[P, R], *, default: bool = False
|
|
390
|
+
) -> Alternatives[P, R]: ...
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def reference[**P, R](
|
|
394
|
+
implementation=UNDEFINED, *, default=False
|
|
395
|
+
) -> Alternatives[P, R] | AlternativesWrapper[P, R]:
|
|
396
|
+
if isinstance(implementation, _UNDEFINED):
|
|
397
|
+
|
|
398
|
+
def inner(f: Callable[P, R]) -> Alternatives[P, R]:
|
|
399
|
+
"""Add the reference implementation to the alternatives"""
|
|
400
|
+
return Alternatives(f, default=default)
|
|
401
|
+
|
|
402
|
+
return cast(AlternativesWrapper[P, R], inner)
|
|
403
|
+
else:
|
|
404
|
+
return Alternatives(implementation, default=default)
|