[library / gdsii] cleanup using new library rework

This commit is contained in:
Jan Petykiewicz 2026-07-13 21:37:52 -07:00
commit 7b589e4f44
14 changed files with 696 additions and 391 deletions

View file

@ -10,7 +10,7 @@ from ..file.gdsii import lazy as gdsii_lazy
from ..error import LibraryError
from ..pattern import Pattern
from ..ports import Port
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary
from ..library import IBorrowing, IMaterializable, LazyLibrary, Library, OverlayLibrary, PortsLibraryView
def _make_lazy_port_library() -> Library:
@ -39,6 +39,43 @@ def test_gdsii_lazy_write_ignores_non_mapping_library_info() -> None:
gdsii_lazy.write(lib, io.BytesIO())
def test_gdsii_lazy_write_plain_library_matches_eager_writer() -> None:
lib = _make_lazy_port_library()
eager_stream = io.BytesIO()
lazy_stream = io.BytesIO()
gdsii.write(lib, eager_stream, meters_per_unit=1e-9, library_name='writer-match')
gdsii_lazy.write(
lib,
lazy_stream,
meters_per_unit=1e-9,
logical_units_per_unit=1,
library_name='writer-match',
)
assert lazy_stream.getvalue() == eager_stream.getvalue()
def test_gdsii_lazy_write_materializes_transiently() -> None:
lib = LazyLibrary()
lib['top'] = Pattern()
stream = io.BytesIO()
gdsii_lazy.write(
lib,
stream,
meters_per_unit=1e-9,
logical_units_per_unit=1,
library_name='transient',
)
assert not lib.cache
stream.seek(0)
roundtrip, info = gdsii.read(stream)
assert set(roundtrip) == {'top'}
assert info['name'] == 'transient'
def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_path: Path) -> None:
gds_file = tmp_path / 'lazy_source.gds'
src = _make_lazy_port_library()
@ -73,6 +110,17 @@ def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_pat
assert set(lib._cache) == {'child'}
def test_gdsii_lazy_graph_hooks_observe_cached_edits(tmp_path: Path) -> None:
gds_file = tmp_path / 'lazy_cached_graph.gds'
gdsii.writefile(_make_lazy_port_library(), gds_file, meters_per_unit=1e-9)
lib, _ = gdsii_lazy.readfile(gds_file)
del lib['child'].refs['leaf']
assert lib.child_graph(dangling='ignore')['child'] == set()
assert lib.find_refs_local('leaf') == {}
def test_gdsii_lazy_subtree_stays_borrowed_and_preserves_write_metadata(tmp_path: Path) -> None:
gds_file = tmp_path / 'lazy_subtree_source.gds'
src = _make_lazy_port_library()
@ -87,6 +135,7 @@ def test_gdsii_lazy_subtree_stays_borrowed_and_preserves_write_metadata(tmp_path
assert isinstance(subtree, IMaterializable)
assert isinstance(subtree, IBorrowing)
assert subtree.source_order() == ('leaf', 'child', 'top')
assert not hasattr(subtree, 'library_info')
assert not raw._cache
out_file = tmp_path / 'lazy_subtree_out.gds'
@ -127,7 +176,8 @@ def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> No
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-ports')
raw, _ = gdsii_lazy.readfile(gds_file)
processed = raw.with_ports_from_data(layers=[(10, 0)], max_depth=2)
processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2)
assert not hasattr(processed, 'library_info')
top = processed['top']
assert set(top.ports) == {'A'}
@ -145,7 +195,7 @@ def test_gdsii_lazy_ports_view_detaches_previously_cached_source(tmp_path: Path)
raw, _ = gdsii_lazy.readfile(gds_file)
raw_top = raw['top']
processed = raw.with_port_overrides({
processed = PortsLibraryView(raw, ports={
'top': {
'P': Port((1, 2), rotation=0, ptype='wire'),
},
@ -166,7 +216,7 @@ def test_gdsii_lazy_port_overrides_without_data_stay_lazy(tmp_path: Path) -> Non
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overrides')
raw, _ = gdsii_lazy.readfile(gds_file)
processed = raw.with_port_overrides({
processed = PortsLibraryView(raw, ports={
'top': {
'P': Port((1, 2), rotation=0, ptype='wire'),
},
@ -189,7 +239,8 @@ def test_gdsii_lazy_port_overrides_apply_after_extraction(tmp_path: Path) -> Non
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-override-extracted')
raw, _ = gdsii_lazy.readfile(gds_file)
processed = raw.with_ports_from_data(
processed = PortsLibraryView(
raw,
layers=[(10, 0)],
max_depth=2,
ports={
@ -217,7 +268,8 @@ def test_gdsii_lazy_port_overrides_replace_extracted_ports(tmp_path: Path) -> No
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-replace-ports')
raw, _ = gdsii_lazy.readfile(gds_file)
processed = raw.with_ports_from_data(
processed = PortsLibraryView(
raw,
layers=[(10, 0)],
max_depth=2,
ports={
@ -240,7 +292,7 @@ def test_gdsii_lazy_overlay_add_source_stays_lazy_for_processed_view(tmp_path: P
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overlay')
raw, _ = gdsii_lazy.readfile(gds_file)
processed = raw.with_ports_from_data(layers=[(10, 0)], max_depth=2)
processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2)
overlay = OverlayLibrary()
overlay.add_source(processed)
@ -258,7 +310,7 @@ def test_gdsii_lazy_overlay_add_source_sees_port_overrides(tmp_path: Path) -> No
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overlay-override')
raw, _ = gdsii_lazy.readfile(gds_file)
processed = raw.with_port_overrides({
processed = PortsLibraryView(raw, ports={
'top': {
'P': Port((1, 2), rotation=0, ptype='wire'),
},
@ -310,7 +362,7 @@ def test_gdsii_lazy_processed_write_roundtrips_without_explicit_units(tmp_path:
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-roundtrip')
raw, _ = gdsii_lazy.readfile(gds_file)
processed = raw.with_ports_from_data(layers=[(10, 0)], max_depth=2)
processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2)
out_file = tmp_path / 'lazy_roundtrip_out.gds'
gdsii_lazy.writefile(processed, out_file)

View file

@ -10,11 +10,12 @@ import pytest
pytest.importorskip('pyarrow')
from .. import PatternError
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary, PortsLibraryView
from ..pattern import Pattern
from ..repetition import Grid
from ..file import gdsii
from ..file.gdsii import lazy_arrow as gdsii_lazy_arrow
from ..file.gdsii import lazy_write as gdsii_lazy_write
from ..file.gdsii.perf import write_fixture
@ -160,6 +161,17 @@ def test_gdsii_lazy_arrow_local_and_global_refs(tmp_path: Path) -> None:
assert global_refs[('top', 'mid', 'leaf')].shape[0] == 5
def test_gdsii_lazy_arrow_graph_hooks_observe_cached_edits(tmp_path: Path) -> None:
gds_file = tmp_path / 'lazy_arrow_cached_graph.gds'
gdsii.writefile(_make_small_library(), gds_file, meters_per_unit=1e-9)
lib, _ = gdsii_lazy_arrow.readfile(gds_file)
del lib['mid'].refs['leaf']
assert lib.child_graph(dangling='ignore')['mid'] == set()
assert lib.find_refs_local('leaf') == {}
def test_gdsii_lazy_arrow_ref_queries_match_eager_reader(tmp_path: Path) -> None:
gds_file = tmp_path / 'complex_refs.gds'
src = _make_complex_ref_library()
@ -223,18 +235,66 @@ def test_gdsii_lazy_arrow_untouched_write_is_copy_through(tmp_path: Path) -> Non
assert out_file.read_bytes() == gds_file.read_bytes()
def test_gdsii_lazy_arrow_processed_cell_edit_disables_raw_copy(tmp_path: Path) -> None:
def test_gdsii_raw_copy_resolves_generic_borrowing_views(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
gds_file = tmp_path / 'provenance_source.gds'
gdsii.writefile(_make_small_library(), gds_file, meters_per_unit=1e-9, library_name='provenance')
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
ports = PortsLibraryView(raw)
subtree = ports.subtree('top')
overlay = OverlayLibrary()
overlay.add_source(subtree)
copied: list[str] = []
raw_reader = raw.raw_struct_bytes
def record_raw_read(name: str) -> bytes:
copied.append(name)
return raw_reader(name)
monkeypatch.setattr(raw, 'raw_struct_bytes', record_raw_read)
out_file = tmp_path / 'provenance_out.gds'
gdsii_lazy_arrow.writefile(overlay, out_file)
assert copied == ['leaf', 'mid', 'top']
assert out_file.read_bytes() == gds_file.read_bytes()
renamed = OverlayLibrary()
renamed.add_source(raw)
renamed.rename('top', 'renamed_top')
assert gdsii_lazy_write._resolve_raw_struct(renamed, 'renamed_top') is None
remapped = OverlayLibrary()
remapped.add_source(raw)
remapped.rename('leaf', 'renamed_leaf', move_references=True)
assert gdsii_lazy_write._resolve_raw_struct(remapped, 'mid') is None
def test_gdsii_lazy_arrow_processed_cell_edit_disables_raw_copy(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gds_file = tmp_path / 'processed_edit_source.gds'
src = _make_small_library()
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='processed-edit')
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
processed = raw.with_port_overrides({})
processed = PortsLibraryView(raw)
processed['top'].polygon((7, 0), vertices=[[0, 0], [4, 0], [0, 4]])
copied: list[str] = []
raw_reader = raw.raw_struct_bytes
def record_raw_read(name: str) -> bytes:
copied.append(name)
return raw_reader(name)
monkeypatch.setattr(raw, 'raw_struct_bytes', record_raw_read)
out_file = tmp_path / 'processed_edit_out.gds'
gdsii_lazy_arrow.writefile(processed, out_file)
assert 'top' not in copied
roundtrip, _ = gdsii.readfile(out_file)
assert len(roundtrip['top'].shapes[(7, 0)]) == 1

View file

@ -1,8 +1,8 @@
import pytest
from collections.abc import Iterator, Mapping, MutableMapping
from collections.abc import Mapping, MutableMapping
from typing import cast, TYPE_CHECKING
from numpy.testing import assert_allclose
from ..library import IBorrowing, INameView, IMaterializable, ILibraryView, Library, LibraryView, LazyLibrary, OverlayLibrary, PortsLibraryView
from ..library import IBorrowing, INameView, IMaterializable, Library, LibraryView, LazyLibrary, OverlayLibrary, PortsLibraryView
from ..pattern import Pattern
from ..error import LibraryError, PatternError
from ..ports import Port
@ -673,41 +673,43 @@ def test_library_materialization_and_borrowing_capabilities() -> None:
assert not isinstance(plain_view, IMaterializable | IBorrowing)
class _RawCopyView(ILibraryView):
def __init__(self) -> None:
self.mapping = {"top": Pattern()}
def test_borrowed_source_cell_tracks_persistent_materialization() -> None:
source = Library({"top": Pattern()})
source.library_info = {"name": "not-forwarded"} # type: ignore[attr-defined]
processed = PortsLibraryView(source)
def __getitem__(self, key: str) -> Pattern:
return self.mapping[key]
def __iter__(self) -> Iterator[str]:
return iter(self.mapping)
def __len__(self) -> int:
return len(self.mapping)
def __contains__(self, key: object) -> bool:
return key in self.mapping
def raw_struct_bytes(self, name: str) -> bytes:
return name.encode()
def can_copy_raw_struct(self, name: str) -> bool:
return name in self.mapping
def test_ports_view_raw_copy_eligibility_tracks_persistent_materialization() -> None:
processed = PortsLibraryView(_RawCopyView())
assert processed.can_copy_raw_struct("top")
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.can_copy_raw_struct("top")
assert processed.source_cell("top") == (source, "top")
subtree = processed.subtree("top")
assert subtree.can_copy_raw_struct("top")
assert subtree.source_cell("top") == (processed, "top")
assert not hasattr(subtree, "library_info")
_ = subtree["top"]
assert not processed.can_copy_raw_struct("top")
assert not subtree.can_copy_raw_struct("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])

View file

@ -0,0 +1,338 @@
from typing import Any
import numpy
import pytest
from numpy import pi
from masque.builder import (
AutoTool,
BendOffer,
PathTool,
PrimitiveKind,
PrimitiveOffer,
RenderStep,
SOffer,
StraightOffer,
Tool,
ToolContractCase,
ToolContractError,
UOffer,
validate_tool_contract,
)
from masque.error import BuildError
from masque.library import ILibrary, Library, SINGLE_USE_PREFIX
from masque.pattern import Pattern
from masque.ports import Port
def make_straight(length: float, *, ptype: str = 'wire') -> Pattern:
return Pattern(ports={
'A': Port((0, 0), 0, ptype=ptype),
'B': Port((length, 0), pi, ptype=ptype),
})
class EmptyTool(Tool):
def primitive_offers(
self,
kind: PrimitiveKind,
*,
in_ptype: str | None = None,
out_ptype: str | None = None,
**kwargs: Any,
) -> tuple[PrimitiveOffer, ...]:
_ = kind, in_ptype, out_ptype, kwargs
return ()
def render(
self,
batch: tuple[RenderStep, ...],
*,
port_names: tuple[str, str] = ('A', 'B'),
) -> ILibrary:
_ = batch
tree, pattern = Library.mktree('empty_tool')
pattern.add_port_pair(names=port_names)
return tree
@pytest.mark.parametrize(
('offer', 'kind', 'opcode'),
[
(StraightOffer('wire', 'wire'), 'straight', 'L'),
(BendOffer('wire', 'wire'), 'bend', 'L'),
(SOffer('wire', 'wire'), 's', 'S'),
(UOffer('wire', 'wire'), 'u', 'U'),
],
)
def test_offer_kind_is_canonical_and_opcode_is_derived(
offer: StraightOffer | BendOffer | SOffer | UOffer,
kind: str,
opcode: str,
) -> None:
assert offer.kind == kind
assert offer.opcode == opcode
def test_render_step_stores_kind_and_derives_opcode() -> None:
tool = PathTool(layer='M1', width=1, ptype='wire')
port = Port((0, 0), 0, ptype='wire')
step = RenderStep('straight', tool, port, port, None)
plug = RenderStep('plug', None, port, port, None)
assert step.kind == 'straight'
assert step.opcode == 'L'
assert step.transformed(numpy.zeros(2), 0, numpy.zeros(2)).kind == 'straight'
assert step.mirrored(0).kind == 'straight'
assert plug.opcode == 'P'
with pytest.raises(BuildError, match='Unrecognized RenderStep kind'):
RenderStep('L', tool, port, port, None) # type: ignore[arg-type]
with pytest.raises(BuildError, match='requires tool=None'):
RenderStep('plug', tool, port, port, None)
def test_standard_offer_endpoint_callback_runs_once_per_solver_evaluation() -> None:
endpoint_calls: list[float] = []
received_endpoints: list[Port] = []
def endpoint(length: float) -> Port:
endpoint_calls.append(length)
return Port((length, 0), pi, ptype='wire')
def cost(length: float, out_port: Port) -> float:
_ = length
received_endpoints.append(out_port)
return out_port.x
class OneOfferTool(EmptyTool):
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
if kind != 'straight':
return ()
return (StraightOffer(
in_ptype='wire',
out_ptype='wire',
cost=cost,
endpoint_planner=endpoint,
commit_planner=lambda length: length,
),)
from masque.builder import Pather
pather = Pather(
Library(),
ports={'A': Port((0, 0), 0, ptype='wire')},
tools=OneOfferTool(),
render='deferred',
)
pather.straight('A', 5)
assert endpoint_calls.count(5) == 1
assert len(received_endpoints) >= 1
def test_custom_cost_at_override_remains_authoritative() -> None:
calls: list[float] = []
class CustomCostOffer(StraightOffer):
def cost_at(self, parameter: float) -> float:
calls.append(parameter)
return 0
offer = CustomCostOffer(
in_ptype='wire',
out_ptype='wire',
endpoint_planner=lambda length: Port((length, 0), pi, ptype='wire'),
commit_planner=lambda length: length,
)
class CustomCostTool(EmptyTool):
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
return (offer,) if kind == 'straight' else ()
from masque.builder import Pather
pather = Pather(
Library(),
ports={'A': Port((0, 0), 0, ptype='wire')},
tools=CustomCostTool(),
render='deferred',
)
pather.straight('A', 5)
assert 5 in calls
def test_solver_rejects_offer_kind_mismatch() -> None:
class WrongKindTool(EmptyTool):
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
if kind != 'straight':
return ()
return (SOffer.generated(
'wire',
lambda jog: Port((1, jog), pi, ptype='wire'),
lambda jog: jog,
),)
from masque.builder import Pather
pather = Pather(
Library(),
ports={'A': Port((0, 0), 0, ptype='wire')},
tools=WrongKindTool(),
render='deferred',
)
with pytest.raises(ToolContractError, match='returned.*s.*offer'):
pather.straight('A', 5)
def test_validate_tool_contract_accepts_pathtool() -> None:
tool = PathTool(layer='M1', width=2, ptype='wire')
validate_tool_contract(tool, (
ToolContractCase('straight', in_ptype='wire', check_bbox=True),
ToolContractCase('bend', in_ptype='wire', ccw=False, check_bbox=True),
ToolContractCase('bend', in_ptype='wire', ccw=True, check_bbox=True),
ToolContractCase('s', in_ptype='wire', check_bbox=True),
ToolContractCase('u', in_ptype='wire', require_offers=False),
))
def test_validate_tool_contract_accepts_autotool_with_explicit_probe() -> None:
tool = AutoTool().add_straight(
make_straight,
'wire',
'A',
length_range=(1, 10),
)
validate_tool_contract(tool, (
ToolContractCase('straight', in_ptype='wire', probe_parameters=(7,)),
))
def test_validate_tool_contract_empty_offer_policy() -> None:
tool = EmptyTool()
validate_tool_contract(tool, (ToolContractCase('u', require_offers=False),))
with pytest.raises(ExceptionGroup) as exc_info:
validate_tool_contract(tool, (ToolContractCase('u'),))
assert all(isinstance(err, ToolContractError) for err in exc_info.value.exceptions)
assert any('no offers' in str(err) for err in exc_info.value.exceptions)
def test_validate_tool_contract_rejects_unmatched_explicit_probe() -> None:
tool = AutoTool().add_straight(make_straight, 'wire', 'A', length_range=(1, 5))
with pytest.raises(ExceptionGroup, match='Tool contract validation') as exc_info:
validate_tool_contract(tool, (
ToolContractCase('straight', in_ptype='wire', probe_parameters=(10,)),
))
assert any('outside every discovered offer domain' in str(err) for err in exc_info.value.exceptions)
def test_validate_tool_contract_aggregates_independent_violations() -> None:
class BrokenTool(EmptyTool):
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
if kind != 'straight':
return ()
return (StraightOffer(
in_ptype='wire',
out_ptype='wire',
endpoint_planner=lambda length: Port((length + 1, 0), 0, ptype='wrong'),
commit_planner=lambda length: length,
),)
def render(self, batch, *, port_names=('A', 'B'), **kwargs): # noqa: ANN001,ANN202,ARG002
return Library()
with pytest.raises(ExceptionGroup) as exc_info:
validate_tool_contract(BrokenTool(), (
ToolContractCase('straight', in_ptype='wire', label='broken straight'),
))
errors = exc_info.value.exceptions
assert len(errors) > 1
assert all(isinstance(err, ToolContractError) for err in errors)
assert all('broken straight' in str(err) for err in errors)
def test_validate_tool_contract_detects_repeated_discovery_changes() -> None:
class ChangingTool(EmptyTool):
calls = 0
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
if kind != 'straight':
return ()
self.calls += 1
shift = float(self.calls - 1)
return (StraightOffer(
in_ptype='wire',
out_ptype='wire',
endpoint_planner=lambda length: Port((length + shift, 0), pi, ptype='wire'),
commit_planner=lambda length: length,
),)
with pytest.raises(ExceptionGroup) as exc_info:
validate_tool_contract(ChangingTool(), (
ToolContractCase('straight', in_ptype='wire'),
))
assert any('changed after repeated discovery' in str(err) for err in exc_info.value.exceptions)
def test_validate_tool_contract_checks_render_ports_and_single_use_refs() -> None:
class BrokenRenderTool(EmptyTool):
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
return (StraightOffer.generated('wire', lambda length: length),) if kind == 'straight' else ()
def render(self, batch, *, port_names=('A', 'B'), **kwargs): # noqa: ANN001,ANN202,ARG002
tree = Library()
pattern = Pattern(ports={port_names[0]: Port((0, 0), 0, ptype='wire')})
pattern.ref(SINGLE_USE_PREFIX + 'missing')
tree['top'] = pattern
return tree
with pytest.raises(ExceptionGroup) as exc_info:
validate_tool_contract(BrokenRenderTool(), (
ToolContractCase('straight', in_ptype='wire'),
))
messages = [str(err) for err in exc_info.value.exceptions]
assert any('missing single-use refs' in message for message in messages)
assert any('missing ports' in message for message in messages)
def test_validate_tool_contract_bbox_is_opt_in() -> None:
tool = AutoTool().add_straight(make_straight, 'wire', 'A', length_range=(1, 5))
validate_tool_contract(tool, (ToolContractCase('straight', in_ptype='wire'),))
# AutoTool supplies bbox support, so use a minimal custom offer without it.
class NoBBoxTool(EmptyTool):
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
return (StraightOffer.generated('wire', lambda length: length),) if kind == 'straight' else ()
def render(self, batch, *, port_names=('A', 'B'), **kwargs): # noqa: ANN001,ANN202,ARG002
length = batch[0].data
tree = Library()
tree['top'] = Pattern(ports={
port_names[0]: Port((0, 0), 0, ptype='wire'),
port_names[1]: Port((length, 0), pi, ptype='wire'),
})
return tree
validate_tool_contract(NoBBoxTool(), (ToolContractCase('straight', in_ptype='wire'),))
with pytest.raises(ExceptionGroup) as exc_info:
validate_tool_contract(NoBBoxTool(), (
ToolContractCase('straight', in_ptype='wire', check_bbox=True),
))
assert any('bbox_at()' in str(err) for err in exc_info.value.exceptions)
@pytest.mark.parametrize(
'kwargs',
[
{'kind': 'straight', 'ccw': True},
{'kind': 'bend'},
{'kind': 'straight', 'probe_parameters': (numpy.inf,)},
{'kind': 'straight', 'tool_options': {'ccw': True}},
],
)
def test_tool_contract_case_validates_configuration(kwargs: dict[str, Any]) -> None:
with pytest.raises(ValueError, match='ccw|requires|finite|reserved'):
ToolContractCase(**kwargs) # type: ignore[arg-type]