summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/commands.py42
-rw-r--r--lib/debug/debugger.py10
-rw-r--r--lib/debug/logging.py7
-rw-r--r--lib/debug/utils.py32
-rw-r--r--lib/elements/clone.py65
-rw-r--r--lib/elements/element.py16
-rw-r--r--lib/elements/fill_stitch.py8
-rw-r--r--lib/elements/image.py6
-rw-r--r--lib/elements/text.py4
-rw-r--r--lib/elements/utils.py9
-rw-r--r--lib/elements/validation.py7
-rw-r--r--lib/extensions/__init__.py148
-rw-r--r--lib/extensions/cleanup.py4
-rw-r--r--lib/extensions/cutwork_segmentation.py4
-rw-r--r--lib/extensions/display_stacking_order.py2
-rw-r--r--lib/extensions/element_info.py6
-rw-r--r--lib/extensions/fill_to_stroke.py4
-rw-r--r--lib/extensions/generate_palette.py2
-rw-r--r--lib/extensions/gradient_blocks.py2
-rw-r--r--lib/extensions/palette_to_text.py2
-rw-r--r--lib/extensions/reorder.py2
-rwxr-xr-xlib/extensions/sew_stack_editor.py4
-rw-r--r--lib/extensions/stroke_to_lpe_satin.py2
-rw-r--r--lib/extensions/unlink_clone.py15
-rw-r--r--lib/gui/edit_json/editable_list.py2
-rw-r--r--lib/i18n.py16
-rw-r--r--lib/lettering/categories.py7
-rw-r--r--lib/lettering/font_info.py2
-rw-r--r--lib/output.py5
-rw-r--r--lib/sew_stack/stitch_layers/stitch_layer.py4
-rwxr-xr-xlib/stitch_plan/lock_stitch.py9
-rw-r--r--lib/stitch_plan/stitch.py100
-rw-r--r--lib/stitches/guided_fill.py2
-rw-r--r--lib/stitches/running_stitch.py28
-rw-r--r--lib/stitches/tartan_fill.py3
-rw-r--r--lib/svg/path.py21
-rw-r--r--lib/tartan/fill_element.py3
-rw-r--r--lib/tartan/palette.py6
-rw-r--r--lib/tartan/svg.py7
-rw-r--r--lib/threads/color.py6
-rw-r--r--lib/utils/cache.py8
-rw-r--r--lib/utils/param.py2
42 files changed, 378 insertions, 256 deletions
diff --git a/lib/commands.py b/lib/commands.py
index c1ac35a0..abef3912 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -7,7 +7,7 @@ import os
import sys
from copy import deepcopy
from random import random
-from typing import List
+from typing import List, Optional, cast
import inkex
from shapely import geometry as shgeo
@@ -110,17 +110,17 @@ class BaseCommand(object):
class Command(BaseCommand):
- def __init__(self, connector):
- self.connector: inkex.Path = connector
+ def __init__(self, connector: inkex.PathElement) -> None:
+ self.connector = connector
self.svg = self.connector.getroottree().getroot()
self.parse_command()
- def parse_connector_path(self):
+ def parse_connector_path(self) -> inkex.Path:
path = inkex.paths.Path(self.connector.get('d')).to_superpath()
return apply_transforms(path, self.connector)
- def parse_command(self):
+ def parse_command(self) -> None:
path = self.parse_connector_path()
if len(path) == 0:
raise CommandParseError("connector has no path information")
@@ -144,7 +144,7 @@ class Command(BaseCommand):
self.target: inkex.BaseElement = neighbors[1]
- pos = [float(self.use.get("x", 0)), float(self.use.get("y", 0))]
+ pos = (float(self.use.get("x", 0)), float(self.use.get("y", 0)))
transform = get_node_transform(self.use)
pos = inkex.Transform(transform).apply_to_point(pos)
self.target_point = pos
@@ -156,13 +156,16 @@ class Command(BaseCommand):
"""
Clone this command and point it to the new target, positioning it relative to the new target the same as the target
"""
- group: inkex.BaseElement = self.connector.getparent()
- transform_relative_to_target = -self.target.composed_transform() @ group.composed_transform()
+ group: Optional[inkex.BaseElement] = cast(Optional[inkex.BaseElement], self.connector.getparent())
+ assert group is not None, "The connector should be part of a group."
+ transform_relative_to_target: inkex.Transform = -self.target.composed_transform() @ group.composed_transform()
# Clone group
- cloned_group = copy_no_children(self.connector.getparent())
+ cloned_group = copy_no_children(group)
cloned_group.transform = new_target.transform @ transform_relative_to_target
- new_target.getparent().append(cloned_group)
+ new_target_parent = new_target.getparent()
+ assert new_target_parent is not None, "The target should be a non-root element."
+ new_target_parent.append(cloned_group)
symbol = copy_no_children(self.use)
cloned_group.append(symbol)
@@ -200,12 +203,11 @@ class StandaloneCommand(BaseCommand):
@property
@cache
- def point(self):
- pos = [float(self.node.get("x", 0)), float(self.node.get("y", 0))]
+ def point(self) -> Point:
+ pos = (float(self.node.get("x", 0)), float(self.node.get("y", 0)))
transform = get_node_transform(self.node)
- pos = inkex.transforms.Transform(transform).apply_to_point(pos)
- return Point(*pos)
+ return Point(*inkex.transforms.Transform(transform).apply_to_point(pos))
def get_command_description(command: str) -> str:
@@ -298,11 +300,11 @@ def _standalone_commands(svg):
pass
-def is_command(node):
+def is_command(node: inkex.BaseElement) -> bool:
return CONNECTION_START in node.attrib or CONNECTION_END in node.attrib
-def is_command_symbol(node):
+def is_command_symbol(node: inkex.BaseElement) -> bool:
symbol = None
xlink = node.get(XLINK_HREF, "")
if xlink.startswith("#inkstitch_"):
@@ -311,23 +313,23 @@ def is_command_symbol(node):
@cache
-def symbols_path():
+def symbols_path() -> str:
return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg")
@cache
-def symbols_svg():
+def symbols_svg() -> inkex.BaseElement:
with open(symbols_path()) as symbols_file:
return inkex.load_svg(symbols_file).getroot()
@cache
-def symbol_defs():
+def symbol_defs() -> inkex.BaseElement:
return symbols_svg().defs
@cache
-def ensure_symbol(svg, command):
+def ensure_symbol(svg, command) -> None:
"""Make sure the command's symbol definition exists in the <svg:defs> tag."""
# using @cache really just makes sure that we don't bother ensuring the
diff --git a/lib/debug/debugger.py b/lib/debug/debugger.py
index 11293c8a..c58ddb3e 100644
--- a/lib/debug/debugger.py
+++ b/lib/debug/debugger.py
@@ -125,6 +125,10 @@
# to see flask server url routes:
# - comment out the line self.disable_logging() in run() of lib/api/server.py
+# We have some ignores so you don't see errors if you don't have one or more of the debugger libraries installed.
+# But in turn those ignores will cause unused-ignore errors if those libraries aren't installed...
+# mypy: disable-error-code="unused-ignore"
+
import os
import sys
@@ -148,11 +152,11 @@ def init_debugger(debug_type:str, ini: dict):
try:
if debugger == 'vscode':
- import debugpy
+ import debugpy # type: ignore[import-untyped, import-not-found]
elif debugger == 'pycharm':
- import pydevd_pycharm
+ import pydevd_pycharm # type: ignore[import-untyped, import-not-found]
elif debugger == 'pydev':
- import pydevd
+ import pydevd # type: ignore[import-untyped, import-not-found]
elif debugger == 'file':
pass
else:
diff --git a/lib/debug/logging.py b/lib/debug/logging.py
index fa474348..c46140ec 100644
--- a/lib/debug/logging.py
+++ b/lib/debug/logging.py
@@ -69,6 +69,7 @@
import os
import sys
from pathlib import Path
+from typing import Dict, Any
if sys.version_info >= (3, 11):
import tomllib # built-in in Python 3.11+
@@ -139,7 +140,7 @@ def disable_warnings():
# in development mode we want to use configuration from some LOGGING.toml file
def activate_for_development(ini: dict, SCRIPTDIR: Path):
logging_config_file = safe_get(ini, "LOGGING", "log_config_file", default=None)
- vars = {'SCRIPTDIR': SCRIPTDIR} # dynamic data for logging configuration
+ vars: Dict[str, Any] = {'SCRIPTDIR': SCRIPTDIR} # dynamic data for logging configuration
if logging_config_file is not None:
logging_config_file = Path(logging_config_file)
@@ -157,7 +158,7 @@ def activate_for_development(ini: dict, SCRIPTDIR: Path):
logger.info("Running in development mode")
logger.info(f"Using logging configuration from file: {logging_config_file}")
- logger.debug(f"Logging configuration: {devel_config = }")
+ logger.debug(f"Logging configuration: {devel_config=}")
# --------------------------------------------------------------------------------------------
@@ -177,7 +178,7 @@ def configure_logging(config: dict, ini: dict, vars: dict):
disable_logging = safe_get(ini, "LOGGING", "disable_logging", default=False)
if disable_logging:
- logger.warning(f"Logging is disabled by configuration in ini file. {disable_logging = }")
+ logger.warning(f"Logging is disabled by configuration in ini file. {disable_logging=}")
logging.disable() # globally disable all logging of all loggers
diff --git a/lib/debug/utils.py b/lib/debug/utils.py
index 10d840d9..a758ab3b 100644
--- a/lib/debug/utils.py
+++ b/lib/debug/utils.py
@@ -14,6 +14,10 @@ import logging
logger = logging.getLogger("inkstitch")
+# We have some ignores so you don't see errors if you don't have one or more of the profiling libraries installed.
+# But in turn those ignores will cause unused-ignore errors if those libraries aren't installed...
+# mypy: disable-error-code="unused-ignore"
+
# safe_get - get value from nested dictionary, return default if key does not exist
# - to read nested values from dict - mimic get method of dict with default value
@@ -67,7 +71,7 @@ def write_offline_debug_script(debug_script_dir: Path, ini: dict):
# environment PATH
f.write('# PATH:\n')
- f.write(f'# {os.environ.get("PATH","")}\n')
+ f.write(f'# {os.environ.get("PATH", "")}\n')
# for p in os.environ.get("PATH", '').split(os.pathsep): # PATH to list
# f.write(f'# {p}\n')
@@ -217,6 +221,8 @@ def profile(profiler_type, profile_dir: Path, ini: dict, extension, remaining_ar
with_profile(extension, remaining_args, profile_file_path)
elif profiler_type == 'pyinstrument':
with_pyinstrument(extension, remaining_args, profile_file_path)
+ elif profiler_type == 'monkeytype':
+ with_monkeytype(extension, remaining_args, profile_file_path)
else:
raise ValueError(f"unknown profiler type: '{profiler_type}'")
@@ -265,7 +271,7 @@ def with_pyinstrument(extension, remaining_args, profile_file_path: Path):
'''
profile with pyinstrument
'''
- import pyinstrument
+ import pyinstrument # type: ignore[import-untyped,import-not-found]
profiler = pyinstrument.Profiler()
profiler.start()
@@ -276,3 +282,25 @@ def with_pyinstrument(extension, remaining_args, profile_file_path: Path):
with open(profile_file_path, 'w') as stats_file:
stats_file.write(profiler.output_html())
print(f"Profiler: pyinstrument, stats written to '{profile_file_path.name}'. Use browser to see it.", file=sys.stderr)
+
+
+def with_monkeytype(extension, remaining_args, profile_file_path: Path) -> None:
+ '''
+ 'profile' with monkeytype to get type information. This may be handy for anyone who wants to
+ add type annotations to older parts of our code that don't have them.
+
+ See https://monkeytype.readthedocs.io/en/stable/generation.html for usage instructions.
+ '''
+ import monkeytype # type: ignore[import-not-found]
+
+ # Monkeytype will use these environment variables for the db path and to filter the modules respectively.
+ # This is easier than using monkeytype's actual config API, anyway.
+ dbpath = profile_file_path.with_suffix('.sqlite')
+ os.environ["MT_DB_PATH"] = str(dbpath)
+ os.environ["MONKEYTYPE_TRACE_MODULES"] = str(Path(__file__).parents[2].name)
+
+ with monkeytype.trace():
+ extension.run(args=remaining_args)
+
+ print(f"Profiler: monkeytype, db written to '{dbpath}'.\n\n" +
+ f"Run 'MT_DB_PATH={dbpath} monkeytype ...' from the inkstitch repo directory.", file=sys.stderr)
diff --git a/lib/elements/clone.py b/lib/elements/clone.py
index 943428b8..9c3e3e3a 100644
--- a/lib/elements/clone.py
+++ b/lib/elements/clone.py
@@ -5,11 +5,11 @@
from contextlib import contextmanager
from math import degrees
-from typing import Dict, Generator, List
+from typing import Dict, Generator, List, Optional, Tuple, Any, cast
-from inkex import BaseElement, Title, Transform
+from inkex import BaseElement, Title, Transform, Vector2d
from lxml.etree import _Comment
-from shapely import MultiLineString
+from shapely import Geometry, MultiLineString, Point as ShapelyPoint
from ..commands import (find_commands, is_command_symbol,
point_command_symbols_up)
@@ -40,12 +40,12 @@ class Clone(EmbroideryElement):
name = "Clone"
element_name = _("Clone")
- def __init__(self, *args, **kwargs):
- super(Clone, self).__init__(*args, **kwargs)
+ def __init__(self, node: BaseElement) -> None:
+ super(Clone, self).__init__(node)
@property
@param('clone', _("Clone"), type='toggle', inverse=False, default=True)
- def clone(self):
+ def clone(self) -> bool:
return self.get_boolean_param("clone", True)
@property
@@ -55,7 +55,7 @@ class Clone(EmbroideryElement):
unit='deg',
type='float')
@cache
- def clone_fill_angle(self):
+ def clone_fill_angle(self) -> float:
return self.get_float_param('angle')
@property
@@ -66,15 +66,15 @@ class Clone(EmbroideryElement):
type='boolean',
default=False)
@cache
- def flip_angle(self):
+ def flip_angle(self) -> bool:
return self.get_boolean_param('flip_angle', False)
- def get_cache_key_data(self, previous_stitch, next_element):
+ def get_cache_key_data(self, previous_stitch: Any, next_element: EmbroideryElement) -> List[str]:
source_node = self.node.href
source_elements = self.clone_to_elements(source_node)
return [element.get_cache_key(previous_stitch, next_element) for element in source_elements]
- def clone_to_elements(self, node) -> List[EmbroideryElement]:
+ def clone_to_elements(self, node: BaseElement) -> List[EmbroideryElement]:
# Only used in get_cache_key_data, actual embroidery uses nodes_to_elements+iterate_nodes
from .utils import node_to_elements
elements = []
@@ -85,7 +85,7 @@ class Clone(EmbroideryElement):
elements.extend(node_to_elements(child, True))
return elements
- def to_stitch_groups(self, last_stitch_group=None, next_element=None) -> List[StitchGroup]:
+ def to_stitch_groups(self, last_stitch_group: Optional[StitchGroup], next_element: Optional[EmbroideryElement] = None) -> List[StitchGroup]:
if not self.clone:
return []
@@ -96,7 +96,7 @@ class Clone(EmbroideryElement):
next_elements = [next_element]
if len(elements) > 1:
- next_elements = elements[1:] + next_elements
+ next_elements = cast(List[Optional[EmbroideryElement]], elements[1:]) + next_elements
for element, next_element in zip(elements, next_elements):
# Using `embroider` here to get trim/stop after commands, etc.
element_stitch_groups = element.embroider(last_stitch_group, next_element)
@@ -107,26 +107,26 @@ class Clone(EmbroideryElement):
return stitch_groups
@property
- def first_stitch(self):
+ def first_stitch(self) -> Optional[ShapelyPoint]:
first, last = self.first_and_last_element()
if first:
return first.first_stitch
return None
- def uses_previous_stitch(self):
+ def uses_previous_stitch(self) -> bool:
first, last = self.first_and_last_element()
if first:
return first.uses_previous_stitch()
- return None
+ return False
- def uses_next_element(self):
+ def uses_next_element(self) -> bool:
first, last = self.first_and_last_element()
if last:
return last.uses_next_element()
- return None
+ return False
@cache
- def first_and_last_element(self):
+ def first_and_last_element(self) -> Tuple[Optional[EmbroideryElement], Optional[EmbroideryElement]]:
with self.clone_elements() as elements:
if len(elements):
return elements[0], elements[-1]
@@ -153,7 +153,7 @@ class Clone(EmbroideryElement):
for cloned_node in cloned_nodes:
cloned_node.delete()
- def resolve_clone(self, recursive=True) -> List[BaseElement]:
+ def resolve_clone(self, recursive: bool = True) -> List[BaseElement]:
"""
"Resolve" this clone element by copying the node it hrefs as if unlinking the clone in Inkscape.
The node will be added as a sibling of this element's node, with its transform and style applied.
@@ -162,9 +162,12 @@ class Clone(EmbroideryElement):
:param recursive: Recursively "resolve" all child clones in the same manner
:returns: A list where the first element is the "resolved" node, and zero or more commands attached to that node
"""
- parent: BaseElement = self.node.getparent()
- source_node: BaseElement = self.node.href
- source_parent: BaseElement = source_node.getparent()
+ parent: Optional[BaseElement] = self.node.getparent()
+ assert parent is not None, f"Element {self.node.get_id()} should have a parent"
+ source_node: Optional[BaseElement] = self.node.href
+ assert source_node is not None, f"Target of {self.node.get_id()} was None!"
+ source_parent: Optional[BaseElement] = source_node.getparent()
+ assert source_parent is not None, f"Target {source_node.get_id()} of {self.node.get_id()} should have a parent"
cloned_node = clone_with_fixup(parent, source_node)
if recursive:
@@ -201,7 +204,7 @@ class Clone(EmbroideryElement):
if cloned_node.tag == SVG_SYMBOL_TAG:
source_transform: Transform = parent.composed_transform()
else:
- source_transform: Transform = source_parent.composed_transform()
+ source_transform = source_parent.composed_transform()
clone_transform: Transform = self.node.composed_transform()
angle_transform = clone_transform @ -source_transform
self.apply_angles(cloned_node, angle_transform)
@@ -242,7 +245,7 @@ class Clone(EmbroideryElement):
# We have to negate the angle because SVG/Inkscape's definition of rotation is clockwise, while Inkstitch uses counter-clockwise
fill_vector = (angle_transform @ Transform(f"rotate(${-element_angle})")).apply_to_point((1, 0))
# Same reason for negation here.
- element_angle = -degrees(fill_vector.angle)
+ element_angle = -degrees(fill_vector.angle or 0) # Fallback to 0 if an insane transform is used.
else: # If clone_fill_angle is specified, override the angle instead.
element_angle = self.clone_fill_angle
@@ -252,26 +255,28 @@ class Clone(EmbroideryElement):
node.set(INKSTITCH_ATTRIBS['angle'], round(element_angle, 6))
@property
- def shape(self):
+ def shape(self) -> Geometry:
path = self.node.get_path()
transform = Transform(self.node.composed_transform())
path = path.transform(transform)
path = path.to_superpath()
return MultiLineString(path[0])
- def center(self, source_node):
+ def center(self, source_node: BaseElement) -> Vector2d:
translate = Transform(f"translate({float(self.node.get('x', '0'))}, {float(self.node.get('y', '0'))})")
- transform = get_node_transform(self.node.getparent()) @ translate
+ parent = self.node.getparent()
+ assert parent is not None, "This should be part of a tree and therefore have a parent"
+ transform = get_node_transform(parent) @ translate
center = self.node.bounding_box(transform).center
return center
- def validation_warnings(self):
+ def validation_warnings(self) -> Generator[CloneWarning, Any, None]:
source_node = self.node.href
point = self.center(source_node)
yield CloneWarning(point)
-def is_clone(node):
+def is_clone(node: BaseElement) -> bool:
if node.tag == SVG_USE_TAG and node.href is not None and not is_command_symbol(node):
return True
return False
@@ -299,7 +304,7 @@ def clone_with_fixup(parent: BaseElement, node: BaseElement) -> BaseElement:
ret = clone_children(parent, node)
- def fixup_id_attr(node: BaseElement, attr: str):
+ def fixup_id_attr(node: BaseElement, attr: str) -> None:
# Replace the id value for this attrib with the corresponding one in the clone subtree, if applicable.
val = node.get(attr)
if val is not None:
diff --git a/lib/elements/element.py b/lib/elements/element.py
index ea2d5d6b..6f2c52e9 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -2,6 +2,7 @@
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+from __future__ import annotations
import json
import sys
from contextlib import contextmanager
@@ -79,8 +80,9 @@ class EmbroideryElement(object):
prop = getattr(cls, attr)
if isinstance(prop, property):
# The 'param' attribute is set by the 'param' decorator defined above.
- if hasattr(prop.fget, 'param'):
- params.append(prop.fget.param)
+ fget = prop.fget
+ if fget is not None and hasattr(fget, 'param'):
+ params.append(fget.param)
return params
@cache
@@ -215,18 +217,18 @@ class EmbroideryElement(object):
# First, figure out the translation component of the transform. Using a zero
# vector completely cancels out the rotation, scale, and skew components.
- zero = [0, 0]
+ zero = (0, 0)
zero = inkex.Transform.apply_to_point(node_transform, zero)
translate = Point(*zero)
# Next, see how the transform affects unit vectors in the X and Y axes. We
# need to subtract off the translation or it will affect the magnitude of
# the resulting vector, which we don't want.
- unit_x = [1, 0]
+ unit_x = (1, 0)
unit_x = inkex.Transform.apply_to_point(node_transform, unit_x)
sx = (Point(*unit_x) - translate).length()
- unit_y = [0, 1]
+ unit_y = (0, 1)
unit_y = inkex.Transform.apply_to_point(node_transform, unit_y)
sy = (Point(*unit_y) - translate).length()
@@ -455,7 +457,7 @@ class EmbroideryElement(object):
raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__)
@property
- def first_stitch(self):
+ def first_stitch(self) -> Optional[ShapelyPoint]:
# first stitch is an approximation to where the first stitch may possibly be
# if not defined through commands or repositioned by the previous element
raise NotImplementedError("INTERNAL ERROR: %s must implement first_stitch()", self.__class__)
@@ -521,7 +523,7 @@ class EmbroideryElement(object):
return lock_start, lock_end
- def to_stitch_groups(self, last_stitch_group: Optional[StitchGroup], next_element: Optional[ShapelyPoint] = None) -> List[StitchGroup]:
+ def to_stitch_groups(self, last_stitch_group: Optional[StitchGroup], next_element: Optional[EmbroideryElement] = None) -> List[StitchGroup]:
raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__)
@debug.time
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
index f8db39d5..ee77cd60 100644
--- a/lib/elements/fill_stitch.py
+++ b/lib/elements/fill_stitch.py
@@ -808,13 +808,17 @@ class FillStitch(EmbroideryElement):
def validation_errors(self):
if not self.shape.is_valid:
why = explain_validity(self.shape)
- message, x, y = re.match(r"(?P<message>.+)\[(?P<x>.+)\s(?P<y>.+)\]", why).groups()
+ match = re.match(r"(?P<message>.+)\[(?P<x>.+)\s(?P<y>.+)\]", why)
+ assert match is not None, f"Could not parse validity message '{why}'"
+ message, x, y = match.groups()
yield InvalidShapeError((x, y))
def validation_warnings(self): # noqa: C901
if not self.original_shape.is_valid:
why = explain_validity(self.original_shape)
- message, x, y = re.match(r"(?P<message>.+)\[(?P<x>.+)\s(?P<y>.+)\]", why).groups()
+ match = re.match(r"(?P<message>.+)\[(?P<x>.+)\s(?P<y>.+)\]", why)
+ assert match is not None, f"Could not parse validity message '{why}'"
+ message, x, y = match.groups()
if "Hole lies outside shell" in message:
yield UnconnectedWarning((x, y))
else:
diff --git a/lib/elements/image.py b/lib/elements/image.py
index 695515dc..ad012975 100644
--- a/lib/elements/image.py
+++ b/lib/elements/image.py
@@ -23,14 +23,16 @@ class ImageObject(EmbroideryElement):
name = "Image"
def center(self):
- transform = get_node_transform(self.node.getparent())
+ parent = self.node.getparent()
+ assert parent is not None, "This should be part of a tree and therefore have a parent"
+ transform = get_node_transform(parent)
center = self.node.bounding_box(transform).center
return center
def validation_warnings(self):
yield ImageTypeWarning(self.center())
- def to_stitch_groups(self, last_stitch_group):
+ def to_stitch_groups(self, last_stitch_group, next_element):
return []
def first_stitch(self):
diff --git a/lib/elements/text.py b/lib/elements/text.py
index dd886dbc..34ce14e2 100644
--- a/lib/elements/text.py
+++ b/lib/elements/text.py
@@ -21,7 +21,9 @@ class TextTypeWarning(ObjectTypeWarning):
class TextObject(EmbroideryElement):
def pointer(self):
- transform = get_node_transform(self.node.getparent())
+ parent = self.node.getparent()
+ assert parent is not None, "This should be part of a tree and therefore have a parent"
+ transform = get_node_transform(parent)
point = self.node.bounding_box(transform).center
return point
diff --git a/lib/elements/utils.py b/lib/elements/utils.py
index dfe1eb3a..cf770af4 100644
--- a/lib/elements/utils.py
+++ b/lib/elements/utils.py
@@ -3,7 +3,7 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from typing import List, Optional
+from typing import List, Optional, Iterable
from inkex import BaseElement
from lxml.etree import Comment
@@ -41,7 +41,7 @@ def node_to_elements(node, clone_to_element=False) -> List[EmbroideryElement]:
return [MarkerObject(node)]
elif node.tag in EMBROIDERABLE_TAGS or is_clone(node):
- elements = []
+ elements: List[EmbroideryElement] = []
from ..sew_stack import SewStack
sew_stack = SewStack(node)
@@ -73,7 +73,7 @@ def node_to_elements(node, clone_to_element=False) -> List[EmbroideryElement]:
return []
-def nodes_to_elements(nodes):
+def nodes_to_elements(nodes: Iterable[BaseElement]) -> List[EmbroideryElement]:
elements = []
for node in nodes:
elements.extend(node_to_elements(node))
@@ -89,7 +89,8 @@ def iterate_nodes(node: BaseElement, # noqa: C901
def walk(node: BaseElement, selected: bool) -> List[BaseElement]:
nodes = []
- if node.tag == Comment:
+ # lxml-stubs types are wrong, node.tag can be Comment.
+ if node.tag is Comment: # type:ignore[comparison-overlap]
return []
element = EmbroideryElement(node)
diff --git a/lib/elements/validation.py b/lib/elements/validation.py
index 9ac8e745..8edcc7c5 100644
--- a/lib/elements/validation.py
+++ b/lib/elements/validation.py
@@ -3,6 +3,7 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+from typing import Optional, List
from shapely.geometry import Point as ShapelyPoint
from ..utils import Point as InkstitchPoint
@@ -21,9 +22,9 @@ class ValidationMessage(object):
'''
# Subclasses will fill these in.
- name = None
- description = None
- steps_to_solve = []
+ name: Optional[str] = None
+ description: Optional[str] = None
+ steps_to_solve: List[str] = []
def __init__(self, position=None, label=""):
if isinstance(position, ShapelyPoint):
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index 9b25ff91..289cac31 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -77,76 +77,78 @@ from .update_svg import UpdateSvg
from .zigzag_line_to_satin import ZigzagLineToSatin
from .zip import Zip
-__all__ = extensions = [About,
- ApplyPalette,
- ApplyThreadlist,
- AutoRun,
- AutoSatin,
- BreakApart,
- Cleanup,
- CommandsScaleSymbols,
- ConvertToSatin,
- ConvertToStroke,
- CutSatin,
- CutworkSegmentation,
- DensityMap,
- DisplayStackingOrder,
- DuplicateParams,
- ElementInfo,
- FillToSatin,
- FillToStroke,
- Flip,
- GeneratePalette,
- GlobalCommands,
- GradientBlocks,
- Input,
- Install,
- InstallCustomPalette,
- JumpToStroke,
- JumpToTrim,
- KnockdownFill,
- LayerCommands,
- Lettering,
- LetteringAlongPath,
- LetteringCustomFontDir,
- LetteringEditJson,
- LetteringFontSample,
- LetteringForceLockStitches,
- LetteringGenerateJson,
- LetteringRemoveKerning,
- LetteringSetColorSortIndex,
- LetteringSvgFontToLayers,
- LettersToFont,
- ObjectCommands,
- ObjectCommandsToggleVisibility,
- Outline,
- Output,
- PaletteSplitText,
- PaletteToText,
- Params,
- PngRealistic,
- PngSimple,
- Preferences,
- Print,
- Redwork,
- RemoveDuplicatedPoints,
- RemoveEmbroiderySettings,
- Reorder,
- SatinMulticolor,
- SelectElements,
- SelectionToAnchorLine,
- SelectionToGuideLine,
- SelectionToPattern,
- SewStackEditor,
- Simulator,
- StitchPlanPreview,
- StitchPlanPreviewUndo,
- StrokeToLpeSatin,
- Tartan,
- TestSwatches,
- ThreadList,
- Troubleshoot,
- UnlinkClone,
- UpdateSvg,
- ZigzagLineToSatin,
- Zip]
+extensions = [
+ About,
+ ApplyPalette,
+ ApplyThreadlist,
+ AutoRun,
+ AutoSatin,
+ BreakApart,
+ Cleanup,
+ CommandsScaleSymbols,
+ ConvertToSatin,
+ ConvertToStroke,
+ CutSatin,
+ CutworkSegmentation,
+ DensityMap,
+ DisplayStackingOrder,
+ DuplicateParams,
+ ElementInfo,
+ FillToSatin,
+ FillToStroke,
+ Flip,
+ GeneratePalette,
+ GlobalCommands,
+ GradientBlocks,
+ Input,
+ Install,
+ InstallCustomPalette,
+ JumpToStroke,
+ JumpToTrim,
+ KnockdownFill,
+ LayerCommands,
+ Lettering,
+ LetteringAlongPath,
+ LetteringCustomFontDir,
+ LetteringEditJson,
+ LetteringFontSample,
+ LetteringForceLockStitches,
+ LetteringGenerateJson,
+ LetteringRemoveKerning,
+ LetteringSetColorSortIndex,
+ LetteringSvgFontToLayers,
+ LettersToFont,
+ ObjectCommands,
+ ObjectCommandsToggleVisibility,
+ Outline,
+ Output,
+ PaletteSplitText,
+ PaletteToText,
+ Params,
+ PngRealistic,
+ PngSimple,
+ Preferences,
+ Print,
+ Redwork,
+ RemoveDuplicatedPoints,
+ RemoveEmbroiderySettings,
+ Reorder,
+ SatinMulticolor,
+ SelectElements,
+ SelectionToAnchorLine,
+ SelectionToGuideLine,
+ SelectionToPattern,
+ SewStackEditor,
+ Simulator,
+ StitchPlanPreview,
+ StitchPlanPreviewUndo,
+ StrokeToLpeSatin,
+ Tartan,
+ TestSwatches,
+ ThreadList,
+ Troubleshoot,
+ UnlinkClone,
+ UpdateSvg,
+ ZigzagLineToSatin,
+ Zip
+]
diff --git a/lib/extensions/cleanup.py b/lib/extensions/cleanup.py
index d5816b5f..366d8547 100644
--- a/lib/extensions/cleanup.py
+++ b/lib/extensions/cleanup.py
@@ -60,12 +60,12 @@ class Cleanup(InkstitchExtension):
def _dry_run(self):
errormsg(_("%s elements to remove:" % len(self.elements_to_remove)))
for element in self.elements_to_remove:
- errormsg(f" - { element.label }: {element.get_id()}")
+ errormsg(f" - {element.label}: {element.get_id()}")
errormsg("\n")
errormsg(_("%s groups/layers to remove:" % len(self.groups_to_remove)))
for group in self.groups_to_remove:
- errormsg(f" - { group.label }: {group.get_id()}")
+ errormsg(f" - {group.label}: {group.get_id()}")
def _remove(self):
num_elements_removed = len(self.elements_to_remove)
diff --git a/lib/extensions/cutwork_segmentation.py b/lib/extensions/cutwork_segmentation.py
index bea14472..c041e359 100644
--- a/lib/extensions/cutwork_segmentation.py
+++ b/lib/extensions/cutwork_segmentation.py
@@ -136,7 +136,7 @@ class CutworkSegmentation(InkstitchExtension):
d = "M "
for point in point_list:
- d += f"{ point.x }, { point.y } "
+ d += f"{point.x}, {point.y} "
stroke_element = inkex.PathElement(attrib={
"style": color,
@@ -188,4 +188,4 @@ class CutworkSegmentation(InkstitchExtension):
def path_style(self, element, color):
# set stroke color and make it a running stitch - they don't want to cut zigzags
- return inkex.Style(element.node.get('style', '')) + inkex.Style(f'stroke-width:1;stroke:{ color };')
+ return inkex.Style(element.node.get('style', '')) + inkex.Style(f'stroke-width:1;stroke:{color};')
diff --git a/lib/extensions/display_stacking_order.py b/lib/extensions/display_stacking_order.py
index ba5fda98..f9f83ca4 100644
--- a/lib/extensions/display_stacking_order.py
+++ b/lib/extensions/display_stacking_order.py
@@ -38,7 +38,7 @@ class DisplayStackingOrder(InkstitchExtension):
'x': str(position[0]),
'y': str(position[1])
})
- text.style = inkex.Style(f"text-anchor: middle;text-align: center;dominant-baseline: middle;font-size: { self.options.font_size }")
+ text.style = inkex.Style(f"text-anchor: middle;text-align: center;dominant-baseline: middle;font-size: {self.options.font_size}")
tspan = inkex.Tspan()
tspan.text = str(num)
text.add(tspan)
diff --git a/lib/extensions/element_info.py b/lib/extensions/element_info.py
index b84232b7..06acbb16 100644
--- a/lib/extensions/element_info.py
+++ b/lib/extensions/element_info.py
@@ -48,7 +48,7 @@ class ElementInfo(InkstitchExtension):
)
self.list_items.append(ListItem(
- name=f"{ element.node.label } ({ element.node.get_id() })",
+ name=f"{element.node.label} ({element.node.get_id()})",
value=stitch_groups[0].color,
headline=True
))
@@ -116,7 +116,7 @@ class ElementInfo(InkstitchExtension):
stitches_per_group = ""
if len(stitch_groups) > 1:
- stitches_per_group = f" ({', '.join([str(len(group.stitches)) for group in stitch_groups]) })"
+ stitches_per_group = f" ({', '.join([str(len(group.stitches)) for group in stitch_groups])})"
self.list_items.append(ListItem(
name=_("Stitches"),
@@ -200,7 +200,7 @@ class ElementInfo(InkstitchExtension):
class ListItem:
- def __init__(self, name="", value="", headline=False, warning=False):
+ def __init__(self, name="", value="", headline=False, warning=False) -> None:
self.name: str = name
self.value: str = value
self.headline: bool = headline
diff --git a/lib/extensions/fill_to_stroke.py b/lib/extensions/fill_to_stroke.py
index 9101eca2..085b3c95 100644
--- a/lib/extensions/fill_to_stroke.py
+++ b/lib/extensions/fill_to_stroke.py
@@ -67,7 +67,7 @@ class FillToStroke(InkstitchExtension):
element_label = element.node.label
group_name = element_label or element_id
- centerline_group = Group.new(f'{ group_name } { _("center line") }', id=self.uniqueId("centerline_group_"))
+ centerline_group = Group.new(f'{group_name} { _("center line") }', id=self.uniqueId("centerline_group_"))
parent = element.node.getparent()
index = parent.index(element.node) + 1
parent.insert(index, centerline_group)
@@ -75,7 +75,7 @@ class FillToStroke(InkstitchExtension):
transform = Transform(get_correction_transform(parent, child=True))
stroke_width = convert_unit(self.options.line_width_mm, 'px', 'mm')
color = element.node.style('fill')
- style = f"fill:none;stroke:{ color };stroke-width:{ stroke_width }"
+ style = f"fill:none;stroke:{color};stroke-width:{stroke_width}"
multipolygon = element.shape
multipolygon = self._apply_cut_lines(cut_lines, multipolygon)
diff --git a/lib/extensions/generate_palette.py b/lib/extensions/generate_palette.py
index b87bc179..6cc839fb 100644
--- a/lib/extensions/generate_palette.py
+++ b/lib/extensions/generate_palette.py
@@ -84,4 +84,4 @@ class GeneratePalette(InkstitchExtension):
if __name__ == '__main__':
e = GeneratePalette()
- e.affect()
+ e.run()
diff --git a/lib/extensions/gradient_blocks.py b/lib/extensions/gradient_blocks.py
index 8e341cc8..9ae24736 100644
--- a/lib/extensions/gradient_blocks.py
+++ b/lib/extensions/gradient_blocks.py
@@ -173,4 +173,4 @@ def gradient_shapes_and_attributes(element, shape, unit_multiplier):
if __name__ == '__main__':
e = GradientBlocks()
- e.effect()
+ e.run()
diff --git a/lib/extensions/palette_to_text.py b/lib/extensions/palette_to_text.py
index 729c92fc..8c3db035 100644
--- a/lib/extensions/palette_to_text.py
+++ b/lib/extensions/palette_to_text.py
@@ -55,4 +55,4 @@ class PaletteToText(InkstitchExtension):
if __name__ == '__main__':
e = PaletteToText()
- e.affect()
+ e.run()
diff --git a/lib/extensions/reorder.py b/lib/extensions/reorder.py
index 956c0615..2c10559d 100644
--- a/lib/extensions/reorder.py
+++ b/lib/extensions/reorder.py
@@ -34,4 +34,4 @@ class Reorder(InkstitchExtension):
if __name__ == '__main__':
e = Reorder()
- e.affect()
+ e.run()
diff --git a/lib/extensions/sew_stack_editor.py b/lib/extensions/sew_stack_editor.py
index a7cc9e38..3b703225 100755
--- a/lib/extensions/sew_stack_editor.py
+++ b/lib/extensions/sew_stack_editor.py
@@ -6,8 +6,8 @@ import sys
import wx
from wx.lib.agw import ultimatelistctrl as ulc
-from wx.lib.checkbox import GenCheckBox
-from wx.lib.splitter import MultiSplitterWindow
+from wx.lib.checkbox import GenCheckBox # type:ignore[import-untyped]
+from wx.lib.splitter import MultiSplitterWindow # type:ignore[import-untyped]
from .base import InkstitchExtension
from ..debug.debug import debug
diff --git a/lib/extensions/stroke_to_lpe_satin.py b/lib/extensions/stroke_to_lpe_satin.py
index 7d4ffb3e..3c0ed017 100644
--- a/lib/extensions/stroke_to_lpe_satin.py
+++ b/lib/extensions/stroke_to_lpe_satin.py
@@ -165,7 +165,7 @@ class StrokeToLpeSatin(InkstitchExtension):
class SatinPattern:
- def __init__(self, path=None, node_types=None, flip=True, rung_node=1):
+ def __init__(self, path=None, node_types=None, flip=True, rung_node=1) -> None:
self.path: str = path
self.node_types: str = node_types
self.flip: bool = flip
diff --git a/lib/extensions/unlink_clone.py b/lib/extensions/unlink_clone.py
index bebfbdb8..b4af7961 100644
--- a/lib/extensions/unlink_clone.py
+++ b/lib/extensions/unlink_clone.py
@@ -3,7 +3,7 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from typing import List, Tuple
+from typing import List, Tuple, cast
from inkex import BaseElement, Boolean, Group, errormsg
@@ -19,7 +19,7 @@ class UnlinkClone(InkstitchExtension):
self.arg_parser.add_argument("--notebook")
self.arg_parser.add_argument("-r", "--recursive", dest="recursive", type=Boolean, default=True)
- def effect(self):
+ def effect(self) -> None:
recursive: bool = self.options.recursive
if not self.get_elements():
@@ -39,15 +39,16 @@ class UnlinkClone(InkstitchExtension):
group = Group()
for child in resolved[0]:
group.append(child)
- resolved[0].getparent().replace(resolved[0], group)
+ parent = cast(BaseElement, resolved[0].getparent()) # Safe assumption that this has a parent.
+ parent.replace(resolved[0], group)
clones_resolved.append((element.node, resolved[0]))
- for (clone, resolved) in clones_resolved:
+ for (clone, resolved_clone) in clones_resolved:
clone.delete()
- orig_id = resolved.get_id()
+ orig_id = resolved_clone.get_id()
new_id = clone.get_id()
# Fix up command backlinks - note this has to happen before we rename so they can actually be found.
- for command in EmbroideryElement(resolved).commands:
+ for command in EmbroideryElement(resolved_clone).commands:
backlink_attrib = CONNECTION_START if command.connector.get(CONNECTION_START) == ("#"+orig_id) else CONNECTION_END
command.connector.set(backlink_attrib, "#"+new_id)
- resolved.set_id(new_id)
+ resolved_clone.set_id(new_id)
diff --git a/lib/gui/edit_json/editable_list.py b/lib/gui/edit_json/editable_list.py
index fc033597..8f62f554 100644
--- a/lib/gui/edit_json/editable_list.py
+++ b/lib/gui/edit_json/editable_list.py
@@ -1,5 +1,5 @@
import wx
-from wx.lib.mixins.listctrl import TextEditMixin
+from wx.lib.mixins.listctrl import TextEditMixin # type: ignore[import-untyped]
class EditableListCtrl(wx.ListCtrl, TextEditMixin):
diff --git a/lib/i18n.py b/lib/i18n.py
index 2a901a0e..b2a2b002 100644
--- a/lib/i18n.py
+++ b/lib/i18n.py
@@ -5,30 +5,27 @@
import gettext
import os
+from typing import Callable, Tuple
from .utils import cache, get_resource_dir
-_ = translation = None
-locale_dir = None
-
# Use N_ to mark a string for translation but _not_ immediately translate it.
# reference: https://docs.python.org/3/library/gettext.html#deferred-translations
# Makefile configures pybabel to treat N_() the same as _()
-def N_(message): return message
+def N_(message: str) -> str:
+ return message
-def _set_locale_dir():
- global locale_dir
+def localize(languages=None) -> Tuple[Callable[[str], str], gettext.NullTranslations]:
locale_dir = get_resource_dir('locales')
-
-def localize(languages=None):
global translation, _
translation = gettext.translation("inkstitch", locale_dir, fallback=True)
_ = translation.gettext
+ return (_, translation)
@cache
@@ -53,5 +50,4 @@ def get_languages():
return languages
-_set_locale_dir()
-localize()
+_, translation = localize()
diff --git a/lib/lettering/categories.py b/lib/lettering/categories.py
index 9a63063a..e7e81b3f 100644
--- a/lib/lettering/categories.py
+++ b/lib/lettering/categories.py
@@ -7,9 +7,9 @@ from ..i18n import _
class FontCategory:
- def __init__(self, cat_id=None, name=None):
- self.id: str = cat_id
- self.name: str = name
+ def __init__(self, cat_id: str, name: str) -> None:
+ self.id = cat_id
+ self.name = name
def __repr__(self):
return "FontCategory(%s, %s)" % (self.id, self.name)
@@ -35,5 +35,4 @@ FONT_CATEGORIES = [
FontCategory('hebrew', _("Script: hebrew")),
FontCategory('japanese', _("Script: japanese")),
FontCategory('latin', _("Script: latin"))
-
]
diff --git a/lib/lettering/font_info.py b/lib/lettering/font_info.py
index 74cc0967..8bf35900 100644
--- a/lib/lettering/font_info.py
+++ b/lib/lettering/font_info.py
@@ -5,7 +5,7 @@
from collections import defaultdict
-from fontTools.agl import toUnicode
+from fontTools.agl import toUnicode # type:ignore[import-untyped]
from inkex import NSS
from lxml import etree
diff --git a/lib/output.py b/lib/output.py
index 4559ca2b..a16b4018 100644
--- a/lib/output.py
+++ b/lib/output.py
@@ -32,11 +32,6 @@ def get_command(stitch):
return pyembroidery.NEEDLE_AT
-def _string_to_floats(string):
- floats = string.split(',')
- return [float(num) for num in floats]
-
-
def get_origin(svg, bounding_box):
(minx, miny, maxx, maxy) = bounding_box
origin_command = global_command(svg, "origin")
diff --git a/lib/sew_stack/stitch_layers/stitch_layer.py b/lib/sew_stack/stitch_layers/stitch_layer.py
index 4b34373a..615a36e8 100644
--- a/lib/sew_stack/stitch_layers/stitch_layer.py
+++ b/lib/sew_stack/stitch_layers/stitch_layer.py
@@ -1,10 +1,12 @@
+from typing import Type
from ...utils import coordinate_list_to_point_list
from ...utils.dotdict import DotDict
+from .stitch_layer_editor import StitchLayerEditor
class StitchLayer:
# must be overridden in child classes and set to a subclass of StitchLayerEditor
- editor_class = None
+ editor_class: Type[StitchLayerEditor] = None # type:ignore[assignment]
# not to be overridden in child classes
_defaults = None
diff --git a/lib/stitch_plan/lock_stitch.py b/lib/stitch_plan/lock_stitch.py
index 899aa382..98219154 100755
--- a/lib/stitch_plan/lock_stitch.py
+++ b/lib/stitch_plan/lock_stitch.py
@@ -1,5 +1,6 @@
import re
from math import degrees
+from typing import List, Optional
from inkex import DirectedLineSegment, Path
from shapely.geometry import LineString
@@ -12,16 +13,16 @@ from .stitch import Stitch
class LockStitchDefinition:
- def __init__(self, lock_id=None, name=None, path=None, preview_image=None):
+ def __init__(self, lock_id=None, name=None, path=None, preview_image=None) -> None:
self.id: str = lock_id
self.name: str = name
self._path: str = path
- self.preview_image: str = None
+ self.preview_image: Optional[str] = None
- def __repr__(self):
+ def __repr__(self) -> str:
return "LockStitchDefinition(%s, %s, %s, %s)" % (self.id, self.name, self._path, self.preview_image)
- def stitches(self, stitches, pos, scale):
+ def stitches(self, stitches, pos, scale) -> List[Stitch]:
raise NotImplementedError(f"{self.__class__.__name__} must implement stitches()")
diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py
index ffa944ae..bce7c05a 100644
--- a/lib/stitch_plan/stitch.py
+++ b/lib/stitch_plan/stitch.py
@@ -3,6 +3,8 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+from __future__ import annotations # Needed for using the Stitch type as a constructor arg
+from typing import Dict, Type, Union, Optional, Set, Any, Iterable, overload
from shapely import geometry as shgeo
from ..utils.geometry import Point
@@ -10,17 +12,80 @@ from ..utils.geometry import Point
class Stitch(Point):
"""A stitch is a Point with extra information telling how to sew it."""
+ x: float
+ y: float
+ color: Any # Todo: What is this
+ jump: bool
+ stop: bool
+ trim: bool
+ color_change: bool
+ min_stitch_length: Optional[float]
+ tags: Set[str]
+
+ @overload
+ def __init__(
+ self,
+ x: Stitch,
+ color: Optional[Any] = None,
+ jump=False,
+ stop=False,
+ trim=False,
+ color_change=False,
+ min_stitch_length: Optional[float] = None,
+ tags: Optional[Iterable[str]] = None
+ ): ...
+ @overload
def __init__(
self,
- x, y=None,
- color=None,
+ x: Point,
+ color: Optional[Any] = None,
jump=False,
stop=False,
trim=False,
color_change=False,
- min_stitch_length=None,
- tags=None
+ min_stitch_length: Optional[float] = None,
+ tags: Optional[Iterable[str]] = None
+ ): ...
+
+ @overload
+ def __init__(
+ self,
+ x: shgeo.Point,
+ color: Optional[Any] = None,
+ jump=False,
+ stop=False,
+ trim=False,
+ color_change=False,
+ min_stitch_length: Optional[float] = None,
+ tags: Optional[Iterable[str]] = None
+ ): ...
+
+ @overload
+ def __init__(
+ self,
+ x: float,
+ y: float,
+ color: Optional[Any] = None,
+ jump: bool = False,
+ stop: bool = False,
+ trim: bool = False,
+ color_change: bool = False,
+ min_stitch_length: Optional[float] = None,
+ tags: Optional[Iterable[str]] = None
+ ): ...
+
+ def __init__(
+ self,
+ x: Union[Stitch, float, Point],
+ y: Optional[float] = None,
+ color: Optional[Any] = None,
+ jump: bool = False,
+ stop: bool = False,
+ trim: bool = False,
+ color_change: bool = False,
+ min_stitch_length: Optional[float] = None,
+ tags: Optional[Iterable[str]] = None
):
# DANGER: if you add new attributes, you MUST also set their default
# values in __new__() below. Otherwise, cached stitch plans can be
@@ -32,14 +97,15 @@ class Stitch(Point):
# Allow creating a Stitch from another Stitch. Attributes passed as
# arguments will override any existing attributes.
base_stitch = x
- self.x: float = base_stitch.x
- self.y: float = base_stitch.y
+ self.x = base_stitch.x
+ self.y = base_stitch.y
elif isinstance(x, (Point, shgeo.Point)):
# Allow creating a Stitch from a Point
point = x
- self.x: float = point.x
- self.y: float = point.y
+ self.x = point.x
+ self.y = point.y
else:
+ assert y is not None, "Bad stitch constructor use: No y component?"
Point.__init__(self, x, y)
self._set('color', color, base_stitch)
@@ -54,7 +120,7 @@ class Stitch(Point):
if base_stitch is not None:
self.add_tags(base_stitch.tags)
- def __new__(cls, *args, **kwargs):
+ def __new__(cls: Type[Stitch], *args, **kwargs) -> Stitch:
instance = super().__new__(cls)
# Set default values for any new attributes here (see note in __init__() above)
@@ -74,7 +140,7 @@ class Stitch(Point):
"COLOR CHANGE" if self.color_change else " "
)
- def _set(self, attribute, value, base_stitch):
+ def _set(self, attribute: str, value: Optional[Any], base_stitch: Optional[Stitch]) -> None:
# Set an attribute. If the caller passed a Stitch object, use its value, unless
# they overrode it with arguments.
if base_stitch is not None:
@@ -86,11 +152,11 @@ class Stitch(Point):
def is_terminator(self) -> bool:
return self.trim or self.stop or self.color_change
- def add_tags(self, tags):
+ def add_tags(self, tags: Iterable[str]) -> None:
for tag in tags:
self.add_tag(tag)
- def add_tag(self, tag):
+ def add_tag(self, tag: str) -> None:
"""Store arbitrary information about a stitch.
Tags can be used to store any information about a stitch. This can be
@@ -105,10 +171,10 @@ class Stitch(Point):
"""
self.tags.add(tag)
- def has_tag(self, tag):
+ def has_tag(self, tag: str) -> bool:
return tag in self.tags
- def copy(self):
+ def copy(self) -> Stitch:
return Stitch(
self.x,
self.y,
@@ -121,18 +187,18 @@ class Stitch(Point):
self.tags
)
- def offset(self, offset: Point):
+ def offset(self, offset: Point) -> Stitch:
out = self.copy()
out.x += offset.x
out.y += offset.y
return out
- def __json__(self):
+ def __json__(self) -> Dict[str, Any]:
attributes = dict(vars(self))
attributes['tags'] = list(attributes['tags'])
return attributes
- def __getstate__(self):
+ def __getstate__(self) -> Dict[str, Any]:
# This is used by pickle. We want to sort the tag list so that the
# pickled representation is stable, since it's used to generate cache
# keys.
diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py
index 6ad80da0..5d9a6018 100644
--- a/lib/stitches/guided_fill.py
+++ b/lib/stitches/guided_fill.py
@@ -172,7 +172,7 @@ def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num,
if len(points) < 2:
coords = line.coords
- points = [coords[0], coords[-1]]
+ points = np.array([coords[0], coords[-1]])
stitched_line = shgeo.LineString(points)
diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py
index 6144a977..a773fa9a 100644
--- a/lib/stitches/running_stitch.py
+++ b/lib/stitches/running_stitch.py
@@ -33,7 +33,9 @@ def split_segment_even_n(a, b, segments: int, jitter_sigma: float = 0.0, random_
splits = splits + jitters * (jitter_sigma / segments)
# sort the splits in case a bad roll transposes any of them
- return [line.interpolate(x, normalized=True) for x in sorted(splits)]
+ splits.sort()
+
+ return [line.interpolate(x, normalized=True) for x in splits]
def split_segment_even_dist(a, b, max_length: float, jitter_sigma: float = 0.0, random_seed=None) -> typing.List[shgeo.Point]:
@@ -80,7 +82,7 @@ class AngleInterval():
# partially based on https://fgiesen.wordpress.com/2015/09/24/intervals-in-modular-arithmetic/
def __init__(self, a: float, b: float, all: bool = False):
- self.all = all
+ self.isAll = all
self.a = a
self.b = b
@@ -111,7 +113,7 @@ class AngleInterval():
return AngleInterval(angleB - 1e-6, angleA + 1e-6)
def containsAngle(self, angle: float):
- if self.all:
+ if self.isAll:
return True
return (angle - self.a) % tau <= (self.b - self.a) % tau
@@ -122,9 +124,9 @@ class AngleInterval():
# assume that each interval contains less than half the circle (or all of it)
if other is None:
return None
- elif self.all:
+ elif self.isAll:
return other
- elif other.all:
+ elif other.isAll:
return self
elif self.containsAngle(other.a):
if other.containsAngle(self.b):
@@ -140,7 +142,7 @@ class AngleInterval():
return None
def cutSegment(self, origin: Point, a: Point, b: Point):
- if self.all:
+ if self.isAll:
return None
segArc = AngleInterval.fromSegment(a - origin, b - origin)
if segArc is None:
@@ -180,7 +182,8 @@ def cut_segment_with_circle(origin: Point, r: float, a: Point, b: Point) -> Poin
return a + d*t
-def take_stitch(start: Point, points: typing.Sequence[Point], idx: int, stitch_length: float, tolerance: float):
+def take_stitch(start: Point, points: typing.Sequence[Point], idx: int, stitch_length: float, tolerance: float) -> \
+ typing.Tuple[typing.Optional[Point], typing.Optional[int]]:
# Based on a single step of the Zhao-Saalfeld curve simplification algorithm.
# https://cartogis.org/docs/proceedings/archive/auto-carto-13/pdf/linear-time-sleeve-fitting-polyline-simplification-algorithms.pdf
# Adds early termination condition based on stitch length.
@@ -207,7 +210,7 @@ def take_stitch(start: Point, points: typing.Sequence[Point], idx: int, stitch_l
return points[-1], None
-def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, tolerance: float):
+def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, tolerance: float) -> typing.List[Point]:
# Will split a straight line into even-length stitches while still handling curves correctly.
# Includes end point but not start point.
if len(points) < 2:
@@ -216,9 +219,9 @@ def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, to
for i in reversed(range(0, len(points) - 1)):
distLeft[i] = distLeft[i + 1] + points[i].distance(points[i+1])
- i = 1
+ i: typing.Optional[int] = 1
last = points[0]
- stitches = []
+ stitches: typing.List[Point] = []
while i is not None and i < len(points):
d = last.distance(points[i]) + distLeft[i]
if d == 0:
@@ -233,7 +236,8 @@ def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, to
return stitches
-def stitch_curve_randomly(points: typing.Sequence[Point], stitch_length: float, tolerance: float, stitch_length_sigma: float, random_seed: str):
+def stitch_curve_randomly(points: typing.Sequence[Point], stitch_length: float, tolerance: float, stitch_length_sigma: float, random_seed: str) ->\
+ typing.List[Point]:
min_stitch_length = max(0, stitch_length * (1 - stitch_length_sigma))
max_stitch_length = stitch_length * (1 + stitch_length_sigma)
# Will split a straight line into stitches of random length within the range.
@@ -242,7 +246,7 @@ def stitch_curve_randomly(points: typing.Sequence[Point], stitch_length: float,
if len(points) < 2:
return []
- i = 1
+ i: typing.Optional[int] = 1
last = points[0]
last_shortened = 0.0
stitches = []
diff --git a/lib/stitches/tartan_fill.py b/lib/stitches/tartan_fill.py
index 1ddd8195..c25bb435 100644
--- a/lib/stitches/tartan_fill.py
+++ b/lib/stitches/tartan_fill.py
@@ -3,6 +3,9 @@
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+# This file needs some more love before it'll pass type checking.
+# mypy: ignore-errors=true
+
from collections import defaultdict
from itertools import chain
from math import cos, radians, sin
diff --git a/lib/svg/path.py b/lib/svg/path.py
index 548a82f2..5e6e7007 100644
--- a/lib/svg/path.py
+++ b/lib/svg/path.py
@@ -9,7 +9,7 @@ from .tags import SVG_GROUP_TAG, SVG_LINK_TAG
from .units import get_viewbox_transform
-def apply_transforms(path, node):
+def apply_transforms(path: inkex.Path, node: inkex.BaseElement) -> inkex.Path:
transform = get_node_transform(node)
# apply the combined transform to this node's path
@@ -18,20 +18,21 @@ def apply_transforms(path, node):
return path
-def compose_parent_transforms(node, mat):
+def compose_parent_transforms(node: inkex.BaseElement, mat: inkex.Transform) -> inkex.Transform:
# This is adapted from Inkscape's simpletransform.py's composeParents()
# function. That one can't handle nodes that are detached from a DOM.
trans = node.get('transform')
if trans:
mat = inkex.transforms.Transform(trans) @ mat
- if node.getparent() is not None:
- if node.getparent().tag in [SVG_GROUP_TAG, SVG_LINK_TAG]:
- mat = compose_parent_transforms(node.getparent(), mat)
+ parent = node.getparent()
+ if parent is not None:
+ if parent.tag in [SVG_GROUP_TAG, SVG_LINK_TAG]:
+ mat = compose_parent_transforms(parent, mat)
return mat
-def get_node_transform(node: inkex.BaseElement):
+def get_node_transform(node: inkex.BaseElement) -> inkex.Transform:
"""
if getattr(node, "composed_transform", None):
return node.composed_transform()
@@ -52,7 +53,7 @@ def get_node_transform(node: inkex.BaseElement):
return transform
-def get_correction_transform(node, child=False):
+def get_correction_transform(node: inkex.BaseElement, child=False) -> str:
"""Get a transform to apply to new siblings or children of this SVG node
Arguments:
@@ -71,7 +72,11 @@ def get_correction_transform(node, child=False):
else:
# we can ignore the transform on the node itself since it won't apply
# to the objects we add
- transform = get_node_transform(node.getparent())
+ parent = node.getparent()
+ if parent is not None:
+ transform = get_node_transform(parent)
+ else:
+ transform = inkex.Transform()
# now invert it, so that we can position our objects in absolute
# coordinates
diff --git a/lib/tartan/fill_element.py b/lib/tartan/fill_element.py
index 34139e6c..6666181e 100644
--- a/lib/tartan/fill_element.py
+++ b/lib/tartan/fill_element.py
@@ -12,12 +12,13 @@ def prepare_tartan_fill_element(element: BaseElement) -> None:
:param element: svg element with a fill color (path, rectangle, or circle)
"""
parent_group = element.getparent()
- if parent_group.get_id().startswith('inkstitch-tartan'):
+ if parent_group is not None and parent_group.get_id().startswith('inkstitch-tartan'):
# apply tartan group transform to element
transform = element.transform @ parent_group.transform
element.set('transform', transform)
# remove tartan group and place element in parent group
outer_group = parent_group.getparent()
+ assert outer_group is not None, f"Tartan element {element.get_id()} should have a parent group"
outer_group.insert(outer_group.index(parent_group), element)
outer_group.remove(parent_group)
# make sure the element is invisible
diff --git a/lib/tartan/palette.py b/lib/tartan/palette.py
index d945eb83..25bd2100 100644
--- a/lib/tartan/palette.py
+++ b/lib/tartan/palette.py
@@ -5,12 +5,14 @@
# Additional credits to: https://github.com/clsn/pyTartan
import re
-from typing import List
+from typing import TYPE_CHECKING, List, cast
import wx
from inkex import Color
from .colors import string_to_color
+if TYPE_CHECKING:
+ from ..gui.tartan.stripe_panel import StripePanel
class Palette:
@@ -59,7 +61,7 @@ class Palette:
stripes = []
for stripe_sizer in outer_sizer.Children:
stripe = {'render': 1, 'color': '#000000', 'width': '5'}
- stripe_panel = stripe_sizer.GetWindow()
+ stripe_panel = cast('StripePanel', stripe_sizer.GetWindow())
stripe['render'] = stripe_panel.visibility.Get3StateValue()
stripe['color'] = stripe_panel.colorpicker.GetColour().GetAsString(wx.C2S_HTML_SYNTAX)
stripe['width'] = stripe_panel.stripe_width.GetValue()
diff --git a/lib/tartan/svg.py b/lib/tartan/svg.py
index 497d0199..62d737c9 100644
--- a/lib/tartan/svg.py
+++ b/lib/tartan/svg.py
@@ -7,7 +7,7 @@ import time
from collections import defaultdict
from copy import copy
from itertools import chain
-from typing import List, Optional, Tuple
+from typing import List, Optional, Tuple, cast
from inkex import BaseElement, Group, Path, PathElement
from networkx import MultiGraph, is_empty
@@ -66,12 +66,12 @@ class TartanSvgGroup:
:param outline: the outline to be filled with the tartan pattern
"""
parent_group = outline.getparent()
- if parent_group.get_id().startswith('inkstitch-tartan'):
+ if parent_group is not None and parent_group.get_id().startswith('inkstitch-tartan'):
# remove everything but the tartan outline
for child in parent_group.iterchildren():
if child != outline:
parent_group.remove(child)
- group = parent_group
+ group = cast(Group, parent_group)
else:
group = Group()
group.set('id', f'inkstitch-tartan-{int(time.time())}')
@@ -86,6 +86,7 @@ class TartanSvgGroup:
# set outline invisible
outline.style['display'] = 'none'
group.append(outline)
+ return group
def _generate_tartan_group_elements(self, group, outline_shape, transform):
dimensions, rotation_center = self._get_dimensions(outline_shape)
diff --git a/lib/threads/color.py b/lib/threads/color.py
index 58861017..e71ebb06 100644
--- a/lib/threads/color.py
+++ b/lib/threads/color.py
@@ -11,8 +11,6 @@ from pyembroidery.EmbThread import EmbThread
class ThreadColor(object):
- hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I)
-
def __init__(self, color, name=None, number=None, manufacturer=None, description=None, chart=None):
'''
avoid error messages:
@@ -36,11 +34,9 @@ class ThreadColor(object):
self.rgb = (color.get_red(), color.get_green(), color.get_blue())
return
elif isinstance(color, str):
- self.rgb = Color.parse_str(color)[1]
+ self.rgb = Color(color).to('rgb').get_values(False)
elif isinstance(color, (list, tuple)):
self.rgb = tuple(color)
- elif self.hex_str_re.match(color):
- self.rgb = Color.parse_str(color)[1]
else:
raise ValueError("Invalid color: " + repr(color))
diff --git a/lib/utils/cache.py b/lib/utils/cache.py
index 18f993e9..cca6296a 100644
--- a/lib/utils/cache.py
+++ b/lib/utils/cache.py
@@ -8,16 +8,12 @@ import os
import pickle
import sqlite3
-import diskcache
+import diskcache # type: ignore[import-untyped]
from lib.utils.settings import global_settings
from .paths import get_user_dir
-
-try:
- from functools import lru_cache
-except ImportError:
- from backports.functools_lru_cache import lru_cache
+from functools import lru_cache
# simplify use of lru_cache decorator
diff --git a/lib/utils/param.py b/lib/utils/param.py
index 162dcddc..6def49f7 100644
--- a/lib/utils/param.py
+++ b/lib/utils/param.py
@@ -1,5 +1,5 @@
class ParamOption:
- def __init__(self, param_id=None, name=None, preview_image=None):
+ def __init__(self, param_id=None, name=None, preview_image=None) -> None:
self.id: str = param_id
self.name: str = name
self.preview_image: str = preview_image