From ffbe15c465a6a13ffed3ca97d1b717b03e644976 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 30 Mar 2026 23:30:03 -0700 Subject: [PATCH 1/3] [Port / PortList] raise PortError on missing port name --- masque/ports.py | 11 +++++++++++ masque/test/test_ports.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/masque/ports.py b/masque/ports.py index ac48681..ff3a0e3 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -328,6 +328,9 @@ class PortList(metaclass=ABCMeta): duplicates = (set(self.ports.keys()) - set(mapping.keys())) & set(mapping.values()) if duplicates: raise PortError(f'Unrenamed ports would be overwritten: {duplicates}') + missing = set(mapping) - set(self.ports) + if missing: + raise PortError(f'Ports to rename were not found: {missing}') for kk, vv in mapping.items(): if vv is None or vv != kk: @@ -395,6 +398,14 @@ class PortList(metaclass=ABCMeta): Raises: `PortError` if the ports are not properly aligned. """ + if not connections: + raise PortError('Must provide at least one port connection') + missing_a = set(connections) - set(self.ports) + if missing_a: + raise PortError(f'Connection source ports were not found: {missing_a}') + missing_b = set(connections.values()) - set(self.ports) + if missing_b: + raise PortError(f'Connection destination ports were not found: {missing_b}') a_names, b_names = list(zip(*connections.items(), strict=True)) a_ports = [self.ports[pp] for pp in a_names] b_ports = [self.ports[pp] for pp in b_names] diff --git a/masque/test/test_ports.py b/masque/test/test_ports.py index 070bf8e..14dc982 100644 --- a/masque/test/test_ports.py +++ b/masque/test/test_ports.py @@ -70,6 +70,25 @@ def test_port_list_rename() -> None: assert "B" in pl.ports +def test_port_list_rename_missing_port_raises() -> None: + class MyPorts(PortList): + def __init__(self) -> None: + self._ports = {"A": Port((0, 0), 0)} + + @property + def ports(self) -> dict[str, Port]: + return self._ports + + @ports.setter + def ports(self, val: dict[str, Port]) -> None: + self._ports = val + + pl = MyPorts() + with pytest.raises(PortError, match="Ports to rename were not found"): + pl.rename_ports({"missing": "B"}) + assert set(pl.ports) == {"A"} + + def test_port_list_plugged() -> None: class MyPorts(PortList): def __init__(self) -> None: @@ -88,6 +107,25 @@ def test_port_list_plugged() -> None: assert not pl.ports # Both should be removed +def test_port_list_plugged_empty_raises() -> None: + class MyPorts(PortList): + def __init__(self) -> None: + self._ports = {"A": Port((10, 10), 0), "B": Port((10, 10), pi)} + + @property + def ports(self) -> dict[str, Port]: + return self._ports + + @ports.setter + def ports(self, val: dict[str, Port]) -> None: + self._ports = val + + pl = MyPorts() + with pytest.raises(PortError, match="Must provide at least one port connection"): + pl.plugged({}) + assert set(pl.ports) == {"A", "B"} + + def test_port_list_plugged_mismatch() -> None: class MyPorts(PortList): def __init__(self) -> None: From d3be6aeba3aba6ae30d8bf28c40a9e2889bc7568 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 30 Mar 2026 23:33:33 -0700 Subject: [PATCH 2/3] [PortList] add_port_pair requires unique port names --- masque/ports.py | 2 ++ masque/test/test_ports.py | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/masque/ports.py b/masque/ports.py index ff3a0e3..3a67003 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -368,6 +368,8 @@ class PortList(metaclass=ABCMeta): Returns: self """ + if names[0] == names[1]: + raise PortError(f'Port names must be distinct: {names[0]!r}') new_ports = { names[0]: Port(offset, rotation=rotation, ptype=ptype), names[1]: Port(offset, rotation=rotation + pi, ptype=ptype), diff --git a/masque/test/test_ports.py b/masque/test/test_ports.py index 14dc982..0291a1c 100644 --- a/masque/test/test_ports.py +++ b/masque/test/test_ports.py @@ -89,6 +89,25 @@ def test_port_list_rename_missing_port_raises() -> None: assert set(pl.ports) == {"A"} +def test_port_list_add_port_pair_requires_distinct_names() -> None: + class MyPorts(PortList): + def __init__(self) -> None: + self._ports: dict[str, Port] = {} + + @property + def ports(self) -> dict[str, Port]: + return self._ports + + @ports.setter + def ports(self, val: dict[str, Port]) -> None: + self._ports = val + + pl = MyPorts() + with pytest.raises(PortError, match="Port names must be distinct"): + pl.add_port_pair(names=("A", "A")) + assert not pl.ports + + def test_port_list_plugged() -> None: class MyPorts(PortList): def __init__(self) -> None: @@ -126,6 +145,29 @@ def test_port_list_plugged_empty_raises() -> None: assert set(pl.ports) == {"A", "B"} +def test_port_list_plugged_missing_port_raises() -> None: + class MyPorts(PortList): + def __init__(self) -> None: + self._ports = {"A": Port((10, 10), 0), "B": Port((10, 10), pi)} + + @property + def ports(self) -> dict[str, Port]: + return self._ports + + @ports.setter + def ports(self, val: dict[str, Port]) -> None: + self._ports = val + + pl = MyPorts() + with pytest.raises(PortError, match="Connection source ports were not found"): + pl.plugged({"missing": "B"}) + assert set(pl.ports) == {"A", "B"} + + with pytest.raises(PortError, match="Connection destination ports were not found"): + pl.plugged({"A": "missing"}) + assert set(pl.ports) == {"A", "B"} + + def test_port_list_plugged_mismatch() -> None: class MyPorts(PortList): def __init__(self) -> None: From d03fafcaf6718d9af4bb9d23667d92c961a17507 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 30 Mar 2026 23:34:31 -0700 Subject: [PATCH 3/3] [ILibraryView] don't fail on nested dangling ref --- masque/library.py | 5 +++-- masque/test/test_library.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/masque/library.py b/masque/library.py index cb50139..bb2e3d2 100644 --- a/masque/library.py +++ b/masque/library.py @@ -294,8 +294,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): def flatten_single(name: str) -> None: flattened[name] = None pat = self[name].deepcopy() + refs_by_target = tuple((target, tuple(refs)) for target, refs in pat.refs.items()) - for target in pat.refs: + for target, refs in refs_by_target: if target is None: continue if dangling_ok and target not in self: @@ -310,7 +311,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): if target_pat.is_empty() and not ports_only: # avoid some extra allocations continue - for ref in pat.refs[target]: + for ref in refs: if flatten_ports and ref.repetition is not None and target_pat.ports: raise PatternError( f'Cannot flatten ports from repeated ref to {target!r}; ' diff --git a/masque/test/test_library.py b/masque/test/test_library.py index 1f65595..0a04d98 100644 --- a/masque/test/test_library.py +++ b/masque/test/test_library.py @@ -166,6 +166,24 @@ def test_library_flatten_repeated_ref_with_ports_raises() -> None: lib.flatten("parent", flatten_ports=True) +def test_library_flatten_dangling_ok_nested_preserves_dangling_refs() -> None: + lib = Library() + child = Pattern() + child.ref("missing") + lib["child"] = child + + parent = Pattern() + parent.ref("child") + lib["parent"] = parent + + flat = lib.flatten("parent", dangling_ok=True) + + assert set(flat["child"].refs) == {"missing"} + assert flat["child"].has_refs() + assert set(flat["parent"].refs) == {"missing"} + assert flat["parent"].has_refs() + + def test_lazy_library() -> None: lib = LazyLibrary() called = 0