124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
"""Shared types and helpers for library implementations."""
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Literal, Protocol, TypeAlias
|
|
from collections.abc import Callable, Container, Mapping, MutableMapping, Sequence
|
|
|
|
from ..error import LibraryError
|
|
|
|
if TYPE_CHECKING:
|
|
import numpy
|
|
from numpy.typing import NDArray
|
|
|
|
from ..pattern import Pattern
|
|
from .base import ILibraryView
|
|
|
|
|
|
class visitor_function_t(Protocol):
|
|
""" Signature for `Library.dfs()` visitor functions. """
|
|
def __call__(
|
|
self,
|
|
pattern: Pattern,
|
|
hierarchy: tuple[str | None, ...],
|
|
memo: dict,
|
|
transform: NDArray[numpy.float64] | Literal[False],
|
|
) -> Pattern:
|
|
...
|
|
|
|
|
|
TreeView: TypeAlias = Mapping[str, 'Pattern']
|
|
""" A name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
|
|
|
Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
|
""" A mutable name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
|
|
|
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
|
|
""" How helpers should handle refs whose targets are not present in the library. """
|
|
SINGLE_USE_PREFIX = '_'
|
|
"""
|
|
Names starting with this prefix are assumed to refer to single-use patterns,
|
|
which may be renamed automatically by `ILibrary.add()` (via
|
|
`rename_theirs=_rename_patterns()` )
|
|
"""
|
|
# TODO what are the consequences of making '_' special? maybe we can make this decision everywhere?
|
|
|
|
|
|
def _rename_patterns(lib: ILibraryView, name: str) -> str:
|
|
"""
|
|
The default `rename_theirs` function for `ILibrary.add`.
|
|
|
|
Treats names starting with `SINGLE_USE_PREFIX` (default: one underscore) as
|
|
"one-offs" for which name conflicts should be automatically resolved.
|
|
Conflicts are resolved by calling `lib.get_name(SINGLE_USE_PREFIX + stem)`
|
|
where `stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0]`.
|
|
Names lacking the prefix are directly returned (not renamed).
|
|
|
|
Args:
|
|
lib: The library into which `name` is to be added (but is presumed to conflict)
|
|
name: The original name, to be modified
|
|
|
|
Returns:
|
|
The new name, not guaranteed to be conflict-free!
|
|
"""
|
|
if not name.startswith(SINGLE_USE_PREFIX):
|
|
return name
|
|
|
|
stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0]
|
|
return lib.get_name(SINGLE_USE_PREFIX + stem)
|
|
|
|
|
|
def _plan_source_names(
|
|
target: ILibraryView,
|
|
source_order: Sequence[str],
|
|
existing_names: Container[str],
|
|
*,
|
|
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
|
|
rename_when: Literal['conflict', 'always'] = 'conflict',
|
|
) -> dict[str, str]:
|
|
if rename_when not in ('conflict', 'always'):
|
|
raise ValueError(f'Unknown source rename mode: {rename_when!r}')
|
|
if rename_when == 'always' and rename_theirs is None:
|
|
raise TypeError('rename_theirs is required when rename_when="always"')
|
|
|
|
source_to_visible: dict[str, str] = {}
|
|
visible_names: set[str] = set()
|
|
|
|
for name in source_order:
|
|
visible = name
|
|
if rename_when == 'always':
|
|
assert rename_theirs is not None
|
|
visible = rename_theirs(target, name)
|
|
elif visible in existing_names or visible in visible_names:
|
|
if rename_theirs is None:
|
|
raise LibraryError(f'Conflicting name while adding source: {name!r}')
|
|
visible = rename_theirs(target, name)
|
|
if visible in existing_names or visible in visible_names:
|
|
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
|
|
source_to_visible[name] = visible
|
|
visible_names.add(visible)
|
|
|
|
return source_to_visible
|
|
|
|
|
|
def _source_rename_map(source_to_visible: Mapping[str, str]) -> dict[str, str]:
|
|
return {
|
|
source_name: visible_name
|
|
for source_name, visible_name in source_to_visible.items()
|
|
if source_name != visible_name
|
|
}
|
|
|
|
def b64suffix(ii: int) -> str:
|
|
"""
|
|
Turn an integer into a base64-equivalent suffix.
|
|
|
|
This could be done with base64.b64encode, but this way is faster for many small `ii`.
|
|
"""
|
|
def i2a(nn: int) -> str:
|
|
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$?'[nn]
|
|
|
|
parts = ['$', i2a(ii % 64)]
|
|
ii >>= 6
|
|
while ii:
|
|
parts.append(i2a(ii % 64))
|
|
ii >>= 6
|
|
return ''.join(parts)
|