diff --git a/inkstitch.py b/inkstitch.py index d85eaba4e..dde884d7f 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -7,8 +7,9 @@ import os import sys from pathlib import Path # to work with paths as objects import configparser # to read DEBUG.ini +from argparse import ArgumentParser # to parse arguments and remove --extension -import lib.debug_utils as debug_utils +import lib.debug_utils as debug_utils SCRIPTDIR = Path(__file__).parent.absolute() @@ -16,8 +17,8 @@ running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running if len(sys.argv) < 2: # no arguments - prevent accidentally running this script - msg = "No arguments given, exiting!" # without gettext localization see _() - if running_as_frozen: # we show dialog only when running from pyinstaller bundle - using wx + msg = "No arguments given, exiting!" # without gettext localization see _() + if running_as_frozen: # we show dialog only when running from pyinstaller bundle - using wx try: import wx app = wx.App() @@ -43,68 +44,33 @@ debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) # ch debug_type = 'none' profiler_type = 'none' -if not running_as_frozen: # debugging/profiling only in development mode +if not running_as_frozen: # debugging/profiling only in development mode # specify debugger type - # - if script was already started from debugger then don't read debug type from ini file or cmd line + # but if script was already started from debugger then don't read debug type from ini file or cmd line if not debug_active: - # enable/disable debugger from bash: -d - if os.environ.get('INKSTITCH_DEBUG_ENABLE', '').lower() in ['true', '1', 'yes', 'y']: - debug_enable = True - else: - debug_enable = ini.getboolean("DEBUG","debug_enable", fallback=False) # enable debugger on startup from ini + debug_type = debug_utils.resole_debug_type(ini) # read debug type from ini file or cmd line - debug_type = ini.get("DEBUG","debug_type", fallback="none") # debugger type vscode, pycharm, pydevd - if not debug_enable: - debug_type = 'none' - - debug_to_file = ini.getboolean("DEBUG","debug_to_file", fallback=False) # write debug output to file - if debug_to_file and debug_type == 'none': - debug_type = 'file' - - # enbale/disable profiling from bash: -p - if os.environ.get('INKSTITCH_PROFILE_ENABLE', '').lower() in ['true', '1', 'yes', 'y']: - profile_enable = True - else: - profile_enable = ini.getboolean("PROFILE","profile_enable", fallback=False) # read from ini - - # specify profiler type - profiler_type = ini.get("PROFILE","profiler_type", fallback="none") # profiler type cprofile, profile, pyinstrument - if not profile_enable: - profiler_type = 'none' + profile_type = debug_utils.resole_profile_type(ini) # read profile type from ini file or cmd line if running_from_inkscape: # process creation of the Bash script - should be done before sys.path is modified, see below in prefere_pip_inkex - if ini.getboolean("DEBUG","create_bash_script", fallback=False): # create script only if enabled in DEBUG.ini + if ini.getboolean("DEBUG", "create_bash_script", fallback=False): # create script only if enabled in DEBUG.ini debug_utils.write_offline_debug_script(SCRIPTDIR, ini) - + # disable debugger when running from inkscape - disable_from_inkscape = ini.getboolean("DEBUG","disable_from_inkscape", fallback=False) + disable_from_inkscape = ini.getboolean("DEBUG", "disable_from_inkscape", fallback=False) if disable_from_inkscape: debug_type = 'none' # do not start debugger when running from inkscape # prefer pip installed inkex over inkscape bundled inkex, pip version is bundled with Inkstitch # - must be be done before importing inkex - prefere_pip_inkex = ini.getboolean("LIBRARY","prefer_pip_inkex", fallback=True) + prefere_pip_inkex = ini.getboolean("LIBRARY", "prefer_pip_inkex", fallback=True) if prefere_pip_inkex and 'PYTHONPATH' in os.environ: debug_utils.reorder_sys_path() -from argparse import ArgumentParser # to parse arguments and remove --extension -import logging # to set logger for shapely -from io import StringIO # to store shapely errors - -from lib.exceptions import InkstitchException, format_uncaught_exception - -from inkex import errormsg # to show error message in inkscape -from lxml.etree import XMLSyntaxError # to catch XMLSyntaxError from inkex - -from lib.debug import debug # import global variable debug - don't import whole module - -from lib import extensions # import all supported extensions of institch -from lib.i18n import _ # see gettext translation function _() -from lib.utils import restore_stderr, save_stderr # to hide GTK spam - # enabling of debug depends on value of debug_type in DEBUG.ini file if debug_type != 'none': + from lib.debug import debug # import global variable debug - don't import whole module debug.enable(debug_type, SCRIPTDIR, ini) # check if debugger is really activated debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) @@ -117,8 +83,9 @@ if running_as_frozen or not debug_active: # TODO - check if this is still needed for shapely, apparently shapely now uses only exceptions instead of io. # all logs were removed from version 2.0.0 and above - # ---- plan to remove this in future ---- +# import logging # to set logger for shapely +# from io import StringIO # to store shapely errors # set logger for shapely - for old versions of shapely # logger = logging.getLogger('shapely.geos') # attach logger of shapely # logger.setLevel(logging.DEBUG) @@ -132,6 +99,10 @@ if running_as_frozen or not debug_active: # pop '--extension' from arguments and generate extension class name from extension name # example: --extension=params will instantiate Params() class from lib.extensions. + +# we need to import only after possible modification of sys.path, we disable here flake8 E402 +from lib import extensions # noqa: E402 # import all supported extensions of institch + parser = ArgumentParser() parser.add_argument("--extension") my_args, remaining_args = parser.parse_known_args() @@ -154,7 +125,13 @@ if debug_active or profiler_type != "none": # if debug or profile mode debug_utils.profile(profiler_type, SCRIPTDIR, ini, extension, remaining_args) else: # if not debug nor profile mode - save_stderr() # hide GTK spam + from lib.exceptions import InkstitchException, format_uncaught_exception + from inkex import errormsg # to show error message in inkscape + from lxml.etree import XMLSyntaxError # to catch XMLSyntaxError from inkex + from lib.i18n import _ # see gettext translation function _() + from lib.utils import restore_stderr, save_stderr # to hide GTK spam + + save_stderr() # hide GTK spam exception = None try: extension.run(args=remaining_args) diff --git a/lib/debug.py b/lib/debug.py index eb49005ae..e72f64353 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -8,11 +8,11 @@ import sys import atexit # to save svg file on exit import socket # to check if debugger is running import time # to measure time of code block, use time.monotonic() instead of time.time() -from datetime import datetime +from datetime import datetime from contextlib import contextmanager # to measure time of with block import configparser # to read DEBUG.ini -from pathlib import Path # to work with paths as objects +from pathlib import Path # to work with paths as objects import inkex from lxml import etree # to create svg file @@ -20,6 +20,7 @@ from lxml import etree # to create svg file from .svg import line_strings_to_path from .svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL + # decorator to check if debugging is enabled # - if debug is not enabled then decorated function is not called def check_enabled(func): @@ -39,6 +40,7 @@ def _unwrap(arg): else: return arg + # decorator to unwrap arguments if they are callable # eg: if argument is lambda function then it is called and return value is used as argument def unwrap_arguments(func): @@ -77,16 +79,16 @@ class Debug(object): self.current_layer = None self.group_stack = [] - def enable(self, debug_type, debug_dir : Path, ini : configparser.ConfigParser): + def enable(self, debug_type, debug_dir: Path, ini: configparser.ConfigParser): # initilize file names and other parameters from DEBUG.ini file self.debug_dir = debug_dir # directory where debug files are stored - self.debug_log_file = ini.get("DEBUG","debug_log_file", fallback="debug.log") - self.debug_svg_file = ini.get("DEBUG","debug_svg_file", fallback="debug.svg") - self.wait_attach = ini.getboolean("DEBUG","wait_attach", fallback=True) # currently only for vscode + self.debug_log_file = ini.get("DEBUG", "debug_log_file", fallback="debug.log") + self.debug_svg_file = ini.get("DEBUG", "debug_svg_file", fallback="debug.svg") + self.wait_attach = ini.getboolean("DEBUG", "wait_attach", fallback=True) # currently only for vscode if debug_type == 'none': return - + self.debugger = debug_type self.enabled = True self.init_log() @@ -100,25 +102,28 @@ class Debug(object): pass self.log("Debug logging enabled.") + # we intentionally disable flakes C901 - function is too complex, beacuse it is used only for debugging + # currently complexity is set 10 see 'make style', this means that function can have max 10 nested blocks, here we have more + # flake8: noqa: C901 def init_debugger(self): - ### General debugging notes: + # ### General debugging notes: # 1. to enable debugging or profiling copy DEBUG_template.ini to DEBUG.ini and edit it - ### How create bash script for offline debugging from console + # ### How create bash script for offline debugging from console # 1. in DEBUG.ini set create_bash_script = True # 2. call inkstitch.py extension from inkscape to create bash script named by bash_file_base in DEBUG.ini # 3. run bash script from console - ### Enable debugging + # ### Enable debugging # 1. set debug_type to one of - vscode, pycharm, pydev, see below for details - # debug_type = vscode - 'debugpy' for vscode editor + # debug_type = vscode - 'debugpy' for vscode editor # debug_type = pycharm - 'pydevd-pycharm' for pycharm editor # debug_type = pydev - 'pydevd' for eclipse editor # 2. set debug_enable = True in DEBUG.ini # or use command line argument -d in bash script # or set environment variable INKSTITCH_DEBUG_ENABLE = True or 1 or yes or y - ### Enable profiling + # ### Enable profiling # 1. set profiler_type to one of - cprofile, profile, pyinstrument # profiler_type = cprofile - 'cProfile' profiler # profiler_type = profile - 'profile' profiler @@ -127,17 +132,16 @@ class Debug(object): # or use command line argument -p in bash script # or set environment variable INKSTITCH_PROFILE_ENABLE = True or 1 or yes or y - ### Miscelaneous notes: + # ### Miscelaneous notes: # - to disable debugger when running from inkscape set disable_from_inkscape = True in DEBUG.ini # - to write debug output to file set debug_to_file = True in DEBUG.ini # - to change various output file names see DEBUG.ini # - to disable waiting for debugger to attach (vscode editor) set wait_attach = False in DEBUG.ini # - to prefer inkscape version of inkex module over pip version set prefer_pip_inkex = False in DEBUG.ini - ### + # ### - - ### How to debug Ink/Stitch with LiClipse: + # ### How to debug Ink/Stitch with LiClipse: # # 1. Install LiClipse (liclipse.com) -- no need to install Eclipse first # 2. Start debug server as described here: http://www.pydev.org/manual_adv_remote_debugger.html @@ -146,9 +150,9 @@ class Debug(object): # and set debug_type = pydev # 4. Run any extension and PyDev will start debugging. - ### + # ### - ### To debug with PyCharm: + # ### To debug with PyCharm: # You must use the PyCharm Professional Edition and _not_ the Community # Edition. Jetbrains has chosen to make remote debugging a Pro feature. @@ -193,12 +197,12 @@ class Debug(object): # PyDev debugger.)" statement, below. Uncheck the box to have it continue # automatically to your first set breakpoint. - ### + # ### - ### To debug with VS Code + # ### To debug with VS Code # see: https://code.visualstudio.com/docs/python/debugging#_command-line-debugging # https://code.visualstudio.com/docs/python/debugging#_debugging-by-attaching-over-a-network-connection - # + # # 1. Install the Python extension for VS Code # pip install debugpy # 2. create .vscode/launch.json containing: @@ -223,7 +227,6 @@ class Debug(object): # Notes: # to see flask server url routes: # - comment out the line self.disable_logging() in run() of lib/api/server.py - try: if self.debugger == 'vscode': @@ -416,4 +419,3 @@ class Debug(object): # global debug object debug = Debug() - diff --git a/lib/debug_utils.py b/lib/debug_utils.py index 169fa4c5b..ef8b364d3 100644 --- a/lib/debug_utils.py +++ b/lib/debug_utils.py @@ -13,7 +13,7 @@ import configparser # to read DEBUG.ini # - later sys.path may be modified that influences importing inkex (see prefere_pip_inkex) -def write_offline_debug_script(debug_script_dir : Path, ini : configparser.ConfigParser): +def write_offline_debug_script(debug_script_dir: Path, ini: configparser.ConfigParser): ''' prepare Bash script for offline debugging from console arguments: @@ -22,9 +22,9 @@ def write_offline_debug_script(debug_script_dir : Path, ini : configparser.Confi ''' # define names of files used by offline Bash script - bash_file_base = ini.get("DEBUG","bash_file_base", fallback="debug_inkstitch") + bash_file_base = ini.get("DEBUG", "bash_file_base", fallback="debug_inkstitch") bash_name = Path(bash_file_base).with_suffix(".sh") # Path object - bash_svg = Path(bash_file_base).with_suffix(".svg") # Path object + bash_svg = Path(bash_file_base).with_suffix(".svg") # Path object # check if input svg file exists in arguments, take argument that not start with '-' as file name svgs = [arg for arg in sys.argv[1:] if not arg.startswith('-')] @@ -34,47 +34,47 @@ def write_offline_debug_script(debug_script_dir : Path, ini : configparser.Confi svg_file = Path(svgs[0]) if svg_file.exists() and bash_svg.exists() and bash_svg.samefile(svg_file): - print(f"WARN: input svg file is same as output svg file. No script created in write debug script.", file=sys.stderr) + print("WARN: input svg file is same as output svg file. No script created in write debug script.", file=sys.stderr) return - + import shutil # to copy svg file bash_file = debug_script_dir / bash_name with open(bash_file, 'w') as f: # "w" text mode, automatic conversion of \n to os.linesep - f.write(f'#!/usr/bin/env bash\n') + f.write('#!/usr/bin/env bash\n') # cmd line arguments for debugging and profiling f.write(bash_parser()) # parse cmd line arguments: -d -p f.write(f'# python version: {sys.version}\n') # python version - + myargs = " ".join(sys.argv[1:]) - f.write(f'# script: {sys.argv[0]} arguments: {myargs}\n') # script name and arguments + f.write(f'# script: {sys.argv[0]} arguments: {myargs}\n') # script name and arguments # environment PATH - f.write(f'# PATH:\n') + f.write('# 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') # python module path - f.write(f'# python sys.path:\n') + f.write('# python sys.path:\n') for p in sys.path: f.write(f'# {p}\n') # see static void set_extensions_env() in inkscape/src/inkscape-main.cpp - f.write(f'# PYTHONPATH:\n') - for p in os.environ.get('PYTHONPATH', '').split(os.pathsep): # PYTHONPATH to list + f.write('# PYTHONPATH:\n') + for p in os.environ.get('PYTHONPATH', '').split(os.pathsep): # PYTHONPATH to list f.write(f'# {p}\n') f.write(f'# copy {svg_file} to {bash_svg}\n#\n') shutil.copy(svg_file, debug_script_dir / bash_svg) # copy file to bash_svg - myargs = myargs.replace(str(svg_file), str(bash_svg)) # replace file name with bash_svg + myargs = myargs.replace(str(svg_file), str(bash_svg)) # replace file name with bash_svg # see void Extension::set_environment() in inkscape/src/extension/extension.cpp f.write('# Export inkscape environment variables:\n') - notexported = ['SELF_CALL'] # if an extension calls inkscape itself - exported = ['INKEX_GETTEXT_DOMAIN', 'INKEX_GETTEXT_DIRECTORY', + notexported = ['SELF_CALL'] # if an extension calls inkscape itself + exported = ['INKEX_GETTEXT_DOMAIN', 'INKEX_GETTEXT_DIRECTORY', 'INKSCAPE_PROFILE_DIR', 'DOCUMENT_PATH', 'PYTHONPATH'] for k in notexported: if k in os.environ: @@ -84,7 +84,7 @@ def write_offline_debug_script(debug_script_dir : Path, ini : configparser.Confi f.write(f'export {k}="{os.environ[k]}"\n') f.write('# signal inkstitch.py that we are running from offline script\n') - f.write(f'export INKSTITCH_OFFLINE_SCRIPT="True"\n') + f.write('export INKSTITCH_OFFLINE_SCRIPT="True"\n') f.write('# call inkstitch\n') f.write(f'python3 inkstitch.py {myargs}\n') @@ -92,7 +92,7 @@ def write_offline_debug_script(debug_script_dir : Path, ini : configparser.Confi def bash_parser(): - return ''' + return r''' set -e # exit on error # parse cmd line arguments: @@ -102,10 +102,10 @@ set -e # exit on error while getopts ":dp" opt; do case $opt in d) - arg_d="true" + export INKSTITCH_DEBUG_ENABLE="True" ;; p) - arg_p="true" + export INKSTITCH_PROFILE_ENABLE="True" ;; \?) echo "Invalid option: -$OPTARG" >&2 @@ -118,14 +118,6 @@ while getopts ":dp" opt; do esac done -# -v: check if variable is set -if [[ -v arg_d ]]; then - export INKSTITCH_DEBUG_ENABLE="True" -fi -if [[ -v arg_p ]]; then - export INKSTITCH_PROFILE_ENABLE="True" -fi - ''' @@ -152,19 +144,56 @@ def reorder_sys_path(): # add pythonpath to the end of sys.path sys.path.extend(pythonpath) + # ----------------------------------------------------------------------------- +# try to resolve debugger type from ini file or cmd line of bash +def resole_debug_type(ini: configparser.ConfigParser): + # enable/disable debugger from bash: -d + if os.environ.get('INKSTITCH_DEBUG_ENABLE', '').lower() in ['true', '1', 'yes', 'y']: + debug_enable = True + else: + debug_enable = ini.getboolean("DEBUG", "debug_enable", fallback=False) # enable debugger on startup from ini + + debug_type = ini.get("DEBUG", "debug_type", fallback="none") # debugger type vscode, pycharm, pydevd + if not debug_enable: + debug_type = 'none' + + debug_to_file = ini.getboolean("DEBUG", "debug_to_file", fallback=False) # write debug output to file + if debug_to_file and debug_type == 'none': + debug_type = 'file' + + return debug_type + + +# try to resolve profiler type from ini file or cmd line of bash +def resole_profile_type(ini: configparser.ConfigParser): + # enable/disable profiling from bash: -p + if os.environ.get('INKSTITCH_PROFILE_ENABLE', '').lower() in ['true', '1', 'yes', 'y']: + profile_enable = True + else: + profile_enable = ini.getboolean("PROFILE", "profile_enable", fallback=False) # read from ini + + # specify profiler type + profiler_type = ini.get("PROFILE", "profiler_type", fallback="none") # profiler type cprofile, profile, pyinstrument + if not profile_enable: + profiler_type = 'none' + + return profiler_type + +# ----------------------------------------------------------------------------- + # Profilers: # currently supported profilers: # - cProfile - standard python profiler # - profile - standard python profiler # - pyinstrument - profiler with nice html output - -def profile(profiler_type, profile_dir : Path, ini : configparser.ConfigParser, extension, remaining_args): + +def profile(profiler_type, profile_dir: Path, ini: configparser.ConfigParser, extension, remaining_args): ''' profile with cProfile, profile or pyinstrument ''' - profile_file_base = ini.get("PROFILE","profile_file_base", fallback="debug_profile") + profile_file_base = ini.get("PROFILE", "profile_file_base", fallback="debug_profile") profile_file_path = profile_dir / profile_file_base # Path object if profiler_type == 'cprofile': @@ -176,6 +205,7 @@ def profile(profiler_type, profile_dir : Path, ini : configparser.ConfigParser, else: raise ValueError(f"unknown profiler type: '{profiler_type}'") + def with_cprofile(extension, remaining_args, profile_file_path): ''' profile with cProfile @@ -193,9 +223,10 @@ def with_cprofile(extension, remaining_args, profile_file_path): stats = pstats.Stats(profiler, stream=stats_file) stats.sort_stats(pstats.SortKey.CUMULATIVE) stats.print_stats() - print(f"Profiler: cprofile, stats written to '{profile_file_path.name}' and '{profile_file_path.name}.prof'. Use snakeviz to see it.", + print(f"Profiler: cprofile, stats written to '{profile_file_path.name}' and '{profile_file_path.name}.prof'. Use snakeviz to see it.", file=sys.stderr) + def with_profile(extension, remaining_args, profile_file_path): ''' profile with profile @@ -211,9 +242,10 @@ def with_profile(extension, remaining_args, profile_file_path): stats = pstats.Stats(profiler, stream=stats_file) stats.sort_stats(pstats.SortKey.CUMULATIVE) stats.print_stats() - print(f"'Profiler: profile, stats written to '{profile_file_path.name}' and '{profile_file_path.name}.prof'. Use of snakeviz is broken.", + print(f"'Profiler: profile, stats written to '{profile_file_path.name}' and '{profile_file_path.name}.prof'. Use of snakeviz is broken.", file=sys.stderr) + def with_pyinstrument(extension, remaining_args, profile_file_path): ''' profile with pyinstrument diff --git a/lib/extensions/generate_palette.py b/lib/extensions/generate_palette.py index fdb76735d..f5d7661cc 100644 --- a/lib/extensions/generate_palette.py +++ b/lib/extensions/generate_palette.py @@ -62,7 +62,7 @@ class GeneratePalette(InkstitchExtension): def _get_color_from_elements(self, elements): colors = [] for element in elements: - if 'fill' not in element.style.keys() or type(element) != inkex.TextElement: + if 'fill' not in element.style.keys() or not isinstance(element, inkex.TextElement): # type(element) != inkex.TextElement: continue color = inkex.Color(element.style['fill']).to_rgb() diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index 1d5af76a5..4e6671302 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -216,7 +216,7 @@ class Print(InkstitchExtension): def strip_namespaces(self, svg): # namespace prefixes seem to trip up HTML, so get rid of them for element in svg.iter(): - if type(element.tag) == str and element.tag[0] == '{': + if isinstance(element.tag, str) and element.tag[0] == '{': element.tag = element.tag[element.tag.index('}', 1) + 1:] def render_svgs(self, stitch_plan, realistic=False): diff --git a/lib/threads/color.py b/lib/threads/color.py index 44fa709ce..8b73c4f33 100644 --- a/lib/threads/color.py +++ b/lib/threads/color.py @@ -20,9 +20,9 @@ class ThreadColor(object): * currentColor/context-fill/context-stroke: should not just be black, but we want to avoid error messages until inkex will be able to handle these css properties ''' - if type(color) == str and color.startswith(('url', 'currentColor', 'context')): + if isinstance(color, str) and color.startswith(('url', 'currentColor', 'context')): color = None - elif type(color) == str and color.startswith('rgb'): + elif isinstance(color, str) and color.startswith('rgb'): color = tuple(int(value) for value in color[4:-1].split(',')) if color is None: