Improve type annotations based on mypy errors

This commit is contained in:
Jan Petykiewicz 2020-05-11 19:09:35 -07:00
parent bd4085365f
commit 157df47884
13 changed files with 151 additions and 117 deletions

View File

@ -6,7 +6,7 @@ import gdsii.library
import gdsii.structure import gdsii.structure
import gdsii.elements 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 re
import io import io
import copy 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, stream: io.BufferedIOBase,
meters_per_unit: float, meters_per_unit: float,
logical_units_per_unit: float = 1, logical_units_per_unit: float = 1,
library_name: str = 'masque-gdsii-write', library_name: str = 'masque-gdsii-write',
modify_originals: bool = False, 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 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 `.polygonize()` to change the shapes into polygons, and then writing patterns
@ -119,8 +119,8 @@ def write(patterns: Pattern or List[Pattern],
return return
def writefile(patterns: List[Pattern] or Pattern, def writefile(patterns: Union[List[Pattern], Pattern],
filename: str or pathlib.Path, filename: Union[str, pathlib.Path],
*args, *args,
**kwargs, **kwargs,
): ):
@ -137,7 +137,7 @@ def writefile(patterns: List[Pattern] or Pattern,
""" """
path = pathlib.Path(filename) path = pathlib.Path(filename)
if path.suffix == '.gz': if path.suffix == '.gz':
open_func = gzip.open open_func: Callable = gzip.open
else: else:
open_func = open open_func = open
@ -185,7 +185,8 @@ def dose2dtype(patterns: List[Pattern],
dose_vals = set() dose_vals = set()
for pat_id, pat_dose in sd_table: for pat_id, pat_dose in sd_table:
pat = patterns_by_id[pat_id] 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: if len(dose_vals) > 256:
raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) 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 return patterns, dose_vals_list
def readfile(filename: str or pathlib.Path, def readfile(filename: Union[str, pathlib.Path],
*args, *args,
**kwargs, **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. 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) path = pathlib.Path(filename)
if path.suffix == '.gz': if path.suffix == '.gz':
open_func = gzip.open open_func: Callable = gzip.open
else: else:
open_func = open open_func = open
@ -256,7 +257,7 @@ def readfile(filename: str or pathlib.Path,
def read(stream: io.BufferedIOBase, def read(stream: io.BufferedIOBase,
use_dtype_as_dose: bool = False, use_dtype_as_dose: bool = False,
clean_vertices: bool = True, 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 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 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 return gridrep
def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] def _subpatterns_to_refs(subpatterns: List[Union[SubPattern, GridRepetition]]
) -> List[gdsii.elements.ARef or gdsii.elements.SRef]: ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
refs = [] refs = []
for subpat in subpatterns: for subpat in subpatterns:
if subpat.pattern is None: if subpat.pattern is None:
@ -574,7 +575,7 @@ def disambiguate_pattern_names(patterns,
# Should never happen since zero-length names are replaced # Should never happen since zero-length names are replaced
raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name)) raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name))
if len(encoded_name) > max_name_length: 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 pat.name = encoded_name
used_names.append(suffixed_name) used_names.append(suffixed_name)

View File

@ -1,7 +1,7 @@
""" """
SVG file format readers and writers SVG file format readers and writers
""" """
from typing import Dict, Optional
import svgwrite import svgwrite
import numpy import numpy
import warnings import warnings
@ -56,7 +56,7 @@ def writefile(pattern: Pattern,
debug=(not custom_attributes)) debug=(not custom_attributes))
# Get a dict of id(pattern) -> pattern # 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) # Now create a group for each row in sd_table (ie, each pattern + dose combination)
# and add in any Boundary and Use elements # and add in any Boundary and Use elements

View File

@ -2,7 +2,8 @@
Base object for containing a lithography mask. 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 copy
import itertools import itertools
import pickle import pickle
@ -39,7 +40,7 @@ class Pattern:
labels: List[Label] labels: List[Label]
""" List of all labels in this Pattern. """ """ 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. """ List of all objects referencing other patterns in this Pattern.
Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays") Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays")
Multiple objects in this list may reference the same Pattern object Multiple objects in this list may reference the same Pattern object
@ -54,9 +55,9 @@ class Pattern:
def __init__(self, def __init__(self,
name: str = '', name: str = '',
shapes: List[Shape] = (), shapes: Sequence[Shape] = (),
labels: List[Label] = (), labels: Sequence[Label] = (),
subpatterns: List[SubPattern] = (), subpatterns: Sequence[Union[SubPattern, GridRepetition]] = (),
locked: bool = False, locked: bool = False,
): ):
""" """
@ -129,7 +130,7 @@ class Pattern:
def subset(self, def subset(self,
shapes_func: Callable[[Shape], bool] = None, shapes_func: Callable[[Shape], bool] = None,
labels_func: Callable[[Label], 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, recursive: bool = False,
) -> 'Pattern': ) -> 'Pattern':
""" """
@ -172,9 +173,9 @@ class Pattern:
return pat return pat
def apply(self, def apply(self,
func: Callable[['Pattern'], 'Pattern'], func: Callable[[Optional['Pattern']], Optional['Pattern']],
memo: Dict[int, 'Pattern'] = None, memo: Optional[Dict[int, Optional['Pattern']]] = None,
) -> 'Pattern': ) -> Optional['Pattern']:
""" """
Recursively apply func() to this pattern and any pattern it references. Recursively apply func() to this pattern and any pattern it references.
func() is expected to take and return a Pattern. func() is expected to take and return a Pattern.
@ -217,9 +218,9 @@ class Pattern:
def dfs(self, def dfs(self,
visit_before: visitor_function_t = None, visit_before: visitor_function_t = None,
visit_after: visitor_function_t = None, visit_after: visitor_function_t = None,
transform: numpy.ndarray or bool or None = False , transform: Union[numpy.ndarray, bool, None] = False,
memo: Dict = None, memo: Optional[Dict] = None,
hierarchy: Tuple['Pattern'] = (), hierarchy: Tuple['Pattern', ...] = (),
) -> 'Pattern': ) -> 'Pattern':
""" """
Experimental convenience function. Experimental convenience function.
@ -270,7 +271,7 @@ class Pattern:
pat = self pat = self
if visit_before is not None: 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: for subpattern in self.subpatterns:
if transform is not False: if transform is not False:
@ -293,12 +294,12 @@ class Pattern:
hierarchy=hierarchy + (self,)) hierarchy=hierarchy + (self,))
if visit_after is not None: 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 return pat
def polygonize(self, def polygonize(self,
poly_num_points: int = None, poly_num_points: Optional[int] = None,
poly_max_arclen: float = None, poly_max_arclen: Optional[float] = None,
) -> 'Pattern': ) -> 'Pattern':
""" """
Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns, Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns,
@ -349,7 +350,7 @@ class Pattern:
def subpatternize(self, def subpatternize(self,
recursive: bool = True, recursive: bool = True,
norm_value: int = int(1e6), norm_value: int = int(1e6),
exclude_types: Tuple[Shape] = (Polygon,) exclude_types: Tuple[Type] = (Polygon,)
) -> 'Pattern': ) -> 'Pattern':
""" """
Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates 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 # 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 # stores `(function_to_create_normalized_shape, [(index_in_shapes, values), ...])`, where
# values are the `(offset, scale, rotation, dose)` values as calculated by `.normalized_form()` # 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): for i, shape in enumerate(self.shapes):
if not any((isinstance(shape, t) for t in exclude_types)): if not any((isinstance(shape, t) for t in exclude_types)):
label, values, func = shape.normalized_form(norm_value) label, values, func = shape.normalized_form(norm_value)
@ -429,9 +430,9 @@ class Pattern:
is of the form `[[x0, y0], [x1, y1],...]`. is of the form `[[x0, y0], [x1, y1],...]`.
""" """
pat = self.deepcopy().deepunlock().polygonize().flatten() 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 Create a dictionary with `{id(pat): pat}` for all Pattern objects referenced by this
Pattern (operates recursively on all referenced Patterns as well) Pattern (operates recursively on all referenced Patterns as well)
@ -447,7 +448,7 @@ class Pattern:
ids.update(subpat.pattern.referenced_patterns_by_id()) ids.update(subpat.pattern.referenced_patterns_by_id())
return ids 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 Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this
Pattern (operates recursively on all referenced Patterns as well). Pattern (operates recursively on all referenced Patterns as well).
@ -507,7 +508,7 @@ class Pattern:
""" """
subpatterns = copy.deepcopy(self.subpatterns) subpatterns = copy.deepcopy(self.subpatterns)
self.subpatterns = [] self.subpatterns = []
shape_counts = {} shape_counts: Dict[Tuple, int] = {}
for subpat in subpatterns: for subpat in subpatterns:
if subpat.pattern is None: if subpat.pattern is None:
continue continue
@ -839,7 +840,7 @@ class Pattern:
pyplot.show() pyplot.show()
@staticmethod @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 Given a list of Pattern objects, return those that are not referenced by
any other pattern. any other pattern.
@ -863,7 +864,7 @@ class Pattern:
return memo return memo
patterns = set(patterns) patterns = set(patterns)
not_toplevel = set() not_toplevel: Set['Pattern'] = set()
for pattern in patterns: for pattern in patterns:
not_toplevel |= get_children(pattern, not_toplevel) not_toplevel |= get_children(pattern, not_toplevel)

View File

@ -12,6 +12,8 @@ from numpy import pi
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .utils import is_scalar, rotation_matrix_2d, vector2 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 # TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied
@ -51,7 +53,7 @@ class GridRepetition:
_scale: float _scale: float
""" Scaling factor applied to individual instances in the grid (not the grid vectors) """ """ 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 """ Whether to mirror individual instances across the x and y axes
(Applies to individual instances in the grid, not the grid vectors) (Applies to individual instances in the grid, not the grid vectors)
""" """
@ -64,7 +66,7 @@ class GridRepetition:
_a_count: int _a_count: int
""" Number of instances along the direction specified by the `a_vector` """ """ 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. """ Vector `[x, y]` specifying a second lattice vector for the grid.
Specifies center-to-center spacing between adjacent elements. Specifies center-to-center spacing between adjacent elements.
Can be `None` for a 1D array. Can be `None` for a 1D array.
@ -80,14 +82,14 @@ class GridRepetition:
""" If `True`, disallows changes to the GridRepetition """ """ If `True`, disallows changes to the GridRepetition """
def __init__(self, def __init__(self,
pattern: 'Pattern', pattern: Optional['Pattern'],
a_vector: numpy.ndarray, a_vector: numpy.ndarray,
a_count: int, a_count: int,
b_vector: numpy.ndarray = None, b_vector: Optional[numpy.ndarray] = None,
b_count: int = 1, b_count: int = 1,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: List[bool] = None, mirrored: Optional[Sequence[bool]] = None,
dose: float = 1.0, dose: float = 1.0,
scale: float = 1.0, scale: float = 1.0,
locked: bool = False): locked: bool = False):
@ -155,7 +157,7 @@ class GridRepetition:
locked=self.locked) locked=self.locked)
return new return new
def __deepcopy__(self, memo: Dict = None) -> 'GridReptition': def __deepcopy__(self, memo: Dict = None) -> 'GridRepetition':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new.pattern = copy.deepcopy(self.pattern, memo) new.pattern = copy.deepcopy(self.pattern, memo)
@ -230,11 +232,11 @@ class GridRepetition:
# Mirrored property # Mirrored property
@property @property
def mirrored(self) -> List[bool]: def mirrored(self) -> numpy.ndarray: # ndarray[bool]
return self._mirrored return self._mirrored
@mirrored.setter @mirrored.setter
def mirrored(self, val: List[bool]): def mirrored(self, val: Sequence[bool]):
if is_scalar(val): if is_scalar(val):
raise PatternError('Mirrored must be a 2-element list of booleans') raise PatternError('Mirrored must be a 2-element list of booleans')
self._mirrored = numpy.array(val, dtype=bool, copy=True) self._mirrored = numpy.array(val, dtype=bool, copy=True)

View File

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict from typing import List, Tuple, Dict, Optional, Sequence
import copy import copy
import math import math
import numpy import numpy
@ -32,10 +32,10 @@ class Arc(Shape):
_width: float _width: float
""" Width of the arc """ """ Width of the arc """
poly_num_points: int poly_num_points: Optional[int]
""" Sets the default number of points for `.polygonize()` """ """ 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()` """ """ Sets the default max segement length for `.polygonize()` """
# radius properties # radius properties
@ -77,7 +77,7 @@ class Arc(Shape):
# arc start/stop angle properties # arc start/stop angle properties
@property @property
def angles(self) -> vector2: def angles(self) -> numpy.ndarray: #ndarray[float]
""" """
Return the start and stop angles `[a_start, a_stop]`. Return the start and stop angles `[a_start, a_stop]`.
Angles are measured from x-axis after rotation Angles are measured from x-axis after rotation
@ -150,11 +150,11 @@ class Arc(Shape):
radii: vector2, radii: vector2,
angles: vector2, angles: vector2,
width: float, width: float,
poly_num_points: int = DEFAULT_POLY_NUM_POINTS, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: float = None, poly_max_arclen: Optional[float] = None,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Tuple[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
locked: bool = False): locked: bool = False):
@ -182,8 +182,8 @@ class Arc(Shape):
return new return new
def to_polygons(self, def to_polygons(self,
poly_num_points: int = None, poly_num_points: Optional[int] = None,
poly_max_arclen: float = None, poly_max_arclen: Optional[float] = None,
) -> List[Polygon]: ) -> List[Polygon]:
if poly_num_points is None: if poly_num_points is None:
poly_num_points = self.poly_num_points poly_num_points = self.poly_num_points

View File

@ -1,4 +1,4 @@
from typing import List, Dict from typing import List, Dict, Optional
import copy import copy
import numpy import numpy
from numpy import pi from numpy import pi
@ -16,10 +16,10 @@ class Circle(Shape):
_radius: float _radius: float
""" Circle radius """ """ Circle radius """
poly_num_points: int poly_num_points: Optional[int]
""" Sets the default number of points for `.polygonize()` """ """ 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()` """ """ Sets the default max segement length for `.polygonize()` """
# radius property # radius property
@ -40,8 +40,8 @@ class Circle(Shape):
def __init__(self, def __init__(self,
radius: float, radius: float,
poly_num_points: int = DEFAULT_POLY_NUM_POINTS, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: float = None, poly_max_arclen: Optional[float] = None,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
@ -64,8 +64,8 @@ class Circle(Shape):
return new return new
def to_polygons(self, def to_polygons(self,
poly_num_points: int = None, poly_num_points: Optional[int] = None,
poly_max_arclen: float = None, poly_max_arclen: Optional[float] = None,
) -> List[Polygon]: ) -> List[Polygon]:
if poly_num_points is None: if poly_num_points is None:
poly_num_points = self.poly_num_points poly_num_points = self.poly_num_points

View File

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict from typing import List, Tuple, Dict, Sequence, Optional
import copy import copy
import math import math
import numpy import numpy
@ -22,10 +22,10 @@ class Ellipse(Shape):
_rotation: float _rotation: float
""" Angle from x-axis to first radius (ccw, radians) """ """ 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()` """ """ 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()` """ """ Sets the default max segement length for `.polygonize()` """
# radius properties # radius properties
@ -85,11 +85,11 @@ class Ellipse(Shape):
def __init__(self, def __init__(self,
radii: vector2, radii: vector2,
poly_num_points: int = DEFAULT_POLY_NUM_POINTS, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: float = None, poly_max_arclen: Optional[float] = None,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Tuple[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
locked: bool = False): locked: bool = False):
@ -114,8 +114,8 @@ class Ellipse(Shape):
return new return new
def to_polygons(self, def to_polygons(self,
poly_num_points: int = None, poly_num_points: Optional[int] = None,
poly_max_arclen: float = None, poly_max_arclen: Optional[float] = None,
) -> List[Polygon]: ) -> List[Polygon]:
if poly_num_points is None: if poly_num_points is None:
poly_num_points = self.poly_num_points poly_num_points = self.poly_num_points

View File

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict from typing import List, Tuple, Dict, Optional, Sequence
import copy import copy
from enum import Enum from enum import Enum
import numpy import numpy
@ -28,8 +28,8 @@ class Path(Shape):
__slots__ = ('_vertices', '_width', '_cap', '_cap_extensions') __slots__ = ('_vertices', '_width', '_cap', '_cap_extensions')
_vertices: numpy.ndarray _vertices: numpy.ndarray
_width: float _width: float
_cap_extensions: numpy.ndarray or None
_cap: PathCap _cap: PathCap
_cap_extensions: Optional[numpy.ndarray]
Cap = PathCap Cap = PathCap
@ -69,7 +69,7 @@ class Path(Shape):
# cap_extensions property # cap_extensions property
@property @property
def cap_extensions(self) -> numpy.ndarray or None: def cap_extensions(self) -> Optional[numpy.ndarray]:
""" """
Path end-cap extension Path end-cap extension
@ -144,11 +144,11 @@ class Path(Shape):
cap_extensions: numpy.ndarray = None, cap_extensions: numpy.ndarray = None,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Tuple[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
locked: bool = False, locked: bool = False,
) -> 'Path': ):
self.unlock() self.unlock()
self._cap_extensions = None # Since .cap setter might access it self._cap_extensions = None # Since .cap setter might access it
@ -182,7 +182,7 @@ class Path(Shape):
cap_extensions = None, cap_extensions = None,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Tuple[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
) -> 'Path': ) -> 'Path':

View File

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict from typing import List, Tuple, Dict, Optional, Sequence
import copy import copy
import numpy import numpy
from numpy import pi from numpy import pi
@ -71,7 +71,7 @@ class Polygon(Shape):
vertices: numpy.ndarray, vertices: numpy.ndarray,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Tuple[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
locked: bool = False, locked: bool = False,
@ -86,7 +86,7 @@ class Polygon(Shape):
[self.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
self.locked = locked 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 memo = {} if memo is None else memo
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new._offset = self._offset.copy() new._offset = self._offset.copy()
@ -154,14 +154,14 @@ class Polygon(Shape):
return poly return poly
@staticmethod @staticmethod
def rect(xmin: float = None, def rect(xmin: Optional[float] = None,
xctr: float = None, xctr: Optional[float] = None,
xmax: float = None, xmax: Optional[float] = None,
lx: float = None, lx: Optional[float] = None,
ymin: float = None, ymin: Optional[float] = None,
yctr: float = None, yctr: Optional[float] = None,
ymax: float = None, ymax: Optional[float] = None,
ly: float = None, ly: Optional[float] = None,
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
) -> 'Polygon': ) -> 'Polygon':
@ -188,11 +188,17 @@ class Polygon(Shape):
""" """
if lx is None: if lx is None:
if xctr is None: if xctr is None:
assert(xmin is not None)
assert(xmax is not None)
xctr = 0.5 * (xmax + xmin) xctr = 0.5 * (xmax + xmin)
lx = xmax - xmin lx = xmax - xmin
elif xmax is None: elif xmax is None:
assert(xmin is not None)
assert(xctr is not None)
lx = 2 * (xctr - xmin) lx = 2 * (xctr - xmin)
elif xmin is None: elif xmin is None:
assert(xctr is not None)
assert(xmax is not None)
lx = 2 * (xmax - xctr) lx = 2 * (xmax - xctr)
else: else:
raise PatternError('Two of xmin, xctr, xmax, lx must be None!') raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
@ -200,19 +206,29 @@ class Polygon(Shape):
if xctr is not None: if xctr is not None:
pass pass
elif xmax is None: elif xmax is None:
assert(xmin is not None)
assert(lx is not None)
xctr = xmin + 0.5 * lx xctr = xmin + 0.5 * lx
elif xmin is None: elif xmin is None:
assert(xmax is not None)
assert(lx is not None)
xctr = xmax - 0.5 * lx xctr = xmax - 0.5 * lx
else: else:
raise PatternError('Two of xmin, xctr, xmax, lx must be None!') raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
if ly is None: if ly is None:
if yctr is None: if yctr is None:
assert(ymin is not None)
assert(ymax is not None)
yctr = 0.5 * (ymax + ymin) yctr = 0.5 * (ymax + ymin)
ly = ymax - ymin ly = ymax - ymin
elif ymax is None: elif ymax is None:
assert(ymin is not None)
assert(yctr is not None)
ly = 2 * (yctr - ymin) ly = 2 * (yctr - ymin)
elif ymin is None: elif ymin is None:
assert(yctr is not None)
assert(ymax is not None)
ly = 2 * (ymax - yctr) ly = 2 * (ymax - yctr)
else: else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!') raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
@ -220,8 +236,12 @@ class Polygon(Shape):
if yctr is not None: if yctr is not None:
pass pass
elif ymax is None: elif ymax is None:
assert(ymin is not None)
assert(ly is not None)
yctr = ymin + 0.5 * ly yctr = ymin + 0.5 * ly
elif ymin is None: elif ymin is None:
assert(ly is not None)
assert(ymax is not None)
yctr = ymax - 0.5 * ly yctr = ymax - 0.5 * ly
else: else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!') raise PatternError('Two of ymin, yctr, ymax, ly must be None!')

View File

@ -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 from abc import ABCMeta, abstractmethod
import copy import copy
import numpy import numpy
@ -6,6 +6,8 @@ import numpy
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
if TYPE_CHECKING:
from . import Polygon
# Type definitions # Type definitions
@ -18,6 +20,9 @@ normalized_shape_tuple = Tuple[Tuple,
DEFAULT_POLY_NUM_POINTS = 24 DEFAULT_POLY_NUM_POINTS = 24
T = TypeVar('T', bound='Shape')
class Shape(metaclass=ABCMeta): class Shape(metaclass=ABCMeta):
""" """
Abstract class specifying functions common to all shapes. Abstract class specifying functions common to all shapes.
@ -53,7 +58,10 @@ class Shape(metaclass=ABCMeta):
# --- Abstract methods # --- Abstract methods
@abstractmethod @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. Returns a list of polygons which approximate the shape.
@ -77,7 +85,7 @@ class Shape(metaclass=ABCMeta):
pass pass
@abstractmethod @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. Rotate the shape around its origin (0, 0), ignoring its offset.
@ -90,7 +98,7 @@ class Shape(metaclass=ABCMeta):
pass pass
@abstractmethod @abstractmethod
def mirror(self, axis: int) -> 'Shape': def mirror(self: T, axis: int) -> T:
""" """
Mirror the shape across an axis. Mirror the shape across an axis.
@ -104,7 +112,7 @@ class Shape(metaclass=ABCMeta):
pass pass
@abstractmethod @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. Scale the shape's size (eg. radius, for a circle) by a constant factor.
@ -117,7 +125,7 @@ class Shape(metaclass=ABCMeta):
pass pass
@abstractmethod @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 Writes the shape in a standardized notation, with offset, scale, rotation, and dose
information separated out from the remaining values. information separated out from the remaining values.
@ -187,7 +195,7 @@ class Shape(metaclass=ABCMeta):
self._dose = val self._dose = val
# ---- Non-abstract methods # ---- Non-abstract methods
def copy(self) -> 'Shape': def copy(self: T) -> T:
""" """
Returns a deep copy of the shape. Returns a deep copy of the shape.
@ -196,7 +204,7 @@ class Shape(metaclass=ABCMeta):
""" """
return copy.deepcopy(self) return copy.deepcopy(self)
def translate(self, offset: vector2) -> 'Shape': def translate(self: T, offset: vector2) -> T:
""" """
Translate the shape by the given offset Translate the shape by the given offset
@ -209,7 +217,7 @@ class Shape(metaclass=ABCMeta):
self.offset += offset self.offset += offset
return self 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. Rotate the shape around a point.
@ -428,7 +436,7 @@ class Shape(metaclass=ABCMeta):
return manhattan_polygons return manhattan_polygons
def lock(self) -> 'Shape': def lock(self: T) -> T:
""" """
Lock the Shape, disallowing further changes Lock the Shape, disallowing further changes
@ -438,7 +446,7 @@ class Shape(metaclass=ABCMeta):
object.__setattr__(self, 'locked', True) object.__setattr__(self, 'locked', True)
return self return self
def unlock(self) -> 'Shape': def unlock(self: T) -> T:
""" """
Unlock the Shape Unlock the Shape

View File

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence
import copy import copy
import numpy import numpy
from numpy import pi, inf from numpy import pi, inf
@ -21,7 +21,7 @@ class Text(Shape):
_string: str _string: str
_height: float _height: float
_rotation: float _rotation: float
_mirrored: List[str] _mirrored: numpy.ndarray #ndarray[bool]
font_path: str font_path: str
# vertices property # vertices property
@ -57,11 +57,11 @@ class Text(Shape):
# Mirrored property # Mirrored property
@property @property
def mirrored(self) -> List[bool]: def mirrored(self) -> numpy.ndarray: #ndarray[bool]
return self._mirrored return self._mirrored
@mirrored.setter @mirrored.setter
def mirrored(self, val: List[bool]): def mirrored(self, val: Sequence[bool]):
if is_scalar(val): if is_scalar(val):
raise PatternError('Mirrored must be a 2-element list of booleans') raise PatternError('Mirrored must be a 2-element list of booleans')
self._mirrored = numpy.ndarray(val, dtype=bool, copy=True) self._mirrored = numpy.ndarray(val, dtype=bool, copy=True)
@ -72,7 +72,7 @@ class Text(Shape):
font_path: str, font_path: str,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Tuple[bool] = (False, False), mirrored: Tuple[bool, bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
locked: bool = False, locked: bool = False,
@ -98,11 +98,11 @@ class Text(Shape):
return new return new
def to_polygons(self, def to_polygons(self,
poly_num_points: int = None, # unused poly_num_points: Optional[int] = None, # unused
poly_max_arclen: float = None, # unused poly_max_arclen: Optional[float] = None, # unused
) -> List[Polygon]: ) -> List[Polygon]:
all_polygons = [] all_polygons = []
total_advance = 0 total_advance = 0.0
for char in self.string: for char in self.string:
raw_polys, advance = get_char_as_polygons(self.font_path, char) 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 = outline.tags[start:end + 1]
tags.append(tags[0]) tags.append(tags[0])
segments = [] segments: List[List[List[float]]] = []
for j, point in enumerate(points): for j, point in enumerate(points):
# If we already have a segment, add this point to it # If we already have a segment, add this point to it
if j > 0: if j > 0:

View File

@ -12,6 +12,8 @@ from numpy import pi
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .utils import is_scalar, rotation_matrix_2d, vector2 from .utils import is_scalar, rotation_matrix_2d, vector2
if TYPE_CHECKING:
from . import Pattern
class SubPattern: class SubPattern:
@ -43,7 +45,7 @@ class SubPattern:
_scale: float _scale: float
""" scale factor for the instance """ """ 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. """ """ Whether to mirror the instanc across the x and/or y axes. """
identifier: Tuple identifier: Tuple
@ -58,11 +60,11 @@ class SubPattern:
pattern: Optional['Pattern'], pattern: Optional['Pattern'],
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: List[bool] = None, mirrored: Optional[Sequence[bool]] = None,
dose: float = 1.0, dose: float = 1.0,
scale: float = 1.0, scale: float = 1.0,
locked: bool = False): locked: bool = False):
self.unlock() object.__setattr__(self, 'locked', False)
self.identifier = () self.identifier = ()
self.pattern = pattern self.pattern = pattern
self.offset = offset self.offset = offset
@ -161,11 +163,11 @@ class SubPattern:
# Mirrored property # Mirrored property
@property @property
def mirrored(self) -> List[bool]: def mirrored(self) -> numpy.ndarray: # ndarray[bool]
return self._mirrored return self._mirrored
@mirrored.setter @mirrored.setter
def mirrored(self, val: List[bool]): def mirrored(self, val: Sequence[bool]):
if is_scalar(val): if is_scalar(val):
raise PatternError('Mirrored must be a 2-element list of booleans') raise PatternError('Mirrored must be a 2-element list of booleans')
self._mirrored = numpy.array(val, dtype=bool, copy=True) self._mirrored = numpy.array(val, dtype=bool, copy=True)

View File

@ -2,12 +2,12 @@
Various helper functions Various helper functions
""" """
from typing import Any, Union, Tuple from typing import Any, Union, Tuple, Sequence
import numpy import numpy
# Type definitions # 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]] 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)]]) [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)` Converts 0-2 mirror operations `(mirror_across_x_axis, mirror_across_y_axis)`
into 0-1 mirror operations and a rotation into 0-1 mirror operations and a rotation