[Pather] move render check from __del__ to __exit__ and use render= enum instead of auto_render

This commit is contained in:
Jan Petykiewicz 2026-07-09 12:28:05 -07:00
commit 9a39a436b2
13 changed files with 277 additions and 126 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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)):

View file

@ -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'):

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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()