[BuildLibrary] eliminate BuiltLibrary and BuiltOverlayLibrary
This commit is contained in:
parent
0fba187d8c
commit
3dea61b05e
6 changed files with 95 additions and 191 deletions
|
|
@ -22,7 +22,7 @@ Contents
|
||||||
- [library](library.py)
|
- [library](library.py)
|
||||||
* Continue from `devices.py` by declaring a mixed library with `BuildLibrary`
|
* Continue from `devices.py` by declaring a mixed library with `BuildLibrary`
|
||||||
* Import source-backed GDS cells and register python-generated recipes together
|
* Import source-backed GDS cells and register python-generated recipes together
|
||||||
* Call `build()` to produce a normal library for downstream `Pather` usage and writing
|
* Call `build()` to produce a normal library and report 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()`
|
||||||
- [pather](pather.py)
|
- [pather](pather.py)
|
||||||
* Use `Pather` to route individual wires and wire bundles
|
* Use `Pather` to route individual wires and wire bundles
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,9 @@ def main() -> None:
|
||||||
# Build the declaration set into a normal library.
|
# Build the declaration set into a normal library.
|
||||||
#
|
#
|
||||||
|
|
||||||
built = builder.build()
|
built, report = builder.build()
|
||||||
print('Built library contains:\n' + pformat(list(built.keys())))
|
print('Built library contains:\n' + pformat(list(built.keys())))
|
||||||
|
print('Build dependency graph:\n' + pformat(report.dependency_graph))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Continue designing against the built library.
|
# Continue designing against the built library.
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ 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,
|
BuildLibrary as BuildLibrary,
|
||||||
BuildReport as BuildReport,
|
BuildReport as BuildReport,
|
||||||
CellProvenance as CellProvenance,
|
CellProvenance as CellProvenance,
|
||||||
|
|
|
||||||
|
|
@ -549,20 +549,6 @@ 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):
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,6 @@ 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)
|
@dataclass(frozen=True)
|
||||||
class CellProvenance:
|
class CellProvenance:
|
||||||
|
|
@ -83,29 +80,22 @@ class CellProvenance:
|
||||||
chosen.
|
chosen.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
final_name: Name exposed by the completed library.
|
|
||||||
requested_name: First name requested for this cell during the build.
|
requested_name: First name requested for this cell during the build.
|
||||||
kind: Whether the cell came from a declaration, helper emission, or an
|
kind: Whether the cell came from a declaration, helper emission, or an
|
||||||
imported source library.
|
imported source library.
|
||||||
owner_declared_name: Declared cell responsible for this output cell, if
|
owner_declared_name: Declared cell responsible for this output cell, if
|
||||||
any. Imported source cells leave this as `None`.
|
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
|
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.
|
renamed_from: Original requested name when the final name differs.
|
||||||
source_name: Original on-source name for imported cells.
|
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
|
requested_name: str
|
||||||
kind: Literal['declared', 'helper', 'source']
|
kind: Literal['declared', 'helper', 'source']
|
||||||
owner_declared_name: str | None
|
owner_declared_name: str | None
|
||||||
emitted_via: emitted_via_t
|
|
||||||
build_chain: tuple[str, ...]
|
build_chain: tuple[str, ...]
|
||||||
renamed_from: str | None = None
|
renamed_from: str | None = None
|
||||||
source_name: str | None = None
|
source_name: str | None = None
|
||||||
source_metadata: dict[str, Any] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -121,15 +111,11 @@ class BuildReport:
|
||||||
requested_roots: Roots explicitly requested for the run. A full
|
requested_roots: Roots explicitly requested for the run. A full
|
||||||
`build()` uses all declared cells.
|
`build()` uses all declared cells.
|
||||||
provenance: Mapping from final output name to provenance metadata.
|
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
|
dependency_graph: Declared-cell dependency graph discovered through
|
||||||
library-mediated reads and explicit recipe hints.
|
library-mediated reads and explicit recipe hints.
|
||||||
"""
|
"""
|
||||||
requested_roots: tuple[str, ...]
|
requested_roots: tuple[str, ...]
|
||||||
provenance: Mapping[str, CellProvenance]
|
provenance: Mapping[str, CellProvenance]
|
||||||
owned_cells: Mapping[str, tuple[str, ...]]
|
|
||||||
dependency_graph: Mapping[str, frozenset[str]]
|
dependency_graph: Mapping[str, frozenset[str]]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1467,25 +1453,6 @@ 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:
|
class _CellFactory:
|
||||||
"""
|
"""
|
||||||
Adapter that turns a plain pattern factory into a deferred recipe factory.
|
Adapter that turns a plain pattern factory into a deferred recipe factory.
|
||||||
|
|
@ -1516,18 +1483,6 @@ class _BuildRecipe:
|
||||||
return self
|
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)
|
@dataclass(frozen=True)
|
||||||
class _SourceDeclaration:
|
class _SourceDeclaration:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1596,16 +1551,15 @@ class BuildLibrary(ILibrary):
|
||||||
The builder itself is not a normal readable library during authoring.
|
The builder itself is not a normal readable library during authoring.
|
||||||
Instead, `validate()` and `build()` create a temporary build-session library
|
Instead, `validate()` and `build()` create a temporary build-session library
|
||||||
that recipes can read from and write helper cells into while dependencies
|
that recipes can read from and write helper cells into while dependencies
|
||||||
are resolved. `build()` then freezes the builder on success and returns a
|
are resolved. `build()` then freezes the builder on success and returns the
|
||||||
normal library-like object carrying a `build_report`.
|
built library plus a `BuildReport`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, check_on_register: bool = False) -> None:
|
def __init__(self, *, check_on_register: bool = False) -> None:
|
||||||
self.check_on_register = check_on_register
|
self.check_on_register = check_on_register
|
||||||
self.cells = BuildCellsView(self)
|
self.cells = BuildCellsView(self)
|
||||||
self.last_build_report: BuildReport | None = None
|
|
||||||
self._frozen = False
|
self._frozen = False
|
||||||
self._declarations: dict[str, _PatternDeclaration | _RecipeDeclaration] = {}
|
self._declarations: dict[str, 'Pattern | _BuildRecipe'] = {}
|
||||||
self._sources: list[_SourceDeclaration] = []
|
self._sources: list[_SourceDeclaration] = []
|
||||||
self._names: set[str] = set()
|
self._names: set[str] = set()
|
||||||
self._order: list[str] = []
|
self._order: list[str] = []
|
||||||
|
|
@ -1664,13 +1618,12 @@ class BuildLibrary(ILibrary):
|
||||||
if key in self._names:
|
if key in self._names:
|
||||||
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
||||||
|
|
||||||
declaration: _PatternDeclaration | _RecipeDeclaration
|
|
||||||
if isinstance(value, _BuildRecipe):
|
if isinstance(value, _BuildRecipe):
|
||||||
declaration = _RecipeDeclaration(value)
|
declaration = value
|
||||||
else:
|
else:
|
||||||
if callable(value):
|
if callable(value):
|
||||||
raise TypeError('BuildLibrary recipes must be wrapped with cell(fn)(...) or @cell.')
|
raise TypeError('BuildLibrary recipes must be wrapped with cell(fn)(...) or @cell.')
|
||||||
declaration = _PatternDeclaration(value)
|
declaration = value
|
||||||
|
|
||||||
self._declarations[key] = declaration
|
self._declarations[key] = declaration
|
||||||
self._names.add(key)
|
self._names.add(key)
|
||||||
|
|
@ -1813,6 +1766,8 @@ 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 self._active_session() is not None:
|
||||||
|
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)
|
||||||
|
|
@ -1862,8 +1817,7 @@ class BuildLibrary(ILibrary):
|
||||||
execution path used by `build()`. Any generated library is discarded
|
execution path used by `build()`. Any generated library is discarded
|
||||||
after validation completes.
|
after validation completes.
|
||||||
"""
|
"""
|
||||||
report, _output = self._run_build(names=names, output='overlay', allow_dangling=allow_dangling, persist_output=False)
|
_session, report = self._run_build(names=names, allow_dangling=allow_dangling)
|
||||||
self.last_build_report = report
|
|
||||||
return report
|
return report
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
|
|
@ -1871,9 +1825,9 @@ class BuildLibrary(ILibrary):
|
||||||
*,
|
*,
|
||||||
output: Literal['overlay', 'library'] = 'overlay',
|
output: Literal['overlay', 'library'] = 'overlay',
|
||||||
allow_dangling: bool = False,
|
allow_dangling: bool = False,
|
||||||
) -> 'BuiltLibrary | ILibrary':
|
) -> tuple[ILibrary, BuildReport]:
|
||||||
"""
|
"""
|
||||||
Materialize declarations and return a usable output library.
|
Materialize declarations and return a usable output library plus report.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
output: `'overlay'` preserves imported source-backed cells where
|
output: `'overlay'` preserves imported source-backed cells where
|
||||||
|
|
@ -1882,20 +1836,23 @@ class BuildLibrary(ILibrary):
|
||||||
allow_dangling: If `False`, fail the build when the completed
|
allow_dangling: If `False`, fail the build when the completed
|
||||||
library still contains dangling references.
|
library still contains dangling references.
|
||||||
"""
|
"""
|
||||||
|
if output not in ('overlay', 'library'):
|
||||||
|
raise ValueError(f'Unknown build output mode: {output!r}')
|
||||||
self._assert_editable()
|
self._assert_editable()
|
||||||
report, built_output = self._run_build(names=None, output=output, allow_dangling=allow_dangling, persist_output=True)
|
session, report = self._run_build(names=None, allow_dangling=allow_dangling)
|
||||||
|
if output == 'library':
|
||||||
|
built_output = session.to_library()
|
||||||
|
else:
|
||||||
|
built_output = session.to_overlay()
|
||||||
self._frozen = True
|
self._frozen = True
|
||||||
self.last_build_report = report
|
return built_output, report
|
||||||
return built_output
|
|
||||||
|
|
||||||
def _run_build(
|
def _run_build(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
names: Sequence[str] | None,
|
names: Sequence[str] | None,
|
||||||
output: Literal['overlay', 'library'],
|
|
||||||
allow_dangling: bool,
|
allow_dangling: bool,
|
||||||
persist_output: bool,
|
) -> tuple['_BuildSessionLibrary', BuildReport]:
|
||||||
) -> tuple[BuildReport, BuiltLibrary | ILibrary | None]:
|
|
||||||
roots = tuple(dict.fromkeys(names if names is not None else self._declarations.keys()))
|
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]
|
unknown = [name for name in roots if name not in self._names]
|
||||||
if unknown:
|
if unknown:
|
||||||
|
|
@ -1909,19 +1866,11 @@ class BuildLibrary(ILibrary):
|
||||||
session.materialize_many(roots)
|
session.materialize_many(roots)
|
||||||
if not allow_dangling:
|
if not allow_dangling:
|
||||||
session.child_graph(dangling='error')
|
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:
|
finally:
|
||||||
_ACTIVE_BUILD_SESSIONS.reset(token)
|
_ACTIVE_BUILD_SESSIONS.reset(token)
|
||||||
|
|
||||||
report = session.build_report(roots)
|
report = session.build_report(roots)
|
||||||
if built_output is not None:
|
return session, report
|
||||||
built_output.build_report = report
|
|
||||||
return report, built_output
|
|
||||||
|
|
||||||
|
|
||||||
class _BuildSessionLibrary(ILibrary):
|
class _BuildSessionLibrary(ILibrary):
|
||||||
|
|
@ -1935,22 +1884,19 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, builder: BuildLibrary) -> None:
|
def __init__(self, builder: BuildLibrary) -> None:
|
||||||
from .file.gdsii_lazy_core import BuiltOverlayLibrary, _SourceEntry, _SourceLayer # noqa: PLC0415
|
from .file.gdsii_lazy_core import OverlayLibrary, _SourceEntry, _SourceLayer # noqa: PLC0415
|
||||||
|
|
||||||
self._builder = builder
|
self._builder = builder
|
||||||
self._overlay = BuiltOverlayLibrary()
|
self._overlay = OverlayLibrary()
|
||||||
self._source_entry_type = _SourceEntry
|
self._source_entry_type = _SourceEntry
|
||||||
self._source_layer_type = _SourceLayer
|
self._source_layer_type = _SourceLayer
|
||||||
self._states: dict[str, Literal['unbuilt', 'building', 'built']] = {
|
self._states: dict[str, Literal['unbuilt', 'building', 'built']] = {
|
||||||
name: 'unbuilt' for name in builder._declarations
|
name: 'unbuilt' for name in builder._declarations
|
||||||
}
|
}
|
||||||
self._declared_stack: list[str] = []
|
self._declared_stack: list[str] = []
|
||||||
self._emission_stack: list[str] = []
|
|
||||||
self._emission_via_stack: list[emitted_via_t] = []
|
|
||||||
self._names = set(builder._names)
|
self._names = set(builder._names)
|
||||||
self._order = list(builder._order)
|
self._order = list(builder._order)
|
||||||
self._provenance: dict[str, CellProvenance] = {}
|
self._provenance: dict[str, CellProvenance] = {}
|
||||||
self._owned_cells: defaultdict[str, list[str]] = defaultdict(list)
|
|
||||||
self._dependency_graph: defaultdict[str, set[str]] = defaultdict(set)
|
self._dependency_graph: defaultdict[str, set[str]] = defaultdict(set)
|
||||||
self._install_sources()
|
self._install_sources()
|
||||||
|
|
||||||
|
|
@ -1962,11 +1908,9 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
visible_to_source = dict(spec.visible_to_source),
|
visible_to_source = dict(spec.visible_to_source),
|
||||||
child_graph = {name: set(children) for name, children in spec.child_graph.items()},
|
child_graph = {name: set(children) for name, children in spec.child_graph.items()},
|
||||||
order = list(spec.order),
|
order = list(spec.order),
|
||||||
)
|
)
|
||||||
layer_index = len(self._overlay._layers)
|
layer_index = len(self._overlay._layers)
|
||||||
self._overlay._layers.append(layer)
|
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():
|
for source_name, visible_name in spec.source_to_visible.items():
|
||||||
self._overlay._entries[visible_name] = self._source_entry_type(
|
self._overlay._entries[visible_name] = self._source_entry_type(
|
||||||
|
|
@ -1976,15 +1920,12 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
if visible_name not in self._overlay._order:
|
if visible_name not in self._overlay._order:
|
||||||
self._overlay._order.append(visible_name)
|
self._overlay._order.append(visible_name)
|
||||||
self._provenance[visible_name] = CellProvenance(
|
self._provenance[visible_name] = CellProvenance(
|
||||||
final_name = visible_name,
|
|
||||||
requested_name = source_name,
|
requested_name = source_name,
|
||||||
kind = 'source',
|
kind = 'source',
|
||||||
owner_declared_name = None,
|
owner_declared_name = None,
|
||||||
emitted_via = 'source_import',
|
|
||||||
build_chain = (),
|
build_chain = (),
|
||||||
renamed_from = source_name if visible_name != source_name else None,
|
renamed_from = source_name if visible_name != source_name else None,
|
||||||
source_name = source_name,
|
source_name = source_name,
|
||||||
source_metadata = source_meta,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
|
@ -2020,14 +1961,6 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
if provenance is not None and provenance.kind == 'source':
|
if provenance is not None and provenance.kind == 'source':
|
||||||
raise BuildError(f'Cannot {operation} imported source cell "{key}" during an active build session.')
|
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(
|
def rename(
|
||||||
self,
|
self,
|
||||||
old_name: str,
|
old_name: str,
|
||||||
|
|
@ -2056,16 +1989,8 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
requested_name = provenance.requested_name
|
requested_name = provenance.requested_name
|
||||||
self._provenance[new_name] = replace(
|
self._provenance[new_name] = replace(
|
||||||
provenance,
|
provenance,
|
||||||
final_name = new_name,
|
|
||||||
renamed_from = requested_name if new_name != requested_name else None,
|
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
|
return self
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> 'Pattern':
|
def __getitem__(self, key: str) -> 'Pattern':
|
||||||
|
|
@ -2090,28 +2015,19 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
self._touch_name(key)
|
self._touch_name(key)
|
||||||
|
|
||||||
kind: Literal['declared', 'helper']
|
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:
|
if current is not None and key == current:
|
||||||
kind = 'declared'
|
kind = 'declared'
|
||||||
via = 'declaration'
|
|
||||||
else:
|
else:
|
||||||
kind = 'helper'
|
kind = 'helper'
|
||||||
if not self._emission_via_stack:
|
|
||||||
via = 'helper_write'
|
|
||||||
|
|
||||||
self._emission_stack.append(key)
|
self._record_provenance(
|
||||||
try:
|
name = key,
|
||||||
self._record_provenance(
|
requested_name = key,
|
||||||
final_name = key,
|
kind = kind,
|
||||||
requested_name = key,
|
owner_declared_name = current if kind == 'helper' else key,
|
||||||
kind = kind,
|
build_chain = tuple(self._declared_stack),
|
||||||
owner_declared_name = current if kind == 'helper' else key,
|
renamed_from = None,
|
||||||
emitted_via = via,
|
)
|
||||||
build_chain = tuple(self._declared_stack),
|
|
||||||
renamed_from = None,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
self._emission_stack.pop()
|
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
def __delitem__(self, key: str) -> None:
|
||||||
if key not in self._overlay:
|
if key not in self._overlay:
|
||||||
|
|
@ -2120,15 +2036,12 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
|
|
||||||
self._guard_mutable_output_name(key, operation='delete')
|
self._guard_mutable_output_name(key, operation='delete')
|
||||||
provenance = self._provenance.get(key)
|
|
||||||
if key in self._overlay:
|
if key in self._overlay:
|
||||||
del self._overlay[key]
|
del self._overlay[key]
|
||||||
self._names.discard(key)
|
self._names.discard(key)
|
||||||
if key in self._order:
|
if key in self._order:
|
||||||
self._order.remove(key)
|
self._order.remove(key)
|
||||||
self._provenance.pop(key, None)
|
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:
|
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
|
||||||
self[key_self] = copy.deepcopy(other[key_other])
|
self[key_self] = copy.deepcopy(other[key_other])
|
||||||
|
|
@ -2139,11 +2052,7 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
|
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
|
||||||
mutate_other: bool = False,
|
mutate_other: bool = False,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
self._emission_via_stack.append('tree_merge')
|
rename_map = super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
||||||
try:
|
|
||||||
rename_map = super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
|
||||||
finally:
|
|
||||||
self._emission_via_stack.pop()
|
|
||||||
|
|
||||||
current = self._current_declared()
|
current = self._current_declared()
|
||||||
for old_name, new_name in rename_map.items():
|
for old_name, new_name in rename_map.items():
|
||||||
|
|
@ -2159,32 +2068,24 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
def _record_provenance(
|
def _record_provenance(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
final_name: str,
|
name: str,
|
||||||
requested_name: str,
|
requested_name: str,
|
||||||
kind: Literal['declared', 'helper'],
|
kind: Literal['declared', 'helper'],
|
||||||
owner_declared_name: str | None,
|
owner_declared_name: str | None,
|
||||||
emitted_via: emitted_via_t,
|
|
||||||
build_chain: tuple[str, ...],
|
build_chain: tuple[str, ...],
|
||||||
renamed_from: str | None,
|
renamed_from: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._provenance[final_name] = CellProvenance(
|
self._provenance[name] = CellProvenance(
|
||||||
final_name = final_name,
|
|
||||||
requested_name = requested_name,
|
requested_name = requested_name,
|
||||||
kind = kind,
|
kind = kind,
|
||||||
owner_declared_name = owner_declared_name,
|
owner_declared_name = owner_declared_name,
|
||||||
emitted_via = emitted_via,
|
|
||||||
build_chain = build_chain,
|
build_chain = build_chain,
|
||||||
renamed_from = renamed_from,
|
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:
|
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)
|
chain = tuple(self._declared_stack)
|
||||||
msg = [f'Failed while building declared cell "{name}"']
|
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:
|
if chain:
|
||||||
msg.append(f'Dependency chain: {" -> ".join(chain)}')
|
msg.append(f'Dependency chain: {" -> ".join(chain)}')
|
||||||
msg.append(f'Cause: {exc}')
|
msg.append(f'Cause: {exc}')
|
||||||
|
|
@ -2213,14 +2114,14 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
self._states[name] = 'building'
|
self._states[name] = 'building'
|
||||||
self._declared_stack.append(name)
|
self._declared_stack.append(name)
|
||||||
try:
|
try:
|
||||||
if isinstance(declaration, _PatternDeclaration):
|
if isinstance(declaration, _BuildRecipe):
|
||||||
pattern = declaration.pattern.deepcopy()
|
for dep in declaration.explicit_dependencies:
|
||||||
else:
|
|
||||||
for dep in declaration.recipe.explicit_dependencies:
|
|
||||||
self._ensure_named(dep)
|
self._ensure_named(dep)
|
||||||
pattern = declaration.recipe.func(*declaration.recipe.args, **declaration.recipe.kwargs)
|
pattern = declaration.func(*declaration.args, **declaration.kwargs)
|
||||||
if not isinstance(pattern, Pattern):
|
if not isinstance(pattern, Pattern):
|
||||||
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern')
|
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern')
|
||||||
|
else:
|
||||||
|
pattern = declaration.deepcopy()
|
||||||
|
|
||||||
if name in self._overlay:
|
if name in self._overlay:
|
||||||
if self._overlay[name] is not pattern:
|
if self._overlay[name] is not pattern:
|
||||||
|
|
@ -2261,23 +2162,18 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
for name in self._builder._declarations
|
for name in self._builder._declarations
|
||||||
if name in self._dependency_graph or name in requested_roots
|
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(
|
return BuildReport(
|
||||||
requested_roots = tuple(dict.fromkeys(requested_roots)),
|
requested_roots = tuple(dict.fromkeys(requested_roots)),
|
||||||
provenance = dict(self._provenance),
|
provenance = dict(self._provenance),
|
||||||
owned_cells = owned_cells,
|
|
||||||
dependency_graph = dependency_graph,
|
dependency_graph = dependency_graph,
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_overlay(self) -> ILibrary:
|
def to_overlay(self) -> ILibrary:
|
||||||
return self._overlay
|
return self._overlay
|
||||||
|
|
||||||
def to_library(self) -> BuiltLibrary:
|
def to_library(self) -> Library:
|
||||||
mapping = {name: self._overlay[name] for name in self._overlay.source_order()}
|
mapping = {name: self._overlay[name] for name in self._overlay.source_order()}
|
||||||
return BuiltLibrary(mapping)
|
return Library(mapping)
|
||||||
|
|
||||||
|
|
||||||
class LazyLibrary(ILibrary):
|
class LazyLibrary(ILibrary):
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,18 @@ import pytest
|
||||||
|
|
||||||
from ..builder import Pather
|
from ..builder import Pather
|
||||||
from ..error import BuildError
|
from ..error import BuildError
|
||||||
from ..library import BuildLibrary, BuiltLibrary, Library, cell
|
from ..library import BuildLibrary, Library, cell
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
|
|
||||||
|
|
||||||
|
def _owned_by(report, owner: str) -> set[str]:
|
||||||
|
return {
|
||||||
|
name for name, prov in report.provenance.items()
|
||||||
|
if prov.owner_declared_name == owner
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
||||||
builder = BuildLibrary()
|
builder = BuildLibrary()
|
||||||
|
|
||||||
|
|
@ -19,12 +26,12 @@ def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
||||||
builder.cells.parent = cell(make_parent)(builder)
|
builder.cells.parent = cell(make_parent)(builder)
|
||||||
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
||||||
|
|
||||||
built = builder.build()
|
built, report = builder.build()
|
||||||
|
|
||||||
assert "parent" in built
|
assert "parent" in built
|
||||||
assert "child" in built
|
assert "child" in built
|
||||||
assert built.build_report.dependency_graph["parent"] == frozenset({"child"})
|
assert report.dependency_graph["parent"] == frozenset({"child"})
|
||||||
assert built.build_report.provenance["parent"].kind == "declared"
|
assert report.provenance["parent"].kind == "declared"
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
||||||
|
|
@ -40,17 +47,15 @@ def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None
|
||||||
return top
|
return top
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder)
|
||||||
built = builder.build()
|
_built, report = builder.build()
|
||||||
report = built.build_report
|
|
||||||
|
|
||||||
helpers = [
|
helpers = [
|
||||||
prov for prov in report.provenance.values()
|
prov for prov in report.provenance.values()
|
||||||
if prov.owner_declared_name == "top" and prov.kind == "helper"
|
if prov.owner_declared_name == "top" and prov.kind == "helper"
|
||||||
]
|
]
|
||||||
|
|
||||||
assert "top" in report.owned_cells["top"]
|
assert "top" in _owned_by(report, "top")
|
||||||
assert len(helpers) == 2
|
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)
|
assert any(prov.renamed_from == "_helper" for prov in helpers)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -64,10 +69,10 @@ def test_build_library_requires_build_session_for_reads_and_freezes_after_build(
|
||||||
with pytest.raises(BuildError, match="write-only"):
|
with pytest.raises(BuildError, match="write-only"):
|
||||||
_ = builder.cells.leaf
|
_ = builder.cells.leaf
|
||||||
|
|
||||||
built = builder.build(output="library")
|
built, report = builder.build(output="library")
|
||||||
|
|
||||||
assert isinstance(built, BuiltLibrary)
|
assert isinstance(built, Library)
|
||||||
assert built.build_report.requested_roots == ("leaf",)
|
assert report.requested_roots == ("leaf",)
|
||||||
|
|
||||||
with pytest.raises(BuildError, match="frozen"):
|
with pytest.raises(BuildError, match="frozen"):
|
||||||
builder["later"] = Pattern()
|
builder["later"] = Pattern()
|
||||||
|
|
@ -134,6 +139,14 @@ def test_build_library_validate_rejects_removed_output_argument() -> None:
|
||||||
builder.validate(output="library") # type: ignore[call-arg]
|
builder.validate(output="library") # type: ignore[call-arg]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_rejects_unknown_build_output_mode() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder["leaf"] = Pattern()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Unknown build output mode"):
|
||||||
|
builder.build(output="bad") # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_allows_helper_writes_via_pather() -> None:
|
def test_build_library_allows_helper_writes_via_pather() -> None:
|
||||||
builder = BuildLibrary()
|
builder = BuildLibrary()
|
||||||
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
|
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
|
||||||
|
|
@ -147,9 +160,9 @@ def test_build_library_allows_helper_writes_via_pather() -> None:
|
||||||
return top
|
return top
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder)
|
||||||
built = builder.build()
|
_built, report = builder.build()
|
||||||
|
|
||||||
helper_prov = built.build_report.provenance["_route"]
|
helper_prov = report.provenance["_route"]
|
||||||
assert helper_prov.kind == "helper"
|
assert helper_prov.kind == "helper"
|
||||||
assert helper_prov.owner_declared_name == "top"
|
assert helper_prov.owner_declared_name == "top"
|
||||||
|
|
||||||
|
|
@ -160,11 +173,23 @@ def test_build_library_preserves_source_cells_and_records_source_provenance() ->
|
||||||
builder.add_source(source)
|
builder.add_source(source)
|
||||||
builder.cells.top = cell(lambda: Pattern())()
|
builder.cells.top = cell(lambda: Pattern())()
|
||||||
|
|
||||||
built = builder.build()
|
built, report = builder.build()
|
||||||
|
|
||||||
assert "src" in built
|
assert "src" in built
|
||||||
assert built.build_report.provenance["src"].kind == "source"
|
assert report.provenance["src"].kind == "source"
|
||||||
assert built.build_report.provenance["src"].emitted_via == "source_import"
|
|
||||||
|
|
||||||
|
def test_build_library_rejects_add_source_during_build() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
|
||||||
|
def make_top(lib: BuildLibrary) -> Pattern:
|
||||||
|
lib.add_source(Library({"src": Pattern()}))
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder)
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match="add_source"):
|
||||||
|
builder.build()
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_can_rename_imported_source_cells_during_authoring() -> None:
|
def test_build_library_can_rename_imported_source_cells_during_authoring() -> None:
|
||||||
|
|
@ -178,12 +203,12 @@ def test_build_library_can_rename_imported_source_cells_during_authoring() -> No
|
||||||
builder.add_source(source)
|
builder.add_source(source)
|
||||||
builder.rename("child", "renamed_child")
|
builder.rename("child", "renamed_child")
|
||||||
|
|
||||||
built = builder.build()
|
built, report = builder.build()
|
||||||
|
|
||||||
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 built.build_report.provenance["renamed_child"].source_name == "child"
|
assert report.provenance["renamed_child"].source_name == "child"
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_move_references_for_source_rename() -> None:
|
def test_build_library_rejects_move_references_for_source_rename() -> None:
|
||||||
|
|
@ -202,7 +227,7 @@ def test_build_library_rejects_renaming_declared_cells_during_authoring() -> Non
|
||||||
builder.rename("declared", "renamed_declared")
|
builder.rename("declared", "renamed_declared")
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_helper_rename_updates_provenance_and_owned_cells() -> None:
|
def test_build_library_helper_rename_updates_provenance_owner() -> None:
|
||||||
builder = BuildLibrary()
|
builder = BuildLibrary()
|
||||||
|
|
||||||
def make_top(lib: BuildLibrary) -> Pattern:
|
def make_top(lib: BuildLibrary) -> Pattern:
|
||||||
|
|
@ -213,18 +238,17 @@ def test_build_library_helper_rename_updates_provenance_and_owned_cells() -> Non
|
||||||
return top
|
return top
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder)
|
||||||
built = builder.build()
|
built, report = builder.build()
|
||||||
report = built.build_report
|
|
||||||
|
|
||||||
assert "final_helper" in built
|
assert "final_helper" in built
|
||||||
assert "_helper" not in built
|
assert "_helper" not in built
|
||||||
assert "final_helper" in report.owned_cells["top"]
|
owned = _owned_by(report, "top")
|
||||||
assert "_helper" not in report.owned_cells["top"]
|
assert "final_helper" in owned
|
||||||
|
assert "_helper" not in owned
|
||||||
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"
|
assert prov.renamed_from == "_helper"
|
||||||
assert prov.final_name == "final_helper"
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
||||||
|
|
@ -236,12 +260,11 @@ def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
||||||
return Pattern()
|
return Pattern()
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder)
|
||||||
built = builder.build()
|
built, report = builder.build()
|
||||||
report = built.build_report
|
|
||||||
|
|
||||||
assert "_helper" not in built
|
assert "_helper" not in built
|
||||||
assert "_helper" not in report.provenance
|
assert "_helper" not in report.provenance
|
||||||
assert report.owned_cells["top"] == ("top",)
|
assert _owned_by(report, "top") == {"top"}
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
|
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
|
||||||
|
|
@ -258,8 +281,7 @@ def test_build_library_helper_rename_after_auto_rename_preserves_requested_name(
|
||||||
return top
|
return top
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder)
|
||||||
built = builder.build()
|
built, report = builder.build()
|
||||||
report = built.build_report
|
|
||||||
|
|
||||||
assert "final_helper" in built
|
assert "final_helper" in built
|
||||||
prov = report.provenance["final_helper"]
|
prov = report.provenance["final_helper"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue