119 lines
3.5 KiB
Python
119 lines
3.5 KiB
Python
# This module contains a single function, `c3linear_merge`.
|
|
# The function is generic enough to be in its own module.
|
|
#
|
|
# - Copyright (c) 2019 Vitaly R. Samigullin
|
|
# - Adapted from https://github.com/pilosus/c3linear
|
|
# - Adapted from https://github.com/tristanlatr/pydocspec
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import deque
|
|
from itertools import islice
|
|
from typing import TypeVar
|
|
|
|
_T = TypeVar("_T")
|
|
|
|
|
|
class _Dependency(deque[_T]):
|
|
"""A class representing a (doubly-ended) queue of items."""
|
|
|
|
@property
|
|
def head(self) -> _T | None:
|
|
"""Head of the dependency."""
|
|
try:
|
|
return self[0]
|
|
except IndexError:
|
|
return None
|
|
|
|
@property
|
|
def tail(self) -> islice:
|
|
"""Tail of the dependency.
|
|
|
|
The `islice` object is sufficient for iteration or testing membership (`in`).
|
|
"""
|
|
try:
|
|
return islice(self, 1, self.__len__())
|
|
except (ValueError, IndexError):
|
|
return islice([], 0, 0)
|
|
|
|
|
|
class _DependencyList:
|
|
"""A class representing a list of linearizations (dependencies).
|
|
|
|
The last element of DependencyList is a list of parents.
|
|
It's needed to the merge process preserves the local
|
|
precedence order of direct parent classes.
|
|
"""
|
|
|
|
def __init__(self, *lists: list[_T | None]) -> None:
|
|
"""Initialize the list.
|
|
|
|
Parameters:
|
|
*lists: Lists of items.
|
|
"""
|
|
self._lists = [_Dependency(lst) for lst in lists]
|
|
|
|
def __contains__(self, item: _T) -> bool:
|
|
"""Return True if any linearization's tail contains an item."""
|
|
return any(item in lst.tail for lst in self._lists)
|
|
|
|
def __len__(self) -> int:
|
|
size = len(self._lists)
|
|
return (size - 1) if size else 0
|
|
|
|
def __repr__(self) -> str:
|
|
return self._lists.__repr__()
|
|
|
|
@property
|
|
def heads(self) -> list[_T | None]:
|
|
"""Return the heads."""
|
|
return [lst.head for lst in self._lists] # type: ignore[misc]
|
|
|
|
@property
|
|
def tails(self) -> _DependencyList:
|
|
"""Return self so that `__contains__` could be called."""
|
|
return self
|
|
|
|
@property
|
|
def exhausted(self) -> bool:
|
|
"""True if all elements of the lists are exhausted."""
|
|
return all(len(x) == 0 for x in self._lists)
|
|
|
|
def remove(self, item: _T | None) -> None:
|
|
"""Remove an item from the lists.
|
|
|
|
Once an item removed from heads, the leftmost elements of the tails
|
|
get promoted to become the new heads.
|
|
"""
|
|
for i in self._lists:
|
|
if i and i.head == item:
|
|
i.popleft()
|
|
|
|
|
|
def c3linear_merge(*lists: list[_T]) -> list[_T]:
|
|
"""Merge lists of lists in the order defined by the C3Linear algorithm.
|
|
|
|
Parameters:
|
|
*lists: Lists of items.
|
|
|
|
Returns:
|
|
The merged list of items.
|
|
"""
|
|
result: list[_T] = []
|
|
linearizations = _DependencyList(*lists) # type: ignore[arg-type]
|
|
|
|
while True:
|
|
if linearizations.exhausted:
|
|
return result
|
|
|
|
for head in linearizations.heads:
|
|
if head and (head not in linearizations.tails):
|
|
result.append(head) # type: ignore[arg-type]
|
|
linearizations.remove(head)
|
|
|
|
# Once candidate is found, continue iteration
|
|
# from the first element of the list.
|
|
break
|
|
else:
|
|
# Loop never broke, no linearization could possibly be found.
|
|
raise ValueError("Cannot compute C3 linearization")
|