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.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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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':

View File

@ -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!')

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
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

View File

@ -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:

View File

@ -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)

View File

@ -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