[BuildLibrary] allow prefixing/renaming/postprocessing in BuildCellsView

This commit is contained in:
Jan Petykiewicz 2026-08-28 20:25:14 -07:00
commit 890e7e5c0f
3 changed files with 611 additions and 40 deletions

View file

@ -45,6 +45,16 @@ def main() -> None:
builder = LibraryBuilder()
cells = builder.cells
# Naming and export policy can be bound once without changing individual
# declarations. For example, a foundry-specific group could use:
#
# cells = builder.cells.prefixed(
# 'foundry_',
# facade=lambda proxy: devices.ports_to_data(proxy),
# )
#
# Dynamic names use the same view: cells[generated_name] = cell(factory)(...)
#
# Load some devices from a GDS file
#

View file

@ -2,6 +2,7 @@
from __future__ import annotations
from collections import defaultdict
from collections.abc import Mapping
from dataclasses import dataclass, replace
from functools import wraps
from types import MappingProxyType
@ -16,7 +17,7 @@ from .mapping import Library, LibraryView
from .overlay import OverlayLibrary
if TYPE_CHECKING:
from collections.abc import Callable, Iterator, KeysView, Mapping, Sequence
from collections.abc import Callable, Iterator, KeysView, Sequence
from ..pattern import Pattern
@ -76,8 +77,8 @@ class BuildReport:
@dataclass
class _BuildRecipe:
""" Captured deferred call to a pattern factory. """
func: Callable[..., Pattern]
""" Captured deferred call to a pattern or tree factory. """
func: Callable[..., Pattern | TreeView]
args: tuple[Any, ...]
kwargs: dict[str, Any]
explicit_dependencies: tuple[str, ...] = ()
@ -87,9 +88,16 @@ class _BuildRecipe:
return self
def cell(func: Callable[..., Pattern]) -> Callable[..., _BuildRecipe]:
@dataclass(frozen=True)
class _BuildDeclaration:
"""One deferred or direct declaration plus its implementation transform."""
value: Pattern | TreeView | _BuildRecipe
postprocess: Callable[[Pattern], Pattern] | None = None
def cell(func: Callable[..., Pattern | TreeView]) -> Callable[..., _BuildRecipe]:
"""
Wrap a plain pattern factory so calls return deferred build recipes.
Wrap a plain Pattern or single-top tree factory so calls return deferred build recipes.
Use as either `cell(fn)(...)` or `@cell`.
"""
@ -111,19 +119,138 @@ class _LibraryPlaceholder:
return '<LibraryBuilder.library>'
def _identity_name(name: str) -> str:
return name
def _make_facade(
library: ILibrary,
target: str,
transform: Callable[[Pattern], Pattern],
) -> Pattern:
from ..pattern import Pattern # noqa: PLC0415
implementation = library[target]
proxy = Pattern(ports=implementation.ports).ref(target)
result = transform(proxy)
if not isinstance(result, Pattern):
raise BuildError(f'Facade transform returned {type(result).__name__}, expected Pattern')
return result
class BuildCellsView:
"""
Attribute-based declaration namespace for `LibraryBuilder`.
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
intentionally write-focused: attribute or item assignment and deletion
register declarations, while reads fail with guidance to build first and
use the returned library.
"""
__slots__ = ('_library',)
def __init__(self, library: LibraryBuilder) -> None:
`prefixed()` and `renamed()` create independent write-through views over
the same builder. Their optional `postprocess` callable transforms the
implementation Pattern. If a name changes, their optional `facade`
callable transforms a generated Pattern with copied ports and a reference
to the implementation.
Example:
`cells = builder.cells.prefixed(prefix, facade=encode_ports)`
`cells.device = cell(make_device)(width=10)`
`cells[generated_name] = cell(make_device)(width=20)`
"""
__slots__ = ('_facade', '_library', '_postprocess', '_rename')
def __init__(
self,
library: LibraryBuilder,
*,
rename: Callable[[str], str] = _identity_name,
facade: Callable[[Pattern], Pattern] | None = None,
postprocess: Callable[[Pattern], Pattern] | None = None,
) -> None:
object.__setattr__(self, '_library', library)
object.__setattr__(self, '_rename', rename)
object.__setattr__(self, '_facade', facade)
object.__setattr__(self, '_postprocess', postprocess)
def prefixed(
self,
prefix: str | None,
*,
facade: Callable[[Pattern], Pattern] | None = None,
postprocess: Callable[[Pattern], Pattern] | None = None,
) -> BuildCellsView:
"""
Return an independent write-through view which conditionally prefixes names.
Names which already start with a non-empty `prefix` are left unchanged.
`None` and the empty string select the identity naming policy.
Args:
prefix: Prefix for implementation names.
facade: Transform applied to a generated logical-name proxy when
prefixing changes a name.
postprocess: Transform applied to the implementation Pattern on
each validation or build.
"""
if prefix is not None and not isinstance(prefix, str):
raise TypeError('Build cell prefixes must be strings or None.')
self._validate_transform('facade', facade)
self._validate_transform('postprocess', postprocess)
def add_prefix(name: str) -> str:
if not prefix or name.startswith(prefix):
return name
return prefix + name
return BuildCellsView(
self._library,
rename=add_prefix,
facade=facade,
postprocess=postprocess,
)
def renamed(
self,
rename: Callable[[str], str],
*,
facade: Callable[[Pattern], Pattern] | None = None,
postprocess: Callable[[Pattern], Pattern] | None = None,
) -> BuildCellsView:
"""
Return an independent write-through view using `rename(name)` for output names.
`rename` must be deterministic and return a string. `facade` and
`postprocess` have the same behavior as in `prefixed()`.
"""
if not callable(rename):
raise TypeError('Build cell name transforms must be callable.')
self._validate_transform('facade', facade)
self._validate_transform('postprocess', postprocess)
return BuildCellsView(
self._library,
rename=rename,
facade=facade,
postprocess=postprocess,
)
@staticmethod
def _validate_transform(
description: str,
transform: Callable[[Pattern], Pattern] | None,
) -> None:
if transform is not None and not callable(transform):
raise TypeError(f'Build cell {description} transforms must be callable or None.')
def _physical_name(self, name: str) -> str:
if not isinstance(name, str):
raise TypeError('Build cell names must be strings.')
physical_name = self._rename(name)
if not isinstance(physical_name, str):
raise TypeError(
f'Build cell name transform returned {type(physical_name).__name__}, expected str.'
)
return physical_name
def __getattr__(self, name: str) -> Pattern:
raise BuildError(
@ -131,16 +258,47 @@ class BuildCellsView:
'Call build() and index the returned library instead.'
)
def __setattr__(self, name: str, value: Pattern | _BuildRecipe) -> None:
if name == '_library':
object.__setattr__(self, name, value)
return
self._library[name] = value
def __getitem__(self, name: str) -> Pattern:
raise BuildError(
f'LibraryBuilder.cells[{name!r}] is write-only during authoring. '
'Call build() and index the returned library instead.'
)
def __setitem__(
self,
name: str,
value: Pattern | TreeView | _BuildRecipe,
) -> None:
self._library._assert_editable()
physical_name = self._physical_name(name)
self._library._register_view_declaration(
logical_name=name,
physical_name=physical_name,
value=value,
facade=self._facade,
postprocess=self._postprocess,
)
def __delitem__(self, name: str) -> None:
self._library._assert_editable()
physical_name = self._physical_name(name)
if physical_name != name:
emitted = [physical_name]
if self._facade is not None:
emitted.append(name)
raise BuildError(
f'Cannot delete renamed build declaration {name!r} through this view; '
f'it emitted {emitted}. Delete concrete names through builder.cells[...] instead.'
)
del self._library[name]
def __setattr__(self, name: str, value: Pattern | TreeView | _BuildRecipe) -> None:
self[name] = value
def __delattr__(self, name: str) -> None:
if name == '_library':
if name in self.__slots__:
raise AttributeError(name)
del self._library[name]
del self[name]
class LibraryBuilder(INameView):
@ -148,8 +306,8 @@ class LibraryBuilder(INameView):
Two-phase declaration surface for mixed imported/generated libraries.
A `LibraryBuilder` collects three kinds of inputs:
- direct declared `Pattern` objects
- deferred recipes created with `cell(...)`
- direct declared `Pattern` objects or single-top trees
- deferred Pattern or tree recipes created with `cell(...)`
- imported source-backed library views added with `add_source(...)`
The builder itself is not a normal readable library during authoring.
@ -164,7 +322,7 @@ class LibraryBuilder(INameView):
self._library_placeholder = _LibraryPlaceholder(self)
self._frozen = False
self._building = False
self._declarations: dict[str, Pattern | _BuildRecipe] = {}
self._declarations: dict[str, _BuildDeclaration] = {}
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
self._names: dict[str, None] = {}
@ -194,11 +352,16 @@ class LibraryBuilder(INameView):
def __setitem__(
self,
key: str,
value: Pattern | _BuildRecipe,
value: Pattern | TreeView | _BuildRecipe,
) -> None:
self._assert_editable()
if key in self._names:
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
self._register_declarations(((key, value, None),))
def _prepare_declaration(
self,
value: Pattern | TreeView | _BuildRecipe,
postprocess: Callable[[Pattern], Pattern] | None,
) -> _BuildDeclaration:
from ..pattern import Pattern # noqa: PLC0415
if isinstance(value, _BuildRecipe):
placeholders = (
@ -207,14 +370,57 @@ class LibraryBuilder(INameView):
)
if any(placeholder is not self.library for placeholder in placeholders):
raise BuildError('A recipe cannot use another LibraryBuilder.library placeholder.')
declaration = value
else:
if callable(value):
elif callable(value):
raise TypeError('LibraryBuilder recipes must be wrapped with cell(fn)(...) or @cell.')
declaration = value
elif not isinstance(value, Pattern | Mapping):
raise TypeError(
f'LibraryBuilder declarations must be a Pattern, tree, or cell recipe, not {type(value).__name__}.'
)
return _BuildDeclaration(value=value, postprocess=postprocess)
self._declarations[key] = declaration
self._names[key] = None
def _register_declarations(
self,
entries: Sequence[tuple[str, Pattern | TreeView | _BuildRecipe, Callable[[Pattern], Pattern] | None]],
) -> None:
self._assert_editable()
entry_names = [name for name, _value, _postprocess in entries]
if any(not isinstance(name, str) for name in entry_names):
raise TypeError('Build cell names must be strings.')
if len(set(entry_names)) != len(entry_names):
raise LibraryError(f'Duplicate names in build declaration: {entry_names}')
conflicts = [name for name in entry_names if name in self._names]
if conflicts:
raise LibraryError(f'Build declaration names already exist: {conflicts}')
prepared = [
(name, self._prepare_declaration(value, postprocess))
for name, value, postprocess in entries
]
for name, declaration in prepared:
self._declarations[name] = declaration
self._names[name] = None
def _register_view_declaration(
self,
*,
logical_name: str,
physical_name: str,
value: Pattern | TreeView | _BuildRecipe,
facade: Callable[[Pattern], Pattern] | None,
postprocess: Callable[[Pattern], Pattern] | None,
) -> None:
entries: list[tuple[str, Pattern | TreeView | _BuildRecipe, Callable[[Pattern], Pattern] | None]] = [
(physical_name, value, postprocess),
]
if physical_name != logical_name and facade is not None:
facade_recipe = _BuildRecipe(
func=_make_facade,
args=(self.library, physical_name, facade),
kwargs={},
explicit_dependencies=(physical_name,),
)
entries.append((logical_name, facade_recipe, None))
self._register_declarations(entries)
def __delitem__(self, key: str) -> None:
self._assert_editable()
@ -431,6 +637,23 @@ class LibraryBuilder(INameView):
return session, report
class _NamesWithout(INameView):
"""Name view which temporarily makes one reserved declaration available."""
def __init__(self, names: INameView, omitted: str) -> None:
self._names = names
self._omitted = omitted
def __iter__(self) -> Iterator[str]:
return (name for name in self._names if name != self._omitted)
def __len__(self) -> int:
return len(self._names) - int(self._omitted in self._names)
def __contains__(self, key: object) -> bool:
return key != self._omitted and key in self._names
class _BuildSessionLibrary(ILibrary):
"""
Internal overlay-backed library used while a `LibraryBuilder` is executing.
@ -629,6 +852,75 @@ class _BuildSessionLibrary(ILibrary):
return
raise BuildError(f'Missing dependency "{name}"')
def _apply_postprocess(
self,
name: str,
pattern: Pattern,
postprocess: Callable[[Pattern], Pattern] | None,
) -> Pattern:
from ..pattern import Pattern # noqa: PLC0415
if postprocess is None:
return pattern
result = postprocess(pattern)
if not isinstance(result, Pattern):
raise BuildError(
f'Postprocess transform for "{name}" returned {type(result).__name__}, expected Pattern'
)
return result
def _commit_declared_tree(
self,
name: str,
tree: TreeView,
postprocess: Callable[[Pattern], Pattern] | None,
) -> Pattern:
from ..pattern import map_targets # noqa: PLC0415
view = tree if isinstance(tree, ILibraryView) else LibraryView(tree)
source_order = tuple(view.source_order())
temp = Library(copy.deepcopy({source_name: view[source_name] for source_name in source_order}))
tops = temp.tops()
if len(tops) != 1:
raise BuildError(f'Tree declaration for "{name}" must have exactly one topcell, found {tops}')
old_top = tops[0]
source_order = (old_top, *(source_name for source_name in temp if source_name != old_top))
def rename_tree_source(names: INameView, source_name: str) -> str:
if source_name == old_top:
return name
if source_name in names:
return _rename_patterns(names, source_name)
return source_name
source_to_visible = _plan_source_names(
_NamesWithout(self, name),
source_order,
rename_theirs=rename_tree_source,
rename_when='always',
)
temp.mapping[old_top] = self._apply_postprocess(name, temp[old_top], postprocess)
rename_map = _source_rename_map(source_to_visible)
if rename_map:
target_map = cast('dict[str | None, str | None]', rename_map)
remapped_refs = {
source_name: map_targets(temp[source_name].refs, lambda target: target_map.get(target, target))
for source_name in source_order
}
for source_name, refs in remapped_refs.items():
temp[source_name].refs = refs
for source_name in source_order:
visible_name = source_to_visible[source_name]
self[visible_name] = temp[source_name]
if source_name not in (old_top, visible_name):
self._provenance[visible_name] = replace(
self._provenance[visible_name],
requested_name=source_name,
)
return temp[old_top]
def _ensure_declared(self, name: str) -> None:
from ..pattern import Pattern # noqa: PLC0415
@ -639,30 +931,44 @@ class _BuildSessionLibrary(ILibrary):
raise BuildError(f'Cycle detected while building declared cells: {chain}')
declaration = self._builder._declarations[name]
value = declaration.value
self._declared_stack.append(name)
try:
if isinstance(declaration, _BuildRecipe):
for dep in declaration.explicit_dependencies:
if isinstance(value, _BuildRecipe):
for dep in value.explicit_dependencies:
self._ensure_named(dep)
args = tuple(
self if arg is self._builder.library else arg
for arg in declaration.args
for arg in value.args
)
kwargs = {
key: self if value is self._builder.library else value
for key, value in declaration.kwargs.items()
key: self if arg_value is self._builder.library else arg_value
for key, arg_value in value.kwargs.items()
}
pattern = declaration.func(*args, **kwargs)
if not isinstance(pattern, Pattern):
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern') # noqa: TRY301
result = value.func(*args, **kwargs)
else:
pattern = declaration.deepcopy()
result = value.deepcopy() if isinstance(value, Pattern) else value
if isinstance(result, Pattern):
published_pattern = result
pattern = self._apply_postprocess(name, result, declaration.postprocess)
elif isinstance(result, Mapping):
pattern = self._commit_declared_tree(name, result, declaration.postprocess)
published_pattern = pattern
else:
raise BuildError( # noqa: TRY301
f'Recipe for "{name}" returned {type(result).__name__}, expected Pattern or single-top tree'
)
if name in self._overlay:
if self._overlay[name] is not pattern:
existing = self._overlay[name]
if existing is not published_pattern:
raise BuildError( # noqa: TRY301
f'Recipe for "{name}" wrote a different pattern into the session under its own name.'
)
if existing is not pattern:
del self._overlay[name]
self._overlay[name] = pattern
else:
self[name] = pattern
self._built.add(name)

View file

@ -21,6 +21,7 @@ from ..library import (
)
from ..pattern import Pattern
from ..ports import Port
from ..utils.ports2data import ports_to_data
def _owned_by(report: BuildReport, owner: str) -> set[str]:
@ -832,3 +833,257 @@ def test_build_session_subtree_preserves_type_and_builds_deferred_dependencies()
assert child_calls == 1
assert set(built) == {"top", "child", "leaf", "unrelated"}
def test_build_cells_prefixed_view_generates_facades_for_attributes_and_items() -> None:
builder = LibraryBuilder()
cells = builder.cells.prefixed(
"vendor_",
facade=lambda proxy: ports_to_data(proxy, (10, 2)),
)
cells.fixed = cell(Pattern)(ports={"p": Port((1, 2), 0)})
dynamic_name = "dynamic"
cells[dynamic_name] = cell(Pattern)(ports={"q": Port((3, 4), 0)})
built, report = builder.build(output="library")
assert set(built) == {"vendor_fixed", "fixed", "vendor_dynamic", "dynamic"}
assert set(built["fixed"].refs) == {"vendor_fixed"}
assert set(built["dynamic"].refs) == {"vendor_dynamic"}
assert set(built["fixed"].ports) == {"p"}
assert len(built["fixed"].labels[(10, 2)]) == 1
assert not built["vendor_fixed"].labels
assert report.dependency_graph["fixed"] == frozenset({"vendor_fixed"})
assert report.dependency_graph["dynamic"] == frozenset({"vendor_dynamic"})
def test_build_cells_prefixed_view_postprocesses_only_the_implementation() -> None:
source = Pattern(ports={"p": Port((0, 0), 0)})
builder = LibraryBuilder()
physical = builder.cells.prefixed(
"vendor_",
postprocess=lambda pattern: ports_to_data(pattern, (20, 1)),
)
physical["cell"] = source
unprefixed = builder.cells.prefixed(
None,
postprocess=lambda pattern: ports_to_data(pattern, (21, 1)),
)
unprefixed["local"] = Pattern(ports={"q": Port((1, 1), 0)})
built, _report = builder.build(output="library")
assert set(built) == {"vendor_cell", "local"}
assert len(built["vendor_cell"].labels[(20, 1)]) == 1
assert len(built["local"].labels[(21, 1)]) == 1
assert not source.labels
def test_build_cells_postprocess_precedes_facade_port_copy() -> None:
builder = LibraryBuilder()
def add_port(pattern: Pattern) -> Pattern:
pattern.ports["added"] = Port((5, 6), 0)
return pattern
cells = builder.cells.prefixed(
"impl_",
postprocess=add_port,
facade=lambda pattern: ports_to_data(pattern, (30, 0)),
)
cells.device = Pattern()
built, _report = builder.build(output="library")
assert set(built["impl_device"].ports) == {"added"}
assert set(built["device"].ports) == {"added"}
assert built["device"].labels[(30, 0)][0].string.startswith("added:")
def test_build_cells_facade_is_ignored_when_name_does_not_change() -> None:
builder = LibraryBuilder()
def fail_if_called(_pattern: Pattern) -> Pattern:
raise AssertionError("facade should not be called")
cells = builder.cells.prefixed("vendor_", facade=fail_if_called)
cells["vendor_cell"] = Pattern()
built, _report = builder.build(output="library")
assert set(built) == {"vendor_cell"}
def test_build_cells_renamed_views_are_independent_and_accept_dynamic_names() -> None:
builder = LibraryBuilder()
upper = builder.cells.renamed(str.upper)
suffixed = builder.cells.renamed(lambda name: name + "_v2")
builder.cells.plain = Pattern()
upper["upper"] = Pattern()
suffixed["other"] = Pattern()
built, _report = builder.build(output="library")
assert set(built) == {"plain", "UPPER", "other_v2"}
def test_build_cells_policy_registration_is_atomic() -> None:
builder = LibraryBuilder()
builder.cells.public = Pattern()
prefixed = builder.cells.prefixed("impl_", facade=lambda pattern: pattern)
with pytest.raises(LibraryError, match="already exist"):
prefixed.public = Pattern()
assert set(builder) == {"public"}
assert "impl_public" not in builder
def test_build_cells_policy_views_validate_names_and_transforms() -> None:
builder = LibraryBuilder()
invalid_name = builder.cells.renamed(lambda _name: None) # type: ignore[arg-type,return-value]
with pytest.raises(TypeError, match="expected str"):
invalid_name["cell"] = Pattern()
with pytest.raises(TypeError, match="must be callable"):
builder.cells.prefixed("x_", facade=object()) # type: ignore[arg-type]
assert not builder
def test_build_cells_view_deletion_requires_concrete_names_after_rename() -> None:
builder = LibraryBuilder()
prefixed = builder.cells.prefixed("impl_", facade=lambda pattern: pattern)
prefixed["cell"] = Pattern()
with pytest.raises(BuildError, match="Delete concrete names"):
del prefixed["cell"]
del builder.cells["cell"]
del builder.cells["impl_cell"]
builder.cells["plain"] = Pattern()
del builder.cells["plain"]
assert not builder
def test_build_cells_item_reads_remain_write_only() -> None:
builder = LibraryBuilder()
builder.cells["cell"] = Pattern()
with pytest.raises(BuildError, match="write-only"):
_ = builder.cells["cell"]
def test_build_cells_prefixed_view_normalizes_recipe_tree_and_generates_facade() -> None:
builder = LibraryBuilder()
builder.cells["_helper"] = Pattern()
cells = builder.cells.prefixed(
"impl_",
postprocess=lambda pattern: Pattern(ports={"out": Port((7, 8), 0)}, refs=pattern.refs),
facade=lambda pattern: ports_to_data(pattern, (40, 0)),
)
def make_tree() -> Library:
tree = Library({"_helper": Pattern(), "old_top": Pattern()})
tree["old_top"].ref("_helper")
return tree
cells.tree = cell(make_tree)()
built, report = builder.build(output="library")
assert {"_helper", "impl_tree", "tree"} <= set(built)
helper_targets = set(built["impl_tree"].refs)
assert "_helper" not in helper_targets
assert len(helper_targets) == 1
renamed_helper = helper_targets.pop()
assert renamed_helper in built
assert report.provenance[renamed_helper].kind == "helper"
assert report.provenance[renamed_helper].requested_name == "_helper"
assert report.provenance[renamed_helper].owner_declared_name == "impl_tree"
assert set(built["tree"].ports) == {"out"}
assert set(built["tree"].refs) == {"impl_tree"}
def test_build_cells_view_accepts_direct_tree_and_rejects_multiple_tops() -> None:
builder = LibraryBuilder()
builder.cells["tree"] = Library({"old_top": Pattern()})
built, _report = builder.build(output="library")
assert set(built) == {"tree"}
invalid = LibraryBuilder()
invalid.cells["tree"] = cell(lambda: Library({"one": Pattern(), "two": Pattern()}))()
with pytest.raises(BuildError, match="exactly one topcell"):
invalid.validate()
def test_build_cells_tree_declared_name_wins_over_single_use_helper() -> None:
builder = LibraryBuilder()
def make_tree() -> Library:
top = Pattern()
top.ref("_device")
return Library({"_device": Pattern(), "old_top": top})
builder.cells["_device"] = cell(make_tree)()
built, report = builder.build(output="library")
assert "_device" in built
assert set(built["_device"].refs) != {"_device"}
assert len(built["_device"].refs) == 1
helper_name = next(iter(built["_device"].refs))
assert helper_name in built
assert report.provenance["_device"].kind == "declared"
assert report.provenance["_device"].requested_name == "_device"
assert report.provenance[helper_name].kind == "helper"
assert report.provenance[helper_name].requested_name == "_device"
assert report.provenance[helper_name].owner_declared_name == "_device"
def test_build_cells_tree_declared_name_rejects_public_helper_conflict() -> None:
builder = LibraryBuilder()
def make_tree() -> Library:
top = Pattern()
top.ref("device")
return Library({"device": Pattern(), "old_top": top})
builder.cells["device"] = cell(make_tree)()
with pytest.raises(BuildError, match="Unresolved duplicate key"):
builder.validate()
del builder.cells["device"]
builder.cells["device"] = Pattern()
builder.validate()
def test_build_cells_replacement_postprocess_updates_self_published_recipe_and_facade() -> None:
builder = LibraryBuilder()
cells = builder.cells.prefixed(
"impl_",
postprocess=lambda _pattern: Pattern(ports={"new": Port((5, 6), 0)}),
facade=lambda pattern: ports_to_data(pattern, (50, 0)),
)
def make_device(lib: ILibrary) -> Pattern:
original = Pattern(ports={"old": Port((1, 2), 0)})
lib["impl_device"] = original
return original
cells.device = cell(make_device)(builder.library)
built, report = builder.build(output="library")
assert set(built) == {"impl_device", "device"}
assert set(built["impl_device"].ports) == {"new"}
assert set(built["device"].ports) == {"new"}
assert built["device"].labels[(50, 0)][0].string.startswith("new:")
assert report.provenance["impl_device"].kind == "declared"
assert report.dependency_graph["device"] == frozenset({"impl_device"})
def test_build_cells_rejects_unrelated_self_published_pattern() -> None:
builder = LibraryBuilder()
def make_device(lib: ILibrary) -> Pattern:
lib["device"] = Pattern()
return Pattern()
builder.cells.device = cell(make_device)(builder.library)
with pytest.raises(BuildError, match="wrote a different pattern"):
builder.validate()