[wip] introduce BuildLibrary and make overlay first-class

This commit is contained in:
Jan Petykiewicz 2026-04-22 21:24:05 -07:00
commit 6d494142fe
7 changed files with 1335 additions and 98 deletions

View file

@ -20,10 +20,10 @@ Contents
* Use `Pather` to snap ports together into a circuit * Use `Pather` to snap ports together into a circuit
* Check for dangling references * Check for dangling references
- [library](library.py) - [library](library.py)
* Continue from `devices.py` using a lazy library * Continue from `devices.py` by declaring a mixed library with `BuildLibrary`
* Create a `LazyLibrary`, which loads / generates patterns only when they are first used * Import source-backed GDS cells and register python-generated recipes together
* Call `build()` to produce a normal library for downstream `Pather` usage and writing
* Explore alternate ways of specifying a pattern for `.plug()` and `.place()` * Explore alternate ways of specifying a pattern for `.plug()` and `.place()`
* Design a pattern which is meant to plug into an existing pattern (via `.interface()`)
- [pather](pather.py) - [pather](pather.py)
* Use `Pather` to route individual wires and wire bundles * Use `Pather` to route individual wires and wire bundles
* Use `AutoTool` to generate paths * Use `AutoTool` to generate paths

View file

@ -1,136 +1,114 @@
""" """
Tutorial: using a source-backed lazy GDS library and `Pather.interface()`. Tutorial: authoring a mixed library with `BuildLibrary`.
This example assumes you have already read `devices.py` and generated the This example assumes you have already read `devices.py` and generated the
`circuit.gds` file it writes. The goal here is not the photonic-crystal geometry `circuit.gds` file it writes. The goal here is not the photonic-crystal geometry
itself, but rather how Masque lets you mix lazily loaded GDS content with itself, but rather how Masque lets you combine imported GDS cells with
python-generated devices inside one library. python-generated recipes, then turn that declaration set into a normal library
for downstream assembly and writing.
""" """
from typing import Any from typing import Any
from pprint import pformat from pprint import pformat
from masque import Pather from masque import BuildLibrary, Pather, Pattern, cell
from masque.file.gdsii import writefile from masque.file.gdsii import writefile
from masque.file.gdsii_lazy import OverlayLibrary, readfile from masque.file.gdsii_lazy import readfile
import basic_shapes import basic_shapes
import devices import devices
from basic_shapes import GDS_OPTS from basic_shapes import GDS_OPTS
def make_mixed_waveguide(lib: BuildLibrary) -> Pattern:
"""
Recipe which assembles imported and generated cells behind the builder API.
"""
circ = Pather(library=lib, ports='tri_l3cav')
# First way to specify what we are plugging in: request an explicit abstract.
circ.plug(lib.abstract('wg10'), {'input': 'right'})
# Second way: use an AbstractView, which behaves like a mapping of names
# to abstracts.
abstracts = lib.abstract_view()
circ.plug(abstracts['wg10'], {'output': 'left'})
# Third way: let Pather resolve a pattern name through its own library.
circ.plug('tri_wg10', {'input': 'right'})
circ.plug('tri_wg10', {'output': 'left'})
return circ.pattern
def main() -> None: def main() -> None:
# `OverlayLibrary` lets us mix source-backed GDS cells with python-generated builder = BuildLibrary()
# patterns behind the same library interface. cells = builder.cells
lib = OverlayLibrary()
# #
# Load some devices from a GDS file # Load some devices from a GDS file
# #
# Scan circuit.gds and prepare to lazy-load its contents. Port labels are # Scan circuit.gds and prepare to lazy-load its contents. Port labels are
# imported on first materialization, but the raw source remains untouched. # imported on first materialization, but the raw source remains untouched
# until we build the final library.
gds_lib, _properties = readfile('circuit.gds') gds_lib, _properties = readfile('circuit.gds')
lib.add_source(gds_lib.with_ports_from_data(layers=[(3, 0)], max_depth=1)) builder.add_source(gds_lib.with_ports_from_data(layers=[(3, 0)], max_depth=1))
print('Patterns loaded from GDS into library:\n' + pformat(list(lib.keys()))) print('Registered imported cells:\n' + pformat(list(gds_lib.keys())))
# #
# Add some new devices to the library, this time from python code rather than GDS # Register some new devices, this time from python code rather than GDS.
# #
lib['triangle'] = basic_shapes.triangle(devices.RADIUS) cells.triangle = basic_shapes.triangle(devices.RADIUS)
opts: dict[str, Any] = dict( opts: dict[str, Any] = dict(
lattice_constant = devices.LATTICE_CONSTANT, lattice_constant=devices.LATTICE_CONSTANT,
hole = 'triangle', hole='triangle',
) )
lib['tri_wg10'] = devices.waveguide(length=10, mirror_periods=5, **opts) cells.tri_wg10 = cell(devices.waveguide)(length=10, mirror_periods=5, **opts)
lib['tri_wg05'] = devices.waveguide(length=5, mirror_periods=5, **opts) cells.tri_wg05 = cell(devices.waveguide)(length=5, mirror_periods=5, **opts)
lib['tri_wg28'] = devices.waveguide(length=28, mirror_periods=5, **opts) cells.tri_wg28 = cell(devices.waveguide)(length=28, mirror_periods=5, **opts)
lib['tri_bend0'] = devices.bend(mirror_periods=5, **opts) cells.tri_bend0 = cell(devices.bend)(mirror_periods=5, **opts)
lib['tri_ysplit'] = devices.y_splitter(mirror_periods=5, **opts) cells.tri_ysplit = cell(devices.y_splitter)(mirror_periods=5, **opts)
lib['tri_l3cav'] = devices.perturbed_l3(xy_size=(4, 10), **opts, hole_lib=lib) cells.tri_l3cav = cell(devices.perturbed_l3)(xy_size=(4, 10), **opts, hole_lib=builder)
cells.mixed_wg_cav = cell(make_mixed_waveguide)(builder)
print('Declared cells waiting to be built:\n' + pformat(list(builder.keys())))
# #
# Build a mixed waveguide with an L3 cavity in the middle # Build the declaration set into a normal library.
# #
# Start a new design by copying the ports from an existing library cell. built = builder.build()
# This gives `circ2` the same external interface as `tri_l3cav`. print('Built library contains:\n' + pformat(list(built.keys())))
circ2 = Pather(library=lib, ports='tri_l3cav')
# First way to specify what we are plugging in: request an explicit abstract.
# This works with `Pattern` methods directly as well as with `Pather`.
circ2.plug(lib.abstract('wg10'), {'input': 'right'})
# Second way: use an `AbstractView`, which behaves like a mapping of names
# to abstracts.
abstracts = lib.abstract_view()
circ2.plug(abstracts['wg10'], {'output': 'left'})
# Third way: let `Pather` resolve a pattern name through its own library.
# This shorthand is convenient, but it is specific to helpers that already
# carry a library reference.
circ2.plug('tri_wg10', {'input': 'right'})
circ2.plug('tri_wg10', {'output': 'left'})
# Add the circuit to the device library.
lib['mixed_wg_cav'] = circ2.pattern
# #
# Build a second device that is explicitly designed to mate with `circ2`. # Continue designing against the built library.
# #
# `Pather.interface()` makes a new pattern whose ports mirror an existing # The built result behaves like a normal mutable library, so downstream code
# design's external interface. That is useful when you want to design an # can use Pather, abstract views, and writing without going back through the
# adapter, continuation, or mating structure. # builder interface.
circ3 = Pather.interface(source=circ2) circ = Pather.interface(source='mixed_wg_cav', library=built)
circ.plug('tri_bend0', {'input': 'right'})
# Continue routing outward from those inherited ports. circ.plug('tri_bend0', {'input': 'left'}, mirrored=True) # mirror since no tri y-symmetry
circ3.plug('tri_bend0', {'input': 'right'}) circ.plug('tri_bend0', {'input': 'right'})
circ3.plug('tri_bend0', {'input': 'left'}, mirrored=True) # mirror since no tri y-symmetry circ.plug('bend0', {'output': 'left'})
circ3.plug('tri_bend0', {'input': 'right'}) circ.plug('bend0', {'output': 'left'})
circ3.plug('bend0', {'output': 'left'}) circ.plug('bend0', {'output': 'left'})
circ3.plug('bend0', {'output': 'left'}) circ.plug('tri_wg10', {'input': 'right'})
circ3.plug('bend0', {'output': 'left'}) circ.plug('tri_wg28', {'input': 'right'})
circ3.plug('tri_wg10', {'input': 'right'}) circ.plug('tri_wg10', {'input': 'right', 'output': 'left'})
circ3.plug('tri_wg28', {'input': 'right'}) built['loop_segment'] = circ.pattern
circ3.plug('tri_wg10', {'input': 'right', 'output': 'left'})
lib['loop_segment'] = circ3.pattern
# #
# Write all devices into a GDS file # Write all devices into a GDS file.
# #
print('Writing library to file...') print('Writing library to file...')
writefile(lib, 'library.gds', **GDS_OPTS) writefile(built, 'library.gds', **GDS_OPTS)
if __name__ == '__main__': if __name__ == '__main__':
main() main()
#
#class prout:
# def place(
# self,
# other: Pattern,
# label_layer: layer_t = 'WATLAYER',
# *,
# port_map: Dict[str, str | None] | None = None,
# **kwargs,
# ) -> 'prout':
#
# Pattern.place(self, other, port_map=port_map, **kwargs)
# name: str | None
# for name in other.ports:
# if port_map:
# assert(name is not None)
# name = port_map.get(name, name)
# if name is None:
# continue
# self.pattern.label(string=name, offset=self.ports[name].offset, layer=label_layer)
# return self
#

View file

@ -63,10 +63,15 @@ from .library import (
ILibrary as ILibrary, ILibrary as ILibrary,
LibraryView as LibraryView, LibraryView as LibraryView,
Library as Library, Library as Library,
BuiltLibrary as BuiltLibrary,
BuildLibrary as BuildLibrary,
BuildReport as BuildReport,
CellProvenance as CellProvenance,
LazyLibrary as LazyLibrary, LazyLibrary as LazyLibrary,
AbstractView as AbstractView, AbstractView as AbstractView,
TreeView as TreeView, TreeView as TreeView,
Tree as Tree, Tree as Tree,
cell as cell,
) )
from .ports import ( from .ports import (
Port as Port, Port as Port,

View file

@ -1,9 +1,18 @@
""" """
Source-backed lazy GDSII reader using the pure-python klamath path. Classic source-backed lazy GDSII reader built on the pure-python klamath path.
This module mirrors the lazy Arrow reader's interface closely enough to share This module provides the non-Arrow half of Masque's lazy GDS architecture:
the same overlay and ports-import helpers, while still materializing cells
through the classic `gdsii` decoder. - `GdsLibrarySource` scans a GDS stream once to discover library metadata,
struct order, and child edges without materializing every cell.
- cells are materialized on demand through the classic `gdsii` decoder
whenever a caller indexes the lazy view
- the source can be wrapped in `PortsLibraryView` or merged through
`OverlayLibrary`, both of which live in `gdsii_lazy_core`
The public surface intentionally parallels `gdsii_lazy_arrow` closely so that
callers can swap between the classic and Arrow-backed implementations with
minimal changes.
""" """
from __future__ import annotations from __future__ import annotations
@ -36,6 +45,7 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class _SourceHandle: class _SourceHandle:
""" Owns the underlying stream and any companion file handle for a source. """
path: pathlib.Path | None path: pathlib.Path | None
stream: IO[bytes] stream: IO[bytes]
handle: IO[bytes] | None = None handle: IO[bytes] | None = None
@ -49,6 +59,7 @@ class _SourceHandle:
@dataclass(frozen=True) @dataclass(frozen=True)
class _CellScan: class _CellScan:
""" Scan-time metadata for one cell in the source stream. """
offset: int offset: int
children: set[str] children: set[str]
@ -107,6 +118,10 @@ class GdsLibrarySource(ILibraryView):
Cells are scanned once up front to discover order and child edges, then Cells are scanned once up front to discover order and child edges, then
materialized one at a time through the classic `gdsii.read_elements` path. materialized one at a time through the classic `gdsii.read_elements` path.
The source owns the stream lifetime, preserves on-disk ordering through
`source_order()`, and answers graph queries from scan metadata whenever
possible so callers can inspect hierarchy without forcing a full load.
""" """
def __init__( def __init__(

View file

@ -1,5 +1,18 @@
""" """
Shared helpers for source-backed lazy GDS views. Shared helpers for source-backed lazy GDS views.
This module contains the reusable pieces that sit between lazy source readers
and ordinary mutable library usage:
- `PortsLibraryView` layers a processed, ports-importing cache on top of a raw
source view without mutating the source itself
- `OverlayLibrary` exposes a mutable library surface that can mix source-backed
cells with overlay-owned materialized patterns
- the write helpers preserve source-backed copy-through behavior where
possible, falling back to normal pattern serialization when a cell has been
materialized or remapped
Both the classic and Arrow-backed lazy GDS readers rely on these helpers.
""" """
from __future__ import annotations from __future__ import annotations
@ -30,6 +43,7 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class _SourceLayer: class _SourceLayer:
""" One imported source layer tracked by an `OverlayLibrary`. """
library: ILibraryView library: ILibraryView
source_to_visible: dict[str, str] source_to_visible: dict[str, str]
visible_to_source: dict[str, str] visible_to_source: dict[str, str]
@ -39,6 +53,7 @@ class _SourceLayer:
@dataclass(frozen=True) @dataclass(frozen=True)
class _SourceEntry: class _SourceEntry:
""" Reference to a single visible source-backed cell in an overlay. """
layer_index: int layer_index: int
source_name: str source_name: str
@ -73,6 +88,10 @@ class PortsLibraryView(ILibraryView):
The wrapped source remains untouched; this view owns a separate processed The wrapped source remains untouched; this view owns a separate processed
cache so direct-copy workflows can continue to use the raw source view. cache so direct-copy workflows can continue to use the raw source view.
Graph queries, source ordering, and copy-through capabilities are delegated
to the wrapped source whenever possible, while `__getitem__` and
`materialize_many()` return port-imported patterns.
""" """
def __init__( def __init__(
@ -231,6 +250,14 @@ class OverlayLibrary(ILibrary):
Source-backed cells remain lazy until accessed through `__getitem__`, at Source-backed cells remain lazy until accessed through `__getitem__`, at
which point that visible cell is promoted into an overlay-owned materialized which point that visible cell is promoted into an overlay-owned materialized
`Pattern`. `Pattern`.
This is the main mutable integration surface for lazy GDS content. It lets
callers:
- expose one or more source-backed libraries behind a normal `ILibrary`
interface
- add or replace cells with overlay-owned patterns
- rename visible source cells
- remap references without immediately rewriting untouched source structs
""" """
def __init__(self) -> None: def __init__(self) -> None:
@ -522,6 +549,20 @@ class OverlayLibrary(ILibrary):
return tuple(name for name in self._order if name in self._entries) return tuple(name for name in self._order if name in self._entries)
class BuiltOverlayLibrary(OverlayLibrary):
"""
Internal overlay output returned by `BuildLibrary.build(output='overlay')`.
The type is intentionally not part of the public API. It exists so build
outputs can carry a `build_report` while still behaving like an
`OverlayLibrary`.
"""
def __init__(self, *, build_report: Any | None = None) -> None:
super().__init__()
self.build_report = build_report
def _iter_library_infos(library: Mapping[str, Pattern] | ILibraryView) -> Iterator[dict[str, Any]]: def _iter_library_infos(library: Mapping[str, Pattern] | ILibraryView) -> Iterator[dict[str, Any]]:
info = getattr(library, 'library_info', None) info = getattr(library, 'library_info', None)
if isinstance(info, dict): if isinstance(info, dict):

View file

@ -14,7 +14,7 @@ Classes include:
- `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked - `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked
library. Generated with `ILibraryView.abstract_view()`. library. Generated with `ILibraryView.abstract_view()`.
""" """
from typing import Self, TYPE_CHECKING, 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 Iterator, Mapping, MutableMapping, Sequence, Callable
import logging import logging
import re import re
@ -22,12 +22,14 @@ import copy
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
from contextvars import ContextVar
from dataclasses import dataclass, replace
from graphlib import TopologicalSorter, CycleError from graphlib import TopologicalSorter, CycleError
import numpy import numpy
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
from .error import LibraryError, PatternError from .error import BuildError, LibraryError, PatternError
from .utils import layer_t, apply_transforms from .utils import layer_t, apply_transforms
from .shapes import Shape, Polygon from .shapes import Shape, Polygon
from .label import Label from .label import Label
@ -40,6 +42,11 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ACTIVE_BUILD_SESSIONS: ContextVar[dict[int, '_BuildSessionLibrary'] | None] = ContextVar(
'masque_active_build_sessions',
default=None,
)
class visitor_function_t(Protocol): class visitor_function_t(Protocol):
""" Signature for `Library.dfs()` visitor functions. """ """ Signature for `Library.dfs()` visitor functions. """
@ -62,6 +69,69 @@ Tree: TypeAlias = MutableMapping[str, 'Pattern']
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include'] dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
""" How helpers should handle refs whose targets are not present in the library. """ """ How helpers should handle refs whose targets are not present in the library. """
emitted_via_t: TypeAlias = Literal['declaration', 'helper_write', 'tree_merge', 'source_import']
""" Build-provenance origin tags for emitted cells. """
@dataclass(frozen=True)
class CellProvenance:
"""
Provenance record for one cell in a completed build output.
Each output name in a `BuildReport` maps to one `CellProvenance`. The
record captures both where the cell came from and how its visible name was
chosen.
Attributes:
final_name: Name exposed by the completed library.
requested_name: First name requested for this cell during the build.
kind: Whether the cell came from a declaration, helper emission, or an
imported source library.
owner_declared_name: Declared cell responsible for this output cell, if
any. Imported source cells leave this as `None`.
emitted_via: High-level path by which the cell entered the output.
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.
source_metadata: Optional source-library metadata copied through from
lazy GDS readers.
"""
final_name: str
requested_name: str
kind: Literal['declared', 'helper', 'source']
owner_declared_name: str | None
emitted_via: emitted_via_t
build_chain: tuple[str, ...]
renamed_from: str | None = None
source_name: str | None = None
source_metadata: dict[str, Any] | None = None
@dataclass(frozen=True)
class BuildReport:
"""
Immutable summary of one `BuildLibrary.validate()` or `.build()` run.
The report is designed to answer two questions after a build completes:
which declared cells depended on which other declared cells, and where each
output cell came from.
Attributes:
requested_roots: Roots explicitly requested for the run. A full
`build()` uses all declared cells.
provenance: Mapping from final output name to provenance metadata.
owned_cells: Mapping from declared cell name to all final output cell
names it owns, including helper cells emitted while that declared
cell was building.
dependency_graph: Declared-cell dependency graph discovered through
library-mediated reads and explicit recipe hints.
"""
requested_roots: tuple[str, ...]
provenance: Mapping[str, CellProvenance]
owned_cells: Mapping[str, tuple[str, ...]]
dependency_graph: Mapping[str, frozenset[str]]
SINGLE_USE_PREFIX = '_' SINGLE_USE_PREFIX = '_'
""" """
@ -1397,6 +1467,819 @@ class Library(ILibrary):
return tree, pat return tree, pat
class BuiltLibrary(Library):
"""
Eager library returned by `BuildLibrary.build(output='library')`.
This is a normal materialized `Library` with one additional attribute,
`build_report`, which records how the library was assembled from
declarations, helper emissions, and imported source-backed cells.
"""
def __init__(
self,
mapping: MutableMapping[str, 'Pattern'] | None = None,
*,
build_report: BuildReport | None = None,
) -> None:
super().__init__(mapping=mapping)
self.build_report = build_report
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. """
func: Callable[..., 'Pattern']
args: tuple[Any, ...]
kwargs: dict[str, Any]
explicit_dependencies: tuple[str, ...] = ()
def depends_on(self, *names: str) -> '_BuildRecipe':
self.explicit_dependencies += tuple(names)
return self
@dataclass(frozen=True)
class _PatternDeclaration:
""" Declared cell backed by an already-built `Pattern`. """
pattern: 'Pattern'
@dataclass(frozen=True)
class _RecipeDeclaration:
""" Declared cell backed by a deferred recipe. """
recipe: _BuildRecipe
@dataclass(frozen=True)
class _SourceDeclaration:
"""
Imported source-backed names registered with a `BuildLibrary`.
The declaration stores visible-name remapping plus pre-scanned graph
metadata. Underlying source cells stay lazy until a build session
materializes or copies them through.
"""
library: ILibraryView
source_to_visible: Mapping[str, str]
visible_to_source: Mapping[str, str]
child_graph: Mapping[str, set[str]]
order: tuple[str, ...]
def cell(func: Callable[..., 'Pattern']) -> _CellFactory:
"""
Wrap a plain pattern factory so calls return deferred build recipes.
Use as either `cell(fn)(...)` or `@cell`.
"""
return _CellFactory(func)
class BuildCellsView:
"""
Attribute-based declaration namespace for `BuildLibrary`.
This is the ergonomic authoring surface exposed as `builder.cells`. It is
intentionally write-focused: attribute assignment and deletion register
declarations, while attribute reads fail with guidance to build first and
use the returned library.
"""
def __init__(self, library: 'BuildLibrary') -> None:
object.__setattr__(self, '_library', library)
def __getattr__(self, name: str) -> 'Pattern':
raise BuildError(
f'BuildLibrary.cells.{name} is write-only during authoring. '
'Call build() and index the returned library instead.'
)
def __setattr__(self, name: str, value: 'Pattern | _BuildRecipe') -> None:
if name.startswith('_'):
object.__setattr__(self, name, value)
return
self._library[name] = value
def __delattr__(self, name: str) -> None:
if name.startswith('_'):
raise AttributeError(name)
del self._library[name]
class BuildLibrary(ILibrary):
"""
Two-phase declaration surface for mixed imported/generated libraries.
A `BuildLibrary` collects three kinds of inputs:
- direct declared `Pattern` objects
- deferred recipes created with `cell(...)`
- imported source-backed library views added with `add_source(...)`
The builder itself is not a normal readable library during authoring.
Instead, `validate()` and `build()` create a temporary build-session library
that recipes can read from and write helper cells into while dependencies
are resolved. `build()` then freezes the builder on success and returns a
normal library-like object carrying a `build_report`.
"""
def __init__(self, *, check_on_register: bool = False) -> None:
self.check_on_register = check_on_register
self.cells = BuildCellsView(self)
self.last_build_report: BuildReport | None = None
self._frozen = False
self._declarations: dict[str, _PatternDeclaration | _RecipeDeclaration] = {}
self._sources: list[_SourceDeclaration] = []
self._names: set[str] = set()
self._order: list[str] = []
def _active_session(self) -> '_BuildSessionLibrary | None':
sessions = _ACTIVE_BUILD_SESSIONS.get()
if sessions is None:
return None
return sessions.get(id(self))
def _require_active_session(self, operation: str) -> '_BuildSessionLibrary':
session = self._active_session()
if session is None:
raise BuildError(
f'BuildLibrary.{operation}() is only available while validate() or build() is running. '
'Use the built output library for reads.'
)
return session
def _assert_editable(self) -> None:
if self._frozen:
raise BuildError('This BuildLibrary has already been built successfully and is now frozen.')
def __iter__(self) -> Iterator[str]:
session = self._active_session()
if session is not None:
return iter(session)
return iter(self._order)
def __len__(self) -> int:
session = self._active_session()
if session is not None:
return len(session)
return len(self._names)
def __contains__(self, key: object) -> bool:
session = self._active_session()
if session is not None:
return key in session
return key in self._names
def __getitem__(self, key: str) -> 'Pattern':
return self._require_active_session('__getitem__')[key]
def __setitem__(
self,
key: str,
value: 'Pattern | _BuildRecipe | Callable[[], Pattern]',
) -> None:
session = self._active_session()
if session is not None:
session[key] = value
return
self._assert_editable()
if key in self._names:
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
declaration: _PatternDeclaration | _RecipeDeclaration
if isinstance(value, _BuildRecipe):
declaration = _RecipeDeclaration(value)
else:
if callable(value):
raise TypeError('BuildLibrary recipes must be wrapped with cell(fn)(...) or @cell.')
declaration = _PatternDeclaration(value)
self._declarations[key] = declaration
self._names.add(key)
self._order.append(key)
if self.check_on_register:
try:
self.validate(names=(key,))
except Exception:
del self._declarations[key]
self._names.remove(key)
self._order.remove(key)
raise
def __delitem__(self, key: str) -> None:
session = self._active_session()
if session is not None:
del session[key]
return
self._assert_editable()
if key not in self._declarations:
raise KeyError(key)
del self._declarations[key]
self._names.remove(key)
self._order.remove(key)
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
session = self._active_session()
if session is not None:
session._merge(key_self, other, key_other)
return
self[key_self] = copy.deepcopy(other[key_other])
def add(
self,
other: Mapping[str, 'Pattern'],
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
mutate_other: bool = False,
) -> dict[str, str]:
session = self._active_session()
if session is not None:
return session.add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
return super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
def rename(
self,
old_name: str,
new_name: str,
move_references: bool = False,
) -> Self:
"""
Rename an imported source-backed visible name during authoring.
Only imported source-backed cells may be renamed on the builder itself.
Declared/generated cells must be registered under their intended final
names. `move_references=True` is intentionally unsupported here because
deferred recipes and declaration internals cannot be rewritten safely.
"""
session = self._active_session()
if session is not None:
session.rename(old_name, new_name, move_references=move_references)
return self
self._assert_editable()
if old_name == new_name:
return self
if old_name in self._declarations:
raise BuildError(
f'Cannot rename declared build cell "{old_name}" during authoring. '
'Register it under the intended final name instead.'
)
if old_name not in self._names:
raise LibraryError(f'"{old_name}" does not exist in the builder.')
if new_name in self._names:
raise LibraryError(f'"{new_name}" already exists in the builder.')
if move_references:
raise BuildError(
'BuildLibrary.rename(..., move_references=True) is not supported for imported source cells. '
'Builder-level renames only change the visible imported name.'
)
source_index = next(
(idx for idx, spec in enumerate(self._sources) if old_name in spec.visible_to_source),
None,
)
if source_index is None:
raise BuildError(
f'Cannot rename "{old_name}" during authoring because only imported source-backed '
'cells may be renamed on a BuildLibrary.'
)
spec = self._sources[source_index]
source_name = spec.visible_to_source[old_name]
source_to_visible = dict(spec.source_to_visible)
visible_to_source = dict(spec.visible_to_source)
order = list(spec.order)
source_to_visible[source_name] = new_name
del visible_to_source[old_name]
visible_to_source[new_name] = source_name
order[order.index(old_name)] = new_name
self._sources[source_index] = replace(
spec,
source_to_visible=source_to_visible,
visible_to_source=visible_to_source,
order=tuple(order),
)
self._names.remove(old_name)
self._names.add(new_name)
self._order[self._order.index(old_name)] = new_name
return self
def abstract(self, name: str) -> Abstract:
return self._require_active_session('abstract').abstract(name)
def resolve(
self,
other: 'Abstract | str | Pattern | TreeView',
append: bool = False,
) -> 'Abstract | Pattern':
return self._require_active_session('resolve').resolve(other, append=append)
def add_source(
self,
source: Mapping[str, 'Pattern'] | ILibraryView,
*,
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
) -> dict[str, str]:
"""
Register an imported source-backed library with the builder.
The source is not materialized immediately. Instead, its names and
child graph are scanned once and stored as an import declaration. The
source may be renamed on entry to avoid collisions with existing
declarations or other imported sources.
Returns:
Mapping of `{source_name: visible_name}` for imported names that
were renamed while being added.
"""
self._assert_editable()
view = source if isinstance(source, ILibraryView) else LibraryView(source)
source_order = tuple(view.source_order())
child_graph = view.child_graph(dangling='include')
source_to_visible: dict[str, str] = {}
visible_to_source: dict[str, str] = {}
rename_map: dict[str, str] = {}
new_names: list[str] = []
for name in source_order:
visible = name
if visible in self._names 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)
if visible in self._names or visible in visible_to_source:
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
rename_map[name] = visible
source_to_visible[name] = visible
visible_to_source[visible] = name
new_names.append(visible)
self._sources.append(_SourceDeclaration(
library=view,
source_to_visible=dict(source_to_visible),
visible_to_source=dict(visible_to_source),
child_graph={name: set(children) for name, children in child_graph.items()},
order=tuple(source_to_visible[name] for name in source_order),
))
for visible in new_names:
self._names.add(visible)
self._order.append(visible)
return rename_map
def validate(
self,
names: Sequence[str] | None = None,
*,
allow_dangling: bool = False,
) -> BuildReport:
"""
Run the full build logic and return a `BuildReport` without producing output.
This is a dry run over the same dependency resolution and recipe
execution path used by `build()`. Any generated library is discarded
after validation completes.
"""
report, _output = self._run_build(names=names, output='overlay', allow_dangling=allow_dangling, persist_output=False)
self.last_build_report = report
return report
def build(
self,
*,
output: Literal['overlay', 'library'] = 'overlay',
allow_dangling: bool = False,
) -> 'BuiltLibrary | ILibrary':
"""
Materialize declarations and return a usable output library.
Args:
output: `'overlay'` preserves imported source-backed cells where
possible, while `'library'` eagerly materializes the full
result.
allow_dangling: If `False`, fail the build when the completed
library still contains dangling references.
"""
self._assert_editable()
report, built_output = self._run_build(names=None, output=output, allow_dangling=allow_dangling, persist_output=True)
self._frozen = True
self.last_build_report = report
return built_output
def _run_build(
self,
*,
names: Sequence[str] | None,
output: Literal['overlay', 'library'],
allow_dangling: bool,
persist_output: bool,
) -> tuple[BuildReport, BuiltLibrary | ILibrary | None]:
roots = tuple(dict.fromkeys(names if names is not None else self._declarations.keys()))
unknown = [name for name in roots if name not in self._names]
if unknown:
raise BuildError(f'Unknown build roots requested: {unknown}')
session = _BuildSessionLibrary(self)
sessions = dict(_ACTIVE_BUILD_SESSIONS.get() or {})
sessions[id(self)] = session
token = _ACTIVE_BUILD_SESSIONS.set(sessions)
try:
session.materialize_many(roots)
if not allow_dangling:
session.child_graph(dangling='error')
if output == 'library':
built_output = session.to_library() if persist_output else None
elif persist_output:
built_output = session.to_overlay()
else:
built_output = None
finally:
_ACTIVE_BUILD_SESSIONS.reset(token)
report = session.build_report(roots)
if built_output is not None:
built_output.build_report = report
return report, built_output
class _BuildSessionLibrary(ILibrary):
"""
Internal overlay-backed library used while a `BuildLibrary` is executing.
This object provides the mutable-library surface that recipes expect while
also tracking declared-cell dependencies, helper-cell provenance, and
imported source cells. It exists only for the duration of a validation or
build run.
"""
def __init__(self, builder: BuildLibrary) -> None:
from .file.gdsii_lazy_core import BuiltOverlayLibrary, _SourceEntry, _SourceLayer # noqa: PLC0415
self._builder = builder
self._overlay = BuiltOverlayLibrary()
self._source_entry_type = _SourceEntry
self._source_layer_type = _SourceLayer
self._states: dict[str, Literal['unbuilt', 'building', 'built']] = {
name: 'unbuilt' for name in builder._declarations
}
self._declared_stack: list[str] = []
self._emission_stack: list[str] = []
self._emission_via_stack: list[emitted_via_t] = []
self._names = set(builder._names)
self._order = list(builder._order)
self._provenance: dict[str, CellProvenance] = {}
self._owned_cells: defaultdict[str, list[str]] = defaultdict(list)
self._dependency_graph: defaultdict[str, set[str]] = defaultdict(set)
self._install_sources()
def _install_sources(self) -> None:
for spec in self._builder._sources:
layer = self._source_layer_type(
library=spec.library,
source_to_visible=dict(spec.source_to_visible),
visible_to_source=dict(spec.visible_to_source),
child_graph={name: set(children) for name, children in spec.child_graph.items()},
order=list(spec.order),
)
layer_index = len(self._overlay._layers)
self._overlay._layers.append(layer)
source_info = getattr(spec.library, 'library_info', None)
source_meta = dict(source_info) if isinstance(source_info, dict) else None
for source_name, visible_name in spec.source_to_visible.items():
self._overlay._entries[visible_name] = self._source_entry_type(
layer_index=layer_index,
source_name=source_name,
)
if visible_name not in self._overlay._order:
self._overlay._order.append(visible_name)
self._provenance[visible_name] = CellProvenance(
final_name=visible_name,
requested_name=source_name,
kind='source',
owner_declared_name=None,
emitted_via='source_import',
build_chain=(),
renamed_from=source_name if visible_name != source_name else None,
source_name=source_name,
source_metadata=source_meta,
)
def __iter__(self) -> Iterator[str]:
return (name for name in self._order if name in self._names)
def __len__(self) -> int:
return len(self._names)
def __contains__(self, key: object) -> bool:
return key in self._names or key in self._overlay
def _touch_name(self, key: str) -> None:
if key not in self._names:
self._names.add(key)
self._order.append(key)
def _current_declared(self) -> str | None:
if not self._declared_stack:
return None
return self._declared_stack[-1]
def _record_dependency(self, target: str) -> None:
current = self._current_declared()
if current is None or current == target or target not in self._builder._declarations:
return
self._dependency_graph[current].add(target)
def _guard_mutable_output_name(self, key: str, *, operation: str) -> None:
if key in self._builder._declarations:
raise BuildError(f'Cannot {operation} declared build cell "{key}" during an active build session.')
provenance = self._provenance.get(key)
if provenance is not None and provenance.kind == 'source':
raise BuildError(f'Cannot {operation} imported source cell "{key}" during an active build session.')
def _remove_owned_cell(self, owner: str | None, name: str) -> None:
if owner is None or owner not in self._owned_cells:
return
cells = self._owned_cells[owner]
self._owned_cells[owner] = [cell for cell in cells if cell != name]
if not self._owned_cells[owner]:
del self._owned_cells[owner]
def rename(
self,
old_name: str,
new_name: str,
move_references: bool = False,
) -> Self:
if old_name == new_name:
return self
if old_name not in self._overlay:
if old_name in self._builder._declarations:
self._guard_mutable_output_name(old_name, operation='rename')
raise LibraryError(f'"{old_name}" does not exist in the library.')
self._guard_mutable_output_name(old_name, operation='rename')
if new_name in self._names:
raise LibraryError(f'"{new_name}" already exists in the library.')
self._overlay.rename(old_name, new_name, move_references=move_references)
self._names.discard(old_name)
self._names.add(new_name)
if old_name in self._order:
idx = self._order.index(old_name)
self._order[idx] = new_name
provenance = self._provenance.pop(old_name)
requested_name = provenance.requested_name
self._provenance[new_name] = replace(
provenance,
final_name=new_name,
renamed_from=requested_name if new_name != requested_name else None,
)
owner = provenance.owner_declared_name
if owner is not None and owner in self._owned_cells:
self._owned_cells[owner] = [
new_name if cell_name == old_name else cell_name
for cell_name in self._owned_cells[owner]
]
return self
def __getitem__(self, key: str) -> 'Pattern':
if key in self._builder._declarations:
self._record_dependency(key)
self._ensure_declared(key)
return self._overlay[key]
def __setitem__(
self,
key: str,
value: 'Pattern | Callable[[], Pattern]',
) -> None:
if key in self._overlay:
raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!')
current = self._current_declared()
if key in self._builder._declarations and key != current:
raise LibraryError(f'"{key}" is reserved for a declared cell and cannot be used as a helper name.')
pattern = value() if callable(value) else value
self._overlay[key] = pattern
self._touch_name(key)
kind: Literal['declared', 'helper']
via = self._emission_via_stack[-1] if self._emission_via_stack else 'helper_write'
if current is not None and key == current:
kind = 'declared'
via = 'declaration'
else:
kind = 'helper'
if not self._emission_via_stack:
via = 'helper_write'
self._emission_stack.append(key)
try:
self._record_provenance(
final_name=key,
requested_name=key,
kind=kind,
owner_declared_name=current if kind == 'helper' else key,
emitted_via=via,
build_chain=tuple(self._declared_stack),
renamed_from=None,
)
finally:
self._emission_stack.pop()
def __delitem__(self, key: str) -> None:
if key not in self._overlay:
if key in self._builder._declarations:
self._guard_mutable_output_name(key, operation='delete')
raise KeyError(key)
self._guard_mutable_output_name(key, operation='delete')
provenance = self._provenance.get(key)
if key in self._overlay:
del self._overlay[key]
self._names.discard(key)
if key in self._order:
self._order.remove(key)
self._provenance.pop(key, None)
if provenance is not None:
self._remove_owned_cell(provenance.owner_declared_name, key)
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
self[key_self] = copy.deepcopy(other[key_other])
def add(
self,
other: Mapping[str, 'Pattern'],
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
mutate_other: bool = False,
) -> dict[str, str]:
self._emission_via_stack.append('tree_merge')
try:
rename_map = super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
finally:
self._emission_via_stack.pop()
current = self._current_declared()
for old_name, new_name in rename_map.items():
if new_name in self._provenance:
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
def _record_provenance(
self,
*,
final_name: str,
requested_name: str,
kind: Literal['declared', 'helper'],
owner_declared_name: str | None,
emitted_via: emitted_via_t,
build_chain: tuple[str, ...],
renamed_from: str | None,
) -> None:
self._provenance[final_name] = CellProvenance(
final_name=final_name,
requested_name=requested_name,
kind=kind,
owner_declared_name=owner_declared_name,
emitted_via=emitted_via,
build_chain=build_chain,
renamed_from=renamed_from,
)
if owner_declared_name is not None and final_name not in self._owned_cells[owner_declared_name]:
self._owned_cells[owner_declared_name].append(final_name)
def _wrap_error(self, name: str, exc: Exception) -> BuildError:
helper = self._emission_stack[-1] if self._emission_stack else None
chain = tuple(self._declared_stack)
msg = [f'Failed while building declared cell "{name}"']
if helper is not None and helper != name:
msg.append(f'while materializing helper/output "{helper}"')
if chain:
msg.append(f'Dependency chain: {" -> ".join(chain)}')
msg.append(f'Cause: {exc}')
return BuildError('\n'.join(msg))
def _ensure_named(self, name: str) -> None:
if name in self._builder._declarations:
self._record_dependency(name)
self._ensure_declared(name)
return
if name in self._overlay:
return
raise BuildError(f'Missing dependency "{name}"')
def _ensure_declared(self, name: str) -> None:
from .pattern import Pattern # noqa: PLC0415
state = self._states[name]
if state == 'built':
return
if state == 'building':
chain = ' -> '.join(self._declared_stack + [name])
raise BuildError(f'Cycle detected while building declared cells: {chain}')
declaration = self._builder._declarations[name]
self._states[name] = 'building'
self._declared_stack.append(name)
try:
if isinstance(declaration, _PatternDeclaration):
pattern = declaration.pattern.deepcopy()
else:
for dep in declaration.recipe.explicit_dependencies:
self._ensure_named(dep)
pattern = declaration.recipe.func(*declaration.recipe.args, **declaration.recipe.kwargs)
if not isinstance(pattern, Pattern):
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern')
if name in self._overlay:
if self._overlay[name] is not pattern:
raise BuildError(
f'Recipe for "{name}" wrote a different pattern into the session under its own name.'
)
else:
self[name] = pattern
self._states[name] = 'built'
except Exception as exc:
self._states[name] = 'unbuilt'
raise self._wrap_error(name, exc) from exc
finally:
self._declared_stack.pop()
def materialize_many(self, names: Sequence[str]) -> None:
for name in dict.fromkeys(names):
self._ensure_named(name)
def source_order(self) -> tuple[str, ...]:
return self._overlay.source_order()
def child_graph(
self,
dangling: dangling_mode_t = 'error',
) -> dict[str, set[str]]:
return self._overlay.child_graph(dangling=dangling)
def parent_graph(
self,
dangling: dangling_mode_t = 'error',
) -> dict[str, set[str]]:
return self._overlay.parent_graph(dangling=dangling)
def build_report(self, requested_roots: Sequence[str]) -> BuildReport:
dependency_graph = {
name: frozenset(self._dependency_graph.get(name, set()))
for name in self._builder._declarations
if name in self._dependency_graph or name in requested_roots
}
owned_cells = {
name: tuple(cells)
for name, cells in self._owned_cells.items()
}
return BuildReport(
requested_roots=tuple(dict.fromkeys(requested_roots)),
provenance=dict(self._provenance),
owned_cells=owned_cells,
dependency_graph=dependency_graph,
)
def to_overlay(self) -> ILibrary:
return self._overlay
def to_library(self) -> BuiltLibrary:
mapping = {name: self._overlay[name] for name in self._overlay.source_order()}
return BuiltLibrary(mapping)
class LazyLibrary(ILibrary): class LazyLibrary(ILibrary):
""" """
This class is usually used to create a library of Patterns by mapping names to This class is usually used to create a library of Patterns by mapping names to

View file

@ -0,0 +1,315 @@
import pytest
from ..builder import Pather
from ..error import BuildError
from ..library import BuildLibrary, BuiltLibrary, Library, cell
from ..pattern import Pattern
from ..ports import Port
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
builder = BuildLibrary()
def make_parent(lib: BuildLibrary) -> Pattern:
pat = Pattern()
pat.ref("child")
assert lib.abstract("child").name == "child"
return pat
builder.cells.parent = cell(make_parent)(builder)
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
built = builder.build()
assert "parent" in built
assert "child" in built
assert built.build_report.dependency_graph["parent"] == frozenset({"child"})
assert built.build_report.provenance["parent"].kind == "declared"
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
builder = BuildLibrary()
def make_top(lib: BuildLibrary) -> Pattern:
tree = Library({"_helper": Pattern()})
name_a = lib << tree
name_b = lib << tree
top = Pattern()
top.ref(name_a)
top.ref(name_b)
return top
builder.cells.top = cell(make_top)(builder)
built = builder.build()
report = built.build_report
helpers = [
prov for prov in report.provenance.values()
if prov.owner_declared_name == "top" and prov.kind == "helper"
]
assert "top" in report.owned_cells["top"]
assert len(helpers) == 2
assert all(prov.emitted_via == "tree_merge" for prov in helpers)
assert any(prov.renamed_from == "_helper" for prov in helpers)
def test_build_library_requires_build_session_for_reads_and_freezes_after_build() -> None:
builder = BuildLibrary()
builder["leaf"] = Pattern()
with pytest.raises(BuildError, match="validate\\(\\) or build\\(\\)"):
_ = builder["leaf"]
with pytest.raises(BuildError, match="write-only"):
_ = builder.cells.leaf
built = builder.build(output="library")
assert isinstance(built, BuiltLibrary)
assert built.build_report.requested_roots == ("leaf",)
with pytest.raises(BuildError, match="frozen"):
builder["later"] = Pattern()
with pytest.raises(BuildError, match="frozen"):
builder.build()
def test_build_library_validate_is_retryable_after_failure() -> None:
builder = BuildLibrary()
def make_parent(lib: BuildLibrary) -> Pattern:
pat = Pattern()
pat.ref("child")
lib.abstract("child")
return pat
builder.cells.parent = cell(make_parent)(builder)
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
builder.validate()
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
report = builder.validate()
assert report.dependency_graph["parent"] == frozenset({"child"})
def test_build_library_check_on_register_rolls_back_failed_declarations() -> None:
builder = BuildLibrary(check_on_register=True)
def make_parent(lib: BuildLibrary) -> Pattern:
pat = Pattern()
pat.ref("child")
lib.abstract("child")
return pat
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
builder.cells.parent = cell(make_parent)(builder)
assert "parent" not in builder
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
builder = BuildLibrary()
builder["child"] = Pattern()
def make_parent() -> Pattern:
pat = Pattern()
pat.ref("child")
return pat
builder.cells.parent = cell(make_parent)().depends_on("child")
report = builder.validate(names=("parent",))
assert report.requested_roots == ("parent",)
assert report.dependency_graph["parent"] == frozenset({"child"})
def test_build_library_validate_rejects_removed_output_argument() -> None:
builder = BuildLibrary()
builder["leaf"] = Pattern()
with pytest.raises(TypeError):
builder.validate(output="library") # type: ignore[call-arg]
def test_build_library_allows_helper_writes_via_pather() -> None:
builder = BuildLibrary()
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
def make_top(lib: BuildLibrary) -> Pattern:
helper = Pather(library=lib, ports="leaf", name="_route")
top = Pattern()
top.ref("_route")
top.ref("leaf")
top.ports.update(helper.pattern.ports)
return top
builder.cells.top = cell(make_top)(builder)
built = builder.build()
helper_prov = built.build_report.provenance["_route"]
assert helper_prov.kind == "helper"
assert helper_prov.owner_declared_name == "top"
def test_build_library_preserves_source_cells_and_records_source_provenance() -> None:
source = Library({"src": Pattern()})
builder = BuildLibrary()
builder.add_source(source)
builder.cells.top = cell(lambda: Pattern())()
built = builder.build()
assert "src" in built
assert built.build_report.provenance["src"].kind == "source"
assert built.build_report.provenance["src"].emitted_via == "source_import"
def test_build_library_can_rename_imported_source_cells_during_authoring() -> None:
source = Library()
source["child"] = Pattern()
parent = Pattern()
parent.ref("child")
source["parent"] = parent
builder = BuildLibrary()
builder.add_source(source)
builder.rename("child", "renamed_child")
built = builder.build()
assert "renamed_child" in built
assert "child" not in built
assert "renamed_child" in built["parent"].refs
assert built.build_report.provenance["renamed_child"].source_name == "child"
def test_build_library_rejects_move_references_for_source_rename() -> None:
builder = BuildLibrary()
builder.add_source(Library({"src": Pattern()}))
with pytest.raises(BuildError, match="move_references=True"):
builder.rename("src", "renamed_src", move_references=True)
def test_build_library_rejects_renaming_declared_cells_during_authoring() -> None:
builder = BuildLibrary()
builder["declared"] = Pattern()
with pytest.raises(BuildError, match='Cannot rename declared build cell "declared"'):
builder.rename("declared", "renamed_declared")
def test_build_library_helper_rename_updates_provenance_and_owned_cells() -> None:
builder = BuildLibrary()
def make_top(lib: BuildLibrary) -> Pattern:
lib["_helper"] = Pattern()
lib.rename("_helper", "final_helper")
top = Pattern()
top.ref("final_helper")
return top
builder.cells.top = cell(make_top)(builder)
built = builder.build()
report = built.build_report
assert "final_helper" in built
assert "_helper" not in built
assert "final_helper" in report.owned_cells["top"]
assert "_helper" not in report.owned_cells["top"]
prov = report.provenance["final_helper"]
assert prov.kind == "helper"
assert prov.requested_name == "_helper"
assert prov.renamed_from == "_helper"
assert prov.final_name == "final_helper"
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
builder = BuildLibrary()
def make_top(lib: BuildLibrary) -> Pattern:
lib["_helper"] = Pattern()
del lib["_helper"]
return Pattern()
builder.cells.top = cell(make_top)(builder)
built = builder.build()
report = built.build_report
assert "_helper" not in built
assert "_helper" not in report.provenance
assert report.owned_cells["top"] == ("top",)
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
builder = BuildLibrary()
def make_top(lib: BuildLibrary) -> Pattern:
tree = Library({"_helper": Pattern()})
_ = lib << tree
renamed = lib << tree
lib.rename(renamed, "final_helper")
top = Pattern()
top.ref("_helper")
top.ref("final_helper")
return top
builder.cells.top = cell(make_top)(builder)
built = builder.build()
report = built.build_report
assert "final_helper" in built
prov = report.provenance["final_helper"]
assert prov.requested_name == "_helper"
assert prov.renamed_from == "_helper"
def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None:
declared = BuildLibrary()
declared["leaf"] = Pattern()
def rename_declared(lib: BuildLibrary) -> Pattern:
lib.rename("leaf", "renamed_leaf")
return Pattern()
declared.cells.top = cell(rename_declared)(declared)
with pytest.raises(BuildError, match='Cannot rename declared build cell "leaf"'):
declared.build()
source = BuildLibrary()
source.add_source(Library({"src": Pattern()}))
def rename_source(lib: BuildLibrary) -> Pattern:
lib.rename("src", "renamed_src")
return Pattern()
source.cells.top = cell(rename_source)(source)
with pytest.raises(BuildError, match='Cannot rename imported source cell "src"'):
source.build()
def test_build_library_rejects_deleting_declared_or_source_cells_during_build() -> None:
declared = BuildLibrary()
declared["leaf"] = Pattern()
def delete_declared(lib: BuildLibrary) -> Pattern:
del lib["leaf"]
return Pattern()
declared.cells.top = cell(delete_declared)(declared)
with pytest.raises(BuildError, match='Cannot delete declared build cell "leaf"'):
declared.build()
source = BuildLibrary()
source.add_source(Library({"src": Pattern()}))
def delete_source(lib: BuildLibrary) -> Pattern:
del lib["src"]
return Pattern()
source.cells.top = cell(delete_source)(source)
with pytest.raises(BuildError, match='Cannot delete imported source cell "src"'):
source.build()