masque/masque/test/test_library.py

1210 lines
40 KiB
Python

import pytest
from collections.abc import Mapping, MutableMapping
from typing import cast, TYPE_CHECKING
from numpy.testing import assert_allclose
from ..library import (
IBorrowing, INameView, IMaterializable, LayerMappedView, Library, LibraryView,
LazyLibrary, OverlayLibrary, PortLoadView,
)
from ..pattern import Pattern
from ..error import LibraryError, PatternError
from ..ports import Port
from ..repetition import Grid
from ..shapes import Arc, Ellipse, Path, Text
from ..file.utils import preflight, preflight_source_aware
if TYPE_CHECKING:
from ..shapes import Polygon
def test_library_basic() -> None:
lib = Library()
pat = Pattern()
lib["cell1"] = pat
assert isinstance(lib, INameView)
assert "cell1" in lib
assert lib["cell1"] is pat
assert len(lib) == 1
with pytest.raises(LibraryError):
lib["cell1"] = Pattern() # Overwriting not allowed
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary, OverlayLibrary])
def test_writable_libraries_are_restricted_mappings(
library_cls: type[Library] | type[LazyLibrary] | type[OverlayLibrary],
) -> None:
lib = library_cls()
assert isinstance(lib, Mapping)
assert not isinstance(lib, MutableMapping)
for method in ("update", "setdefault", "pop", "popitem", "clear"):
assert not hasattr(lib, method)
lib["top"] = Pattern()
del lib["top"]
assert not lib
@pytest.mark.parametrize('cached', [False, True])
def test_overlay_reuses_names_and_keeps_new_sources_independent(cached: bool) -> None:
source = Library({'a': Pattern(), 'parent': Pattern().ref('a')})
overlay = OverlayLibrary()
overlay.add_source(source)
if cached:
overlay['parent']
overlay.rename('a', 'b', move_references=True)
overlay.rename('b', 'a', move_references=True)
assert overlay.child_graph()['parent'] == {'a'}
assert set(overlay.materialize('parent', persist=False).refs) == {'a'}
overlay.rename('a', 'b', move_references=True)
overlay.add_source(Library({'a': Pattern(), 'new_parent': Pattern().ref('a')}))
assert overlay.child_graph()['parent'] == {'b'}
assert overlay.child_graph()['new_parent'] == {'a'}
assert set(overlay['new_parent'].refs) == {'a'}
assert set(source['parent'].refs) == {'a'}
def test_overlay_reuses_dangling_targets_and_preserves_failed_rename() -> None:
overlay = OverlayLibrary()
overlay.add_source(Library({'parent': Pattern().ref('missing')}))
overlay.move_references('missing', 'other')
overlay.move_references('other', 'missing')
assert set(overlay['parent'].refs) == {'missing'}
overlay['used'] = Pattern()
before = list(overlay)
with pytest.raises(LibraryError, match='already exists'):
overlay.rename('parent', 'used', move_references=True)
assert list(overlay) == before
assert set(overlay['parent'].refs) == {'missing'}
del overlay['used']
overlay.rename('parent', 'used')
assert list(overlay) == ['used']
def test_library_tops() -> None:
lib = Library()
lib["child"] = Pattern()
lib["parent"] = Pattern()
lib["parent"].ref("child")
assert set(lib.tops()) == {"parent"}
assert lib.top() == "parent"
def test_empty_ref_buckets_do_not_create_hierarchy_edges() -> None:
parent = Pattern()
parent.refs["ghost"]
parent.refs["parent"]
lib = Library({"parent": parent, "ghost": Pattern()})
assert not parent.has_refs()
assert parent.referenced_patterns() == set()
assert set(lib.tops()) == {"parent", "ghost"}
assert lib.child_graph() == {"parent": set(), "ghost": set()}
assert lib.parent_graph() == {"parent": set(), "ghost": set()}
lib.dfs(parent, hierarchy=("parent",))
flat = lib.flatten("parent")["parent"]
assert not flat.refs
def test_library_dangling() -> None:
lib = Library()
lib["parent"] = Pattern()
lib["parent"].ref("missing")
assert lib.dangling_refs() == {"missing"}
def test_library_reachability_ignores_unnamed_refs() -> None:
pattern = Pattern()
pattern.ref(None)
lib = Library({"top": pattern})
assert pattern.referenced_patterns() == {None}
assert lib.referenced_patterns() == set()
assert lib.dangling_refs() == set()
def test_library_dangling_graph_modes() -> None:
lib = Library()
lib["parent"] = Pattern()
lib["parent"].ref("missing")
with pytest.raises(LibraryError, match="Dangling refs found"):
lib.child_graph()
with pytest.raises(LibraryError, match="Dangling refs found"):
lib.parent_graph()
with pytest.raises(LibraryError, match="Dangling refs found"):
lib.child_order()
assert lib.child_graph(dangling="ignore") == {"parent": set()}
assert lib.parent_graph(dangling="ignore") == {"parent": set()}
assert lib.child_order(dangling="ignore") == ["parent"]
assert lib.child_graph(dangling="include") == {"parent": {"missing"}, "missing": set()}
assert lib.parent_graph(dangling="include") == {"parent": set(), "missing": {"parent"}}
assert lib.child_order(dangling="include") == ["missing", "parent"]
@pytest.mark.parametrize(
("method", "args"),
[
("child_graph", ()),
("parent_graph", ()),
("child_order", ()),
("find_refs_local", ("top",)),
("find_refs_global", ("top",)),
("prune_empty", ()),
],
)
def test_library_rejects_unknown_dangling_mode(method: str, args: tuple[object, ...]) -> None:
lib = Library({"top": Pattern()})
with pytest.raises(ValueError, match="dangling-reference mode"):
getattr(lib, method)(*args, dangling="typo")
def test_find_refs_with_dangling_modes() -> None:
lib = Library()
lib["target"] = Pattern()
mid = Pattern()
mid.ref("target", offset=(2, 0))
lib["mid"] = mid
top = Pattern()
top.ref("mid", offset=(5, 0))
top.ref("missing", offset=(9, 0))
lib["top"] = top
assert lib.find_refs_local("missing", dangling="ignore") == {}
assert lib.find_refs_global("missing", dangling="ignore") == {}
local_missing = lib.find_refs_local("missing", dangling="include")
assert set(local_missing) == {"top"}
assert_allclose(local_missing["top"][0], [[9, 0, 0, 0, 1]])
global_missing = lib.find_refs_global("missing", dangling="include")
assert_allclose(global_missing[("top", "missing")], [[9, 0, 0, 0, 1]])
with pytest.raises(LibraryError, match="missing"):
lib.find_refs_local("missing")
with pytest.raises(LibraryError, match="missing"):
lib.find_refs_global("missing")
global_target = lib.find_refs_global("target")
assert_allclose(global_target[("top", "mid", "target")], [[7, 0, 0, 0, 1]])
def test_find_refs_global_includes_composed_scale_column() -> None:
lib = Library({"leaf": Pattern(), "child": Pattern(), "top": Pattern()})
lib["child"].ref("leaf", scale=2)
lib["top"].ref("child", scale=3)
transforms = lib.find_refs_global("leaf")
assert_allclose(transforms[("top", "child", "leaf")], [[0, 0, 0, 0, 6]])
def test_preflight_prune_empty_preserves_dangling_policy(caplog: pytest.LogCaptureFixture) -> None:
def make_lib() -> Library:
lib = Library()
lib["empty"] = Pattern()
lib["top"] = Pattern()
lib["top"].ref("missing")
return lib
caplog.set_level("WARNING")
warned = preflight(make_lib(), allow_dangling_refs=None, prune_empty_patterns=True)
assert "empty" not in warned
assert any("Dangling refs found" in record.message for record in caplog.records)
allowed = preflight(make_lib(), allow_dangling_refs=True, prune_empty_patterns=True)
assert "empty" not in allowed
with pytest.raises(LibraryError, match="Dangling refs found"):
preflight(make_lib(), allow_dangling_refs=False, prune_empty_patterns=True)
def test_preflight_source_aware_skips_source_backed_patterns() -> None:
source = Library()
source["copied"] = Pattern()
source["materialized"] = Pattern()
mapped_names: list[str] = []
def map_layer(layer: int | tuple[int, int] | str) -> int | tuple[int, int] | str:
mapped_names.append(str(layer))
return layer
view = LayerMappedView(source, map_layer, copy_through=True)
view["materialized"].rect("named", xmin=0, xmax=1, ymin=0, ymax=1)
result = preflight_source_aware(view, allow_named_layers=True)
assert isinstance(result, OverlayLibrary)
assert mapped_names == []
assert result.source_cell("copied") is not None
assert result.source_cell("materialized") is None
assert not source["materialized"].shapes
assert "named" in result["materialized"].shapes
def test_preflight_source_aware_checks_only_cells_without_provenance() -> None:
source = Library()
source["copied"] = Pattern().rect("copied_layer", xmin=0, xmax=1, ymin=0, ymax=1)
source["materialized"] = Pattern().rect("materialized_layer", xmin=0, xmax=1, ymin=0, ymax=1)
view = LayerMappedView(source, lambda layer: layer, copy_through=True)
preflight_source_aware(view, allow_named_layers=False)
view["materialized"]
with pytest.raises(PatternError, match="materialized_layer"):
preflight_source_aware(view, allow_named_layers=False)
def test_preflight_source_aware_prunes_only_safe_checked_patterns() -> None:
source = Library({"child": Pattern(), "parent": Pattern()})
source["parent"].ref("child")
view = LayerMappedView(source, lambda layer: layer, copy_through=True)
view["child"]
result = preflight_source_aware(view, prune_empty_patterns=True)
assert isinstance(result, OverlayLibrary)
assert "child" in result
assert result.source_cell("parent") is not None
view["parent"]
result = preflight_source_aware(view, prune_empty_patterns=True)
assert "child" not in result
assert "parent" not in result
assert "child" in source
assert "child" in source["parent"].refs
def test_preflight_source_aware_wraps_only_checked_patterns() -> None:
source = Library({"copied": Pattern(), "materialized": Pattern()})
for name in source:
source[name].rect((1, 0), xmin=0, xmax=1, ymin=0, ymax=1)
source[name].shapes[(1, 0)][0].repetition = Grid(a_vector=(10, 0), a_count=2)
view = LayerMappedView(source, lambda layer: layer, copy_through=True)
view["materialized"]
result = preflight_source_aware(view, wrap_repeated_shapes=True)
assert isinstance(result, OverlayLibrary)
assert result.source_cell("copied") is not None
assert source["copied"].shapes[(1, 0)][0].repetition is not None
assert result["materialized"].shapes[(1, 0)] == []
wrapper_names = result["materialized"].referenced_patterns()
assert len(wrapper_names) == 1
wrapper_name = next(iter(wrapper_names))
assert wrapper_name is not None
assert result[wrapper_name].shapes[(1, 0)][0].repetition is None
def test_library_flatten() -> None:
lib = Library()
child = Pattern()
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
lib["child"] = child
parent = Pattern()
parent.ref("child", offset=(10, 10))
lib["parent"] = parent
flat_lib = lib.flatten("parent")
flat_parent = flat_lib["parent"]
assert not flat_parent.has_refs()
assert len(flat_parent.shapes[(1, 0)]) == 1
# Transformations are baked into vertices for Polygon
assert_vertices = cast("Polygon", flat_parent.shapes[(1, 0)][0]).vertices
assert tuple(assert_vertices[0]) == (10.0, 10.0)
def test_pattern_polygon_traversal_ignores_empty_dangling_bucket() -> None:
child = Pattern()
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
parent = Pattern()
parent.ref("child")
parent.refs["missing"]
lib = Library({"child": child, "parent": parent})
polygons = parent.layer_as_polygons((1, 0), flatten=True, library=lib)
assert len(polygons) == 1
def test_recursive_geometry_rejects_reference_cycles() -> None:
lib = Library({"a": Pattern(), "b": Pattern()})
lib["a"].ref("b")
lib["b"].ref("a")
with pytest.raises(PatternError, match=r"calculating bounds: .* -> .* ->"):
lib["a"].get_bounds(library=lib)
with pytest.raises(PatternError, match=r"collecting layer polygons: .* -> .* ->"):
lib["a"].layer_as_polygons((1, 0), library=lib)
with pytest.raises(PatternError, match=r"visualizing pattern hierarchy: .* -> .* ->"):
lib["a"].visualize(library=lib)
with pytest.raises(PatternError, match="Circular reference"):
lib["a"].deepcopy().flatten(library=lib)
def test_nonrecursive_geometry_allows_reference_cycles() -> None:
lib = Library({"loop": Pattern()})
lib["loop"].ref("loop")
assert lib["loop"].get_bounds(library=lib, recurse=False) is None
assert lib["loop"].layer_as_polygons((1, 0), flatten=False, library=lib) == []
def test_library_flatten_preserves_ports_only_child() -> None:
lib = Library()
child = Pattern(ports={"P1": Port((1, 2), 0)})
lib["child"] = child
parent = Pattern()
parent.ref("child", offset=(10, 10))
lib["parent"] = parent
flat_parent = lib.flatten("parent", flatten_ports=True)["parent"]
assert set(flat_parent.ports) == {"P1"}
assert cast("Port", flat_parent.ports["P1"]).rotation == 0
assert tuple(flat_parent.ports["P1"].offset) == (11.0, 12.0)
def test_library_flatten_repeated_ref_with_ports_raises() -> None:
lib = Library()
child = Pattern(ports={"P1": Port((1, 2), 0)})
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
lib["child"] = child
parent = Pattern()
parent.ref("child", repetition=Grid(a_vector=(10, 0), a_count=2))
lib["parent"] = parent
with pytest.raises(PatternError, match='Cannot flatten ports from repeated ref'):
lib.flatten("parent", flatten_ports=True)
def test_library_flatten_dangling_ok_nested_preserves_dangling_refs() -> None:
lib = Library()
child = Pattern()
child.ref("missing")
lib["child"] = child
parent = Pattern()
parent.ref("child")
lib["parent"] = parent
flat = lib.flatten("parent", dangling_ok=True)
assert set(flat["child"].refs) == {"missing"}
assert flat["child"].has_refs()
assert set(flat["parent"].refs) == {"missing"}
assert flat["parent"].has_refs()
def test_lazy_library() -> None:
lib = LazyLibrary()
called = 0
def make_pat() -> Pattern:
nonlocal called
called += 1
return Pattern()
lib["lazy"] = make_pat
assert called == 0
pat = lib["lazy"]
assert called == 1
assert isinstance(pat, Pattern)
# Second access should be cached
pat2 = lib["lazy"]
assert called == 1
assert pat is pat2
def test_lazy_library_reachability_loads_only_reachable_cells() -> None:
lib = LazyLibrary()
calls = {"top": 0, "child": 0, "unused": 0}
def make(name: str, target: str | None = None) -> Pattern:
calls[name] += 1
pattern = Pattern()
if target is not None:
pattern.ref(target)
return pattern
lib["top"] = lambda: make("top", "child")
lib["child"] = lambda: make("child")
lib["unused"] = lambda: make("unused")
assert lib.referenced_patterns("top") == {"child"}
assert calls == {"top": 1, "child": 1, "unused": 0}
def test_abstract_view_membership_does_not_materialize_lazy_cells() -> None:
lib = LazyLibrary()
calls = 0
def make_pat() -> Pattern:
nonlocal calls
calls += 1
return Pattern()
lib["lazy"] = make_pat
abstracts = lib.abstract_view()
assert "lazy" in abstracts
assert "missing" not in abstracts
assert calls == 0
def test_library_rename() -> None:
lib = Library()
lib["old"] = Pattern()
lib["parent"] = Pattern()
lib["parent"].ref("old")
lib.rename("old", "new", move_references=True)
assert "old" not in lib
assert "new" in lib
assert "new" in lib["parent"].refs
assert "old" not in lib["parent"].refs
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
def test_library_rename_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
lib = library_cls()
lib["top"] = Pattern()
lib["parent"] = Pattern()
lib["parent"].ref("top")
lib.rename("top", "top", move_references=True)
assert set(lib.keys()) == {"top", "parent"}
assert "top" in lib["parent"].refs
assert len(lib["parent"].refs["top"]) == 1
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
def test_library_rename_top_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
lib = library_cls()
lib["top"] = Pattern()
lib.rename_top("top")
assert list(lib.keys()) == ["top"]
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
def test_library_rename_missing_raises_library_error(library_cls: type[Library] | type[LazyLibrary]) -> None:
lib = library_cls()
lib["top"] = Pattern()
with pytest.raises(LibraryError, match="does not exist"):
lib.rename("missing", "new")
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
def test_library_move_references_same_target_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
lib = library_cls()
lib["top"] = Pattern()
lib["parent"] = Pattern()
lib["parent"].ref("top")
lib.move_references("top", "top")
assert "top" in lib["parent"].refs
assert len(lib["parent"].refs["top"]) == 1
def test_library_dfs_can_replace_existing_patterns() -> None:
lib = Library()
child = Pattern()
lib["child"] = child
top = Pattern()
top.ref("child")
lib["top"] = top
replacement_top = Pattern(ports={"T": Port((1, 2), 0)})
replacement_child = Pattern(ports={"C": Port((3, 4), 0)})
def visit_after(pattern: Pattern, hierarchy: tuple[str | None, ...], **kwargs) -> Pattern: # noqa: ARG001
if hierarchy[-1] == "child":
return replacement_child
if hierarchy[-1] == "top":
return replacement_top
return pattern
lib.dfs(lib["top"], visit_after=visit_after, hierarchy=("top",), transform=True)
assert lib["top"] is replacement_top
assert lib["child"] is replacement_child
def test_lazy_library_dfs_can_replace_existing_patterns() -> None:
lib = LazyLibrary()
lib["child"] = lambda: Pattern()
lib["top"] = lambda: Pattern(refs={"child": []})
top = lib["top"]
top.ref("child")
replacement_top = Pattern(ports={"T": Port((1, 2), 0)})
replacement_child = Pattern(ports={"C": Port((3, 4), 0)})
def visit_after(pattern: Pattern, hierarchy: tuple[str | None, ...], **kwargs) -> Pattern: # noqa: ARG001
if hierarchy[-1] == "child":
return replacement_child
if hierarchy[-1] == "top":
return replacement_top
return pattern
lib.dfs(top, visit_after=visit_after, hierarchy=("top",), transform=True)
assert lib["top"] is replacement_top
assert lib["child"] is replacement_child
def test_library_add_no_duplicates_respects_mutate_other_false() -> None:
src_pat = Pattern(ports={"A": Port((0, 0), 0)})
lib = Library({"a": Pattern()})
lib.add({"b": src_pat}, mutate_other=False)
assert lib["b"] is not src_pat
lib["b"].ports["A"].offset[0] = 123
assert tuple(src_pat.ports["A"].offset) == (0.0, 0.0)
def test_library_add_returns_only_renamed_entries() -> None:
lib = Library({"a": Pattern(), "_shape": Pattern()})
assert lib.add({"b": Pattern(), "c": Pattern()}, mutate_other=False) == {}
rename_map = lib.add({"_shape": Pattern(), "keep": Pattern()}, mutate_other=False)
assert set(rename_map) == {"_shape"}
assert rename_map["_shape"] != "_shape"
assert "keep" not in rename_map
def test_library_add_name_failure_is_atomic() -> None:
destination = Library({"x": Pattern(), "y": Pattern()})
source_parent = Pattern()
source_parent.ref("x")
source = Library({"x": Pattern(), "y": source_parent})
with pytest.raises(LibraryError, match="Unresolved duplicate"):
destination.add(source, rename_theirs=lambda _lib, _name: "z", mutate_other=True)
assert set(destination) == {"x", "y"}
assert set(source) == {"x", "y"}
assert set(source["y"].refs) == {"x"}
def test_library_add_callback_sees_earlier_name_reservations() -> None:
destination = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
callback_views: list[set[str]] = []
def rename(view: INameView, _name: str) -> str:
assert isinstance(view, INameView)
assert not isinstance(view, Mapping)
assert not hasattr(view, "__getitem__")
callback_views.append(set(view))
return view.get_name("_shape")
rename_map = destination.add(source, rename_theirs=rename)
assert len(set(rename_map.values())) == 2
assert rename_map["_shape$A"] in callback_views[1]
assert set(rename_map.values()) <= set(destination)
def test_library_can_add_itself_with_mutate_other() -> None:
lib = Library({"_helper": Pattern()})
rename_map = lib.add(lib, mutate_other=True)
assert rename_map["_helper"] in lib
assert len(lib) == 2
def test_overlay_add_source_callback_sees_earlier_name_reservations() -> None:
overlay = OverlayLibrary()
overlay["_shape$A"] = Pattern()
overlay["_shape$B"] = Pattern()
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
rename_map = overlay.add_source(
source,
rename_theirs=lambda view, _name: view.get_name("_shape"),
)
assert len(set(rename_map.values())) == 2
assert set(rename_map.values()) <= set(overlay)
def test_overlay_add_source_renames_conflicting_single_use_name_by_default() -> None:
overlay = OverlayLibrary()
overlay['_myCellName$F'] = Pattern()
source = Library({'_myCellName$F': Pattern(), 'source_top': Pattern()})
source['source_top'].ref('_myCellName$F')
rename_map = overlay.add_source(source)
assert rename_map == {'_myCellName$F': '_myCellName'}
assert set(overlay['source_top'].refs) == {'_myCellName'}
def test_overlay_add_source_can_explicitly_disable_default_renaming() -> None:
overlay = OverlayLibrary()
overlay['_helper$A'] = Pattern()
with pytest.raises(LibraryError, match='Conflicting name'):
overlay.add_source(Library({'_helper$A': Pattern()}), rename_theirs=None)
def test_port_load_view_detaches_already_materialized_overlay_pattern() -> None:
overlay = OverlayLibrary()
overlay.add_source(Library({"top": Pattern()}))
raw = overlay["top"]
processed = PortLoadView(
overlay,
ports={"top": {"P": Port((1, 2), 0)}},
)
processed_top = processed["top"]
assert processed_top is not raw
assert not raw.ports
assert set(processed_top.ports) == {"P"}
assert not hasattr(processed, "close")
def test_layer_mapped_view_detaches_and_maps_shapes_and_labels() -> None:
import masque
child = Pattern()
source_pattern = Pattern(ports={'P': Port((1, 2), 0, ptype='wire')})
source_pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
source_pattern.polygon('B', vertices=[[2, 0], [3, 0], [2, 1]])
source_pattern.label('TEXT', string='port', offset=(1, 2))
source_pattern.ref('child')
source_pattern.annotations['note'] = ['unchanged']
source = Library({'child': child, 'top': source_pattern})
mapped = LayerMappedView(
source,
lambda layer: {'A': 'DRAW', 'B': 'DRAW', 'TEXT': 'LABEL'}.get(layer, layer),
)
mapped_top = mapped['top']
assert mapped_top is not source_pattern
assert set(mapped_top.shapes) == {'DRAW'}
assert len(mapped_top.shapes['DRAW']) == 2
assert set(mapped_top.labels) == {'LABEL'}
assert set(mapped_top.ports) == {'P'}
assert set(mapped_top.refs) == {'child'}
assert mapped_top.annotations == {'note': ['unchanged']}
assert set(source_pattern.shapes) == {'A', 'B'}
assert set(source_pattern.labels) == {'TEXT'}
assert mapped.child_graph() == source.child_graph()
assert mapped.source_order() == source.source_order()
assert masque.LayerMappedView is LayerMappedView
assert masque.PortLoadView is PortLoadView
assert not hasattr(masque, 'PortsLibraryView')
assert not hasattr(masque.library, 'PortsLibraryView')
def test_layer_mapped_view_preflight_rejects_none_layer() -> None:
pattern = Pattern()
pattern.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
mapped = LayerMappedView(Library({'top': pattern}), lambda _layer: None) # type: ignore[arg-type]
with pytest.raises(PatternError, match=r"returned None for source layer \(1, 0\)"):
preflight_source_aware(mapped)
def test_layer_mapped_view_materialization_and_copy_through() -> None:
pattern = Pattern()
pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
source = Library({'top': pattern})
mapped = LayerMappedView(source, lambda _layer: 'B')
passthrough = LayerMappedView(source, lambda _layer: 'B', copy_through=True)
assert mapped.source_cell('top') is None
assert passthrough.source_cell('top') == (source, 'top')
transient = passthrough.materialize('top', persist=False)
assert set(transient.shapes) == {'B'}
assert passthrough.source_cell('top') == (source, 'top')
persistent = passthrough['top']
assert passthrough['top'] is persistent
assert passthrough.source_cell('top') is None
assert set(persistent.shapes) == {'B'}
def test_layer_mapped_view_composes_with_ports_and_overlay() -> None:
pattern = Pattern()
pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
source = Library({'top': pattern})
ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}})
mapped = LayerMappedView(ports, lambda _layer: 'B')
overlay = OverlayLibrary()
overlay.add_source(mapped)
top = overlay['top']
assert set(top.shapes) == {'B'}
assert set(top.ports) == {'P'}
assert mapped.borrowed_sources() == (ports,)
assert not pattern.ports
def test_library_subtree() -> None:
lib = Library()
lib["a"] = Pattern()
lib["b"] = Pattern()
lib["c"] = Pattern()
lib["a"].ref("b")
sub = lib.subtree("a")
assert isinstance(sub, Library)
assert "a" in sub
assert "b" in sub
assert "c" not in sub
assert sub["a"] is lib["a"]
del sub["b"]
assert "b" in lib
def test_lazy_library_subtree_preserves_type_and_shares_materialized_patterns() -> None:
lib = LazyLibrary()
child = Pattern()
top = Pattern()
top.ref("child")
lib["child"] = lambda: child
lib["top"] = lambda: top
lib["unused"] = Pattern()
subtree = lib.subtree("top")
assert isinstance(subtree, LazyLibrary)
assert set(subtree) == {"child", "top"}
assert subtree["top"] is lib["top"]
del subtree["child"]
assert "child" in lib
def test_overlay_subtree_preserves_type_sources_and_independent_materialization() -> None:
source = Library({"child": Pattern(), "top": Pattern(), "unused": Pattern()})
source["top"].ref("child")
overlay = OverlayLibrary()
overlay.add_source(source)
subtree = overlay.subtree("top")
assert isinstance(subtree, OverlayLibrary)
assert set(subtree) == {"child", "top"}
assert subtree.borrowed_sources() == overlay.borrowed_sources()
subtree_top = subtree["top"]
overlay_top = overlay["top"]
assert subtree_top is not overlay_top
shared_subtree = overlay.subtree("top")
assert shared_subtree["top"] is overlay_top
del shared_subtree["child"]
assert "child" in overlay
def test_overlay_subtree_preserves_reference_remaps() -> None:
source = Library({"leaf": Pattern(), "parent": Pattern()})
source["parent"].ref("leaf")
overlay = OverlayLibrary()
overlay.add_source(source)
overlay.rename("leaf", "renamed", move_references=True)
subtree = overlay.subtree("parent")
assert isinstance(subtree, OverlayLibrary)
assert set(subtree) == {"parent", "renamed"}
assert set(subtree["parent"].refs) == {"renamed"}
def test_read_only_subtree_is_a_borrowed_subtree_view() -> None:
source = Library({"child": Pattern(), "top": Pattern(), "unused": Pattern()})
source["top"].ref("child")
view = LibraryView(source)
subtree = view.subtree("top")
assert subtree.source_order() == ("child", "top")
assert subtree["top"] is source["top"]
assert "unused" not in subtree
with pytest.raises(KeyError):
_ = subtree["unused"]
def test_library_materialization_and_borrowing_capabilities() -> None:
lazy = LazyLibrary()
lazy["top"] = lambda: Pattern()
assert isinstance(lazy, IMaterializable)
assert not isinstance(lazy, IBorrowing)
assert not hasattr(lazy, "borrowed_sources")
transient = lazy.materialize("top", persist=False)
assert "top" not in lazy.cache
assert transient is not lazy["top"]
overlay = OverlayLibrary()
overlay.add_source(lazy)
ports = PortLoadView(overlay)
subtree = ports.subtree("top")
assert isinstance(overlay, IMaterializable)
assert isinstance(overlay, IBorrowing)
assert isinstance(ports, IMaterializable)
assert isinstance(ports, IBorrowing)
assert isinstance(subtree, IMaterializable)
assert isinstance(subtree, IBorrowing)
assert overlay.borrowed_sources() == (lazy,)
assert ports.borrowed_sources() == (overlay,)
assert subtree.borrowed_sources() == (ports,)
eager = Library({"top": Pattern()})
plain_view = LibraryView(lazy)
assert not isinstance(eager, IMaterializable | IBorrowing)
assert not isinstance(plain_view, IMaterializable | IBorrowing)
def test_detached_materialization_default_is_owned_and_deduplicated() -> None:
shared_a = Pattern()
shared_b = Pattern()
lazy = LazyLibrary()
lazy['a'] = lambda: shared_a
lazy['b'] = lambda: shared_b
detached = lazy.materialize_many_detached(('b', 'a', 'b'))
assert tuple(detached) == ('b', 'a')
assert detached['a'] is not shared_a
assert detached['b'] is not shared_b
detached['a'].polygon('L', vertices=[[0, 0], [1, 0], [0, 1]])
assert not shared_a.shapes
assert not lazy.cache
def test_nested_detached_views_copy_plain_source_once(monkeypatch: pytest.MonkeyPatch) -> None:
source_pattern = Pattern()
source_pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
source = Library({'top': source_pattern})
ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}})
mapped = LayerMappedView(ports, lambda _layer: 'B')
overlay = OverlayLibrary()
overlay.add_source(mapped)
original_deepcopy = Pattern.deepcopy
copied: list[Pattern] = []
def count_deepcopy(pattern: Pattern) -> Pattern:
copied.append(pattern)
return original_deepcopy(pattern)
monkeypatch.setattr(Pattern, 'deepcopy', count_deepcopy)
detached = overlay.materialize_detached('top')
assert copied == [source_pattern]
assert set(detached.shapes) == {'B'}
assert set(detached.ports) == {'P'}
assert not source_pattern.ports
assert not ports._cache
assert not mapped._cache
assert overlay.source_cell('top') == (mapped, 'top')
def test_nested_detached_views_copy_cached_pattern_once(monkeypatch: pytest.MonkeyPatch) -> None:
source_pattern = Pattern()
source = Library({'top': source_pattern})
ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}})
cached = ports['top']
mapped = LayerMappedView(ports, lambda layer: layer)
original_deepcopy = Pattern.deepcopy
copied: list[Pattern] = []
def count_deepcopy(pattern: Pattern) -> Pattern:
copied.append(pattern)
return original_deepcopy(pattern)
monkeypatch.setattr(Pattern, 'deepcopy', count_deepcopy)
detached = mapped.materialize_detached('top')
assert copied == [cached]
assert detached is not cached
detached.ports.clear()
assert set(cached.ports) == {'P'}
def test_borrowed_source_cell_tracks_persistent_materialization() -> None:
source = Library({"top": Pattern()})
source.library_info = {"name": "not-forwarded"} # type: ignore[attr-defined]
processed = PortLoadView(source)
assert processed.source_cell("top") == (source, "top")
assert not hasattr(processed, "library_info")
assert not hasattr(processed, "raw_struct_bytes")
_ = processed.materialize_many(("top",), persist=False)
assert processed.source_cell("top") == (source, "top")
subtree = processed.subtree("top")
assert subtree.source_cell("top") == (processed, "top")
assert not hasattr(subtree, "library_info")
_ = subtree["top"]
assert processed.source_cell("top") is None
assert subtree.source_cell("top") == (processed, "top")
def test_overlay_source_cell_tracks_names_references_and_materialization() -> None:
source = Library({"leaf": Pattern(), "parent": Pattern()})
source["parent"].ref("leaf")
overlay = OverlayLibrary()
overlay.add_source(source)
assert overlay.source_cell("parent") == (source, "parent")
overlay.rename("parent", "renamed_parent")
assert overlay.source_cell("renamed_parent") == (source, "parent")
overlay.rename("leaf", "renamed_leaf", move_references=True)
assert overlay.source_cell("renamed_parent") is None
materialized = OverlayLibrary()
materialized.add_source(source)
_ = materialized["parent"]
assert materialized.source_cell("parent") is None
@pytest.mark.parametrize("materialize_parent", [False, True])
@pytest.mark.parametrize("move_references", [False, True])
def test_overlay_rename_is_independent_of_materialization(
materialize_parent: bool,
move_references: bool,
) -> None:
source = Library({"leaf": Pattern(), "parent": Pattern()})
source["parent"].ref("leaf")
overlay = OverlayLibrary()
overlay.add_source(source)
if materialize_parent:
_ = overlay["parent"]
overlay.rename("leaf", "renamed", move_references=move_references)
expected_target = "renamed" if move_references else "leaf"
assert overlay.child_graph(dangling="include")["parent"] == {expected_target}
assert set(overlay["parent"].refs) == {expected_target}
assert overlay.child_graph(dangling="include")["parent"] == {expected_target}
def test_overlay_rejects_unknown_dangling_mode() -> None:
overlay = OverlayLibrary()
overlay.add_source(Library({"top": Pattern()}))
with pytest.raises(ValueError, match="dangling-reference mode"):
overlay.child_graph(dangling="typo")
with pytest.raises(ValueError, match="dangling-reference mode"):
overlay.find_refs_local("top", dangling="typo")
def test_overlay_rename_preserves_initial_import_target_without_moving_refs() -> None:
source = Library({"leaf": Pattern(), "parent": Pattern()})
source["parent"].ref("leaf")
overlay = OverlayLibrary()
overlay["leaf"] = Pattern()
rename_map = overlay.add_source(source, rename_theirs=lambda lib, name: lib.get_name(name))
imported_leaf = rename_map["leaf"]
overlay.rename(imported_leaf, "renamed", move_references=False)
assert set(overlay["parent"].refs) == {imported_leaf}
def test_overlay_chained_rename_moves_unmaterialized_references() -> None:
source = Library({"leaf": Pattern(), "parent": Pattern()})
source["parent"].ref("leaf")
overlay = OverlayLibrary()
overlay.add_source(source)
overlay.rename("leaf", "middle", move_references=True)
overlay.rename("middle", "final", move_references=True)
assert set(overlay["parent"].refs) == {"final"}
def _assert_tree_merge_rejected(tree: Library) -> None:
destination = Library({"existing": Pattern()})
with pytest.raises(LibraryError, match="exactly one topcell"):
destination << tree
assert set(destination) == {"existing"}
def test_library_tree_merge_rejects_empty_tree() -> None:
_assert_tree_merge_rejected(Library())
def test_library_tree_merge_rejects_cycle_without_top() -> None:
tree = Library({"a": Pattern(), "b": Pattern()})
tree["a"].ref("b")
tree["b"].ref("a")
_assert_tree_merge_rejected(tree)
def test_library_tree_merge_rejects_multiple_tops() -> None:
_assert_tree_merge_rejected(Library({"a": Pattern(), "b": Pattern()}))
def test_library_child_order_cycle_raises_library_error() -> None:
lib = Library()
lib["a"] = Pattern()
lib["a"].ref("b")
lib["b"] = Pattern()
lib["b"].ref("a")
with pytest.raises(LibraryError, match="Cycle found while building child order"):
lib.child_order()
def test_library_find_refs_global_cycle_raises_library_error() -> None:
lib = Library()
lib["a"] = Pattern()
lib["a"].ref("a")
with pytest.raises(LibraryError, match="Cycle found while building child order"):
lib.find_refs_global("a")
def test_library_get_name() -> None:
lib = Library()
lib["cell"] = Pattern()
name1 = lib.get_name("cell")
assert name1 != "cell"
assert name1.startswith("cell")
name2 = lib.get_name("other")
assert name2 == "other"
def test_library_dedup_shapes_does_not_merge_custom_capped_paths() -> None:
lib = Library()
pat = Pattern()
pat.shapes[(1, 0)] += [
Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(1, 2)),
Path(vertices=[[20, 0], [30, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(3, 4)),
]
lib["top"] = pat
lib.dedup(norm_value=1, threshold=2)
assert not lib["top"].refs
assert len(lib["top"].shapes[(1, 0)]) == 2
def test_library_dedup_text_preserves_scale_and_mirror_flag() -> None:
lib = Library()
pat = Pattern()
pat.shapes[(1, 0)] += [
Text("A", 10, "dummy.ttf", offset=(0, 0)),
Text("A", 10, "dummy.ttf", offset=(100, 0)),
]
lib["top"] = pat
lib.dedup(exclude_types=(), norm_value=5, threshold=2)
target_name = next(iter(lib["top"].refs))
refs = lib["top"].refs[target_name]
assert [ref.mirrored for ref in refs] == [False, False]
assert [ref.scale for ref in refs] == [2.0, 2.0]
assert cast("Text", lib[target_name].shapes[(1, 0)][0]).height == 5
flat = lib.flatten("top")["top"]
assert [cast("Text", shape).height for shape in flat.shapes[(1, 0)]] == [10, 10]
def test_library_dedup_handles_arc_and_ellipse_labels() -> None:
lib = Library()
pat = Pattern()
pat.shapes[(1, 0)] += [
Arc(radii=(10, 20), angles=(0, 1), width=2, offset=(0, 0)),
Arc(radii=(10, 20), angles=(0, 1), width=2, offset=(50, 0)),
]
pat.shapes[(2, 0)] += [
Ellipse(radii=(10, 20), offset=(0, 0)),
Ellipse(radii=(10, 20), offset=(50, 0)),
]
lib["top"] = pat
lib.dedup(exclude_types=(), norm_value=1, threshold=2)
assert len(lib["top"].refs) == 2
assert lib["top"].shapes[(1, 0)] == []
assert lib["top"].shapes[(2, 0)] == []
flat = lib.flatten("top")["top"]
assert sum(isinstance(shape, Arc) for shape in flat.shapes[(1, 0)]) == 2
assert sum(isinstance(shape, Ellipse) for shape in flat.shapes[(2, 0)]) == 2
def test_library_dedup_handles_multiple_duplicate_groups() -> None:
from ..shapes import Circle
lib = Library()
pat = Pattern()
pat.shapes[(1, 0)] += [Circle(radius=1, offset=(0, 0)), Circle(radius=1, offset=(10, 0))]
pat.shapes[(2, 0)] += [Path(vertices=[[0, 0], [5, 0]], width=2), Path(vertices=[[10, 0], [15, 0]], width=2)]
lib["top"] = pat
lib.dedup(exclude_types=(), norm_value=1, threshold=2)
assert len(lib["top"].refs) == 2
assert all(len(refs) == 2 for refs in lib["top"].refs.values())
assert len(lib["top"].shapes[(1, 0)]) == 0
assert len(lib["top"].shapes[(2, 0)]) == 0
def test_library_dedup_uses_stable_target_names_per_label() -> None:
from ..shapes import Circle
lib = Library()
p1 = Pattern()
p1.shapes[(1, 0)] += [Circle(radius=1, offset=(0, 0)), Circle(radius=1, offset=(10, 0))]
lib["p1"] = p1
p2 = Pattern()
p2.shapes[(2, 0)] += [Path(vertices=[[0, 0], [5, 0]], width=2), Path(vertices=[[10, 0], [15, 0]], width=2)]
lib["p2"] = p2
lib.dedup(exclude_types=(), norm_value=1, threshold=2)
circle_target = next(iter(lib["p1"].refs))
path_target = next(iter(lib["p2"].refs))
assert circle_target != path_target
assert all(isinstance(shape, Circle) for shapes in lib[circle_target].shapes.values() for shape in shapes)
assert all(isinstance(shape, Path) for shapes in lib[path_target].shapes.values() for shape in shapes)