diff --git a/masque/file/utils.py b/masque/file/utils.py index a973840..ca6cb6d 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -1,12 +1,13 @@ """ Helper functions for file reading and writing """ -from typing import IO, Iterator +from typing import IO, Iterator, Mapping import re import pathlib import logging import tempfile import shutil +from collections import defaultdict from contextlib import contextmanager from pprint import pformat from itertools import chain @@ -24,6 +25,7 @@ def preflight( allow_dangling_refs: bool | None = None, allow_named_layers: bool = True, prune_empty_patterns: bool = False, + wrap_repeated_shapes: bool = False, ) -> Library: """ Run a standard set of useful operations and checks, usually done immediately prior @@ -38,37 +40,45 @@ def preflight( allow_named_layers: If `False`, raises a `PatternError` if any layer is referred to by a string instead of a number (or tuple). prune_empty_patterns: Runs `Library.prune_empty()`, recursively deleting any empty patterns. + wrap_repeated_shapes: Runs `Library.wrap_repeated_shapes()`, turning repeated shapes into + repeated refs containing non-repeated shapes. Returns: `lib` or an equivalent sorted library """ if sort: lib = Library(dict(sorted( - (nn: pp.sort()) for nn, pp in lib.items() + (nn, pp.sort()) for nn, pp in lib.items() ))) if not allow_dangling_refs: refs = lib.referenced_patterns() dangling = refs - set(lib.keys()) - msg = 'Dangling refs in found: ' + pformat(dangling) - if allow_dangling_refs is None: - logger.warning(msg) - else: - raise LibraryError(msg) + if dangling: + msg = 'Dangling refs in found: ' + pformat(dangling) + if allow_dangling_refs is None: + logger.warning(msg) + else: + raise LibraryError(msg) if not allow_named_layers: - named_layers = defaultdict(set) + named_layers: Mapping[str, set] = defaultdict(set) for name, pat in lib.items(): for layer in chain(pat.shapes.keys(), pat.labels.keys()): if isinstance(layer, str): named_layers[name].add(layer) - raise PatternError('Non-numeric layers found:' + pformat(named_layers)) + named_layers = dict(named_layers) + if named_layers: + raise PatternError('Non-numeric layers found:' + pformat(named_layers)) if prune_empty_patterns: pruned = lib.prune_empty() logger.info(f'Preflight pruned {len(pruned)} empty patterns') logger.debug('Pruned: ' + pformat(pruned)) + if wrap_repeated_shapes: + lib.wrap_repeated_shapes() + return lib diff --git a/masque/pattern.py b/masque/pattern.py index 6a46535..0acdf3f 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -247,6 +247,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): return False def __eq__(self, other: Any) -> bool: + if type(self) is not type(other): + return False + self_nonempty_targets = [target for target, reflist in self.refs.items() if reflist] other_nonempty_targets = [target for target, reflist in self.refs.items() if reflist] self_tgtkeys = tuple(sorted((target is None, target) for target in self_nonempty_targets)) @@ -305,16 +308,16 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - self.refs = dict(sorted( + self.refs = defaultdict(list, sorted( (tgt, sorted(rrs)) for tgt, rrs in self.refs.items() )) - self.labels = dict(sorted( - (layer, sorted(lls)) for layer, lls in self.labels.items(), - key=lambda kk, vv: layer2key(ll), + self.labels = defaultdict(list, sorted( + ((layer, sorted(lls)) for layer, lls in self.labels.items()), + key=lambda tt: layer2key(tt[0]), )) - self.shapes = dict(sorted( - (layer, sorted(sss)) for layer, sss in self.shapes.items(), - key=lambda kk, vv: layer2key(ll), + self.shapes = defaultdict(list, sorted( + ((layer, sorted(sss)) for layer, sss in self.shapes.items()), + key=lambda tt: layer2key(tt[0]), )) self.ports = dict(sorted(self.ports.items())) @@ -1217,7 +1220,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): ports specified by `map_out`. Examples: - ========= + ======list, === - `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})` instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B' of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports diff --git a/masque/ports.py b/masque/ports.py index 9cc4e45..2cff75c 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -135,7 +135,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable): def __eq__(self, other: Any) -> bool: return ( - type(self) == type(other) + type(self) is type(other) and self.ptype == other.ptype and numpy.array_equal(self.offset, other.offset) and self.rotation == other.rotation diff --git a/masque/repetition.py b/masque/repetition.py index 30e27b0..5436c11 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -280,7 +280,7 @@ class Grid(Repetition): return (f'') def __eq__(self, other: Any) -> bool: - if type(other) != type(self): + if type(other) is not type(self): return False if self.a_count != other.a_count or self.b_count != other.b_count: return False @@ -295,7 +295,7 @@ class Grid(Repetition): return True def __le__(self, other: Repetition) -> bool: - if type(self) != type(other): + if type(self) is not type(other): return repr(type(self)) < repr(type(other)) other = cast(Grid, other) if self.a_count != other.a_count: @@ -353,12 +353,12 @@ class Arbitrary(Repetition): return (f'') def __eq__(self, other: Any) -> bool: - if not type(other) != type(self): + if not type(other) is not type(self): return False return numpy.array_equal(self.displacements, other.displacements) def __le__(self, other: Repetition) -> bool: - if type(self) != type(other): + if type(self) is not type(other): return repr(type(self)) < repr(type(other)) other = cast(Arbitrary, other) if self.displacements.size != other.displacements.size: diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 60c077f..aa171ed 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -191,7 +191,7 @@ class Arc(Shape): def __eq__(self, other: Any) -> bool: return ( - type(self) != type(other) + type(self) is type(other) and numpy.array_equal(self.offset, other.offset) and numpy.array_equal(self.radii, other.radii) and numpy.array_equal(self.angles, other.angles) @@ -202,8 +202,10 @@ class Arc(Shape): ) def __lt__(self, other: Shape) -> bool: - if type(self) != type(other): - return repr(type(self)) < repr(type(other)) + if type(self) is not type(other): + if repr(type(self)) != repr(type(other)): + return repr(type(self)) < repr(type(other)) + return id(type(self)) < id(type(other)) other = cast(Arc, other) if self.width != other.width: return self.width < other.width diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index f03f9f3..628f8b8 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -72,7 +72,7 @@ class Circle(Shape): def __eq__(self, other: Any) -> bool: return ( - type(self) != type(other) + type(self) is type(other) and numpy.array_equal(self.offset, other.offset) and self.radius == other.radius and self.repetition == other.repetition @@ -80,8 +80,10 @@ class Circle(Shape): ) def __lt__(self, other: Shape) -> bool: - if type(self) != type(other): - return repr(type(self)) < repr(type(other)) + if type(self) is not type(other): + if repr(type(self)) != repr(type(other)): + return repr(type(self)) < repr(type(other)) + return id(type(self)) < id(type(other)) other = cast(Circle, other) if not self.radius == other.radius: return self.radius < other.radius diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index a0c7a92..9c671d6 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -121,7 +121,7 @@ class Ellipse(Shape): def __eq__(self, other: Any) -> bool: return ( - type(self) != type(other) + type(self) is type(other) and numpy.array_equal(self.offset, other.offset) and numpy.array_equal(self.radii, other.radii) and self.rotation == other.rotation @@ -130,8 +130,10 @@ class Ellipse(Shape): ) def __lt__(self, other: Shape) -> bool: - if type(self) != type(other): - return repr(type(self)) < repr(type(other)) + if type(self) is not type(other): + if repr(type(self)) != repr(type(other)): + return repr(type(self)) < repr(type(other)) + return id(type(self)) < id(type(other)) other = cast(Ellipse, other) if not numpy.array_equal(self.radii, other.radii): return tuple(self.radii) < tuple(other.radii) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 5fc5a2a..d87286a 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -209,7 +209,7 @@ class Path(Shape): def __eq__(self, other: Any) -> bool: return ( - type(self) != type(other) + type(self) is type(other) and numpy.array_equal(self.offset, other.offset) and numpy.array_equal(self.vertices, other.vertices) and self.width == other.width @@ -220,8 +220,10 @@ class Path(Shape): ) def __lt__(self, other: Shape) -> bool: - if type(self) != type(other): - return repr(type(self)) < repr(type(other)) + if type(self) is not type(other): + if repr(type(self)) != repr(type(other)): + return repr(type(self)) < repr(type(other)) + return id(type(self)) < id(type(other)) other = cast(Path, other) if self.width != other.width: return self.width < other.width diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 508f867..191ef9e 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -117,7 +117,7 @@ class Polygon(Shape): def __eq__(self, other: Any) -> bool: return ( - type(self) != type(other) + type(self) is type(other) and numpy.array_equal(self.offset, other.offset) and numpy.array_equal(self.vertices, other.vertices) and self.repetition == other.repetition @@ -125,8 +125,10 @@ class Polygon(Shape): ) def __lt__(self, other: Shape) -> bool: - if type(self) != type(other): - return repr(type(self)) < repr(type(other)) + if type(self) is not type(other): + if repr(type(self)) != repr(type(other)): + return repr(type(self)) < repr(type(other)) + return id(type(self)) < id(type(other)) other = cast(Polygon, other) if not numpy.array_equal(self.vertices, other.vertices): return tuple(tuple(xy) for xy in self.vertices) < tuple(tuple(xy) for xy in other.vertices) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index fc2e05c..d19d6c5 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -100,7 +100,7 @@ class Text(RotatableImpl, Shape): def __eq__(self, other: Any) -> bool: return ( - type(self) != type(other) + type(self) is type(other) and numpy.array_equal(self.offset, other.offset) and self.string == other.string and self.height == other.height @@ -111,8 +111,10 @@ class Text(RotatableImpl, Shape): ) def __lt__(self, other: Shape) -> bool: - if type(self) != type(other): - return repr(type(self)) < repr(type(other)) + if type(self) is not type(other): + if repr(type(self)) != repr(type(other)): + return repr(type(self)) < repr(type(other)) + return id(type(self)) < id(type(other)) other = cast(Text, other) if not self.height == other.height: return self.height < other.height