[PortPather] complain if the user gives ambiguous port names
This commit is contained in:
parent
d366db5a62
commit
b3a1489258
2 changed files with 69 additions and 7 deletions
|
|
@ -857,20 +857,46 @@ class PortPather:
|
||||||
self.ports = [pp for pp in self.ports if pp not in ports_set]
|
self.ports = [pp for pp in self.ports if pp not in ports_set]
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def _normalize_copy_map(self, name: str | Mapping[str, str], action: str) -> dict[str, str]:
|
||||||
|
if isinstance(name, str):
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
raise BuildError(f'Use a mapping to {action} >1 port')
|
||||||
|
name_map = {self.ports[0]: name}
|
||||||
|
else:
|
||||||
|
name_map = dict(name)
|
||||||
|
|
||||||
|
missing_selected = set(name_map) - set(self.ports)
|
||||||
|
if missing_selected:
|
||||||
|
raise PortError(f'Can only {action} selected ports: {missing_selected}')
|
||||||
|
|
||||||
|
missing_pattern = set(name_map) - set(self.pather.pattern.ports)
|
||||||
|
if missing_pattern:
|
||||||
|
raise PortError(f'Ports to {action} were not found: {missing_pattern}')
|
||||||
|
|
||||||
|
targets = list(name_map.values())
|
||||||
|
duplicate_targets = {vv for vv in targets if targets.count(vv) > 1}
|
||||||
|
if duplicate_targets:
|
||||||
|
raise PortError(f'{action.capitalize()} targets would collide: {duplicate_targets}')
|
||||||
|
|
||||||
|
overwritten = {
|
||||||
|
dst for src, dst in name_map.items()
|
||||||
|
if dst in self.pather.pattern.ports and dst != src
|
||||||
|
}
|
||||||
|
if overwritten:
|
||||||
|
raise PortError(f'{action.capitalize()} would overwrite existing ports: {overwritten}')
|
||||||
|
|
||||||
|
return name_map
|
||||||
|
|
||||||
def mark(self, name: str | Mapping[str, str]) -> Self:
|
def mark(self, name: str | Mapping[str, str]) -> Self:
|
||||||
""" Bookmark current port(s). """
|
""" Bookmark current port(s). """
|
||||||
name_map: Mapping[str, str] = {self.ports[0]: name} if isinstance(name, str) else name
|
name_map = self._normalize_copy_map(name, 'mark')
|
||||||
if isinstance(name, str) and len(self.ports) > 1:
|
|
||||||
raise BuildError('Use a mapping to mark >1 port')
|
|
||||||
for src, dst in name_map.items():
|
for src, dst in name_map.items():
|
||||||
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def fork(self, name: str | Mapping[str, str]) -> Self:
|
def fork(self, name: str | Mapping[str, str]) -> Self:
|
||||||
""" Split and follow new name. """
|
""" Split and follow new name. """
|
||||||
name_map: Mapping[str, str] = {self.ports[0]: name} if isinstance(name, str) else name
|
name_map = self._normalize_copy_map(name, 'fork')
|
||||||
if isinstance(name, str) and len(self.ports) > 1:
|
|
||||||
raise BuildError('Use a mapping to fork >1 port')
|
|
||||||
for src, dst in name_map.items():
|
for src, dst in name_map.items():
|
||||||
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
||||||
self.ports = [(dst if pp == src else pp) for pp in self.ports]
|
self.ports = [(dst if pp == src else pp) for pp in self.ports]
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from masque import Pather, RenderPather, Library, Pattern, Port
|
from masque import Pather, RenderPather, Library, Pattern, Port
|
||||||
from masque.builder.tools import PathTool, Tool
|
from masque.builder.tools import PathTool, Tool
|
||||||
from masque.error import BuildError
|
from masque.error import BuildError, PortError
|
||||||
|
|
||||||
def test_pather_trace_basic() -> None:
|
def test_pather_trace_basic() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
|
|
@ -121,6 +121,42 @@ def test_mark_fork() -> None:
|
||||||
assert 'C' in p.pattern.ports
|
assert 'C' in p.pattern.ports
|
||||||
assert pp.ports == ['C'] # fork switches to new name
|
assert pp.ports == ['C'] # fork switches to new name
|
||||||
|
|
||||||
|
|
||||||
|
def test_mark_fork_reject_overwrite_and_duplicate_targets() -> None:
|
||||||
|
lib = Library()
|
||||||
|
|
||||||
|
p_mark = Pather(lib, pattern=Pattern(ports={
|
||||||
|
'A': Port((0, 0), rotation=0),
|
||||||
|
'C': Port((2, 0), rotation=0),
|
||||||
|
}))
|
||||||
|
with pytest.raises(PortError, match='overwrite existing ports'):
|
||||||
|
p_mark.at('A').mark('C')
|
||||||
|
assert numpy.allclose(p_mark.pattern.ports['C'].offset, (2, 0))
|
||||||
|
|
||||||
|
p_fork = Pather(lib, pattern=Pattern(ports={
|
||||||
|
'A': Port((0, 0), rotation=0),
|
||||||
|
'B': Port((1, 0), rotation=0),
|
||||||
|
}))
|
||||||
|
pp = p_fork.at(['A', 'B'])
|
||||||
|
with pytest.raises(PortError, match='targets would collide'):
|
||||||
|
pp.fork({'A': 'X', 'B': 'X'})
|
||||||
|
assert set(p_fork.pattern.ports) == {'A', 'B'}
|
||||||
|
assert pp.ports == ['A', 'B']
|
||||||
|
|
||||||
|
|
||||||
|
def test_mark_fork_reject_missing_sources() -> None:
|
||||||
|
lib = Library()
|
||||||
|
p = Pather(lib, pattern=Pattern(ports={
|
||||||
|
'A': Port((0, 0), rotation=0),
|
||||||
|
'B': Port((1, 0), rotation=0),
|
||||||
|
}))
|
||||||
|
|
||||||
|
with pytest.raises(PortError, match='selected ports'):
|
||||||
|
p.at(['A', 'B']).mark({'Z': 'C'})
|
||||||
|
|
||||||
|
with pytest.raises(PortError, match='selected ports'):
|
||||||
|
p.at(['A', 'B']).fork({'Z': 'C'})
|
||||||
|
|
||||||
def test_rename() -> None:
|
def test_rename() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
p = Pather(lib)
|
p = Pather(lib)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue