From 0f2b4d713bfaa135f5c0d36c8a8c2d2a040c9853 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 1 Apr 2026 23:34:40 -0700 Subject: [PATCH] [Pattern] make plug/place atomic wrt. annotation conflicts --- masque/pattern.py | 8 ++++++++ masque/test/test_pattern.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index b670493..9586140 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -1387,6 +1387,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): if append: if isinstance(other, Abstract): raise PatternError('Must provide a full `Pattern` (not an `Abstract`) when appending!') + if other.annotations is not None and self.annotations is not None: + annotation_conflicts = set(self.annotations.keys()) & set(other.annotations.keys()) + if annotation_conflicts: + raise PatternError(f'Annotation keys overlap: {annotation_conflicts}') else: if isinstance(other, Pattern): raise PatternError('Must provide an `Abstract` (not a `Pattern`) when creating a reference. ' @@ -1562,6 +1566,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): if append: if isinstance(other, Abstract): raise PatternError('Must provide a full `Pattern` (not an `Abstract`) when appending!') + if other.annotations is not None and self.annotations is not None: + annotation_conflicts = set(self.annotations.keys()) & set(other.annotations.keys()) + if annotation_conflicts: + raise PatternError(f'Annotation keys overlap: {annotation_conflicts}') elif isinstance(other, Pattern): raise PatternError('Must provide an `Abstract` (not a `Pattern`) when creating a reference. ' 'Use `append=True` if you intended to append the full geometry.') diff --git a/masque/test/test_pattern.py b/masque/test/test_pattern.py index 6048bf1..c100079 100644 --- a/masque/test/test_pattern.py +++ b/masque/test/test_pattern.py @@ -148,6 +148,17 @@ def test_pattern_place_append_requires_pattern_atomically() -> None: assert not parent.ports +def test_pattern_place_append_annotation_conflict_is_atomic() -> None: + parent = Pattern(annotations={"k": [1]}) + child = Pattern(annotations={"k": [2]}, ports={"A": Port((1, 2), 0)}) + + with pytest.raises(PatternError, match="Annotation keys overlap"): + parent.place(child, append=True) + + assert not parent.ports + assert parent.annotations == {"k": [1]} + + def test_pattern_interface() -> None: source = Pattern() source.ports["A"] = Port((10, 20), 0, ptype="test") @@ -192,6 +203,25 @@ def test_pattern_plug_requires_abstract_for_reference_atomically() -> None: assert set(parent.ports) == {"X"} +def test_pattern_plug_append_annotation_conflict_is_atomic() -> None: + parent = Pattern( + annotations={"k": [1]}, + ports={"X": Port((0, 0), 0), "Q": Port((9, 9), 0)}, + ) + child = Pattern( + annotations={"k": [2]}, + ports={"A": Port((0, 0), pi), "B": Port((5, 0), 0)}, + ) + + with pytest.raises(PatternError, match="Annotation keys overlap"): + parent.plug(child, {"X": "A"}, map_out={"B": "Y"}, append=True) + + assert set(parent.ports) == {"X", "Q"} + assert_allclose(parent.ports["X"].offset, (0, 0)) + assert_allclose(parent.ports["Q"].offset, (9, 9)) + assert parent.annotations == {"k": [1]} + + def test_pattern_append_port_conflict_is_atomic() -> None: pat1 = Pattern() pat1.ports["A"] = Port((0, 0), 0)