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/core.py CHANGED
@@ -2,328 +2,283 @@
2
2
 
3
3
  import inspect
4
4
  import itertools
5
- import math
6
5
  import sys
7
6
  import textwrap
8
7
  import typing
9
- from functools import partial
10
- from types import CodeType
8
+ from collections import defaultdict
9
+ from dataclasses import dataclass, field, replace
10
+ from functools import cached_property, partial
11
+ from types import GenericAlias
11
12
 
12
- from .mro import compose_mro
13
- from .recode import Conformer, rename_function
14
- from .utils import BOOTSTRAP, MISSING, keyword_decorator
13
+ from .recode import (
14
+ Conformer,
15
+ adapt_function,
16
+ generate_dispatch,
17
+ rename_function,
18
+ )
19
+ from .typemap import MultiTypeMap, is_type_of_type
20
+ from .types import normalize_type
21
+ from .utils import UsageError, keyword_decorator
15
22
 
16
- try:
17
- from types import UnionType
18
- except ImportError: # pragma: no cover
19
- UnionType = None
23
+ _current_id = itertools.count()
20
24
 
21
25
 
22
- class GenericAliasMC(type):
23
- def __instancecheck__(cls, obj):
24
- return hasattr(obj, "__origin__")
25
-
26
+ def _fresh(t):
27
+ """Returns a new subclass of type t.
26
28
 
27
- class GenericAlias(metaclass=GenericAliasMC):
28
- pass
29
+ Each Ovld corresponds to its own class, which allows for specialization of
30
+ methods.
31
+ """
32
+ methods = {}
33
+ if not isinstance(getattr(t, "__doc__", None), str):
34
+ methods["__doc__"] = t.__doc__
35
+ return type(t.__name__, (t,), methods)
29
36
 
30
37
 
31
- def is_type_of_type(t):
32
- return getattr(t, "__origin__", None) is type
38
+ @keyword_decorator
39
+ def _setattrs(fn, **kwargs):
40
+ for k, v in kwargs.items():
41
+ setattr(fn, k, v)
42
+ return fn
33
43
 
34
44
 
35
- class TypeMap(dict):
36
- """Represents a mapping from types to handlers.
45
+ @keyword_decorator
46
+ def _compile_first(fn, rename=None):
47
+ def first_entry(self, *args, **kwargs):
48
+ self.compile()
49
+ method = getattr(self, fn.__name__)
50
+ assert method is not first_entry
51
+ return method(*args, **kwargs)
37
52
 
38
- The mro of a type is considered when getting the handler, so setting the
39
- [object] key creates a default for all objects.
53
+ first_entry._replace_by = fn
54
+ first_entry._rename = rename
55
+ return first_entry
40
56
 
41
- typemap[some_type] returns a tuple of a handler and a "level" that
42
- represents the distance from the handler to the type `object`. Essentially,
43
- the level is the index of the type for which the handler was registered
44
- in the mro of `some_type`. So for example, `object` has level 0, a class
45
- that inherits directly from `object` has level 1, and so on.
46
- """
47
57
 
48
- def __init__(self):
49
- self.entries = {}
50
- self.types = set()
58
+ def arg0_is_self(fn):
59
+ sgn = inspect.signature(fn)
60
+ params = list(sgn.parameters.values())
61
+ return params and params[0].name == "self"
51
62
 
52
- def register(self, obj_t, handler):
53
- """Register a handler for the given object type."""
54
- if isinstance(obj_t, str):
55
- obj_t = eval(obj_t, getattr(handler[0], "__globals__", {}))
56
63
 
57
- self.clear()
58
- if is_type_of_type(obj_t):
59
- self.types.add(obj_t.__args__[0])
60
- else:
61
- self.types.add(obj_t)
62
- s = self.entries.setdefault(obj_t, set())
63
- s.add(handler)
64
+ @dataclass(frozen=True)
65
+ class Arginfo:
66
+ position: typing.Optional[int]
67
+ name: typing.Optional[str]
68
+ required: bool
69
+ ann: type
64
70
 
65
- def __missing__(self, obj_t):
66
- """Get the handler for the given type.
71
+ @cached_property
72
+ def is_complex(self):
73
+ return isinstance(self.ann, GenericAlias)
67
74
 
68
- The result is cached so that the normal dict getitem will find it
69
- the next time getitem is called.
70
- """
71
- results = {}
72
- abscollect = set()
73
- if is_type_of_type(obj_t):
74
- mro = [
75
- type[t]
76
- for t in compose_mro(obj_t.__args__[0], self.types, abscollect)
77
- ]
78
- mro.append(type)
79
- mro.append(object)
80
- else:
81
- mro = compose_mro(obj_t, self.types, abscollect)
82
-
83
- lvl = -1
84
- prev_is_abstract = False
85
- for cls in reversed(mro):
86
- if cls not in abscollect or not prev_is_abstract:
87
- lvl += 1
88
- prev_is_abstract = cls in abscollect
89
- handlers = self.entries.get(cls, None)
90
- if handlers:
91
- results.update({h: lvl for h in handlers})
92
-
93
- if results:
94
- self[obj_t] = results
95
- return results
96
- else:
97
- raise KeyError(obj_t)
75
+ @cached_property
76
+ def canonical(self):
77
+ return self.name if self.position is None else self.position
98
78
 
99
79
 
100
- class MultiTypeMap(dict):
101
- """Represents a mapping from tuples of types to handlers.
80
+ @dataclass(frozen=True)
81
+ class Signature:
82
+ types: tuple
83
+ req_pos: int
84
+ max_pos: int
85
+ req_names: frozenset
86
+ vararg: bool
87
+ priority: float
88
+ is_method: bool = False
89
+ arginfo: list[Arginfo] = field(
90
+ default_factory=list, hash=False, compare=False
91
+ )
102
92
 
103
- The mro is taken into account to find a match. If multiple registered
104
- handlers match the tuple of types that's given, if one of the handlers is
105
- more specific than every other handler, that handler is returned.
106
- Otherwise, the resolution is considered ambiguous and an error is raised.
93
+ @classmethod
94
+ def extract(cls, fn):
95
+ typelist = []
96
+ sig = inspect.signature(fn)
97
+ max_pos = 0
98
+ req_pos = 0
99
+ req_names = set()
100
+ is_method = False
101
+
102
+ arginfo = []
103
+ for i, (name, param) in enumerate(sig.parameters.items()):
104
+ if name == "self":
105
+ assert i == 0
106
+ is_method = True
107
+ continue
108
+ pos = nm = None
109
+ ann = normalize_type(param.annotation, fn)
110
+ if param.kind is inspect._POSITIONAL_ONLY:
111
+ pos = i - is_method
112
+ typelist.append(ann)
113
+ req_pos += param.default is inspect._empty
114
+ max_pos += 1
115
+ elif param.kind is inspect._POSITIONAL_OR_KEYWORD:
116
+ pos = i - is_method
117
+ nm = param.name
118
+ typelist.append(ann)
119
+ req_pos += param.default is inspect._empty
120
+ max_pos += 1
121
+ elif param.kind is inspect._KEYWORD_ONLY:
122
+ nm = param.name
123
+ typelist.append((param.name, ann))
124
+ if param.default is inspect._empty:
125
+ req_names.add(param.name)
126
+ elif param.kind is inspect._VAR_POSITIONAL:
127
+ raise TypeError("ovld does not support *args")
128
+ elif param.kind is inspect._VAR_KEYWORD:
129
+ raise TypeError("ovld does not support **kwargs")
130
+ arginfo.append(
131
+ Arginfo(
132
+ position=pos,
133
+ name=nm,
134
+ required=param.default is inspect._empty,
135
+ ann=normalize_type(param.annotation, fn),
136
+ )
137
+ )
107
138
 
108
- Handler A, registered for types (A1, A2, ..., An), is more specific than
109
- handler B, registered for types (B1, B2, ..., Bn), if there exists n such
110
- that An is more specific than Bn, and for all n, either An == Bn or An is
111
- more specific than Bn. An is more specific than Bn if An is a direct or
112
- indirect subclass of Bn.
139
+ return cls(
140
+ types=tuple(typelist),
141
+ req_pos=req_pos,
142
+ max_pos=max_pos,
143
+ req_names=frozenset(req_names),
144
+ vararg=False,
145
+ is_method=is_method,
146
+ priority=None,
147
+ arginfo=arginfo,
148
+ )
113
149
 
114
- In other words, [int, object] is more specific than [object, object] and
115
- less specific than [int, int], but it is neither less specific nor more
116
- specific than [object, int] (which means there is an ambiguity).
117
- """
118
150
 
119
- def __init__(self, key_error=KeyError):
120
- self.maps = {}
121
- self.priorities = {}
122
- self.empty = MISSING
123
- self.key_error = key_error
124
- self.transform = type
125
- self.all = {}
126
- self.errors = {}
127
-
128
- def _transform(self, obj):
129
- if isinstance(obj, GenericAlias):
130
- return type[obj]
131
- elif obj is typing.Any:
132
- return type[object]
133
- elif isinstance(obj, type):
134
- return type[obj]
151
+ def clsstring(cls):
152
+ if cls is object:
153
+ return "*"
154
+ elif isinstance(cls, tuple):
155
+ key, typ = cls
156
+ return f"{key}: {clsstring(typ)}"
157
+ elif is_type_of_type(cls):
158
+ arg = clsstring(cls.__args__[0])
159
+ return f"type[{arg}]"
160
+ elif hasattr(cls, "__origin__"):
161
+ if cls.__origin__ is typing.Union:
162
+ return "|".join(map(clsstring, cls.__args__))
135
163
  else:
136
- return type(obj)
137
-
138
- def register(self, obj_t_tup, nargs, handler):
139
- """Register a handler for a tuple of argument types.
140
-
141
- Arguments:
142
- obj_t_tup: A tuple of argument types.
143
- nargs: A (amin, amax, varargs) tuple where amin is the minimum
144
- number of arguments needed to match this tuple (if there are
145
- default arguments, it is possible that amin < len(obj_t_tup)),
146
- amax is the maximum number of arguments, and varargs is a
147
- boolean indicating whether there can be an arbitrary number
148
- of arguments.
149
- handler: A function to handle the tuple.
150
- """
151
- self.clear()
152
-
153
- if any(isinstance(x, GenericAlias) for x in obj_t_tup):
154
- self.transform = self._transform
155
-
156
- amin, amax, vararg, priority = nargs
157
-
158
- entry = (handler, amin, amax, vararg)
159
- if not obj_t_tup:
160
- self.empty = entry
161
-
162
- self.priorities[handler] = priority
163
-
164
- for i, cls in enumerate(obj_t_tup):
165
- if i not in self.maps:
166
- self.maps[i] = TypeMap()
167
- self.maps[i].register(cls, entry)
168
- if vararg:
169
- if -1 not in self.maps:
170
- self.maps[-1] = TypeMap()
171
- self.maps[-1].register(object, entry)
172
-
173
- def resolve(self, obj_t_tup):
174
- specificities = {}
175
- candidates = None
176
- nargs = len(obj_t_tup)
177
-
178
- for i, cls in enumerate(obj_t_tup):
179
- try:
180
- results = self.maps[i][cls]
181
- except KeyError:
182
- results = {}
183
-
184
- results = {
185
- handler: spc
186
- for (handler, min, max, va), spc in results.items()
187
- if min <= nargs <= (math.inf if va else max)
188
- }
189
-
190
- try:
191
- vararg_results = self.maps[-1][cls]
192
- except KeyError:
193
- vararg_results = {}
194
-
195
- vararg_results = {
196
- handler: spc
197
- for (handler, min, max, va), spc in vararg_results.items()
198
- if min <= nargs and i >= max
199
- }
200
-
201
- results.update(vararg_results)
202
-
203
- if candidates is None:
204
- candidates = set(results.keys())
205
- else:
206
- candidates &= results.keys()
207
- for c in candidates:
208
- specificities.setdefault(c, []).append(results[c])
164
+ return repr(cls)
165
+ elif hasattr(cls, "__name__"):
166
+ return cls.__name__
167
+ else:
168
+ return repr(cls)
209
169
 
210
- if not candidates:
211
- raise self.key_error(obj_t_tup, ())
212
170
 
213
- candidates = [
214
- (c, self.priorities.get(c, 0), tuple(specificities[c]))
215
- for c in candidates
216
- ]
171
+ def sigstring(types):
172
+ return ", ".join(map(clsstring, types))
217
173
 
218
- # The sort ensures that if candidate A dominates candidate B, A will
219
- # appear before B in the list. That's because it must dominate all
220
- # other possibilities on all arguments, so the sum of all specificities
221
- # has to be greater.
222
- # Note: priority is always more important than specificity
223
- candidates.sort(key=lambda cspc: (cspc[1], sum(cspc[2])), reverse=True)
224
-
225
- self.all[obj_t_tup] = {
226
- getattr(c[0], "__code__", None) for c in candidates
227
- }
228
-
229
- def _pull(candidates):
230
- if not candidates:
231
- return
232
- rval = [candidates[0]]
233
- c1, p1, spc1 = candidates[0]
234
- for c2, p2, spc2 in candidates[1:]:
235
- if p1 > p2 or (
236
- spc1 != spc2 and all(s1 >= s2 for s1, s2 in zip(spc1, spc2))
237
- ):
238
- # Candidate 1 dominates candidate 2
239
- continue
240
- else:
241
- # Candidate 1 does not dominate candidate 2, so we add it
242
- # to the list.
243
- rval.append((c2, p2, spc2))
244
- yield rval
245
- if len(rval) == 1:
246
- # Only groups of length 1 are correct and reachable, so we don't
247
- # care about the rest.
248
- yield from _pull(candidates[1:])
249
-
250
- results = list(_pull(candidates))
251
- parent = None
252
- for group in results:
253
- tup = obj_t_tup if parent is None else (parent, *obj_t_tup)
254
- if len(group) != 1:
255
- self.errors[tup] = self.key_error(obj_t_tup, group)
256
- break
257
- else:
258
- ((fn, _, _),) = group
259
- self[tup] = fn
260
- if hasattr(fn, "__code__"):
261
- parent = fn.__code__
262
- else:
263
- break
264
-
265
- return True
266
-
267
- def __missing__(self, obj_t_tup):
268
- if obj_t_tup and isinstance(obj_t_tup[0], CodeType):
269
- real_tup = obj_t_tup[1:]
270
- self[real_tup]
271
- if obj_t_tup[0] not in self.all[real_tup]:
272
- return self[real_tup]
273
- elif obj_t_tup in self.errors:
274
- raise self.errors[obj_t_tup]
275
- elif obj_t_tup in self: # pragma: no cover
276
- # PROBABLY not reachable
277
- return self[obj_t_tup]
278
- else:
279
- raise self.key_error(real_tup, ())
280
-
281
- if not obj_t_tup:
282
- if self.empty is MISSING:
283
- raise self.key_error(obj_t_tup, ())
284
- else:
285
- return self.empty[0]
286
174
 
287
- self.resolve(obj_t_tup)
288
- if obj_t_tup in self.errors:
289
- raise self.errors[obj_t_tup]
290
- else:
291
- return self[obj_t_tup]
175
+ class ArgumentAnalyzer:
176
+ def __init__(self):
177
+ self.name_to_positions = defaultdict(set)
178
+ self.position_to_names = defaultdict(set)
179
+ self.counts = defaultdict(lambda: [0, 0])
180
+ self.complex_transforms = set()
181
+ self.total = 0
182
+ self.is_method = None
183
+
184
+ def add(self, fn):
185
+ sig = Signature.extract(fn)
186
+ self.complex_transforms.update(
187
+ arg.canonical for arg in sig.arginfo if arg.is_complex
188
+ )
189
+ for arg in sig.arginfo:
190
+ if arg.position is not None:
191
+ self.position_to_names[arg.position].add(arg.name)
192
+ if arg.name is not None:
193
+ self.name_to_positions[arg.name].add(arg.canonical)
194
+
195
+ cnt = self.counts[arg.canonical]
196
+ cnt[0] += arg.required
197
+ cnt[1] += 1
198
+
199
+ self.total += 1
200
+
201
+ if self.is_method is None:
202
+ self.is_method = sig.is_method
203
+ elif self.is_method != sig.is_method: # pragma: no cover
204
+ raise TypeError(
205
+ "Some, but not all registered methods define `self`. It should be all or none."
206
+ )
292
207
 
208
+ def compile(self):
209
+ for name, pos in self.name_to_positions.items():
210
+ if len(pos) != 1:
211
+ if all(isinstance(p, int) for p in pos):
212
+ raise TypeError(
213
+ f"Argument '{name}' is declared in different positions by different methods. The same argument name should always be in the same position unless it is strictly positional."
214
+ )
215
+ else:
216
+ raise TypeError(
217
+ f"Argument '{name}' is declared in a positional and keyword setting by different methods. It should be either."
218
+ )
293
219
 
294
- def _fresh(t):
295
- """Returns a new subclass of type t.
220
+ p_to_n = [
221
+ list(names) for _, names in sorted(self.position_to_names.items())
222
+ ]
296
223
 
297
- Each Ovld corresponds to its own class, which allows for specialization of
298
- methods.
299
- """
300
- return type(t.__name__, (t,), {})
224
+ positional = list(
225
+ itertools.takewhile(
226
+ lambda names: len(names) == 1 and isinstance(names[0], str),
227
+ reversed(p_to_n),
228
+ )
229
+ )
230
+ positional.reverse()
231
+ strict_positional = p_to_n[: len(p_to_n) - len(positional)]
301
232
 
233
+ assert strict_positional + positional == p_to_n
302
234
 
303
- @keyword_decorator
304
- def _setattrs(fn, **kwargs):
305
- for k, v in kwargs.items():
306
- setattr(fn, k, v)
307
- return fn
235
+ strict_positional_required = [
236
+ f"ARG{pos + 1}"
237
+ for pos, _ in enumerate(strict_positional)
238
+ if self.counts[pos][0] == self.total
239
+ ]
240
+ strict_positional_optional = [
241
+ f"ARG{pos + 1}"
242
+ for pos, _ in enumerate(strict_positional)
243
+ if self.counts[pos][0] != self.total
244
+ ]
308
245
 
246
+ positional_required = [
247
+ names[0]
248
+ for pos, names in enumerate(positional)
249
+ if self.counts[pos + len(strict_positional)][0] == self.total
250
+ ]
251
+ positional_optional = [
252
+ names[0]
253
+ for pos, names in enumerate(positional)
254
+ if self.counts[pos + len(strict_positional)][0] != self.total
255
+ ]
309
256
 
310
- @keyword_decorator
311
- def _compile_first(fn, rename=None):
312
- def deco(self, *args, **kwargs):
313
- self.compile()
314
- method = getattr(self, fn.__name__)
315
- assert method is not deco
316
- return method(*args, **kwargs)
257
+ keywords = [
258
+ name
259
+ for _, (name,) in self.name_to_positions.items()
260
+ if not isinstance(name, int)
261
+ ]
262
+ keyword_required = [
263
+ name for name in keywords if self.counts[name][0] == self.total
264
+ ]
265
+ keyword_optional = [
266
+ name for name in keywords if self.counts[name][0] != self.total
267
+ ]
317
268
 
318
- def setalt(alt):
319
- deco._alt = alt
320
- return None
269
+ return (
270
+ strict_positional_required,
271
+ strict_positional_optional,
272
+ positional_required,
273
+ positional_optional,
274
+ keyword_required,
275
+ keyword_optional,
276
+ )
321
277
 
322
- deco.setalt = setalt
323
- deco._replace_by = fn
324
- deco._alt = None
325
- deco._rename = rename
326
- return deco
278
+ def lookup_for(self, key):
279
+ return (
280
+ "self.map.transform" if key in self.complex_transforms else "type"
281
+ )
327
282
 
328
283
 
329
284
  class _Ovld:
@@ -334,19 +289,11 @@ class _Ovld:
334
289
  function should annotate the same parameter.
335
290
 
336
291
  Arguments:
337
- dispatch: A function to use as the entry point. It must find the
338
- function to dispatch to and call it.
339
- postprocess: A function to call on the return value. It is not called
340
- after recursive calls.
341
292
  mixins: A list of Ovld instances that contribute functions to this
342
293
  Ovld.
343
- type_error: The error type to raise when no function can be found to
344
- dispatch to (default: TypeError).
345
294
  name: Optional name for the Ovld. If not provided, it will be
346
295
  gotten automatically from the first registered function or
347
296
  dispatch.
348
- mapper: Class implementing a mapping interface from a tuple of
349
- types to a handler (default: MultiTypeMap).
350
297
  linkback: Whether to keep a pointer in the parent mixins to this
351
298
  ovld so that updates can be propagated. (default: False)
352
299
  allow_replacement: Allow replacing a method by another with the
@@ -356,43 +303,27 @@ class _Ovld:
356
303
  def __init__(
357
304
  self,
358
305
  *,
359
- dispatch=None,
360
- postprocess=None,
361
- type_error=TypeError,
362
306
  mixins=[],
363
307
  bootstrap=None,
364
308
  name=None,
365
- mapper=MultiTypeMap,
366
309
  linkback=False,
367
310
  allow_replacement=True,
368
311
  ):
369
312
  """Initialize an Ovld."""
313
+ self.id = next(_current_id)
370
314
  self._compiled = False
371
- self._dispatch = dispatch
372
- self.maindoc = None
373
- self.mapper = mapper
374
315
  self.linkback = linkback
375
316
  self.children = []
376
- self.type_error = type_error
377
- self.postprocess = postprocess
378
317
  self.allow_replacement = allow_replacement
379
- self.bootstrap_class = OvldCall
380
- if self.postprocess:
381
- assert bootstrap is not False
382
- self.bootstrap = True
383
- elif isinstance(bootstrap, type):
384
- self.bootstrap_class = bootstrap
385
- self.bootstrap = True
386
- else:
387
- self.bootstrap = bootstrap
318
+ self.bootstrap = bootstrap
388
319
  self.name = name
320
+ self.shortname = name or f"__OVLD{self.id}"
389
321
  self.__name__ = name
390
322
  self._defns = {}
391
323
  self._locked = False
392
324
  self.mixins = []
393
325
  self.add_mixins(*mixins)
394
- self.ocls = _fresh(self.bootstrap_class)
395
- self._make_signature()
326
+ self.ocls = _fresh(OvldCall)
396
327
 
397
328
  @property
398
329
  def defns(self):
@@ -402,6 +333,42 @@ class _Ovld:
402
333
  defns.update(self._defns)
403
334
  return defns
404
335
 
336
+ @property
337
+ def __doc__(self):
338
+ if not self._compiled:
339
+ self.compile()
340
+
341
+ docs = [fn.__doc__ for fn in self.defns.values() if fn.__doc__]
342
+ if len(docs) == 1:
343
+ maindoc = docs[0]
344
+ else:
345
+ maindoc = f"Ovld with {len(self.defns)} methods."
346
+
347
+ doc = f"{maindoc}\n\n"
348
+ for fn in self.defns.values():
349
+ fndef = inspect.signature(fn)
350
+ fdoc = fn.__doc__
351
+ if not fdoc or fdoc == maindoc:
352
+ doc += f"{self.__name__}{fndef}\n\n"
353
+ else:
354
+ if not fdoc.strip(" ").endswith("\n"):
355
+ fdoc += "\n"
356
+ fdoc = textwrap.indent(fdoc, " " * 4)
357
+ doc += f"{self.__name__}{fndef}\n{fdoc}\n"
358
+ return doc
359
+
360
+ @property
361
+ def __signature__(self):
362
+ if not self._compiled:
363
+ self.compile()
364
+
365
+ sig = inspect.signature(self._dispatch)
366
+ if not self.argument_analysis.is_method:
367
+ sig = inspect.Signature(
368
+ [v for k, v in sig.parameters.items() if k != "self"]
369
+ )
370
+ return sig
371
+
405
372
  def lock(self):
406
373
  self._locked = True
407
374
 
@@ -414,40 +381,21 @@ class _Ovld:
414
381
  for mixin in mixins:
415
382
  if self.linkback:
416
383
  mixin.children.append(self)
417
- if mixin._defns:
418
- assert mixin.bootstrap is not None
419
- if self.bootstrap is None:
420
- self.bootstrap = mixin.bootstrap
421
- assert mixin.bootstrap is self.bootstrap
422
- if mixin._dispatch:
423
- self._dispatch = mixin._dispatch
384
+ if mixin._defns and self.bootstrap is None:
385
+ self.bootstrap = mixin.bootstrap
424
386
  self.mixins += mixins
425
387
 
426
- def _sig_string(self, type_tuple):
427
- def clsname(cls):
428
- if cls is object:
429
- return "*"
430
- elif is_type_of_type(cls):
431
- arg = clsname(cls.__args__[0])
432
- return f"type[{arg}]"
433
- elif hasattr(cls, "__name__"):
434
- return cls.__name__
435
- else:
436
- return repr(cls)
437
-
438
- return ", ".join(map(clsname, type_tuple))
439
-
440
388
  def _key_error(self, key, possibilities=None):
441
- typenames = self._sig_string(key)
389
+ typenames = sigstring(key)
442
390
  if not possibilities:
443
- return self.type_error(
391
+ return TypeError(
444
392
  f"No method in {self} for argument types [{typenames}]"
445
393
  )
446
394
  else:
447
395
  hlp = ""
448
396
  for p, prio, spc in possibilities:
449
397
  hlp += f"* {p.__name__} (priority: {prio}, specificity: {list(spc)})\n"
450
- return self.type_error(
398
+ return TypeError(
451
399
  f"Ambiguous resolution in {self} for"
452
400
  f" argument types [{typenames}]\n"
453
401
  f"Candidates are:\n{hlp}"
@@ -458,58 +406,15 @@ class _Ovld:
458
406
  """Rename this Ovld."""
459
407
  self.name = name
460
408
  self.__name__ = name
461
- self._make_signature()
462
-
463
- def _make_signature(self):
464
- """Make the __doc__ and __signature__."""
465
-
466
- def modelA(*args, **kwargs): # pragma: no cover
467
- pass
468
409
 
469
- def modelB(self, *args, **kwargs): # pragma: no cover
470
- pass
471
-
472
- seen = set()
473
- doc = (
474
- f"{self.maindoc}\n"
475
- if self.maindoc
476
- else f"Ovld with {len(self.defns)} methods.\n\n"
477
- )
478
- for key, fn in self.defns.items():
479
- if fn in seen:
480
- continue
481
- seen.add(fn)
482
- fndef = inspect.signature(fn)
483
- fdoc = fn.__doc__
484
- if not fdoc or fdoc == self.maindoc:
485
- doc += f" ``{self.__name__}{fndef}``\n\n"
486
- else:
487
- if not fdoc.strip(" ").endswith("\n"):
488
- fdoc += "\n"
489
- fdoc = textwrap.indent(fdoc, " " * 8)
490
- doc += f" ``{self.__name__}{fndef}``\n{fdoc}\n"
491
- self.__doc__ = doc
492
- if self.bootstrap:
493
- self.__signature__ = inspect.signature(modelB)
494
- else:
495
- self.__signature__ = inspect.signature(modelA)
496
-
497
- def _set_attrs_from(self, fn, dispatch=False):
410
+ def _set_attrs_from(self, fn):
498
411
  """Inherit relevant attributes from the function."""
499
412
  if self.bootstrap is None:
500
- sign = inspect.signature(fn)
501
- params = list(sign.parameters.values())
502
- if not dispatch:
503
- if params and params[0].name == "self":
504
- self.bootstrap = True
505
- else:
506
- self.bootstrap = False
413
+ self.bootstrap = arg0_is_self(fn)
507
414
 
508
415
  if self.name is None:
509
416
  self.name = f"{fn.__module__}.{fn.__qualname__}"
510
- self.maindoc = fn.__doc__
511
- if self.maindoc and not self.maindoc.strip(" ").endswith("\n"):
512
- self.maindoc += "\n"
417
+ self.shortname = fn.__name__
513
418
  self.__name__ = fn.__name__
514
419
  self.__qualname__ = fn.__qualname__
515
420
  self.__module__ = fn.__module__
@@ -533,57 +438,51 @@ class _Ovld:
533
438
  for mixin in self.mixins:
534
439
  if self not in mixin.children:
535
440
  mixin.lock()
536
- self._compiled = True
537
- self.map = self.mapper(key_error=self._key_error)
538
441
 
539
442
  cls = type(self)
540
443
  if self.name is None:
541
- self.name = self.__name__ = f"ovld{id(self)}"
444
+ self.name = self.__name__ = f"ovld{self.id}"
542
445
 
543
446
  name = self.__name__
447
+ self.map = MultiTypeMap(name=name, key_error=self._key_error)
544
448
 
545
449
  # Replace the appropriate functions by their final behavior
546
450
  for method in dir(cls):
547
451
  value = getattr(cls, method)
548
452
  repl = getattr(value, "_replace_by", None)
549
453
  if repl:
550
- if self.bootstrap and value._alt:
551
- repl = value._alt
552
454
  repl = self._maybe_rename(repl)
553
455
  setattr(cls, method, repl)
554
456
 
555
457
  target = self.ocls if self.bootstrap else cls
556
- if self._dispatch:
557
- target.__call__ = self._dispatch
558
458
 
559
- # Rename the dispatch
560
- target.__call__ = rename_function(target.__call__, f"{name}.dispatch")
459
+ anal = ArgumentAnalyzer()
460
+ for key, fn in list(self.defns.items()):
461
+ anal.add(fn)
462
+ self.argument_analysis = anal
463
+ dispatch = generate_dispatch(anal)
464
+ self._dispatch = dispatch
465
+ target.__call__ = rename_function(dispatch, f"{name}.dispatch")
561
466
 
562
467
  for key, fn in list(self.defns.items()):
563
468
  self.register_signature(key, fn)
564
469
 
565
- def dispatch(self, dispatch):
566
- """Set a dispatch function."""
567
- if self._dispatch is not None:
568
- raise TypeError(f"dispatch for {self} is already set")
569
- self._dispatch = dispatch
570
- self._set_attrs_from(dispatch, dispatch=True)
571
- return self
470
+ self._compiled = True
572
471
 
472
+ @_compile_first
573
473
  def resolve(self, *args):
574
474
  """Find the correct method to call for the given arguments."""
575
475
  return self.map[tuple(map(self.map.transform, args))]
576
476
 
577
- def register_signature(self, key, orig_fn):
477
+ def register_signature(self, sig, orig_fn):
578
478
  """Register a function for the given signature."""
579
- sig, min, max, vararg, priority = key
580
- fn = rename_function(
581
- orig_fn, f"{self.__name__}[{self._sig_string(sig)}]"
479
+ fn = adapt_function(
480
+ orig_fn, self, f"{self.__name__}[{sigstring(sig.types)}]"
582
481
  )
583
482
  # We just need to keep the Conformer pointer alive for jurigged
584
483
  # to find it, if jurigged is used with ovld
585
484
  fn._conformer = Conformer(self, orig_fn, fn)
586
- self.map.register(sig, (min, max, vararg, priority), fn)
485
+ self.map.register(sig, fn)
587
486
  return self
588
487
 
589
488
  def register(self, fn=None, priority=0):
@@ -595,60 +494,17 @@ class _Ovld:
595
494
  def _register(self, fn, priority):
596
495
  """Register a function."""
597
496
 
598
- def _normalize_type(t, force_tuple=False):
599
- origin = getattr(t, "__origin__", None)
600
- if UnionType and isinstance(t, UnionType):
601
- return _normalize_type(t.__args__)
602
- elif origin is type:
603
- return (t,) if force_tuple else t
604
- elif origin is typing.Union:
605
- return _normalize_type(t.__args__)
606
- elif origin is not None:
607
- raise TypeError(
608
- f"ovld does not accept generic types except type, Union or Optional, not {t}"
609
- )
610
- elif isinstance(t, tuple):
611
- return tuple(_normalize_type(t2) for t2 in t)
612
- elif force_tuple:
613
- return (t,)
614
- else:
615
- return t
616
-
617
497
  self._attempt_modify()
618
498
 
619
499
  self._set_attrs_from(fn)
620
500
 
621
- ann = fn.__annotations__
622
- argspec = inspect.getfullargspec(fn)
623
- argnames = argspec.args
624
- if self.bootstrap:
625
- if argnames[0] != "self":
626
- raise TypeError(
627
- "The first argument of the function must be named `self`"
628
- )
629
- argnames = argnames[1:]
630
-
631
- typelist = []
632
- for i, name in enumerate(argnames):
633
- t = ann.get(name, None)
634
- if t is None:
635
- typelist.append(object)
636
- else:
637
- typelist.append(t)
638
-
639
- max_pos = len(argnames)
640
- req_pos = max_pos - len(argspec.defaults or ())
641
-
642
- typelist_tups = tuple(
643
- _normalize_type(t, force_tuple=True) for t in typelist
644
- )
645
- for tl in itertools.product(*typelist_tups):
646
- sig = (tuple(tl), req_pos, max_pos, bool(argspec.varargs), priority)
647
- if not self.allow_replacement and sig in self._defns:
648
- raise TypeError(f"There is already a method for {tl}")
649
- self._defns[(*sig,)] = fn
501
+ sig = replace(Signature.extract(fn), priority=priority)
502
+ if not self.allow_replacement and sig in self._defns:
503
+ raise TypeError(
504
+ f"There is already a method for {sigstring(sig.types)}"
505
+ )
506
+ self._defns[sig] = fn
650
507
 
651
- self._make_signature()
652
508
  self._update()
653
509
  return self
654
510
 
@@ -664,13 +520,7 @@ class _Ovld:
664
520
  for child in self.children:
665
521
  child._update()
666
522
 
667
- def copy(
668
- self,
669
- dispatch=MISSING,
670
- postprocess=None,
671
- mixins=[],
672
- linkback=False,
673
- ):
523
+ def copy(self, mixins=[], linkback=False):
674
524
  """Create a copy of this Ovld.
675
525
 
676
526
  New functions can be registered to the copy without affecting the
@@ -678,13 +528,11 @@ class _Ovld:
678
528
  """
679
529
  return _fresh(_Ovld)(
680
530
  bootstrap=self.bootstrap,
681
- dispatch=self._dispatch if dispatch is MISSING else dispatch,
682
531
  mixins=[self, *mixins],
683
- postprocess=postprocess or self.postprocess,
684
532
  linkback=linkback,
685
533
  )
686
534
 
687
- def variant(self, fn=None, **kwargs):
535
+ def variant(self, fn=None, priority=0, **kwargs):
688
536
  """Decorator to create a variant of this Ovld.
689
537
 
690
538
  New functions can be registered to the variant without affecting the
@@ -692,73 +540,58 @@ class _Ovld:
692
540
  """
693
541
  ov = self.copy(**kwargs)
694
542
  if fn is None:
695
- return ov.register
543
+ return partial(ov.register, priority=priority)
696
544
  else:
697
- ov.register(fn)
545
+ ov.register(fn, priority=priority)
698
546
  return ov
699
547
 
700
- @_compile_first
701
- def get_map(self):
702
- return self.map
703
-
704
548
  @_compile_first
705
549
  def __get__(self, obj, cls):
706
550
  if obj is None:
707
551
  return self
708
- return self.ocls(
709
- map=self.map,
710
- bind_to=obj,
711
- super=self.mixins[0] if len(self.mixins) == 1 else None,
712
- )
552
+ key = self.shortname
553
+ rval = obj.__dict__.get(key, None)
554
+ if rval is None:
555
+ obj.__dict__[key] = rval = self.ocls(self, obj)
556
+ return rval
713
557
 
714
558
  @_compile_first
715
559
  def __getitem__(self, t):
716
560
  if not isinstance(t, tuple):
717
561
  t = (t,)
718
- assert not self.bootstrap
719
562
  return self.map[t]
720
563
 
721
564
  @_compile_first
722
565
  @_setattrs(rename="dispatch")
723
- def __call__(self, *args, **kwargs):
566
+ def __call__(self, *args): # pragma: no cover
724
567
  """Call the overloaded function.
725
568
 
726
- This version of __call__ is used when bootstrap is False.
727
-
728
- If bootstrap is False and a dispatch function is provided, it
729
- replaces this function.
569
+ This should be replaced by an auto-generated function.
730
570
  """
731
571
  key = tuple(map(self.map.transform, args))
732
572
  method = self.map[key]
733
- return method(*args, **kwargs)
573
+ return method(*args)
734
574
 
735
575
  @_compile_first
736
576
  @_setattrs(rename="next")
737
- def next(self, *args, **kwargs):
577
+ def next(self, *args):
738
578
  """Call the next matching method after the caller, in terms of priority or specificity."""
739
579
  fr = sys._getframe(1)
740
580
  key = (fr.f_code, *map(self.map.transform, args))
741
581
  method = self.map[key]
742
- return method(*args, **kwargs)
743
-
744
- @__call__.setalt
745
- @_setattrs(rename="entry")
746
- def __ovldcall__(self, *args, **kwargs):
747
- """Call the overloaded function.
748
-
749
- This version of __call__ is used when bootstrap is True. This function is
750
- only called once at the entry point: recursive calls will will be to
751
- OvldCall.__call__.
752
- """
753
- ovc = self.__get__(BOOTSTRAP, None)
754
- res = ovc(*args, **kwargs)
755
- if self.postprocess:
756
- res = self.postprocess(self, res)
757
- return res
582
+ return method(*args)
758
583
 
759
584
  def __repr__(self):
760
585
  return f"<Ovld {self.name or hex(id(self))}>"
761
586
 
587
+ @_compile_first
588
+ def display_methods(self):
589
+ self.map.display_methods()
590
+
591
+ @_compile_first
592
+ def display_resolution(self, *args, **kwargs):
593
+ self.map.display_resolution(*args, **kwargs)
594
+
762
595
 
763
596
  def is_ovld(x):
764
597
  """Return whether the argument is an ovld function/method."""
@@ -768,47 +601,43 @@ def is_ovld(x):
768
601
  class OvldCall:
769
602
  """Context for an Ovld call."""
770
603
 
771
- def __init__(self, map, bind_to, super=None):
604
+ def __init__(self, ovld, bind_to):
772
605
  """Initialize an OvldCall."""
773
- self.map = map
774
- self._parent = super
775
- self.obj = self if bind_to is BOOTSTRAP else bind_to
606
+ self.ovld = ovld
607
+ self.map = ovld.map
608
+ self.obj = bind_to
776
609
 
777
- def __getitem__(self, t):
778
- """Find the right method to call given a tuple of types."""
779
- if not isinstance(t, tuple):
780
- t = (t,)
781
- return self.map[t].__get__(self.obj)
610
+ @property
611
+ def __name__(self):
612
+ return self.ovld.__name__
613
+
614
+ @property
615
+ def __doc__(self):
616
+ return self.ovld.__doc__
617
+
618
+ @property
619
+ def __signature__(self):
620
+ return self.ovld.__signature__
782
621
 
783
- def next(self, *args, **kwargs):
622
+ def next(self, *args):
784
623
  """Call the next matching method after the caller, in terms of priority or specificity."""
785
624
  fr = sys._getframe(1)
786
625
  key = (fr.f_code, *map(self.map.transform, args))
787
626
  method = self.map[key]
788
- return method(self.obj, *args, **kwargs)
789
-
790
- def super(self, *args, **kwargs):
791
- """Use the parent ovld's method for this call."""
792
- pmap = self._parent.get_map()
793
- method = pmap[tuple(map(pmap.transform, args))]
794
- return method.__get__(self.obj)(*args, **kwargs)
627
+ return method(self.obj, *args)
795
628
 
796
629
  def resolve(self, *args):
797
630
  """Find the right method to call for the given arguments."""
798
- return self[tuple(map(self.map.transform, args))]
799
-
800
- def call(self, *args):
801
- """Call the right method for the given arguments."""
802
- return self[tuple(map(self.map.transform, args))](*args)
631
+ return self.map[tuple(map(self.map.transform, args))].__get__(self.obj)
803
632
 
804
- def __call__(self, *args, **kwargs):
633
+ def __call__(self, *args): # pragma: no cover
805
634
  """Call this overloaded function.
806
635
 
807
- If a dispatch function is provided, it replaces this function.
636
+ This should be replaced by an auto-generated function.
808
637
  """
809
638
  key = tuple(map(self.map.transform, args))
810
639
  method = self.map[key]
811
- return method(self.obj, *args, **kwargs)
640
+ return method(self.obj, *args)
812
641
 
813
642
 
814
643
  def Ovld(*args, **kwargs):
@@ -823,7 +652,7 @@ def extend_super(fn):
823
652
  plus this definition and others with the same name.
824
653
  """
825
654
  if not is_ovld(fn):
826
- fn = ovld(fn)
655
+ fn = ovld(fn, fresh=True)
827
656
  fn._extend_super = True
828
657
  return fn
829
658
 
@@ -835,29 +664,45 @@ class ovld_cls_dict(dict):
835
664
  """
836
665
 
837
666
  def __init__(self, bases):
838
- self._mock = type("MockSuper", bases, {})
667
+ self._bases = bases
839
668
 
840
669
  def __setitem__(self, attr, value):
670
+ prev = None
841
671
  if attr in self:
842
672
  prev = self[attr]
673
+ if inspect.isfunction(prev):
674
+ prev = ovld(prev, fresh=True)
675
+ elif not is_ovld(prev): # pragma: no cover
676
+ prev = None
843
677
  elif is_ovld(value) and getattr(value, "_extend_super", False):
844
- prev = getattr(self._mock, attr, None)
845
- if is_ovld(prev):
846
- prev = prev.copy()
678
+ mixins = []
679
+ for base in self._bases:
680
+ if (candidate := getattr(base, attr, None)) is not None:
681
+ if is_ovld(candidate) or inspect.isfunction(candidate):
682
+ mixins.append(candidate)
683
+ if mixins:
684
+ prev, *others = mixins
685
+ if is_ovld(prev):
686
+ prev = prev.copy()
687
+ else:
688
+ prev = ovld(prev, fresh=True)
689
+ for other in others:
690
+ if is_ovld(other):
691
+ prev.add_mixins(other)
692
+ else:
693
+ prev.register(other)
847
694
  else:
848
695
  prev = None
849
696
 
850
697
  if prev is not None:
851
- if inspect.isfunction(prev):
852
- prev = ovld(prev)
853
-
854
- if is_ovld(prev):
855
- if is_ovld(value):
856
- prev.add_mixins(value)
857
- value = prev
858
- elif inspect.isfunction(value):
859
- prev.register(value)
860
- value = prev
698
+ if is_ovld(value) and prev is not value:
699
+ if prev.name is None:
700
+ prev.rename(value.name)
701
+ prev.add_mixins(value)
702
+ value = prev
703
+ elif inspect.isfunction(value):
704
+ prev.register(value)
705
+ value = prev
861
706
 
862
707
  super().__setitem__(attr, value)
863
708
 
@@ -905,19 +750,31 @@ class OvldBase(metaclass=OvldMC):
905
750
 
906
751
 
907
752
  def _find_overload(fn, **kwargs):
908
- mod = __import__(fn.__module__, fromlist="_")
909
- dispatch = getattr(mod, fn.__qualname__, None)
753
+ fr = sys._getframe(1) # We typically expect to get to frame 3.
754
+ while fr and fn.__code__ not in fr.f_code.co_consts:
755
+ # We are basically searching for the function's code object in the stack.
756
+ # When a class/function A is nested in a class/function B, the former's
757
+ # code object is in the latter's co_consts. If ovld is used as a decorator,
758
+ # on A, then necessarily we are inside the execution of B, so B should be
759
+ # on the stack and we should be able to find A's code object in there.
760
+ fr = fr.f_back
761
+
762
+ if not fr:
763
+ raise UsageError("@ovld only works as a decorator.")
764
+
765
+ dispatch = fr.f_locals.get(fn.__name__, None)
766
+
910
767
  if dispatch is None:
911
768
  dispatch = _fresh(_Ovld)(**kwargs)
769
+ elif not is_ovld(dispatch): # pragma: no cover
770
+ raise TypeError("@ovld requires Ovld instance")
912
771
  elif kwargs: # pragma: no cover
913
772
  raise TypeError("Cannot configure an overload that already exists")
914
- if not is_ovld(dispatch): # pragma: no cover
915
- raise TypeError("@ovld requires Ovld instance")
916
773
  return dispatch
917
774
 
918
775
 
919
776
  @keyword_decorator
920
- def ovld(fn, priority=0, **kwargs):
777
+ def ovld(fn, priority=0, fresh=False, **kwargs):
921
778
  """Overload a function.
922
779
 
923
780
  Overloading is based on the function name.
@@ -931,71 +788,29 @@ def ovld(fn, priority=0, **kwargs):
931
788
 
932
789
  Arguments:
933
790
  fn: The function to register.
934
- dispatch: A function to use as the entry point. It must find the
935
- function to dispatch to and call it.
936
- postprocess: A function to call on the return value. It is not called
937
- after recursive calls.
791
+ priority: The priority of the function in the resolution order.
792
+ fresh: Whether to create a new ovld or try to reuse an existing one.
938
793
  mixins: A list of Ovld instances that contribute functions to this
939
794
  Ovld.
940
- type_error: The error type to raise when no function can be found to
941
- dispatch to (default: TypeError).
942
795
  name: Optional name for the Ovld. If not provided, it will be
943
796
  gotten automatically from the first registered function or
944
797
  dispatch.
945
- mapper: Class implementing a mapping interface from a tuple of
946
- types to a handler (default: MultiTypeMap).
947
798
  linkback: Whether to keep a pointer in the parent mixins to this
948
799
  ovld so that updates can be propagated. (default: False)
949
800
  """
950
- dispatch = _find_overload(fn, **kwargs)
801
+ if fresh:
802
+ dispatch = _fresh(_Ovld)(**kwargs)
803
+ else:
804
+ dispatch = _find_overload(fn, **kwargs)
951
805
  return dispatch.register(fn, priority=priority)
952
806
 
953
807
 
954
- @keyword_decorator
955
- def ovld_dispatch(dispatch, **kwargs):
956
- """Overload a function using the decorated function as a dispatcher.
957
-
958
- The dispatch is the entry point for the function and receives a `self`
959
- which is an Ovld or OvldCall instance, and the rest of the arguments.
960
- It may call `self.resolve(arg1, arg2, ...)` to get the right method to
961
- call.
962
-
963
- The decorator optionally takes keyword arguments, *only* on the first
964
- use.
965
-
966
- Arguments:
967
- dispatch: The function to use as the entry point. It must find the
968
- function to dispatch to and call it.
969
- postprocess: A function to call on the return value. It is not called
970
- after recursive calls.
971
- mixins: A list of Ovld instances that contribute functions to this
972
- Ovld.
973
- type_error: The error type to raise when no function can be found to
974
- dispatch to (default: TypeError).
975
- name: Optional name for the Ovld. If not provided, it will be
976
- gotten automatically from the first registered function or
977
- dispatch.
978
- mapper: Class implementing a mapping interface from a tuple of
979
- types to a handler (default: MultiTypeMap).
980
- linkback: Whether to keep a pointer in the parent mixins to this
981
- ovld so that updates can be propagated. (default: False)
982
- """
983
- ov = _find_overload(dispatch, **kwargs)
984
- return ov.dispatch(dispatch)
985
-
986
-
987
- ovld.dispatch = ovld_dispatch
988
-
989
-
990
808
  __all__ = [
991
- "MultiTypeMap",
992
809
  "Ovld",
993
810
  "OvldBase",
994
811
  "OvldCall",
995
812
  "OvldMC",
996
- "TypeMap",
997
813
  "extend_super",
998
814
  "is_ovld",
999
815
  "ovld",
1000
- "ovld_dispatch",
1001
816
  ]