diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md index c03e4a0..aab8ad2 100644 --- a/examples/tutorial/README.md +++ b/examples/tutorial/README.md @@ -29,7 +29,7 @@ Contents * Define a custom `Tool` that exposes primitive routing offers * Use primitive offers to automatically transition between path types - [renderpather](renderpather.py) - * Use `Pather(auto_render=False)` and `PathTool` to build a layout similar to the one in [pather](pather.py), + * Use `Pather(render='deferred')` and `PathTool` to build a layout similar to the one in [pather](pather.py), but using `Path` shapes instead of `Polygon`s. - [port_pather](port_pather.py) * Use `PortPather` and the `.at()` syntax for more concise routing diff --git a/examples/tutorial/port_pather.py b/examples/tutorial/port_pather.py index 48e948e..8dabf82 100644 --- a/examples/tutorial/port_pather.py +++ b/examples/tutorial/port_pather.py @@ -13,7 +13,7 @@ def main() -> None: library, M1_tool, M2_tool = prepare_tools() # Create a deferred Pather and place some initial pads (same as Pather tutorial) - rpather = Pather(library, tools=M2_tool, auto_render=False) + rpather = Pather(library, tools=M2_tool, render='deferred') rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'}) rpather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'}) @@ -157,7 +157,7 @@ def main() -> None: # # Rendering and Saving # - # Since we deferred auto-rendering, we must call .render() to generate the geometry. + # Since routing is deferred, we must call .render() to generate the geometry. rpather.render() library['PortPather_Tutorial'] = rpather.pattern diff --git a/examples/tutorial/renderpather.py b/examples/tutorial/renderpather.py index 01fd352..d048d65 100644 --- a/examples/tutorial/renderpather.py +++ b/examples/tutorial/renderpather.py @@ -13,7 +13,7 @@ def main() -> None: # # To illustrate deferred routing with `Pather`, we use `PathTool` instead # of `AutoTool`. `PathTool` lacks some sophistication (e.g. no automatic transitions) - # but when used with `Pather(auto_render=False)`, it can consolidate multiple routing steps into + # but when used with `Pather(render='deferred')`, it can consolidate multiple routing steps into # a single `Path` shape. # # We'll try to nearly replicate the layout from the `Pather` tutorial; see `pather.py` @@ -39,7 +39,7 @@ def main() -> None: # and what port type to present. M1_ptool = PathTool(layer='M1', width=M1_WIDTH, ptype='m1wire') M2_ptool = PathTool(layer='M2', width=M2_WIDTH, ptype='m2wire') - rpather = Pather(tools=M2_ptool, library=library, auto_render=False) + rpather = Pather(tools=M2_ptool, library=library, render='deferred') # As in the pather tutorial, we make some pads and labels... rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'}) diff --git a/masque/builder/pather.py b/masque/builder/pather.py index 1206d60..89bff0e 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -3,7 +3,7 @@ Unified Pattern assembly and routing (`Pather`). `Pather` is the public object that owns layout state. It holds the working Pattern, Library, per-port Tool assignments, pending `RenderStep`s, and routing -side effects such as plug consumption, port renames, and auto-render. The +side effects such as plug consumption, port renames, and automatic rendering. The planner package is intentionally internal: custom route generators should extend `Tool.primitive_offers()` and `Tool.render()` rather than depending on planner classes or search details. @@ -16,7 +16,7 @@ Routing is split into four ownership phases: `RenderStep.data`, and returns prepared actions, - application: `Pather` appends the prepared steps to its pending queue, replaces live output ports, consumes plug destinations, applies deferred - trace-thru renames, and batches auto-render around the whole route, + trace-thru renames, and batches immediate rendering around the whole route, - rendering: pending steps are grouped by live port, Tool, and continuity before `Tool.render()` builds geometry for each compatible batch. @@ -27,7 +27,7 @@ prepared actions are applied, later commit, plug, rename, render, or insertion failures may leave partial output; that mutation boundary belongs to Pather, not to Tool implementations or the route solver. """ -from typing import Self, Any, overload +from typing import Self, Any, Literal, overload from collections.abc import Iterator, Iterable, Mapping, MutableMapping, Sequence import copy import logging @@ -36,6 +36,7 @@ from functools import wraps from pprint import pformat from contextlib import contextmanager from itertools import chain +from types import TracebackType import numpy from numpy import pi @@ -64,6 +65,8 @@ from .logging import PatherLogger logger = logging.getLogger(__name__) +RenderPolicy = Literal['auto', 'immediate', 'deferred', 'warn', 'error', 'ignore'] +RENDER_POLICIES: tuple[RenderPolicy, ...] = ('auto', 'immediate', 'deferred', 'warn', 'error', 'ignore') class Pather(PortList): @@ -76,9 +79,9 @@ class Pather(PortList): pattern, and a set of `Tool`s for generating routing segments. Routing operations (`trace`, `jog`, `uturn`, etc.) select primitive offers - from the active `Tool` and compose them into deferred `RenderStep`s. - Set `auto_render=False` to defer geometry generation until an explicit call - to `render()`. + from the active `Tool` and compose them into `RenderStep`s. By default, + geometry is rendered after each route, unless the `Pather` is used as a + context manager. Examples: Creating a Pather =========================== @@ -94,12 +97,12 @@ class Pather(PortList): connects port 'A' of the current pattern to port 'C' of `subdevice`. - `pather.trace('my_port', ccw=True, length=100)` plans a 100-unit bend - starting at 'my_port'. Geometry is added immediately by default. - Set `auto_render=False` to defer and call `pather.render()` later. + starting at 'my_port'. If the `Pather` is used as a context manager, + geometry is generated on clean context exit. """ __slots__ = ( 'pattern', 'library', 'tools', 'planner', '_paths', - '_dead', '_logger', '_auto_render', '_auto_render_append' + '_dead', '_logger', '_render_policy', '_render_append', '_context_depth' ) pattern: Pattern @@ -128,8 +131,14 @@ class Pather(PortList): _logger: PatherLogger """ Handles diagnostic logging of operations """ - _auto_render: bool - """ If True, routing operations call render() immediately """ + _render_policy: RenderPolicy + """ Routing render behavior """ + + _render_append: bool + """ If True, automatic render calls append geometry instead of adding references """ + + _context_depth: int + """ Number of active context-manager entries """ _paths: defaultdict[str, list[RenderStep]] """ Per-port pending render steps, consumed by `render()` """ @@ -168,8 +177,8 @@ class Pather(PortList): tools: Tool | MutableMapping[str | None, Tool] | None = None, name: str | None = None, debug: bool = False, - auto_render: bool = True, - auto_render_append: bool = True, + render: RenderPolicy = 'auto', + render_append: bool = True, planner: RoutingPlanner | None = None, ) -> None: """ @@ -181,16 +190,26 @@ class Pather(PortList): tools: Tool(s) to use for routing segments. name: If specified, `library[name]` is set to `self.pattern`. debug: If True, enables detailed logging. - auto_render: If True, enables immediate rendering of routing steps. - auto_render_append: If `auto_render` is True, determines whether - to append geometry or add a reference. + render: Routing render policy. `'auto'` renders after each route + outside a context manager and defers until clean context exit + inside one. Use `'immediate'` to always render after each route, + `'deferred'` to keep paths pending until `render()` or clean + context exit, `'warn'` to log pending paths on clean context + exit, `'error'` to reject pending paths on clean context exit, + or `'ignore'` to leave pending paths silent. + render_append: If an automatic render is triggered, determines + whether to append geometry or add a reference. planner: Optional stateless route-selection planner. If omitted, a new `RoutingPlanner` is used. """ + if render not in RENDER_POLICIES: + raise BuildError(f'Invalid render policy {render!r}; expected one of {RENDER_POLICIES}') + self._dead = False self._logger = PatherLogger(debug=debug) - self._auto_render = auto_render - self._auto_render_append = auto_render_append + self._render_policy = render + self._render_append = render_append + self._context_depth = 0 self.library = library self.pattern = pattern if pattern is not None else Pattern() self.planner = RoutingPlanner() if planner is None else planner @@ -213,9 +232,40 @@ class Pather(PortList): if name is not None: library[name] = self.pattern - def __del__(self) -> None: - if any(self._paths.values()): - logger.warning(f'Pather {self} had pending render steps', stack_info=True) + def __enter__(self) -> Self: + self._context_depth += 1 + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> bool: + _ = exc_value, traceback + self._context_depth -= 1 + if exc_type is not None or self._context_depth != 0 or not any(self._paths.values()): + return False + + if self._render_policy in ('auto', 'deferred'): + self.render(append=self._render_append) + elif self._render_policy == 'warn': + logger.warning( + 'Pather context exited with %s; call render() or use render="deferred"', + self._pending_render_summary(), + ) + elif self._render_policy == 'error': + raise BuildError(f'Pather context exited with {self._pending_render_summary()}') + return False + + def _pending_render_summary(self) -> str: + ports = [(portspec, len(steps)) for portspec, steps in self._paths.items() if steps] + port_count = len(ports) + step_count = sum(count for _portspec, count in ports) + return ( + f'{step_count} pending render step{"s" if step_count != 1 else ""} ' + f'on {port_count} port{"s" if port_count != 1 else ""}' + ) def __repr__(self) -> str: s = f'' @@ -397,30 +447,28 @@ class Pather(PortList): Apply every action and deferred rename in a prepared route result. Route actions may contain several primitive render steps and port - mutations. Temporarily disabling auto-render prevents each step from - becoming its own rendered cell. + mutations. Immediate rendering happens once after the whole prepared + result has been applied. """ - saved_auto_render = self._auto_render - self._auto_render = False - try: - for action in result.actions: - if not action.render_steps: - raise BuildError('Prepared route action has no render steps') + for action in result.actions: + if not action.render_steps: + raise BuildError('Prepared route action has no render steps') - if not self._dead: - self._paths[action.portspec].extend(action.render_steps) + if not self._dead: + self._paths[action.portspec].extend(action.render_steps) - self.pattern.ports[action.portspec] = action.final_port.copy() + self.pattern.ports[action.portspec] = action.final_port.copy() - if action.plug_into is not None: - self.plugged({action.portspec: action.plug_into}) - for old_name, new_name in result.renames: - self.rename_ports({old_name: new_name}) - self._auto_render = saved_auto_render - if saved_auto_render and any(self._paths.values()): - self.render(append = self._auto_render_append) - finally: - self._auto_render = saved_auto_render + if action.plug_into is not None: + self.plugged({action.portspec: action.plug_into}) + for old_name, new_name in result.renames: + self.rename_ports({old_name: new_name}) + render_immediately = ( + self._render_policy == 'immediate' + or (self._render_policy == 'auto' and self._context_depth == 0) + ) + if render_immediately and any(self._paths.values()): + self.render(append=self._render_append) def _apply_dead_fallback( self, diff --git a/masque/builder/tools.py b/masque/builder/tools.py index e4569bd..24ca9d1 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -560,7 +560,7 @@ class RenderStep: """ A single deferred routing operation. - `Pather(auto_render=False)` stores these records while routing and later + `Pather(render='deferred')` stores these records while routing and later passes batches of compatible steps to `Tool.render()` when `Pather.render()` is called. """ diff --git a/masque/test/test_autotool_planning.py b/masque/test/test_autotool_planning.py index db7e8f4..1ae17b9 100644 --- a/masque/test/test_autotool_planning.py +++ b/masque/test/test_autotool_planning.py @@ -122,7 +122,7 @@ def test_autotool_straight_offer_supports_requested_output_transition( ) -> None: _p, tool, lib = autotool_setup - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["start"] = Port((0, 0), pi, ptype="wire_m1") p.straight("start", 10, out_ptype="wire_m2") @@ -158,7 +158,7 @@ def test_pather_straight_topology_allows_transition_offset_cancellation() -> Non .add_transition(lib.abstract("trans_out"), "EXT", "CORE") ) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), pi, ptype="ext_in") p.trace("A", None, length=10, out_ptype="ext_out") @@ -550,7 +550,7 @@ def test_autotool_s_offer_bbox_matches_rendered_sbend() -> None: assert_offer_bbox_matches_rendered_offer(tool, offer, 4, lib) - pather = Pather(lib, tools=tool, auto_render=False) + pather = Pather(lib, tools=tool, render='deferred') pather.ports["A"] = Port((0, 0), 0, ptype="ext") pather.jog("A", 4) @@ -581,7 +581,7 @@ def test_autotool_s_offer_uses_absolute_jog_range_for_both_signs() -> None: assert valid_positive assert valid_negative - pather = Pather(Library(), tools=tool, auto_render=False) + pather = Pather(Library(), tools=tool, render='deferred') pather.ports["A"] = Port((0, 0), 0, ptype="core") pather.jog("A", -4) assert_allclose(pather.ports["A"].offset, [-10, 4]) @@ -623,7 +623,7 @@ def test_autotool_s_offer_singleton_jog_range_includes_both_signs() -> None: assert [offer.endpoint_at(-4).y for offer in offers if offer.jog_domain == (-4.0, -4.0)] == [-4] for jog, expected_y in ((4, -4), (-4, 4)): - pather = Pather(Library(), tools=tool, auto_render=False) + pather = Pather(Library(), tools=tool) pather.ports["A"] = Port((0, 0), 0, ptype="core") pather.jog("A", jog) assert_allclose(pather.ports["A"].offset, [-10, expected_y]) @@ -669,7 +669,7 @@ def test_autotool_uturn_offer_mirror_exposes_both_signs() -> None: def test_pather_autotool_uses_prebaked_uturn_offer() -> None: tool, lib = make_uturn_tool() - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="core") p.uturn("A", 4, length=10) @@ -695,7 +695,7 @@ def test_pather_autotool_omitted_uturn_composes_l_offers( multi_bend_tool: tuple[AutoTool, Library], ) -> None: tool, lib = multi_bend_tool - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="wire") p.uturn("A", 20) @@ -714,7 +714,7 @@ def test_pather_autotool_explicit_uturn_composes_l_offers( multi_bend_tool: tuple[AutoTool, Library], ) -> None: tool, lib = multi_bend_tool - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="wire") p.uturn("A", 20, length=10) @@ -739,7 +739,7 @@ def test_autotool_offer_bbox_rejects_invalid_parameter(multi_bend_tool: tuple[Au def test_pather_autotool_uses_l_offer_domains(multi_bend_tool: tuple[AutoTool, Library]) -> None: tool, lib = multi_bend_tool - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="wire") p.trace("A", True, length=15) @@ -760,7 +760,7 @@ def test_autotool_generated_primitives_do_not_capture_route_kwargs() -> None: return make_straight(length, ptype="wire") tool = AutoTool().add_straight("wire", make_marked_straight, "A", length_range=(0, 1e8)) - p = Pather(Library(), tools=tool, auto_render=False) + p = Pather(Library(), tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="wire") p.straight("A", 5, marker="route") @@ -790,7 +790,7 @@ def test_autotool_bend_offer_supports_requested_output_transition(ccw: bool) -> .add_transition(lib.abstract("out_trans"), "EXT", "CORE") ) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="core") p.trace("A", ccw, length=10, out_ptype="ext") @@ -824,7 +824,7 @@ def test_autotool_bend_offer_supports_bend_input_transition(ccw: bool) -> None: .add_transition(lib.abstract("bend_trans"), "MID", "CORE") ) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="core") p.trace("A", ccw, length=10, out_ptype="mid") @@ -857,7 +857,7 @@ def test_pather_accepts_bend_offer_with_zero_lateral_endpoint() -> None: .add_transition(lib.abstract("bend_trans"), "MID", "CORE") ) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="core") p.trace("A", True, length=10, out_ptype="mid") @@ -897,7 +897,7 @@ def test_autotool_bend_offer_supports_bend_and_output_transitions(ccw: bool) -> .add_transition(lib.abstract("out_trans"), "EXT", "MID") ) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="core") p.trace("A", ccw, length=12, out_ptype="ext") @@ -955,7 +955,7 @@ def test_pather_autotool_pure_sbend_with_transition_dx() -> None: assert isinstance(s_data, AutoTool.GeneratedData) assert s_offer.out_ptype == "core" - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.ports["A"] = Port((0, 0), 0, ptype="ext") p.jog("A", 4) diff --git a/masque/test/test_pather_autotool.py b/masque/test/test_pather_autotool.py index 98be1a1..e5928ab 100644 --- a/masque/test/test_pather_autotool.py +++ b/masque/test/test_pather_autotool.py @@ -70,7 +70,7 @@ def test_autotool_uturn() -> None: .add_bend(lib.abstract('bend'), 'in', 'out', clockwise=True) ) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), 0) p.at('A').uturn(offset=-2000, length=1000) @@ -82,7 +82,7 @@ def test_autotool_uturn() -> None: def test_deferred_render_autotool_double_L(multi_bend_tool: tuple[AutoTool, Library]) -> None: tool, lib = multi_bend_tool - rp = Pather(lib, tools=tool, auto_render=False) + rp = Pather(lib, tools=tool) rp.ports["A"] = Port((0,0), 0, ptype="wire") rp.jog("A", 10, length=20) diff --git a/masque/test/test_pather_constraints.py b/masque/test/test_pather_constraints.py index e1d77ba..fab1c68 100644 --- a/masque/test/test_pather_constraints.py +++ b/masque/test/test_pather_constraints.py @@ -117,7 +117,7 @@ class CountingPathTool(PathTool): def test_pather_jog_failed_two_bend_route_is_atomic() -> None: lib = Library() tool = PathTool(layer='M1', width=2, ptype='wire') - p = Pather(lib, tools=tool) + p = Pather(lib, tools=tool, render='immediate') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises(BuildError, match='S-bend'): @@ -130,7 +130,7 @@ def test_pather_jog_failed_two_bend_route_is_atomic() -> None: def test_pather_jog_accepts_sub_width_offset_when_length_is_sufficient() -> None: lib = Library() tool = PathTool(layer='M1', width=2, ptype='wire') - p = Pather(lib, tools=tool) + p = Pather(lib, tools=tool, render='immediate') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.jog('A', 1.5, length=5) @@ -139,10 +139,10 @@ def test_pather_jog_accepts_sub_width_offset_when_length_is_sufficient() -> None assert p.pattern.ports['A'].rotation == 0 assert len(p._paths['A']) == 0 -def test_pather_auto_render_batches_multi_step_selected_route_once() -> None: +def test_pather_immediate_render_batches_multi_step_selected_route_once() -> None: lib = Library() tool = CountingPathTool(layer='M1', width=2, ptype='wire') - p = Pather(lib, tools=tool, auto_render=True) + p = Pather(lib, tools=tool, render='immediate') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.jog('A', 4, length=10) @@ -169,7 +169,7 @@ def test_pather_jog_length_solved_from_single_position_bound() -> None: def test_pather_positional_bound_requires_port_rotation() -> None: - p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'), auto_render=False) + p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'), render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=None, ptype='wire') with pytest.raises(BuildError, match='Ports must have rotation'): @@ -179,7 +179,7 @@ def test_pather_positional_bound_requires_port_rotation() -> None: def test_pather_jog_omitted_length_uses_minimum_length_route() -> None: lib = Library() tool = PathTool(layer='M1', width=1, ptype='wire') - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.jog('A', 2) @@ -195,7 +195,7 @@ def test_pather_jog_omitted_length_uses_minimum_length_route() -> None: def test_pather_trace_omitted_length_uses_minimum_offer() -> None: lib = Library() tool = PathTool(layer='M1', width=2, ptype='wire') - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.trace('A', None) @@ -211,7 +211,7 @@ def test_pather_trace_omitted_length_uses_minimum_offer() -> None: def test_pather_trace_to_without_bound_uses_single_port_trace_minimum() -> None: lib = Library() tool = PathTool(layer='M1', width=2, ptype='wire') - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.trace_to('A', False) @@ -229,7 +229,7 @@ def test_pather_trace_to_rejects_conflicting_position_bounds() -> None: with pytest.raises(BuildError, match='exactly one positional bound'): p.trace_to('A', None, **kwargs) - p = Pather(Library(), tools=tool) + p = Pather(Library(), tools=tool, render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises(BuildError, match='length cannot be combined'): p.trace_to('A', None, x=-5, length=3) @@ -261,7 +261,7 @@ def test_planner_constrained_bend_requires_jog() -> None: def test_pather_trace_each_plans_all_ports_before_mutation() -> None: tool = FirstPortOnlyTraceTool() - p = Pather(Library(), tools=tool, auto_render=False) + p = Pather(Library(), tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.pattern.ports['B'] = Port((-2, 5), rotation=0, ptype='blocked') @@ -277,7 +277,7 @@ def test_pather_trace_each_plans_all_ports_before_mutation() -> None: def test_pather_bundle_trace_plans_all_ports_before_mutation_or_render() -> None: tool = FirstPortOnlyTraceTool() - p = Pather(Library(), tools=tool, auto_render=True) + p = Pather(Library(), tools=tool, render='immediate') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.pattern.ports['B'] = Port((0, 4), rotation=0, ptype='blocked') @@ -343,7 +343,7 @@ def test_pather_route_commit_failure_is_atomic_for_multi_port_trace() -> None: return tree tool = CommitFailureTool() - p = Pather(Library(), tools=tool, auto_render=True) + p = Pather(Library(), tools=tool, render='immediate') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.pattern.ports['B'] = Port((0, 4), rotation=0, ptype='bad') @@ -433,7 +433,7 @@ def test_pather_uturn_does_not_use_direct_planl_fallback() -> None: jog = -1 return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length} - p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False) + p = Pather(Library(), tools=PlanLOnlyTool(), render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises(BuildError, match='No legal primitive offer for omitted-length U-turn'): @@ -476,7 +476,7 @@ def test_pather_su_topology_rejects_out_ptype_sensitive_planl_jog() -> None: ptype = out_ptype or in_ptype or 'wire' return Port((length, jog), rotation=rotation, ptype=ptype), {'ccw': ccw, 'length': length} - p = Pather(Library(), tools=OutPtypeSensitiveTool(), auto_render=False) + p = Pather(Library(), tools=OutPtypeSensitiveTool(), render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises((BuildError, NotImplementedError)): @@ -509,7 +509,7 @@ def test_pather_two_l_planl_only_uturn_is_not_supported() -> None: jog = -1 return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length} - p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False) + p = Pather(Library(), tools=PlanLOnlyTool(), render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises((BuildError, NotImplementedError)): @@ -543,7 +543,7 @@ def test_pather_two_l_planl_only_jog_is_not_supported() -> None: jog = -1 return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length} - p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False) + p = Pather(Library(), tools=PlanLOnlyTool(), render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises((BuildError, NotImplementedError)): diff --git a/masque/test/test_pather_core.py b/masque/test/test_pather_core.py index 96162af..ddf869b 100644 --- a/masque/test/test_pather_core.py +++ b/masque/test/test_pather_core.py @@ -62,7 +62,7 @@ def test_builder_tool_symbol_exports() -> None: def test_pather_pending_render_steps_are_private() -> None: - p = Pather(Library(), tools=PathTool(layer=(1, 0), width=1), auto_render=False) + p = Pather(Library(), tools=PathTool(layer=(1, 0), width=1)) assert not hasattr(p, 'paths') assert hasattr(p, '_paths') @@ -81,7 +81,7 @@ def test_pather_accepts_and_reuses_planner_instance() -> None: p = Pather( Library(), tools=PathTool(layer=(1, 0), width=1), - auto_render=False, + render='deferred', planner=planner, ) p.ports['A'] = Port((0, 0), rotation=0, ptype='wire') @@ -148,7 +148,7 @@ def test_pather_dead_ports() -> None: def test_pather_trace_basic() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) # Routing extends opposite the port's inward-facing rotation. p.pattern.ports['A'] = Port((0, 0), rotation=0) @@ -164,7 +164,7 @@ def test_pather_trace_basic() -> None: def test_pather_trace_to() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) @@ -177,7 +177,7 @@ def test_pather_trace_to() -> None: def test_pather_bundle_trace() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 2000), rotation=0) @@ -194,13 +194,13 @@ def test_pather_bundle_trace() -> None: def test_portpather_default_spacing_matches_explicit_spacing() -> None: lib_default = Library() tool_default = PathTool(layer='M1', width=1000) - p_default = Pather(lib_default, tools=tool_default, auto_render=False) + p_default = Pather(lib_default, tools=tool_default) p_default.pattern.ports['A'] = Port((0, 0), rotation=0) p_default.pattern.ports['B'] = Port((0, 2000), rotation=0) lib_explicit = Library() tool_explicit = PathTool(layer='M1', width=1000) - p_explicit = Pather(lib_explicit, tools=tool_explicit, auto_render=False) + p_explicit = Pather(lib_explicit, tools=tool_explicit) p_explicit.pattern.ports['A'] = Port((0, 0), rotation=0) p_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0) @@ -211,11 +211,11 @@ def test_portpather_default_spacing_matches_explicit_spacing() -> None: assert_allclose(p_default.pattern.ports['B'].offset, p_explicit.pattern.ports['B'].offset) def test_portpather_default_spacing_reused_and_overridden() -> None: - p_default = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False) + p_default = Pather(Library(), tools=PathTool(layer='M1', width=1000)) p_default.pattern.ports['A'] = Port((0, 0), rotation=0) p_default.pattern.ports['B'] = Port((0, 2000), rotation=0) - p_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False) + p_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000)) p_explicit.pattern.ports['A'] = Port((0, 0), rotation=0) p_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0) @@ -227,11 +227,11 @@ def test_portpather_default_spacing_reused_and_overridden() -> None: assert_allclose(p_default.pattern.ports['A'].offset, p_explicit.pattern.ports['A'].offset) assert_allclose(p_default.pattern.ports['B'].offset, p_explicit.pattern.ports['B'].offset) - p_override = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False) + p_override = Pather(Library(), tools=PathTool(layer='M1', width=1000)) p_override.pattern.ports['A'] = Port((0, 0), rotation=0) p_override.pattern.ports['B'] = Port((0, 2000), rotation=0) - p_override_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False) + p_override_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000)) p_override_explicit.pattern.ports['A'] = Port((0, 0), rotation=0) p_override_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0) @@ -244,7 +244,7 @@ def test_portpather_default_spacing_reused_and_overridden() -> None: def test_portpather_default_spacing_not_injected_for_straight_bundle() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 2000), rotation=0) @@ -256,7 +256,7 @@ def test_portpather_default_spacing_not_injected_for_straight_bundle() -> None: def test_portpather_default_spacing_vector_revalidated_after_selection_change() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 2000), rotation=0) p.pattern.ports['C'] = Port((0, 4000), rotation=0) @@ -270,7 +270,7 @@ def test_portpather_default_spacing_vector_revalidated_after_selection_change() def test_pather_each_bound() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((-1000, 2000), rotation=0) @@ -390,7 +390,7 @@ def test_rename() -> None: def test_pather_dead_fallback_preserves_out_ptype() -> None: lib = Library() tool = PathTool(layer='M1', width=1000, ptype='wire') - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.set_dead() @@ -424,7 +424,7 @@ def test_pather_dead_fallback_does_not_hide_fatal_route_errors() -> None: commit_planner=lambda length: {'length': length}, ),) - p = Pather(Library(), tools=MismatchedEndpointTool(layer='M1', width=1000, ptype='wire'), auto_render=False) + p = Pather(Library(), tools=MismatchedEndpointTool(layer='M1', width=1000, ptype='wire')) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.set_dead() @@ -472,7 +472,7 @@ def test_pather_valid_candidate_does_not_hide_fatal_route_errors() -> None: ), ) - p = Pather(Library(), tools=PartlyMismatchedEndpointTool(layer='M1', width=1000, ptype='wire'), auto_render=False) + p = Pather(Library(), tools=PartlyMismatchedEndpointTool(layer='M1', width=1000, ptype='wire')) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises(BuildError, match='does not match declared offer out_ptype'): diff --git a/masque/test/test_pather_place_plug.py b/masque/test/test_pather_place_plug.py index 11e9bd2..660d080 100644 --- a/masque/test/test_pather_place_plug.py +++ b/masque/test/test_pather_place_plug.py @@ -10,7 +10,7 @@ from masque.error import PortError, PatternError def test_pather_place_treeview_resolves_once() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool) + p = Pather(lib, tools=tool, render='deferred') tree = {'child': Pattern(ports={'B': Port((1, 0), pi)})} @@ -24,7 +24,7 @@ def test_pather_place_treeview_resolves_once() -> None: def test_pather_plug_treeview_resolves_once() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool) + p = Pather(lib, tools=tool, render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0) tree = {'child': Pattern(ports={'B': Port((0, 0), pi)})} @@ -39,7 +39,7 @@ def test_pather_plug_treeview_resolves_once() -> None: def test_pather_failed_plug_does_not_add_break_marker() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.pattern.annotations = {'k': [1]} p.pattern.ports['A'] = Port((0, 0), rotation=0) @@ -60,7 +60,7 @@ def test_pather_failed_plug_does_not_add_break_marker() -> None: def test_pather_place_reused_deleted_name_keeps_break_marker() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0) p.at('A').straight(5000) @@ -80,7 +80,7 @@ def test_pather_place_reused_deleted_name_keeps_break_marker() -> None: def test_pather_plug_reused_deleted_name_keeps_break_marker() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 0), rotation=0) @@ -107,7 +107,7 @@ def test_pather_plug_reused_deleted_name_keeps_break_marker() -> None: def test_pather_failed_plugged_does_not_add_break_marker() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool, render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0) p.at('A').straight(5000) diff --git a/masque/test/test_pather_primitive_offers.py b/masque/test/test_pather_primitive_offers.py index 06916ae..7d99355 100644 --- a/masque/test/test_pather_primitive_offers.py +++ b/masque/test/test_pather_primitive_offers.py @@ -360,7 +360,7 @@ def test_pather_treats_notimplemented_offer_query_as_no_offers() -> None: _ = kind, in_ptype, out_ptype, kwargs raise NotImplementedError - p = Pather(Library(), tools=NoOfferTool(), auto_render=False) + p = Pather(Library(), tools=NoOfferTool()) p.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises(BuildError, match='No legal primitive offer for trace'): @@ -381,7 +381,7 @@ def test_pather_propagates_offer_query_errors(err_type: type[Exception]) -> None _ = kind, in_ptype, out_ptype, kwargs raise err_type('offer query failed') - p = Pather(Library(), tools=BrokenTool(), auto_render=False) + p = Pather(Library(), tools=BrokenTool()) p.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises(err_type): @@ -413,7 +413,7 @@ def test_pather_selects_lowest_cost_offer() -> None: StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, priority_bias=0, **offer_callbacks(low)), ) - p = Pather(Library(), tools=MultiOfferTool(), auto_render=False) + p = Pather(Library(), tools=MultiOfferTool(), render='deferred') p.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 7) @@ -460,7 +460,7 @@ def test_solver_rejects_rotation_impossible_candidates_before_parameter_solving( ), ) - p = Pather(Library(), tools=RotationOfferTool(), auto_render=False) + p = Pather(Library(), tools=RotationOfferTool(), render='deferred') p.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 7) @@ -503,7 +503,7 @@ def test_pather_commits_only_selected_offer() -> None: return (make('expensive', 100), make('cheap', 0)) - p = Pather(Library(), tools=RecordingTool(), auto_render=False) + p = Pather(Library(), tools=RecordingTool(), render='deferred') p.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 5) @@ -553,7 +553,7 @@ def test_pather_routes_with_offer_only_s_and_u_tool() -> None: ),) return () - p = Pather(Library(), tools=OfferOnlyTool(), auto_render=False) + p = Pather(Library(), tools=OfferOnlyTool(), render='deferred') p.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.jog('A', 3) p.uturn('A', 4) diff --git a/masque/test/test_pather_rendering.py b/masque/test/test_pather_rendering.py index f539a51..c5ddd68 100644 --- a/masque/test/test_pather_rendering.py +++ b/masque/test/test_pather_rendering.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING, cast +import logging import pytest import numpy @@ -20,10 +21,27 @@ if TYPE_CHECKING: def deferred_render_setup() -> tuple[Pather, PathTool, Library]: lib = Library() tool = PathTool(layer=(1, 0), width=2, ptype="wire") - rp = Pather(lib, tools=tool, auto_render=False) + rp = Pather(lib, tools=tool, render='deferred') rp.ports["start"] = Port((0, 0), pi / 2, ptype="wire") return rp, tool, lib + +def add_pending_straight(pather: Pather) -> None: + pather.ports["start"] = Port((0, 0), pi / 2, ptype="wire") + pather.straight("start", 10) + + +def route_in_context(pather: Pather) -> None: + with pather: + add_pending_straight(pather) + + +def fail_in_context(pather: Pather) -> None: + with pather: + add_pending_straight(pather) + raise RuntimeError('body failed') + + def test_deferred_render_stores_pending_paths_until_render(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None: rp, tool, lib = deferred_render_setup rp.at("start").straight(10).straight(10) @@ -101,7 +119,7 @@ def test_portpather_translate_only_affects_future_steps(deferred_render_setup: t def test_deferred_render_dead_ports() -> None: lib = Library() tool = PathTool(layer=(1, 0), width=1) - rp = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool, auto_render=False) + rp = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool, render='deferred') rp.set_dead() rp.straight("in", -10) @@ -113,6 +131,91 @@ def test_deferred_render_dead_ports() -> None: rp.render() assert not rp.pattern.has_shapes() + +def test_pather_default_auto_policy_renders_immediately_outside_context() -> None: + lib = Library() + tool = PathTool(layer=(1, 0), width=2, ptype="wire") + p = Pather(lib, tools=tool) + p.ports["start"] = Port((0, 0), pi / 2, ptype="wire") + + p.straight("start", 10) + + assert p.pattern.has_shapes() + assert not any(p._paths.values()) + + +def test_pather_default_auto_policy_defers_until_clean_context_exit() -> None: + lib = Library() + tool = PathTool(layer=(1, 0), width=2, ptype="wire") + + with Pather(lib, tools=tool) as p: + add_pending_straight(p) + assert not p.pattern.has_shapes() + assert len(p._paths["start"]) == 1 + + assert p.pattern.has_shapes() + assert not any(p._paths.values()) + + +def test_pather_context_warn_policy_logs_pending_paths(caplog: pytest.LogCaptureFixture) -> None: + lib = Library() + tool = PathTool(layer=(1, 0), width=2, ptype="wire") + + with caplog.at_level(logging.WARNING, logger="masque.builder.pather"), Pather(lib, tools=tool, render='warn') as p: + add_pending_straight(p) + + records = [ + record + for record in caplog.records + if 'Pather context exited with 1 pending render step on 1 port' in record.getMessage() + ] + assert len(records) == 1 + assert records[0].stack_info is None + assert len(p._paths["start"]) == 1 + assert not p.pattern.has_shapes() + + +def test_pather_context_error_policy_rejects_pending_paths() -> None: + lib = Library() + tool = PathTool(layer=(1, 0), width=2, ptype="wire") + p = Pather(lib, tools=tool, render='error') + + with pytest.raises(BuildError, match='1 pending render step on 1 port'): + route_in_context(p) + + assert len(p._paths["start"]) == 1 + assert not p.pattern.has_shapes() + + +def test_pather_context_ignore_policy_leaves_pending_paths_silent(caplog: pytest.LogCaptureFixture) -> None: + lib = Library() + tool = PathTool(layer=(1, 0), width=2, ptype="wire") + + with caplog.at_level(logging.WARNING, logger="masque.builder.pather"), Pather(lib, tools=tool, render='ignore') as p: + add_pending_straight(p) + + assert not caplog.records + assert len(p._paths["start"]) == 1 + assert not p.pattern.has_shapes() + + +def test_pather_context_policy_does_not_mask_body_exception() -> None: + lib = Library() + tool = PathTool(layer=(1, 0), width=2, ptype="wire") + p = Pather(lib, tools=tool, render='error') + + with pytest.raises(RuntimeError, match='body failed'): + fail_in_context(p) + + assert len(p._paths["start"]) == 1 + assert not p.pattern.has_shapes() + + +def test_pather_rejects_invalid_render_policy() -> None: + with pytest.raises(BuildError, match='Invalid render policy'): + Pather(Library(), tools=PathTool(layer=(1, 0), width=2), render='later') # type: ignore[arg-type] + + def test_deferred_render_rename_port(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None: rp, tool, lib = deferred_render_setup rp.at("start").straight(10) @@ -174,7 +277,7 @@ def test_pathtool_s_offer_render_geometry_matches_ports() -> None: def test_deferred_render_uturn_fallback() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - rp = Pather(lib, tools=tool, auto_render=False) + rp = Pather(lib, tools=tool, render='deferred') rp.pattern.ports['A'] = Port((0, 0), rotation=0) rp.at('A').uturn(offset=10000, length=5000) @@ -213,7 +316,7 @@ def test_pather_render_auto_renames_single_use_tool_children() -> None: return tree lib = Library() - p = Pather(lib, tools=FullTreeTool(), auto_render=False) + p = Pather(lib, tools=FullTreeTool()) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 10) @@ -258,7 +361,7 @@ def test_custom_tool_render_preserves_segment_subtrees() -> None: return batch[0].data lib = Library() - p = Pather(lib, tools=TraceTreeTool(), auto_render=False) + p = Pather(lib, tools=TraceTreeTool()) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 10) @@ -293,7 +396,7 @@ def test_pather_render_rejects_missing_single_use_tool_refs() -> None: lib = Library() lib['_seg'] = Pattern(annotations={'stale': [1]}) - p = Pather(lib, tools=MissingSingleUseTool(), auto_render=False) + p = Pather(lib, tools=MissingSingleUseTool(), render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 10) @@ -329,7 +432,7 @@ def test_pather_render_allows_missing_non_single_use_tool_refs() -> None: lib = Library() lib['shared'] = Pattern(annotations={'shared': [1]}) - p = Pather(lib, tools=SharedRefTool(), auto_render=False) + p = Pather(lib, tools=SharedRefTool()) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 10) @@ -362,7 +465,7 @@ def test_pather_render_rejects_output_port_that_misses_planned_endpoint(append: return tree lib = Library() - p = Pather(lib, tools=WrongEndpointTool(), auto_render=False) + p = Pather(lib, tools=WrongEndpointTool(), render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 10) @@ -396,7 +499,7 @@ def test_pather_render_rejects_output_port_with_wrong_ptype(append: bool) -> Non return tree lib = Library() - p = Pather(lib, tools=WrongPtypeTool(), auto_render=False) + p = Pather(lib, tools=WrongPtypeTool(), render='deferred') p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.straight('A', 10) @@ -409,7 +512,7 @@ def test_pather_render_rejects_output_port_with_wrong_ptype(append: bool) -> Non def test_deferred_render_rename_to_none_keeps_pending_geometry_without_port() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - rp = Pather(lib, tools=tool, auto_render=False) + rp = Pather(lib, tools=tool, render='deferred') rp.pattern.ports['A'] = Port((0, 0), rotation=0) rp.at('A').straight(5000) diff --git a/masque/test/test_pather_trace_into.py b/masque/test/test_pather_trace_into.py index 8f66b28..ed967dd 100644 --- a/masque/test/test_pather_trace_into.py +++ b/masque/test/test_pather_trace_into.py @@ -16,7 +16,7 @@ from masque.error import BuildError, PortError def trace_into_setup() -> tuple[Pather, PathTool, Library]: lib = Library() tool = PathTool(layer=(1, 0), width=2, ptype="wire") - p = Pather(lib, tools=tool, auto_render=True, auto_render_append=False) + p = Pather(lib, tools=tool, render='immediate', render_append=False) return p, tool, lib @@ -71,7 +71,7 @@ def test_path_into_thru(trace_into_setup: tuple[Pather, PathTool, Library]) -> N def test_pather_trace_into_shapes() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((-10000, 0), rotation=pi) @@ -175,7 +175,7 @@ def test_pather_trace_into_refines_output_adapter_route_before_plug() -> None: pat.add_port_pair(names=port_names, ptype=batch[-1].end_port.ptype if batch else 'm1wire') return tree - p = Pather(Library(), tools=TransitionTool(), auto_render=False) + p = Pather(Library(), tools=TransitionTool(), render='deferred') p.pattern.ports['src'] = Port((-65000, -11500), rotation=pi / 2, ptype='m1wire') p.pattern.ports['dst'] = Port((-100000, -100000), rotation=pi, ptype='m2wire') @@ -188,7 +188,7 @@ def test_pather_trace_into_refines_output_adapter_route_before_plug() -> None: def test_pather_trace_into_dead_updates_ports_without_geometry() -> None: lib = Library() tool = PathTool(layer='M1', width=1000, ptype='wire') - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.pattern.ports['B'] = Port((-10000, 0), rotation=pi, ptype='wire') p.set_dead()