When two overloads are equally specific
When two registered overloads both accept a call and neither is more
specific than the other, dispatch does not guess between them. It raises
AmbiguousMethodError, whose
message lists the overloads that tie:
>>> from bagof.dispatchers import dispatch, AmbiguousMethodError
>>> @dispatch
... def combine(a: float, b: object) -> str:
... return "left"
>>> @dispatch
... def combine(a: object, b: float) -> str:
... return "right"
>>> try:
... combine(1.0, 2.0)
... except AmbiguousMethodError as error:
... print([m.name for m in error.candidates])
['combine', 'combine']
Registering the two overloads raises nothing and warns about nothing. The
tie is reported by the call that meets it, and only a call whose
arguments match both overloads meets it. combine(1.0, "x"),
for instance, matches only the first overload and runs it.
To resolve a tie like this one, give one overload a higher priority, or
register a signature that is strictly more specific than both, such as
(float, float).
Finding ambiguities before a call
A tie that no call has met yet can still be found.
ambiguities lists each pair
of overloads that some call would match with nothing to choose between
them:
>>> for first, second in combine.ambiguities():
... print(first.signature, second.signature)
Signature(a: float, b: object) Signature(a: object, b: float)
The check takes every registered overload into account, as a call does.
Once an overload for (float, float) is registered, that overload wins
every call the pair has in common, so the pair is no longer reported,
whichever order the three overloads were registered in:
>>> @dispatch
... def combine(a: float, b: float) -> str:
... return "both"
>>> combine.ambiguities()
[]
>>> combine(1.0, 2.0)
'both'
ambiguities is meant to be run in a test suite, where
assert not combine.ambiguities() fails as soon as a new
overload leaves some call without a most specific overload. The check
looks only at calls written the way the overloads' own parameters are,
so it does not find a tie that is reachable only by spreading arguments
into *args. It can also report a pair that no call actually trips
over, when several narrower overloads together win every call the pair
has in common but none of them wins all of those calls alone.
Inspecting the candidates of a call
An ambiguity error names the tied overloads only after a call has
failed. The same information can be read from a function beforehand,
without calling anything.
bestcandidates takes the
arguments of a call and returns the overloads that survive every step of
selection, including the tie-breaks on priority and on the argument's
own MRO. candidates returns
every overload that accepts the call, ordered from the most specific to
the least specific, with the overloads that bestcandidates returns at
the front.
The function below has two overloads that tie when both arguments are polygons, and a third overload that accepts any two shapes:
>>> class Shape: pass
>>> class Polygon(Shape): pass
>>> class Square(Polygon): pass
>>> @dispatch
... def overlap(a: Polygon, b: Shape) -> str:
... return "polygon first"
>>> @dispatch
... def overlap(a: Shape, b: Polygon) -> str:
... return "polygon second"
>>> @dispatch
... def overlap(a: Shape, b: Shape) -> str:
... return "any shapes"
For two squares, bestcandidates returns both of the tied overloads.
These are exactly the overloads that the AmbiguousMethodError raised by
overlap(Square(), Square()) would list. candidates returns
the same two overloads, followed by the less specific overload for any
two shapes:
>>> for method in overlap.bestcandidates(Square(), Square()):
... print(method.signature)
Signature(a: Polygon, b: Shape)
Signature(a: Shape, b: Polygon)
>>> for method in overlap.candidates(Square(), Square()):
... print(method.signature)
Signature(a: Polygon, b: Shape)
Signature(a: Shape, b: Polygon)
Signature(a: Shape, b: Shape)
When a single overload is more specific than every other one,
bestcandidates returns only that overload, and it is the overload that
the call runs:
>>> for method in overlap.bestcandidates(Square(), Shape()):
... print(method.signature)
Signature(a: Polygon, b: Shape)
>>> overlap(Square(), Shape())
'polygon first'
Neither method raises an error: a call that no overload accepts gives an
empty tuple from both of them. Each of the two also has an iterator form,
itercandidates and iterbestcandidates. For a call described by type
hints rather than by values, as in
resolve,
resolve_candidates and
resolve_bestcandidates
answer the same two questions.