From 157df47884c64ca74bfe535e1556f7ad6ca91097 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 19:09:35 -0700 Subject: [PATCH] Improve type annotations based on mypy errors --- masque/file/gdsii.py | 29 ++++++++++++------------ masque/file/svg.py | 4 ++-- masque/pattern.py | 49 ++++++++++++++++++++-------------------- masque/repetition.py | 18 ++++++++------- masque/shapes/arc.py | 18 +++++++-------- masque/shapes/circle.py | 14 ++++++------ masque/shapes/ellipse.py | 16 ++++++------- masque/shapes/path.py | 12 +++++----- masque/shapes/polygon.py | 42 +++++++++++++++++++++++++--------- masque/shapes/shape.py | 30 +++++++++++++++--------- masque/shapes/text.py | 18 +++++++-------- masque/subpattern.py | 12 ++++++---- masque/utils.py | 6 ++--- 13 files changed, 151 insertions(+), 117 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index b28333b..c0dcfc6 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -6,7 +6,7 @@ import gdsii.library import gdsii.structure import gdsii.elements -from typing import List, Any, Dict, Tuple, Callable +from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional import re import io import copy @@ -39,13 +39,13 @@ path_cap_map = { } -def write(patterns: Pattern or List[Pattern], +def write(patterns: Union[Pattern, List[Pattern]], stream: io.BufferedIOBase, meters_per_unit: float, logical_units_per_unit: float = 1, library_name: str = 'masque-gdsii-write', modify_originals: bool = False, - disambiguate_func: Callable[[List[Pattern]], None] = None): + disambiguate_func: Callable[[Iterable[Pattern]], None] = None): """ Write a `Pattern` or list of patterns to a GDSII file, by first calling `.polygonize()` to change the shapes into polygons, and then writing patterns @@ -119,8 +119,8 @@ def write(patterns: Pattern or List[Pattern], return -def writefile(patterns: List[Pattern] or Pattern, - filename: str or pathlib.Path, +def writefile(patterns: Union[List[Pattern], Pattern], + filename: Union[str, pathlib.Path], *args, **kwargs, ): @@ -137,7 +137,7 @@ def writefile(patterns: List[Pattern] or Pattern, """ path = pathlib.Path(filename) if path.suffix == '.gz': - open_func = gzip.open + open_func: Callable = gzip.open else: open_func = open @@ -185,7 +185,8 @@ def dose2dtype(patterns: List[Pattern], dose_vals = set() for pat_id, pat_dose in sd_table: pat = patterns_by_id[pat_id] - [dose_vals.add(shape.dose * pat_dose) for shape in pat.shapes] + for shape in pat.shapes: + dose_vals.add(shape.dose * pat_dose) if len(dose_vals) > 256: raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) @@ -228,10 +229,10 @@ def dose2dtype(patterns: List[Pattern], return patterns, dose_vals_list -def readfile(filename: str or pathlib.Path, +def readfile(filename: Union[str, pathlib.Path], *args, **kwargs, - ) -> (Dict[str, Pattern], Dict[str, Any]): + ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: """ Wrapper for `gdsii.read()` that takes a filename or path instead of a stream. @@ -244,7 +245,7 @@ def readfile(filename: str or pathlib.Path, """ path = pathlib.Path(filename) if path.suffix == '.gz': - open_func = gzip.open + open_func: Callable = gzip.open else: open_func = open @@ -256,7 +257,7 @@ def readfile(filename: str or pathlib.Path, def read(stream: io.BufferedIOBase, use_dtype_as_dose: bool = False, clean_vertices: bool = True, - ) -> (Dict[str, Pattern], Dict[str, Any]): + ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: """ Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs @@ -466,8 +467,8 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: return gridrep -def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] - ) -> List[gdsii.elements.ARef or gdsii.elements.SRef]: +def _subpatterns_to_refs(subpatterns: List[Union[SubPattern, GridRepetition]] + ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]: refs = [] for subpat in subpatterns: if subpat.pattern is None: @@ -574,7 +575,7 @@ def disambiguate_pattern_names(patterns, # Should never happen since zero-length names are replaced raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name)) if len(encoded_name) > max_name_length: - raise PatternError('Pattern name "{}" length > {} after encode,\n originally "{}"'.format(encoded_name, max_name_length, pat.name)) + raise PatternError('Pattern name "{!r}" length > {} after encode,\n originally "{}"'.format(encoded_name, max_name_length, pat.name)) pat.name = encoded_name used_names.append(suffixed_name) diff --git a/masque/file/svg.py b/masque/file/svg.py index 2251f54..deef59a 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -1,7 +1,7 @@ """ SVG file format readers and writers """ - +from typing import Dict, Optional import svgwrite import numpy import warnings @@ -56,7 +56,7 @@ def writefile(pattern: Pattern, debug=(not custom_attributes)) # Get a dict of id(pattern) -> pattern - patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} + patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} # type: Dict[int, Optional[Pattern]] # Now create a group for each row in sd_table (ie, each pattern + dose combination) # and add in any Boundary and Use elements diff --git a/masque/pattern.py b/masque/pattern.py index 2f5b769..cefcdbd 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -2,7 +2,8 @@ Base object for containing a lithography mask. """ -from typing import List, Callable, Tuple, Dict, Union, Set +from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type +from typing import MutableMapping, Iterable import copy import itertools import pickle @@ -39,7 +40,7 @@ class Pattern: labels: List[Label] """ List of all labels in this Pattern. """ - subpatterns: List[SubPattern or GridRepetition] + subpatterns: List[Union[SubPattern, GridRepetition]] """ List of all objects referencing other patterns in this Pattern. Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays") Multiple objects in this list may reference the same Pattern object @@ -54,9 +55,9 @@ class Pattern: def __init__(self, name: str = '', - shapes: List[Shape] = (), - labels: List[Label] = (), - subpatterns: List[SubPattern] = (), + shapes: Sequence[Shape] = (), + labels: Sequence[Label] = (), + subpatterns: Sequence[Union[SubPattern, GridRepetition]] = (), locked: bool = False, ): """ @@ -129,7 +130,7 @@ class Pattern: def subset(self, shapes_func: Callable[[Shape], bool] = None, labels_func: Callable[[Label], bool] = None, - subpatterns_func: Callable[[SubPattern], bool] = None, + subpatterns_func: Callable[[Union[SubPattern, GridRepetition]], bool] = None, recursive: bool = False, ) -> 'Pattern': """ @@ -172,9 +173,9 @@ class Pattern: return pat def apply(self, - func: Callable[['Pattern'], 'Pattern'], - memo: Dict[int, 'Pattern'] = None, - ) -> 'Pattern': + func: Callable[[Optional['Pattern']], Optional['Pattern']], + memo: Optional[Dict[int, Optional['Pattern']]] = None, + ) -> Optional['Pattern']: """ Recursively apply func() to this pattern and any pattern it references. func() is expected to take and return a Pattern. @@ -217,9 +218,9 @@ class Pattern: def dfs(self, visit_before: visitor_function_t = None, visit_after: visitor_function_t = None, - transform: numpy.ndarray or bool or None = False , - memo: Dict = None, - hierarchy: Tuple['Pattern'] = (), + transform: Union[numpy.ndarray, bool, None] = False, + memo: Optional[Dict] = None, + hierarchy: Tuple['Pattern', ...] = (), ) -> 'Pattern': """ Experimental convenience function. @@ -270,7 +271,7 @@ class Pattern: pat = self if visit_before is not None: - pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) + pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore for subpattern in self.subpatterns: if transform is not False: @@ -293,12 +294,12 @@ class Pattern: hierarchy=hierarchy + (self,)) if visit_after is not None: - pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) + pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore return pat def polygonize(self, - poly_num_points: int = None, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = None, + poly_max_arclen: Optional[float] = None, ) -> 'Pattern': """ Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns, @@ -349,7 +350,7 @@ class Pattern: def subpatternize(self, recursive: bool = True, norm_value: int = int(1e6), - exclude_types: Tuple[Shape] = (Polygon,) + exclude_types: Tuple[Type] = (Polygon,) ) -> 'Pattern': """ Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates @@ -387,7 +388,7 @@ class Pattern: # Create a dict which uses the label tuple from `.normalized_form()` as a key, and which # stores `(function_to_create_normalized_shape, [(index_in_shapes, values), ...])`, where # values are the `(offset, scale, rotation, dose)` values as calculated by `.normalized_form()` - shape_table = defaultdict(lambda: [None, list()]) + shape_table: MutableMapping[Tuple, List] = defaultdict(lambda: [None, list()]) for i, shape in enumerate(self.shapes): if not any((isinstance(shape, t) for t in exclude_types)): label, values, func = shape.normalized_form(norm_value) @@ -429,9 +430,9 @@ class Pattern: is of the form `[[x0, y0], [x1, y1],...]`. """ pat = self.deepcopy().deepunlock().polygonize().flatten() - return [shape.vertices + shape.offset for shape in pat.shapes] + return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now - def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']: + def referenced_patterns_by_id(self) -> Dict[int, Optional['Pattern']]: """ Create a dictionary with `{id(pat): pat}` for all Pattern objects referenced by this Pattern (operates recursively on all referenced Patterns as well) @@ -447,7 +448,7 @@ class Pattern: ids.update(subpat.pattern.referenced_patterns_by_id()) return ids - def referenced_patterns_by_name(self) -> List[Tuple[str, 'Pattern']]: + def referenced_patterns_by_name(self) -> List[Tuple[Optional[str], Optional['Pattern']]]: """ Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this Pattern (operates recursively on all referenced Patterns as well). @@ -507,7 +508,7 @@ class Pattern: """ subpatterns = copy.deepcopy(self.subpatterns) self.subpatterns = [] - shape_counts = {} + shape_counts: Dict[Tuple, int] = {} for subpat in subpatterns: if subpat.pattern is None: continue @@ -839,7 +840,7 @@ class Pattern: pyplot.show() @staticmethod - def find_toplevel(patterns: List['Pattern']) -> List['Pattern']: + def find_toplevel(patterns: Iterable['Pattern']) -> List['Pattern']: """ Given a list of Pattern objects, return those that are not referenced by any other pattern. @@ -863,7 +864,7 @@ class Pattern: return memo patterns = set(patterns) - not_toplevel = set() + not_toplevel: Set['Pattern'] = set() for pattern in patterns: not_toplevel |= get_children(pattern, not_toplevel) diff --git a/masque/repetition.py b/masque/repetition.py index 79cc38d..37bd350 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -12,6 +12,8 @@ from numpy import pi from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 +if TYPE_CHECKING: + from . import Pattern # TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied @@ -51,7 +53,7 @@ class GridRepetition: _scale: float """ Scaling factor applied to individual instances in the grid (not the grid vectors) """ - _mirrored: List[bool] + _mirrored: numpy.ndarray # ndarray[bool] """ Whether to mirror individual instances across the x and y axes (Applies to individual instances in the grid, not the grid vectors) """ @@ -64,7 +66,7 @@ class GridRepetition: _a_count: int """ Number of instances along the direction specified by the `a_vector` """ - _b_vector: numpy.ndarray or None + _b_vector: Optional[numpy.ndarray] """ Vector `[x, y]` specifying a second lattice vector for the grid. Specifies center-to-center spacing between adjacent elements. Can be `None` for a 1D array. @@ -80,14 +82,14 @@ class GridRepetition: """ If `True`, disallows changes to the GridRepetition """ def __init__(self, - pattern: 'Pattern', + pattern: Optional['Pattern'], a_vector: numpy.ndarray, a_count: int, - b_vector: numpy.ndarray = None, + b_vector: Optional[numpy.ndarray] = None, b_count: int = 1, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, - mirrored: List[bool] = None, + mirrored: Optional[Sequence[bool]] = None, dose: float = 1.0, scale: float = 1.0, locked: bool = False): @@ -155,7 +157,7 @@ class GridRepetition: locked=self.locked) return new - def __deepcopy__(self, memo: Dict = None) -> 'GridReptition': + def __deepcopy__(self, memo: Dict = None) -> 'GridRepetition': memo = {} if memo is None else memo new = copy.copy(self).unlock() new.pattern = copy.deepcopy(self.pattern, memo) @@ -230,11 +232,11 @@ class GridRepetition: # Mirrored property @property - def mirrored(self) -> List[bool]: + def mirrored(self) -> numpy.ndarray: # ndarray[bool] return self._mirrored @mirrored.setter - def mirrored(self, val: List[bool]): + def mirrored(self, val: Sequence[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') self._mirrored = numpy.array(val, dtype=bool, copy=True) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index c6cc24a..f082c3b 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional, Sequence import copy import math import numpy @@ -32,10 +32,10 @@ class Arc(Shape): _width: float """ Width of the arc """ - poly_num_points: int + poly_num_points: Optional[int] """ Sets the default number of points for `.polygonize()` """ - poly_max_arclen: float + poly_max_arclen: Optional[float] """ Sets the default max segement length for `.polygonize()` """ # radius properties @@ -77,7 +77,7 @@ class Arc(Shape): # arc start/stop angle properties @property - def angles(self) -> vector2: + def angles(self) -> numpy.ndarray: #ndarray[float] """ Return the start and stop angles `[a_start, a_stop]`. Angles are measured from x-axis after rotation @@ -150,11 +150,11 @@ class Arc(Shape): radii: vector2, angles: vector2, width: float, - poly_num_points: int = DEFAULT_POLY_NUM_POINTS, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False): @@ -182,8 +182,8 @@ class Arc(Shape): return new def to_polygons(self, - poly_num_points: int = None, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = None, + poly_max_arclen: Optional[float] = None, ) -> List[Polygon]: if poly_num_points is None: poly_num_points = self.poly_num_points diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 96c46e4..1f6b5c9 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -1,4 +1,4 @@ -from typing import List, Dict +from typing import List, Dict, Optional import copy import numpy from numpy import pi @@ -16,10 +16,10 @@ class Circle(Shape): _radius: float """ Circle radius """ - poly_num_points: int + poly_num_points: Optional[int] """ Sets the default number of points for `.polygonize()` """ - poly_max_arclen: float + poly_max_arclen: Optional[float] """ Sets the default max segement length for `.polygonize()` """ # radius property @@ -40,8 +40,8 @@ class Circle(Shape): def __init__(self, radius: float, - poly_num_points: int = DEFAULT_POLY_NUM_POINTS, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), layer: layer_t = 0, dose: float = 1.0, @@ -64,8 +64,8 @@ class Circle(Shape): return new def to_polygons(self, - poly_num_points: int = None, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = None, + poly_max_arclen: Optional[float] = None, ) -> List[Polygon]: if poly_num_points is None: poly_num_points = self.poly_num_points diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 48cd9af..fc8a2d4 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Sequence, Optional import copy import math import numpy @@ -22,10 +22,10 @@ class Ellipse(Shape): _rotation: float """ Angle from x-axis to first radius (ccw, radians) """ - poly_num_points: int + poly_num_points: Optional[int] """ Sets the default number of points for `.polygonize()` """ - poly_max_arclen: float + poly_max_arclen: Optional[float] """ Sets the default max segement length for `.polygonize()` """ # radius properties @@ -85,11 +85,11 @@ class Ellipse(Shape): def __init__(self, radii: vector2, - poly_num_points: int = DEFAULT_POLY_NUM_POINTS, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False): @@ -114,8 +114,8 @@ class Ellipse(Shape): return new def to_polygons(self, - poly_num_points: int = None, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = None, + poly_max_arclen: Optional[float] = None, ) -> List[Polygon]: if poly_num_points is None: poly_num_points = self.poly_num_points diff --git a/masque/shapes/path.py b/masque/shapes/path.py index e320f4b..899e462 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional, Sequence import copy from enum import Enum import numpy @@ -28,8 +28,8 @@ class Path(Shape): __slots__ = ('_vertices', '_width', '_cap', '_cap_extensions') _vertices: numpy.ndarray _width: float - _cap_extensions: numpy.ndarray or None _cap: PathCap + _cap_extensions: Optional[numpy.ndarray] Cap = PathCap @@ -69,7 +69,7 @@ class Path(Shape): # cap_extensions property @property - def cap_extensions(self) -> numpy.ndarray or None: + def cap_extensions(self) -> Optional[numpy.ndarray]: """ Path end-cap extension @@ -144,11 +144,11 @@ class Path(Shape): cap_extensions: numpy.ndarray = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False, - ) -> 'Path': + ): self.unlock() self._cap_extensions = None # Since .cap setter might access it @@ -182,7 +182,7 @@ class Path(Shape): cap_extensions = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, ) -> 'Path': diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 4bd8384..da98d4d 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional, Sequence import copy import numpy from numpy import pi @@ -71,7 +71,7 @@ class Polygon(Shape): vertices: numpy.ndarray, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False, @@ -86,7 +86,7 @@ class Polygon(Shape): [self.mirror(a) for a, do in enumerate(mirrored) if do] self.locked = locked - def __deepcopy__(self, memo: Dict = None) -> 'Polygon': + def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon': memo = {} if memo is None else memo new = copy.copy(self).unlock() new._offset = self._offset.copy() @@ -154,14 +154,14 @@ class Polygon(Shape): return poly @staticmethod - def rect(xmin: float = None, - xctr: float = None, - xmax: float = None, - lx: float = None, - ymin: float = None, - yctr: float = None, - ymax: float = None, - ly: float = None, + def rect(xmin: Optional[float] = None, + xctr: Optional[float] = None, + xmax: Optional[float] = None, + lx: Optional[float] = None, + ymin: Optional[float] = None, + yctr: Optional[float] = None, + ymax: Optional[float] = None, + ly: Optional[float] = None, layer: layer_t = 0, dose: float = 1.0, ) -> 'Polygon': @@ -188,11 +188,17 @@ class Polygon(Shape): """ if lx is None: if xctr is None: + assert(xmin is not None) + assert(xmax is not None) xctr = 0.5 * (xmax + xmin) lx = xmax - xmin elif xmax is None: + assert(xmin is not None) + assert(xctr is not None) lx = 2 * (xctr - xmin) elif xmin is None: + assert(xctr is not None) + assert(xmax is not None) lx = 2 * (xmax - xctr) else: raise PatternError('Two of xmin, xctr, xmax, lx must be None!') @@ -200,19 +206,29 @@ class Polygon(Shape): if xctr is not None: pass elif xmax is None: + assert(xmin is not None) + assert(lx is not None) xctr = xmin + 0.5 * lx elif xmin is None: + assert(xmax is not None) + assert(lx is not None) xctr = xmax - 0.5 * lx else: raise PatternError('Two of xmin, xctr, xmax, lx must be None!') if ly is None: if yctr is None: + assert(ymin is not None) + assert(ymax is not None) yctr = 0.5 * (ymax + ymin) ly = ymax - ymin elif ymax is None: + assert(ymin is not None) + assert(yctr is not None) ly = 2 * (yctr - ymin) elif ymin is None: + assert(yctr is not None) + assert(ymax is not None) ly = 2 * (ymax - yctr) else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') @@ -220,8 +236,12 @@ class Polygon(Shape): if yctr is not None: pass elif ymax is None: + assert(ymin is not None) + assert(ly is not None) yctr = ymin + 0.5 * ly elif ymin is None: + assert(ly is not None) + assert(ymax is not None) yctr = ymax - 0.5 * ly else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 3aae0bb..7db14c3 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Callable +from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from abc import ABCMeta, abstractmethod import copy import numpy @@ -6,6 +6,8 @@ import numpy from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t +if TYPE_CHECKING: + from . import Polygon # Type definitions @@ -18,6 +20,9 @@ normalized_shape_tuple = Tuple[Tuple, DEFAULT_POLY_NUM_POINTS = 24 +T = TypeVar('T', bound='Shape') + + class Shape(metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. @@ -53,7 +58,10 @@ class Shape(metaclass=ABCMeta): # --- Abstract methods @abstractmethod - def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']: + def to_polygons(self, + num_vertices: Optional[int] = None, + max_arclen: Optional[float] = None, + ) -> List['Polygon']: """ Returns a list of polygons which approximate the shape. @@ -77,7 +85,7 @@ class Shape(metaclass=ABCMeta): pass @abstractmethod - def rotate(self, theta: float) -> 'Shape': + def rotate(self: T, theta: float) -> T: """ Rotate the shape around its origin (0, 0), ignoring its offset. @@ -90,7 +98,7 @@ class Shape(metaclass=ABCMeta): pass @abstractmethod - def mirror(self, axis: int) -> 'Shape': + def mirror(self: T, axis: int) -> T: """ Mirror the shape across an axis. @@ -104,7 +112,7 @@ class Shape(metaclass=ABCMeta): pass @abstractmethod - def scale_by(self, c: float) -> 'Shape': + def scale_by(self: T, c: float) -> T: """ Scale the shape's size (eg. radius, for a circle) by a constant factor. @@ -117,7 +125,7 @@ class Shape(metaclass=ABCMeta): pass @abstractmethod - def normalized_form(self, norm_value: int) -> normalized_shape_tuple: + def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple: """ Writes the shape in a standardized notation, with offset, scale, rotation, and dose information separated out from the remaining values. @@ -187,7 +195,7 @@ class Shape(metaclass=ABCMeta): self._dose = val # ---- Non-abstract methods - def copy(self) -> 'Shape': + def copy(self: T) -> T: """ Returns a deep copy of the shape. @@ -196,7 +204,7 @@ class Shape(metaclass=ABCMeta): """ return copy.deepcopy(self) - def translate(self, offset: vector2) -> 'Shape': + def translate(self: T, offset: vector2) -> T: """ Translate the shape by the given offset @@ -209,7 +217,7 @@ class Shape(metaclass=ABCMeta): self.offset += offset return self - def rotate_around(self, pivot: vector2, rotation: float) -> 'Shape': + def rotate_around(self: T, pivot: vector2, rotation: float) -> T: """ Rotate the shape around a point. @@ -428,7 +436,7 @@ class Shape(metaclass=ABCMeta): return manhattan_polygons - def lock(self) -> 'Shape': + def lock(self: T) -> T: """ Lock the Shape, disallowing further changes @@ -438,7 +446,7 @@ class Shape(metaclass=ABCMeta): object.__setattr__(self, 'locked', True) return self - def unlock(self) -> 'Shape': + def unlock(self: T) -> T: """ Unlock the Shape diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 684c637..af7beae 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence import copy import numpy from numpy import pi, inf @@ -21,7 +21,7 @@ class Text(Shape): _string: str _height: float _rotation: float - _mirrored: List[str] + _mirrored: numpy.ndarray #ndarray[bool] font_path: str # vertices property @@ -57,11 +57,11 @@ class Text(Shape): # Mirrored property @property - def mirrored(self) -> List[bool]: + def mirrored(self) -> numpy.ndarray: #ndarray[bool] return self._mirrored @mirrored.setter - def mirrored(self, val: List[bool]): + def mirrored(self, val: Sequence[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') self._mirrored = numpy.ndarray(val, dtype=bool, copy=True) @@ -72,7 +72,7 @@ class Text(Shape): font_path: str, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, - mirrored: Tuple[bool] = (False, False), + mirrored: Tuple[bool, bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False, @@ -98,11 +98,11 @@ class Text(Shape): return new def to_polygons(self, - poly_num_points: int = None, # unused - poly_max_arclen: float = None, # unused + poly_num_points: Optional[int] = None, # unused + poly_max_arclen: Optional[float] = None, # unused ) -> List[Polygon]: all_polygons = [] - total_advance = 0 + total_advance = 0.0 for char in self.string: raw_polys, advance = get_char_as_polygons(self.font_path, char) @@ -198,7 +198,7 @@ def get_char_as_polygons(font_path: str, tags = outline.tags[start:end + 1] tags.append(tags[0]) - segments = [] + segments: List[List[List[float]]] = [] for j, point in enumerate(points): # If we already have a segment, add this point to it if j > 0: diff --git a/masque/subpattern.py b/masque/subpattern.py index 16c3c7c..7115ed6 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -12,6 +12,8 @@ from numpy import pi from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 +if TYPE_CHECKING: + from . import Pattern class SubPattern: @@ -43,7 +45,7 @@ class SubPattern: _scale: float """ scale factor for the instance """ - _mirrored: List[bool] + _mirrored: numpy.ndarray # ndarray[bool] """ Whether to mirror the instanc across the x and/or y axes. """ identifier: Tuple @@ -58,11 +60,11 @@ class SubPattern: pattern: Optional['Pattern'], offset: vector2 = (0.0, 0.0), rotation: float = 0.0, - mirrored: List[bool] = None, + mirrored: Optional[Sequence[bool]] = None, dose: float = 1.0, scale: float = 1.0, locked: bool = False): - self.unlock() + object.__setattr__(self, 'locked', False) self.identifier = () self.pattern = pattern self.offset = offset @@ -161,11 +163,11 @@ class SubPattern: # Mirrored property @property - def mirrored(self) -> List[bool]: + def mirrored(self) -> numpy.ndarray: # ndarray[bool] return self._mirrored @mirrored.setter - def mirrored(self, val: List[bool]): + def mirrored(self, val: Sequence[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') self._mirrored = numpy.array(val, dtype=bool, copy=True) diff --git a/masque/utils.py b/masque/utils.py index bdb06a2..22495d9 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -2,12 +2,12 @@ Various helper functions """ -from typing import Any, Union, Tuple +from typing import Any, Union, Tuple, Sequence import numpy # Type definitions -vector2 = Union[numpy.ndarray, Tuple[float, float]] +vector2 = Union[numpy.ndarray, Tuple[float, float], Sequence[float]] layer_t = Union[int, Tuple[int, int]] @@ -68,7 +68,7 @@ def rotation_matrix_2d(theta: float) -> numpy.ndarray: [numpy.sin(theta), +numpy.cos(theta)]]) -def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: +def normalize_mirror(mirrored: Sequence[bool]) -> Tuple[bool, float]: """ Converts 0-2 mirror operations `(mirror_across_x_axis, mirror_across_y_axis)` into 0-1 mirror operations and a rotation