Commit 3720c77e authored by Łukasz Langa's avatar Łukasz Langa

Issue #18244: Adopt C3-based linearization in functools.singledispatch for improved ABC support

parent 04926aeb
...@@ -365,46 +365,138 @@ def lru_cache(maxsize=128, typed=False): ...@@ -365,46 +365,138 @@ def lru_cache(maxsize=128, typed=False):
### singledispatch() - single-dispatch generic function decorator ### singledispatch() - single-dispatch generic function decorator
################################################################################ ################################################################################
def _compose_mro(cls, haystack): def _c3_merge(sequences):
"""Calculates the MRO for a given class `cls`, including relevant abstract """Merges MROs in *sequences* to a single MRO using the C3 algorithm.
base classes from `haystack`.
Adapted from http://www.python.org/download/releases/2.3/mro/.
""" """
bases = set(cls.__mro__) result = []
mro = list(cls.__mro__) while True:
for needle in haystack: sequences = [s for s in sequences if s] # purge empty sequences
if (needle in bases or not hasattr(needle, '__mro__') if not sequences:
or not issubclass(cls, needle)): return result
continue # either present in the __mro__ already or unrelated for s1 in sequences: # find merge candidates among seq heads
for index, base in enumerate(mro): candidate = s1[0]
if not issubclass(base, needle): for s2 in sequences:
if candidate in s2[1:]:
candidate = None
break # reject the current head, it appears later
else:
break break
if base in bases and not issubclass(needle, base): if not candidate:
# Conflict resolution: put classes present in __mro__ and their raise RuntimeError("Inconsistent hierarchy")
# subclasses first. See test_mro_conflicts() in test_functools.py result.append(candidate)
# for examples. # remove the chosen candidate
index += 1 for seq in sequences:
mro.insert(index, needle) if seq[0] == candidate:
return mro del seq[0]
def _c3_mro(cls, abcs=None):
"""Computes the method resolution order using extended C3 linearization.
If no *abcs* are given, the algorithm works exactly like the built-in C3
linearization used for method resolution.
If given, *abcs* is a list of abstract base classes that should be inserted
into the resulting MRO. Unrelated ABCs are ignored and don't end up in the
result. The algorithm inserts ABCs where their functionality is introduced,
i.e. issubclass(cls, abc) returns True for the class itself but returns
False for all its direct base classes. Implicit ABCs for a given class
(either registered or inferred from the presence of a special method like
__len__) are inserted directly after the last ABC explicitly listed in the
MRO of said class. If two implicit ABCs end up next to each other in the
resulting MRO, their ordering depends on the order of types in *abcs*.
"""
for i, base in enumerate(reversed(cls.__bases__)):
if hasattr(base, '__abstractmethods__'):
boundary = len(cls.__bases__) - i
break # Bases up to the last explicit ABC are considered first.
else:
boundary = 0
abcs = list(abcs) if abcs else []
explicit_bases = list(cls.__bases__[:boundary])
abstract_bases = []
other_bases = list(cls.__bases__[boundary:])
for base in abcs:
if issubclass(cls, base) and not any(
issubclass(b, base) for b in cls.__bases__
):
# If *cls* is the class that introduces behaviour described by
# an ABC *base*, insert said ABC to its MRO.
abstract_bases.append(base)
for base in abstract_bases:
abcs.remove(base)
explicit_c3_mros = [_c3_mro(base, abcs=abcs) for base in explicit_bases]
abstract_c3_mros = [_c3_mro(base, abcs=abcs) for base in abstract_bases]
other_c3_mros = [_c3_mro(base, abcs=abcs) for base in other_bases]
return _c3_merge(
[[cls]] +
explicit_c3_mros + abstract_c3_mros + other_c3_mros +
[explicit_bases] + [abstract_bases] + [other_bases]
)
def _compose_mro(cls, types):
"""Calculates the method resolution order for a given class *cls*.
Includes relevant abstract base classes (with their respective bases) from
the *types* iterable. Uses a modified C3 linearization algorithm.
"""
bases = set(cls.__mro__)
# Remove entries which are already present in the __mro__ or unrelated.
def is_related(typ):
return (typ not in bases and hasattr(typ, '__mro__')
and issubclass(cls, typ))
types = [n for n in types if is_related(n)]
# Remove entries which are strict bases of other entries (they will end up
# in the MRO anyway.
def is_strict_base(typ):
for other in types:
if typ != other and typ in other.__mro__:
return True
return False
types = [n for n in types if not is_strict_base(n)]
# Subclasses of the ABCs in *types* which are also implemented by
# *cls* can be used to stabilize ABC ordering.
type_set = set(types)
mro = []
for typ in types:
found = []
for sub in typ.__subclasses__():
if sub not in bases and issubclass(cls, sub):
found.append([s for s in sub.__mro__ if s in type_set])
if not found:
mro.append(typ)
continue
# Favor subclasses with the biggest number of useful bases
found.sort(key=len, reverse=True)
for sub in found:
for subcls in sub:
if subcls not in mro:
mro.append(subcls)
return _c3_mro(cls, abcs=mro)
def _find_impl(cls, registry): def _find_impl(cls, registry):
"""Returns the best matching implementation for the given class `cls` in """Returns the best matching implementation from *registry* for type *cls*.
`registry`. Where there is no registered implementation for a specific
type, its method resolution order is used to find a more generic Where there is no registered implementation for a specific type, its method
implementation. resolution order is used to find a more generic implementation.
Note: if `registry` does not contain an implementation for the base Note: if *registry* does not contain an implementation for the base
`object` type, this function may return None. *object* type, this function may return None.
""" """
mro = _compose_mro(cls, registry.keys()) mro = _compose_mro(cls, registry.keys())
match = None match = None
for t in mro: for t in mro:
if match is not None: if match is not None:
# If `match` is an ABC but there is another unrelated, equally # If *match* is an implicit ABC but there is another unrelated,
# matching ABC. Refuse the temptation to guess. # equally matching implicit ABC, refuse the temptation to guess.
if (t in registry and not issubclass(match, t) if (t in registry and t not in cls.__mro__
and match not in cls.__mro__): and match not in cls.__mro__
and not issubclass(match, t)):
raise RuntimeError("Ambiguous dispatch: {} or {}".format( raise RuntimeError("Ambiguous dispatch: {} or {}".format(
match, t)) match, t))
break break
...@@ -418,19 +510,19 @@ def singledispatch(func): ...@@ -418,19 +510,19 @@ def singledispatch(func):
Transforms a function into a generic function, which can have different Transforms a function into a generic function, which can have different
behaviours depending upon the type of its first argument. The decorated behaviours depending upon the type of its first argument. The decorated
function acts as the default implementation, and additional function acts as the default implementation, and additional
implementations can be registered using the 'register()' attribute of implementations can be registered using the register() attribute of the
the generic function. generic function.
""" """
registry = {} registry = {}
dispatch_cache = WeakKeyDictionary() dispatch_cache = WeakKeyDictionary()
cache_token = None cache_token = None
def dispatch(typ): def dispatch(cls):
"""generic_func.dispatch(type) -> <function implementation> """generic_func.dispatch(cls) -> <function implementation>
Runs the dispatch algorithm to return the best available implementation Runs the dispatch algorithm to return the best available implementation
for the given `type` registered on `generic_func`. for the given *cls* registered on *generic_func*.
""" """
nonlocal cache_token nonlocal cache_token
...@@ -440,26 +532,26 @@ def singledispatch(func): ...@@ -440,26 +532,26 @@ def singledispatch(func):
dispatch_cache.clear() dispatch_cache.clear()
cache_token = current_token cache_token = current_token
try: try:
impl = dispatch_cache[typ] impl = dispatch_cache[cls]
except KeyError: except KeyError:
try: try:
impl = registry[typ] impl = registry[cls]
except KeyError: except KeyError:
impl = _find_impl(typ, registry) impl = _find_impl(cls, registry)
dispatch_cache[typ] = impl dispatch_cache[cls] = impl
return impl return impl
def register(typ, func=None): def register(cls, func=None):
"""generic_func.register(type, func) -> func """generic_func.register(cls, func) -> func
Registers a new implementation for the given `type` on a `generic_func`. Registers a new implementation for the given *cls* on a *generic_func*.
""" """
nonlocal cache_token nonlocal cache_token
if func is None: if func is None:
return lambda f: register(typ, f) return lambda f: register(cls, f)
registry[typ] = func registry[cls] = func
if cache_token is None and hasattr(typ, '__abstractmethods__'): if cache_token is None and hasattr(cls, '__abstractmethods__'):
cache_token = get_cache_token() cache_token = get_cache_token()
dispatch_cache.clear() dispatch_cache.clear()
return func return func
......
...@@ -929,22 +929,55 @@ class TestSingleDispatch(unittest.TestCase): ...@@ -929,22 +929,55 @@ class TestSingleDispatch(unittest.TestCase):
self.assertEqual(g(rnd), ("Number got rounded",)) self.assertEqual(g(rnd), ("Number got rounded",))
def test_compose_mro(self): def test_compose_mro(self):
# None of the examples in this test depend on haystack ordering.
c = collections c = collections
mro = functools._compose_mro mro = functools._compose_mro
bases = [c.Sequence, c.MutableMapping, c.Mapping, c.Set] bases = [c.Sequence, c.MutableMapping, c.Mapping, c.Set]
for haystack in permutations(bases): for haystack in permutations(bases):
m = mro(dict, haystack) m = mro(dict, haystack)
self.assertEqual(m, [dict, c.MutableMapping, c.Mapping, object]) self.assertEqual(m, [dict, c.MutableMapping, c.Mapping, c.Sized,
c.Iterable, c.Container, object])
bases = [c.Container, c.Mapping, c.MutableMapping, c.OrderedDict] bases = [c.Container, c.Mapping, c.MutableMapping, c.OrderedDict]
for haystack in permutations(bases): for haystack in permutations(bases):
m = mro(c.ChainMap, haystack) m = mro(c.ChainMap, haystack)
self.assertEqual(m, [c.ChainMap, c.MutableMapping, c.Mapping, self.assertEqual(m, [c.ChainMap, c.MutableMapping, c.Mapping,
c.Sized, c.Iterable, c.Container, object]) c.Sized, c.Iterable, c.Container, object])
# Note: The MRO order below depends on haystack ordering.
m = mro(c.defaultdict, [c.Sized, c.Container, str]) # If there's a generic function with implementations registered for
self.assertEqual(m, [c.defaultdict, dict, c.Container, c.Sized, object]) # both Sized and Container, passing a defaultdict to it results in an
m = mro(c.defaultdict, [c.Container, c.Sized, str]) # ambiguous dispatch which will cause a RuntimeError (see
self.assertEqual(m, [c.defaultdict, dict, c.Sized, c.Container, object]) # test_mro_conflicts).
bases = [c.Container, c.Sized, str]
for haystack in permutations(bases):
m = mro(c.defaultdict, [c.Sized, c.Container, str])
self.assertEqual(m, [c.defaultdict, dict, c.Sized, c.Container,
object])
# MutableSequence below is registered directly on D. In other words, it
# preceeds MutableMapping which means single dispatch will always
# choose MutableSequence here.
class D(c.defaultdict):
pass
c.MutableSequence.register(D)
bases = [c.MutableSequence, c.MutableMapping]
for haystack in permutations(bases):
m = mro(D, bases)
self.assertEqual(m, [D, c.MutableSequence, c.Sequence,
c.defaultdict, dict, c.MutableMapping,
c.Mapping, c.Sized, c.Iterable, c.Container,
object])
# Container and Callable are registered on different base classes and
# a generic function supporting both should always pick the Callable
# implementation if a C instance is passed.
class C(c.defaultdict):
def __call__(self):
pass
bases = [c.Sized, c.Callable, c.Container, c.Mapping]
for haystack in permutations(bases):
m = mro(C, haystack)
self.assertEqual(m, [C, c.Callable, c.defaultdict, dict, c.Mapping,
c.Sized, c.Iterable, c.Container, object])
def test_register_abc(self): def test_register_abc(self):
c = collections c = collections
...@@ -1040,17 +1073,37 @@ class TestSingleDispatch(unittest.TestCase): ...@@ -1040,17 +1073,37 @@ class TestSingleDispatch(unittest.TestCase):
self.assertEqual(g(f), "frozen-set") self.assertEqual(g(f), "frozen-set")
self.assertEqual(g(t), "tuple") self.assertEqual(g(t), "tuple")
def test_mro_conflicts(self): def test_c3_abc(self):
c = collections c = collections
mro = functools._c3_mro
class A(object):
pass
class B(A):
def __len__(self):
return 0 # implies Sized
@c.Container.register
class C(object):
pass
class D(object):
pass # unrelated
class X(D, C, B):
def __call__(self):
pass # implies Callable
expected = [X, c.Callable, D, C, c.Container, B, c.Sized, A, object]
for abcs in permutations([c.Sized, c.Callable, c.Container]):
self.assertEqual(mro(X, abcs=abcs), expected)
# unrelated ABCs don't appear in the resulting MRO
many_abcs = [c.Mapping, c.Sized, c.Callable, c.Container, c.Iterable]
self.assertEqual(mro(X, abcs=many_abcs), expected)
def test_mro_conflicts(self):
c = collections
@functools.singledispatch @functools.singledispatch
def g(arg): def g(arg):
return "base" return "base"
class O(c.Sized): class O(c.Sized):
def __len__(self): def __len__(self):
return 0 return 0
o = O() o = O()
self.assertEqual(g(o), "base") self.assertEqual(g(o), "base")
g.register(c.Iterable, lambda arg: "iterable") g.register(c.Iterable, lambda arg: "iterable")
...@@ -1062,35 +1115,114 @@ class TestSingleDispatch(unittest.TestCase): ...@@ -1062,35 +1115,114 @@ class TestSingleDispatch(unittest.TestCase):
self.assertEqual(g(o), "sized") # because it's explicitly in __mro__ self.assertEqual(g(o), "sized") # because it's explicitly in __mro__
c.Container.register(O) c.Container.register(O)
self.assertEqual(g(o), "sized") # see above: Sized is in __mro__ self.assertEqual(g(o), "sized") # see above: Sized is in __mro__
c.Set.register(O)
self.assertEqual(g(o), "set") # because c.Set is a subclass of
# c.Sized and c.Container
class P: class P:
pass pass
p = P() p = P()
self.assertEqual(g(p), "base") self.assertEqual(g(p), "base")
c.Iterable.register(P) c.Iterable.register(P)
self.assertEqual(g(p), "iterable") self.assertEqual(g(p), "iterable")
c.Container.register(P) c.Container.register(P)
with self.assertRaises(RuntimeError) as re: with self.assertRaises(RuntimeError) as re_one:
g(p) g(p)
self.assertEqual( self.assertIn(
str(re), str(re_one.exception),
("Ambiguous dispatch: <class 'collections.abc.Container'> " (("Ambiguous dispatch: <class 'collections.abc.Container'> "
"or <class 'collections.abc.Iterable'>"), "or <class 'collections.abc.Iterable'>"),
) ("Ambiguous dispatch: <class 'collections.abc.Iterable'> "
"or <class 'collections.abc.Container'>")),
)
class Q(c.Sized): class Q(c.Sized):
def __len__(self): def __len__(self):
return 0 return 0
q = Q() q = Q()
self.assertEqual(g(q), "sized") self.assertEqual(g(q), "sized")
c.Iterable.register(Q) c.Iterable.register(Q)
self.assertEqual(g(q), "sized") # because it's explicitly in __mro__ self.assertEqual(g(q), "sized") # because it's explicitly in __mro__
c.Set.register(Q) c.Set.register(Q)
self.assertEqual(g(q), "set") # because c.Set is a subclass of self.assertEqual(g(q), "set") # because c.Set is a subclass of
# c.Sized which is explicitly in # c.Sized and c.Iterable
# __mro__ @functools.singledispatch
def h(arg):
return "base"
@h.register(c.Sized)
def _(arg):
return "sized"
@h.register(c.Container)
def _(arg):
return "container"
# Even though Sized and Container are explicit bases of MutableMapping,
# this ABC is implicitly registered on defaultdict which makes all of
# MutableMapping's bases implicit as well from defaultdict's
# perspective.
with self.assertRaises(RuntimeError) as re_two:
h(c.defaultdict(lambda: 0))
self.assertIn(
str(re_two.exception),
(("Ambiguous dispatch: <class 'collections.abc.Container'> "
"or <class 'collections.abc.Sized'>"),
("Ambiguous dispatch: <class 'collections.abc.Sized'> "
"or <class 'collections.abc.Container'>")),
)
class R(c.defaultdict):
pass
c.MutableSequence.register(R)
@functools.singledispatch
def i(arg):
return "base"
@i.register(c.MutableMapping)
def _(arg):
return "mapping"
@i.register(c.MutableSequence)
def _(arg):
return "sequence"
r = R()
self.assertEqual(i(r), "sequence")
class S:
pass
class T(S, c.Sized):
def __len__(self):
return 0
t = T()
self.assertEqual(h(t), "sized")
c.Container.register(T)
self.assertEqual(h(t), "sized") # because it's explicitly in the MRO
class U:
def __len__(self):
return 0
u = U()
self.assertEqual(h(u), "sized") # implicit Sized subclass inferred
# from the existence of __len__()
c.Container.register(U)
# There is no preference for registered versus inferred ABCs.
with self.assertRaises(RuntimeError) as re_three:
h(u)
self.assertIn(
str(re_three.exception),
(("Ambiguous dispatch: <class 'collections.abc.Container'> "
"or <class 'collections.abc.Sized'>"),
("Ambiguous dispatch: <class 'collections.abc.Sized'> "
"or <class 'collections.abc.Container'>")),
)
class V(c.Sized, S):
def __len__(self):
return 0
@functools.singledispatch
def j(arg):
return "base"
@j.register(S)
def _(arg):
return "s"
@j.register(c.Container)
def _(arg):
return "container"
v = V()
self.assertEqual(j(v), "s")
c.Container.register(V)
self.assertEqual(j(v), "container") # because it ends up right after
# Sized in the MRO
def test_cache_invalidation(self): def test_cache_invalidation(self):
from collections import UserDict from collections import UserDict
......
...@@ -195,6 +195,7 @@ Brett Cannon ...@@ -195,6 +195,7 @@ Brett Cannon
Mike Carlton Mike Carlton
Pierre Carrier Pierre Carrier
Terry Carroll Terry Carroll
Edward Catmur
Lorenzo M. Catucci Lorenzo M. Catucci
Donn Cave Donn Cave
Charles Cazabon Charles Cazabon
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment