[BuildLibrary / OverlayLibrary] further simplifications

This commit is contained in:
Jan Petykiewicz 2026-06-19 23:11:23 -07:00
commit 13d75dc079
3 changed files with 105 additions and 133 deletions

View file

@ -15,10 +15,11 @@ Classes include:
library. Generated with `ILibraryView.abstract_view()`.
"""
from typing import Self, TYPE_CHECKING, Any, cast, TypeAlias, Protocol, Literal
from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable
from collections.abc import Container, Iterator, Mapping, MutableMapping, Sequence, Callable
import logging
import re
import copy
from functools import wraps
from pprint import pformat
from collections import defaultdict
from abc import ABCMeta, abstractmethod
@ -87,15 +88,11 @@ class CellProvenance:
any. Imported source cells leave this as `None`.
build_chain: Declared-cell dependency chain that was active when the
cell was emitted.
renamed_from: Original requested name when the final name differs.
source_name: Original on-source name for imported cells.
"""
requested_name: str
kind: Literal['declared', 'helper', 'source']
owner_declared_name: str | None
build_chain: tuple[str, ...]
renamed_from: str | None = None
source_name: str | None = None
@dataclass(frozen=True)
@ -152,6 +149,47 @@ def _rename_patterns(lib: 'ILibraryView', name: str) -> str:
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',
) -> tuple[dict[str, str], 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()
rename_map: dict[str, str] = {}
for name in source_order:
visible = name
renamed = False
if rename_when == 'always':
assert rename_theirs is not None
visible = rename_theirs(target, name)
renamed = True
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)
renamed = True
if visible in existing_names or visible in visible_names:
if not renamed:
raise LibraryError(f'Conflicting name while adding source: {name!r}')
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
if visible != name:
rename_map[name] = visible
source_to_visible[name] = visible
visible_names.add(visible)
return source_to_visible, rename_map
class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
"""
Interface for a read-only library.
@ -1453,23 +1491,6 @@ class Library(ILibrary):
return tree, pat
class _CellFactory:
"""
Adapter that turns a plain pattern factory into a deferred recipe factory.
Calling the wrapper captures arguments and returns a `_BuildRecipe`
instead of executing the function immediately.
"""
def __init__(self, func: Callable[..., 'Pattern']) -> None:
self.func = func
self.__name__ = getattr(func, '__name__', type(self).__name__)
self.__doc__ = getattr(func, '__doc__')
def __call__(self, *args: Any, **kwargs: Any) -> '_BuildRecipe':
return _BuildRecipe(func=self.func, args=args, kwargs=kwargs)
@dataclass
class _BuildRecipe:
""" Captured deferred call to a pattern factory. """
@ -1483,25 +1504,17 @@ class _BuildRecipe:
return self
@dataclass(frozen=True)
class _SourceDeclaration:
"""
Imported source-backed names registered with a `BuildLibrary`.
The declaration stores visible-name remapping. Underlying source cells stay
lazy until a build session materializes or copies them through.
"""
library: ILibraryView
source_to_visible: Mapping[str, str]
def cell(func: Callable[..., 'Pattern']) -> _CellFactory:
def cell(func: Callable[..., 'Pattern']) -> Callable[..., _BuildRecipe]:
"""
Wrap a plain pattern factory so calls return deferred build recipes.
Use as either `cell(fn)(...)` or `@cell`.
"""
return _CellFactory(func)
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> _BuildRecipe:
return _BuildRecipe(func=func, args=args, kwargs=kwargs)
return wrapper
class BuildCellsView:
@ -1555,7 +1568,7 @@ class BuildLibrary(ILibrary):
self.cells = BuildCellsView(self)
self._frozen = False
self._declarations: dict[str, 'Pattern | _BuildRecipe'] = {}
self._sources: list[_SourceDeclaration] = []
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
self._names: set[str] = set()
self._order: list[str] = []
@ -1694,8 +1707,8 @@ class BuildLibrary(ILibrary):
source_index: int | None = None
source_name: str | None = None
for idx, spec in enumerate(self._sources):
for candidate_source, candidate_visible in spec.source_to_visible.items():
for idx, (_source, source_to_visible) in enumerate(self._sources):
for candidate_source, candidate_visible in source_to_visible.items():
if candidate_visible == old_name:
source_index = idx
source_name = candidate_source
@ -1708,16 +1721,13 @@ class BuildLibrary(ILibrary):
'cells may be renamed on a BuildLibrary.'
)
spec = self._sources[source_index]
source_to_visible = dict(spec.source_to_visible)
source_library, source_to_visible = self._sources[source_index]
source_to_visible = dict(source_to_visible)
assert source_name is not None
source_to_visible[source_name] = new_name
self._sources[source_index] = replace(
spec,
source_to_visible = source_to_visible,
)
self._sources[source_index] = (source_library, source_to_visible)
self._names.remove(old_name)
self._names.add(new_name)
self._order[self._order.index(old_name)] = new_name
@ -1761,48 +1771,23 @@ class BuildLibrary(ILibrary):
Mapping of `{source_name: visible_name}` for imported names that
were renamed while being added.
"""
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"')
if self._active_session() is not None:
raise BuildError('BuildLibrary.add_source() is only available while authoring, not during validate() or build().')
self._assert_editable()
view = source if isinstance(source, ILibraryView) else LibraryView(source)
source_order = tuple(view.source_order())
source_to_visible, rename_map = _plan_source_names(
self,
source_order,
self._names,
rename_theirs = rename_theirs,
rename_when = rename_when,
)
source_to_visible: dict[str, str] = {}
visible_names: set[str] = set()
rename_map: dict[str, str] = {}
new_names: list[str] = []
for name in source_order:
visible = name
renamed = False
if rename_when == 'always':
visible = cast('Callable[[ILibraryView, str], str]', rename_theirs)(self, name)
renamed = True
elif visible in self._names or visible in visible_names:
if rename_theirs is None:
raise LibraryError(f'Conflicting name while adding source: {name!r}')
visible = rename_theirs(self, name)
renamed = True
if visible in self._names or visible in visible_names:
if not renamed:
raise LibraryError(f'Conflicting name while adding source: {name!r}')
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
if visible != name:
rename_map[name] = visible
source_to_visible[name] = visible
visible_names.add(visible)
new_names.append(visible)
self._sources.append(_SourceDeclaration(
library = view,
source_to_visible = dict(source_to_visible),
))
for visible in new_names:
self._sources.append((view, dict(source_to_visible)))
for source_name in source_order:
visible = source_to_visible[source_name]
self._names.add(visible)
self._order.append(visible)
return rename_map
@ -1902,9 +1887,9 @@ class _BuildSessionLibrary(ILibrary):
self._install_sources()
def _install_sources(self) -> None:
for spec in self._builder._sources:
source_order = spec.library.source_order()
expected_names = set(spec.source_to_visible)
for source_library, source_to_visible in self._builder._sources:
source_order = source_library.source_order()
expected_names = set(source_to_visible)
actual_names = set(source_order)
if actual_names != expected_names:
added_names = sorted(actual_names - expected_names)
@ -1920,24 +1905,22 @@ class _BuildSessionLibrary(ILibrary):
'Do not structurally mutate source libraries between add_source() and build()/validate().'
)
def rename_source(_lib: ILibraryView, name: str, *, mapping: Mapping[str, str] = spec.source_to_visible) -> str:
def rename_source(_lib: ILibraryView, name: str, *, mapping: Mapping[str, str] = source_to_visible) -> str:
return mapping[name]
self._overlay.add_source(
spec.library,
source_library,
rename_theirs = rename_source,
rename_when = 'always',
)
for source_name in source_order:
visible_name = spec.source_to_visible[source_name]
visible_name = source_to_visible[source_name]
self._provenance[visible_name] = CellProvenance(
requested_name = source_name,
kind = 'source',
owner_declared_name = None,
build_chain = (),
renamed_from = source_name if visible_name != source_name else None,
source_name = source_name,
)
def __iter__(self) -> Iterator[str]:
@ -1947,7 +1930,7 @@ class _BuildSessionLibrary(ILibrary):
return len(self._names)
def __contains__(self, key: object) -> bool:
return key in self._names or key in self._overlay
return key in self._names
def _touch_name(self, key: str) -> None:
if key not in self._names:
@ -1998,11 +1981,7 @@ class _BuildSessionLibrary(ILibrary):
self._order[idx] = new_name
provenance = self._provenance.pop(old_name)
requested_name = provenance.requested_name
self._provenance[new_name] = replace(
provenance,
renamed_from = requested_name if new_name != requested_name else None,
)
self._provenance[new_name] = provenance
return self
def __getitem__(self, key: str) -> 'Pattern':
@ -2038,7 +2017,6 @@ class _BuildSessionLibrary(ILibrary):
kind = kind,
owner_declared_name = current if kind == 'helper' else key,
build_chain = tuple(self._declared_stack),
renamed_from = None,
)
def __delitem__(self, key: str) -> None:
@ -2072,7 +2050,6 @@ class _BuildSessionLibrary(ILibrary):
self._provenance[new_name] = replace(
self._provenance[new_name],
requested_name = old_name,
renamed_from = old_name,
owner_declared_name = current if current is not None else self._provenance[new_name].owner_declared_name,
)
return rename_map
@ -2085,14 +2062,12 @@ class _BuildSessionLibrary(ILibrary):
kind: Literal['declared', 'helper'],
owner_declared_name: str | None,
build_chain: tuple[str, ...],
renamed_from: str | None,
) -> None:
self._provenance[name] = CellProvenance(
requested_name = requested_name,
kind = kind,
owner_declared_name = owner_declared_name,
build_chain = build_chain,
renamed_from = renamed_from,
)
def _wrap_error(self, name: str, exc: Exception) -> BuildError: