Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3720c77

Browse files
committed
Issue #18244: Adopt C3-based linearization in functools.singledispatch for improved ABC support
1 parent 04926ae commit 3720c77

3 files changed

Lines changed: 289 additions & 64 deletions

File tree

Lib/functools.py

Lines changed: 135 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -365,46 +365,138 @@ def cache_clear():
365365
### singledispatch() - single-dispatch generic function decorator
366366
################################################################################
367367

368-
def _compose_mro(cls, haystack):
369-
"""Calculates the MRO for a given class `cls`, including relevant abstract
370-
base classes from `haystack`.
368+
def _c3_merge(sequences):
369+
"""Merges MROs in *sequences* to a single MRO using the C3 algorithm.
370+
371+
Adapted from http://www.python.org/download/releases/2.3/mro/.
371372
372373
"""
373-
bases = set(cls.__mro__)
374-
mro = list(cls.__mro__)
375-
for needle in haystack:
376-
if (needle in bases or not hasattr(needle, '__mro__')
377-
or not issubclass(cls, needle)):
378-
continue # either present in the __mro__ already or unrelated
379-
for index, base in enumerate(mro):
380-
if not issubclass(base, needle):
374+
result = []
375+
while True:
376+
sequences = [s for s in sequences if s] # purge empty sequences
377+
if not sequences:
378+
return result
379+
for s1 in sequences: # find merge candidates among seq heads
380+
candidate = s1[0]
381+
for s2 in sequences:
382+
if candidate in s2[1:]:
383+
candidate = None
384+
break # reject the current head, it appears later
385+
else:
381386
break
382-
if base in bases and not issubclass(needle, base):
383-
# Conflict resolution: put classes present in __mro__ and their
384-
# subclasses first. See test_mro_conflicts() in test_functools.py
385-
# for examples.
386-
index += 1
387-
mro.insert(index, needle)
388-
return mro
387+
if not candidate:
388+
raise RuntimeError("Inconsistent hierarchy")
389+
result.append(candidate)
390+
# remove the chosen candidate
391+
for seq in sequences:
392+
if seq[0] == candidate:
393+
del seq[0]
394+
395+
def _c3_mro(cls, abcs=None):
396+
"""Computes the method resolution order using extended C3 linearization.
397+
398+
If no *abcs* are given, the algorithm works exactly like the built-in C3
399+
linearization used for method resolution.
400+
401+
If given, *abcs* is a list of abstract base classes that should be inserted
402+
into the resulting MRO. Unrelated ABCs are ignored and don't end up in the
403+
result. The algorithm inserts ABCs where their functionality is introduced,
404+
i.e. issubclass(cls, abc) returns True for the class itself but returns
405+
False for all its direct base classes. Implicit ABCs for a given class
406+
(either registered or inferred from the presence of a special method like
407+
__len__) are inserted directly after the last ABC explicitly listed in the
408+
MRO of said class. If two implicit ABCs end up next to each other in the
409+
resulting MRO, their ordering depends on the order of types in *abcs*.
410+
411+
"""
412+
for i, base in enumerate(reversed(cls.__bases__)):
413+
if hasattr(base, '__abstractmethods__'):
414+
boundary = len(cls.__bases__) - i
415+
break # Bases up to the last explicit ABC are considered first.
416+
else:
417+
boundary = 0
418+
abcs = list(abcs) if abcs else []
419+
explicit_bases = list(cls.__bases__[:boundary])
420+
abstract_bases = []
421+
other_bases = list(cls.__bases__[boundary:])
422+
for base in abcs:
423+
if issubclass(cls, base) and not any(
424+
issubclass(b, base) for b in cls.__bases__
425+
):
426+
# If *cls* is the class that introduces behaviour described by
427+
# an ABC *base*, insert said ABC to its MRO.
428+
abstract_bases.append(base)
429+
for base in abstract_bases:
430+
abcs.remove(base)
431+
explicit_c3_mros = [_c3_mro(base, abcs=abcs) for base in explicit_bases]
432+
abstract_c3_mros = [_c3_mro(base, abcs=abcs) for base in abstract_bases]
433+
other_c3_mros = [_c3_mro(base, abcs=abcs) for base in other_bases]
434+
return _c3_merge(
435+
[[cls]] +
436+
explicit_c3_mros + abstract_c3_mros + other_c3_mros +
437+
[explicit_bases] + [abstract_bases] + [other_bases]
438+
)
439+
440+
def _compose_mro(cls, types):
441+
"""Calculates the method resolution order for a given class *cls*.
442+
443+
Includes relevant abstract base classes (with their respective bases) from
444+
the *types* iterable. Uses a modified C3 linearization algorithm.
445+
446+
"""
447+
bases = set(cls.__mro__)
448+
# Remove entries which are already present in the __mro__ or unrelated.
449+
def is_related(typ):
450+
return (typ not in bases and hasattr(typ, '__mro__')
451+
and issubclass(cls, typ))
452+
types = [n for n in types if is_related(n)]
453+
# Remove entries which are strict bases of other entries (they will end up
454+
# in the MRO anyway.
455+
def is_strict_base(typ):
456+
for other in types:
457+
if typ != other and typ in other.__mro__:
458+
return True
459+
return False
460+
types = [n for n in types if not is_strict_base(n)]
461+
# Subclasses of the ABCs in *types* which are also implemented by
462+
# *cls* can be used to stabilize ABC ordering.
463+
type_set = set(types)
464+
mro = []
465+
for typ in types:
466+
found = []
467+
for sub in typ.__subclasses__():
468+
if sub not in bases and issubclass(cls, sub):
469+
found.append([s for s in sub.__mro__ if s in type_set])
470+
if not found:
471+
mro.append(typ)
472+
continue
473+
# Favor subclasses with the biggest number of useful bases
474+
found.sort(key=len, reverse=True)
475+
for sub in found:
476+
for subcls in sub:
477+
if subcls not in mro:
478+
mro.append(subcls)
479+
return _c3_mro(cls, abcs=mro)
389480

390481
def _find_impl(cls, registry):
391-
"""Returns the best matching implementation for the given class `cls` in
392-
`registry`. Where there is no registered implementation for a specific
393-
type, its method resolution order is used to find a more generic
394-
implementation.
482+
"""Returns the best matching implementation from *registry* for type *cls*.
483+
484+
Where there is no registered implementation for a specific type, its method
485+
resolution order is used to find a more generic implementation.
395486
396-
Note: if `registry` does not contain an implementation for the base
397-
`object` type, this function may return None.
487+
Note: if *registry* does not contain an implementation for the base
488+
*object* type, this function may return None.
398489
399490
"""
400491
mro = _compose_mro(cls, registry.keys())
401492
match = None
402493
for t in mro:
403494
if match is not None:
404-
# If `match` is an ABC but there is another unrelated, equally
405-
# matching ABC. Refuse the temptation to guess.
406-
if (t in registry and not issubclass(match, t)
407-
and match not in cls.__mro__):
495+
# If *match* is an implicit ABC but there is another unrelated,
496+
# equally matching implicit ABC, refuse the temptation to guess.
497+
if (t in registry and t not in cls.__mro__
498+
and match not in cls.__mro__
499+
and not issubclass(match, t)):
408500
raise RuntimeError("Ambiguous dispatch: {} or {}".format(
409501
match, t))
410502
break
@@ -418,19 +510,19 @@ def singledispatch(func):
418510
Transforms a function into a generic function, which can have different
419511
behaviours depending upon the type of its first argument. The decorated
420512
function acts as the default implementation, and additional
421-
implementations can be registered using the 'register()' attribute of
422-
the generic function.
513+
implementations can be registered using the register() attribute of the
514+
generic function.
423515
424516
"""
425517
registry = {}
426518
dispatch_cache = WeakKeyDictionary()
427519
cache_token = None
428520

429-
def dispatch(typ):
430-
"""generic_func.dispatch(type) -> <function implementation>
521+
def dispatch(cls):
522+
"""generic_func.dispatch(cls) -> <function implementation>
431523
432524
Runs the dispatch algorithm to return the best available implementation
433-
for the given `type` registered on `generic_func`.
525+
for the given *cls* registered on *generic_func*.
434526
435527
"""
436528
nonlocal cache_token
@@ -440,26 +532,26 @@ def dispatch(typ):
440532
dispatch_cache.clear()
441533
cache_token = current_token
442534
try:
443-
impl = dispatch_cache[typ]
535+
impl = dispatch_cache[cls]
444536
except KeyError:
445537
try:
446-
impl = registry[typ]
538+
impl = registry[cls]
447539
except KeyError:
448-
impl = _find_impl(typ, registry)
449-
dispatch_cache[typ] = impl
540+
impl = _find_impl(cls, registry)
541+
dispatch_cache[cls] = impl
450542
return impl
451543

452-
def register(typ, func=None):
453-
"""generic_func.register(type, func) -> func
544+
def register(cls, func=None):
545+
"""generic_func.register(cls, func) -> func
454546
455-
Registers a new implementation for the given `type` on a `generic_func`.
547+
Registers a new implementation for the given *cls* on a *generic_func*.
456548
457549
"""
458550
nonlocal cache_token
459551
if func is None:
460-
return lambda f: register(typ, f)
461-
registry[typ] = func
462-
if cache_token is None and hasattr(typ, '__abstractmethods__'):
552+
return lambda f: register(cls, f)
553+
registry[cls] = func
554+
if cache_token is None and hasattr(cls, '__abstractmethods__'):
463555
cache_token = get_cache_token()
464556
dispatch_cache.clear()
465557
return func

0 commit comments

Comments
 (0)