masque/masque/library/utils.py

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)