[BuildLibrary / OverlayLibrary] further simplifications

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

View file

@ -32,7 +32,7 @@ from numpy.typing import NDArray
from . import gdsii from . import gdsii
from .utils import tmpfile from .utils import tmpfile
from ..error import LibraryError from ..error import LibraryError
from ..library import ILibrary, ILibraryView, LibraryView, dangling_mode_t from ..library import ILibrary, ILibraryView, LibraryView, _plan_source_names, dangling_mode_t
from ..pattern import Pattern, map_targets from ..pattern import Pattern, map_targets
from ..utils import apply_transforms from ..utils import apply_transforms
from ..utils.ports2data import data_to_ports from ..utils.ports2data import data_to_ports
@ -315,38 +315,18 @@ class OverlayLibrary(ILibrary):
If `'always'`, every imported source name is passed through If `'always'`, every imported source name is passed through
`rename_theirs`. `rename_theirs`.
""" """
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"')
view = _coerce_library_view(source) view = _coerce_library_view(source)
source_order = list(view.source_order()) source_order = list(view.source_order())
child_graph = view.child_graph(dangling='include') child_graph = view.child_graph(dangling='include')
source_to_visible: dict[str, str] = {} source_to_visible, rename_map = _plan_source_names(
visible_to_source: dict[str, str] = {} self,
rename_map: dict[str, str] = {} source_order,
self._entries,
for name in source_order: rename_theirs = rename_theirs,
visible = name rename_when = rename_when,
renamed = False )
if rename_when == 'always': visible_to_source = {visible: source_name for source_name, visible in source_to_visible.items()}
visible = cast('Callable[[ILibraryView, str], str]', rename_theirs)(self, name)
renamed = True
elif visible in self._entries or visible in visible_to_source:
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._entries or visible in visible_to_source:
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_to_source[visible] = name
layer = _SourceLayer( layer = _SourceLayer(
library=view, library=view,

View file

@ -15,10 +15,11 @@ Classes include:
library. Generated with `ILibraryView.abstract_view()`. library. Generated with `ILibraryView.abstract_view()`.
""" """
from typing import Self, TYPE_CHECKING, Any, cast, TypeAlias, Protocol, Literal 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 logging
import re import re
import copy import copy
from functools import wraps
from pprint import pformat from pprint import pformat
from collections import defaultdict from collections import defaultdict
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
@ -87,15 +88,11 @@ class CellProvenance:
any. Imported source cells leave this as `None`. any. Imported source cells leave this as `None`.
build_chain: Declared-cell dependency chain that was active when the build_chain: Declared-cell dependency chain that was active when the
cell was emitted. 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 requested_name: str
kind: Literal['declared', 'helper', 'source'] kind: Literal['declared', 'helper', 'source']
owner_declared_name: str | None owner_declared_name: str | None
build_chain: tuple[str, ...] build_chain: tuple[str, ...]
renamed_from: str | None = None
source_name: str | None = None
@dataclass(frozen=True) @dataclass(frozen=True)
@ -152,6 +149,47 @@ def _rename_patterns(lib: 'ILibraryView', name: str) -> str:
return lib.get_name(SINGLE_USE_PREFIX + stem) 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): class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
""" """
Interface for a read-only library. Interface for a read-only library.
@ -1453,23 +1491,6 @@ class Library(ILibrary):
return tree, pat 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 @dataclass
class _BuildRecipe: class _BuildRecipe:
""" Captured deferred call to a pattern factory. """ """ Captured deferred call to a pattern factory. """
@ -1483,25 +1504,17 @@ class _BuildRecipe:
return self return self
@dataclass(frozen=True) def cell(func: Callable[..., 'Pattern']) -> Callable[..., _BuildRecipe]:
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:
""" """
Wrap a plain pattern factory so calls return deferred build recipes. Wrap a plain pattern factory so calls return deferred build recipes.
Use as either `cell(fn)(...)` or `@cell`. 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: class BuildCellsView:
@ -1555,7 +1568,7 @@ class BuildLibrary(ILibrary):
self.cells = BuildCellsView(self) self.cells = BuildCellsView(self)
self._frozen = False self._frozen = False
self._declarations: dict[str, 'Pattern | _BuildRecipe'] = {} self._declarations: dict[str, 'Pattern | _BuildRecipe'] = {}
self._sources: list[_SourceDeclaration] = [] self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
self._names: set[str] = set() self._names: set[str] = set()
self._order: list[str] = [] self._order: list[str] = []
@ -1694,8 +1707,8 @@ class BuildLibrary(ILibrary):
source_index: int | None = None source_index: int | None = None
source_name: str | None = None source_name: str | None = None
for idx, spec in enumerate(self._sources): for idx, (_source, source_to_visible) in enumerate(self._sources):
for candidate_source, candidate_visible in spec.source_to_visible.items(): for candidate_source, candidate_visible in source_to_visible.items():
if candidate_visible == old_name: if candidate_visible == old_name:
source_index = idx source_index = idx
source_name = candidate_source source_name = candidate_source
@ -1708,16 +1721,13 @@ class BuildLibrary(ILibrary):
'cells may be renamed on a BuildLibrary.' 'cells may be renamed on a BuildLibrary.'
) )
spec = self._sources[source_index] source_library, source_to_visible = self._sources[source_index]
source_to_visible = dict(spec.source_to_visible) source_to_visible = dict(source_to_visible)
assert source_name is not None assert source_name is not None
source_to_visible[source_name] = new_name source_to_visible[source_name] = new_name
self._sources[source_index] = replace( self._sources[source_index] = (source_library, source_to_visible)
spec,
source_to_visible = source_to_visible,
)
self._names.remove(old_name) self._names.remove(old_name)
self._names.add(new_name) self._names.add(new_name)
self._order[self._order.index(old_name)] = 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 Mapping of `{source_name: visible_name}` for imported names that
were renamed while being added. 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: if self._active_session() is not None:
raise BuildError('BuildLibrary.add_source() is only available while authoring, not during validate() or build().') raise BuildError('BuildLibrary.add_source() is only available while authoring, not during validate() or build().')
self._assert_editable() self._assert_editable()
view = source if isinstance(source, ILibraryView) else LibraryView(source) view = source if isinstance(source, ILibraryView) else LibraryView(source)
source_order = tuple(view.source_order()) 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] = {} self._sources.append((view, dict(source_to_visible)))
visible_names: set[str] = set() for source_name in source_order:
rename_map: dict[str, str] = {} visible = source_to_visible[source_name]
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._names.add(visible) self._names.add(visible)
self._order.append(visible) self._order.append(visible)
return rename_map return rename_map
@ -1902,9 +1887,9 @@ class _BuildSessionLibrary(ILibrary):
self._install_sources() self._install_sources()
def _install_sources(self) -> None: def _install_sources(self) -> None:
for spec in self._builder._sources: for source_library, source_to_visible in self._builder._sources:
source_order = spec.library.source_order() source_order = source_library.source_order()
expected_names = set(spec.source_to_visible) expected_names = set(source_to_visible)
actual_names = set(source_order) actual_names = set(source_order)
if actual_names != expected_names: if actual_names != expected_names:
added_names = sorted(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().' '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] return mapping[name]
self._overlay.add_source( self._overlay.add_source(
spec.library, source_library,
rename_theirs = rename_source, rename_theirs = rename_source,
rename_when = 'always', rename_when = 'always',
) )
for source_name in source_order: 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( self._provenance[visible_name] = CellProvenance(
requested_name = source_name, requested_name = source_name,
kind = 'source', kind = 'source',
owner_declared_name = None, owner_declared_name = None,
build_chain = (), build_chain = (),
renamed_from = source_name if visible_name != source_name else None,
source_name = source_name,
) )
def __iter__(self) -> Iterator[str]: def __iter__(self) -> Iterator[str]:
@ -1947,7 +1930,7 @@ class _BuildSessionLibrary(ILibrary):
return len(self._names) return len(self._names)
def __contains__(self, key: object) -> bool: 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: def _touch_name(self, key: str) -> None:
if key not in self._names: if key not in self._names:
@ -1998,11 +1981,7 @@ class _BuildSessionLibrary(ILibrary):
self._order[idx] = new_name self._order[idx] = new_name
provenance = self._provenance.pop(old_name) provenance = self._provenance.pop(old_name)
requested_name = provenance.requested_name self._provenance[new_name] = provenance
self._provenance[new_name] = replace(
provenance,
renamed_from = requested_name if new_name != requested_name else None,
)
return self return self
def __getitem__(self, key: str) -> 'Pattern': def __getitem__(self, key: str) -> 'Pattern':
@ -2038,7 +2017,6 @@ class _BuildSessionLibrary(ILibrary):
kind = kind, kind = kind,
owner_declared_name = current if kind == 'helper' else key, owner_declared_name = current if kind == 'helper' else key,
build_chain = tuple(self._declared_stack), build_chain = tuple(self._declared_stack),
renamed_from = None,
) )
def __delitem__(self, key: str) -> None: def __delitem__(self, key: str) -> None:
@ -2072,7 +2050,6 @@ class _BuildSessionLibrary(ILibrary):
self._provenance[new_name] = replace( self._provenance[new_name] = replace(
self._provenance[new_name], self._provenance[new_name],
requested_name = old_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, owner_declared_name = current if current is not None else self._provenance[new_name].owner_declared_name,
) )
return rename_map return rename_map
@ -2085,14 +2062,12 @@ class _BuildSessionLibrary(ILibrary):
kind: Literal['declared', 'helper'], kind: Literal['declared', 'helper'],
owner_declared_name: str | None, owner_declared_name: str | None,
build_chain: tuple[str, ...], build_chain: tuple[str, ...],
renamed_from: str | None,
) -> None: ) -> None:
self._provenance[name] = CellProvenance( self._provenance[name] = CellProvenance(
requested_name = requested_name, requested_name = requested_name,
kind = kind, kind = kind,
owner_declared_name = owner_declared_name, owner_declared_name = owner_declared_name,
build_chain = build_chain, build_chain = build_chain,
renamed_from = renamed_from,
) )
def _wrap_error(self, name: str, exc: Exception) -> BuildError: def _wrap_error(self, name: str, exc: Exception) -> BuildError:

View file

@ -50,13 +50,13 @@ def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None
_built, report = builder.build() _built, report = builder.build()
helpers = [ helpers = [
prov for prov in report.provenance.values() (name, prov) for name, prov in report.provenance.items()
if prov.owner_declared_name == "top" and prov.kind == "helper" if prov.owner_declared_name == "top" and prov.kind == "helper"
] ]
assert "top" in _owned_by(report, "top") assert "top" in _owned_by(report, "top")
assert len(helpers) == 2 assert len(helpers) == 2
assert any(prov.renamed_from == "_helper" for prov in helpers) assert any(name != prov.requested_name for name, prov in helpers)
def test_build_library_requires_build_session_for_reads_and_freezes_after_build() -> None: def test_build_library_requires_build_session_for_reads_and_freezes_after_build() -> None:
@ -152,6 +152,25 @@ def test_build_library_allows_helper_writes_via_pather() -> None:
assert helper_prov.owner_declared_name == "top" assert helper_prov.owner_declared_name == "top"
def test_build_library_contains_tracks_active_session_names() -> None:
builder = BuildLibrary()
builder["leaf"] = Pattern()
builder.add_source(Library({"src": Pattern()}))
def make_top(lib: BuildLibrary) -> Pattern:
assert "leaf" in lib
assert "src" in lib
assert "_helper" not in lib
lib["_helper"] = Pattern()
assert "_helper" in lib
return Pattern()
builder.cells.top = cell(make_top)(builder)
built, _report = builder.build()
assert "_helper" in built
def test_build_library_preserves_source_cells_and_records_source_provenance() -> None: def test_build_library_preserves_source_cells_and_records_source_provenance() -> None:
source = Library({"src": Pattern()}) source = Library({"src": Pattern()})
builder = BuildLibrary() builder = BuildLibrary()
@ -184,7 +203,7 @@ def test_build_library_add_source_can_rename_every_source_cell() -> None:
"parent": "mapped_parent", "parent": "mapped_parent",
} }
assert "mapped_child" in built["mapped_parent"].refs assert "mapped_child" in built["mapped_parent"].refs
assert report.provenance["mapped_child"].source_name == "child" assert report.provenance["mapped_child"].requested_name == "child"
def test_build_library_rejects_source_cells_added_after_add_source() -> None: def test_build_library_rejects_source_cells_added_after_add_source() -> None:
@ -236,7 +255,7 @@ def test_build_library_can_rename_imported_source_cells_during_authoring() -> No
assert "renamed_child" in built assert "renamed_child" in built
assert "child" not in built assert "child" not in built
assert "renamed_child" in built["parent"].refs assert "renamed_child" in built["parent"].refs
assert report.provenance["renamed_child"].source_name == "child" assert report.provenance["renamed_child"].requested_name == "child"
def test_build_library_rejects_move_references_for_source_rename() -> None: def test_build_library_rejects_move_references_for_source_rename() -> None:
@ -276,7 +295,6 @@ def test_build_library_helper_rename_updates_provenance_owner() -> None:
prov = report.provenance["final_helper"] prov = report.provenance["final_helper"]
assert prov.kind == "helper" assert prov.kind == "helper"
assert prov.requested_name == "_helper" assert prov.requested_name == "_helper"
assert prov.renamed_from == "_helper"
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None: def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
@ -314,7 +332,6 @@ def test_build_library_helper_rename_after_auto_rename_preserves_requested_name(
assert "final_helper" in built assert "final_helper" in built
prov = report.provenance["final_helper"] prov = report.provenance["final_helper"]
assert prov.requested_name == "_helper" assert prov.requested_name == "_helper"
assert prov.renamed_from == "_helper"
def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None: def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None: