From cc016b193e091fb3a376f4284a3f060c4abeec7f Mon Sep 17 00:00:00 2001 From: karnigen Date: Sun, 17 Dec 2023 23:03:39 +0100 Subject: initial changes --- .gitignore | 3 + inkstitch.py | 167 +++++++++++++++++++++++++++++++++++++++++------------ lib/debug_utils.py | 84 +++++++++++++++++++++++++++ 3 files changed, 218 insertions(+), 36 deletions(-) create mode 100644 lib/debug_utils.py diff --git a/.gitignore b/.gitignore index a1d4a5e9..2287a056 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,11 @@ locales/ .DS_Store /PROFILE /profile_stats +/profile_stats.html /profile_stats.prof /.vscode __pycache__ flaskserverport.json electron/yarn.lock +.ink.sh +.ink.svg diff --git a/inkstitch.py b/inkstitch.py index 91a0f18a..e5ef3d7a 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -2,27 +2,82 @@ # # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import cProfile -import pstats -import logging + import os import sys +import lib.debug_utils as debug_utils +from pathlib import Path + +SCRIPTDIR = Path(__file__).parent.absolute() + +if len(sys.argv) < 2: + exit(1) # no arguments - prevent uncidentally running this script + +running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle +running_from_inkscape = '.ink.svg' not in sys.argv # inkscape never starts extension with .ink.svg file in args +# running_from_inkscape = True # for testing + +debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) # check if debugger is active on startup +debug_file = SCRIPTDIR / "DEBUG" +debug_type = 'none' + +profile_file = SCRIPTDIR / "PROFILE" +profile_type = 'none' + +# print(f"debug_type:'{debug_type}' profile_type:'{profile_type}'", file=sys.stderr) # for testing + +# if script was already started from debugger then don't read debug file +if not running_as_frozen and not debug_active and os.path.exists(debug_file): + debug_type = debug_utils.parse_file(debug_file) # read type of debugger from debug_file DEBUG + if debug_type == 'none': # for better backward compatibility + print(f"Debug file exists but no debugger type found in '{debug_file.name}'", file=sys.stderr) + +if os.path.exists(profile_file): + profile_type = debug_utils.parse_file(profile_file) # read type of profiler from profile_file PROFILE + if profile_type == 'none': # for better backward compatibility + print(f"Profile file exists but no profiler type found in '{profile_file.name}'", file=sys.stderr) + +if running_from_inkscape: + if debug_type.endswith('-script'): # if offline debugging just create script for later debugging + debug_utils.write_offline_debug_script(SCRIPTDIR) + debug_type = 'none' # do not start debugger when running from inkscape +else: # not running from inkscape + if debug_type.endswith('-script'): # remove '-script' to propely initialize debugger packages for each editor + debug_type = debug_type.replace('-script', '') + +if not running_as_frozen: + # When running in development mode, we prefer inkex installed by pip, not the one bundled with Inkscape. + # - move inkscape extensions path to the end of sys.path + # - we compare PYTHONPATH with sys.path and move PYTHONPATH to the end of sys.path + # - also user inkscape extensions path is moved to the end of sys.path - may cause problems? + # - path for deprecated-simple are removed from sys.path, will be added later by importing inkex + + # PYTHONPATH to list + pythonpath = os.environ.get('PYTHONPATH', '').split(os.pathsep) + # remove pythonpath from sys.path + sys.path = [p for p in sys.path if p not in pythonpath] + # remove deprecated-simple, it will be added later by importing inkex + pythonpath = [p for p in pythonpath if not p.endswith('deprecated-simple')] + # add pythonpath to the end of sys.path + sys.path.extend(pythonpath) + + # >> should be removed after previous code was tested << + # if sys.platform == "darwin": + # extensions_path = "/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions" # Mac + # else: + # extensions_path = "/usr/share/inkscape/extensions" # Linux + # # windows not solved + # move inkscape extensions path to the end of sys.path + # sys.path.remove(extensions_path) + # sys.path.append(extensions_path) + # >> ------------------------------------------------- << + +import logging from argparse import ArgumentParser from io import StringIO from lib.exceptions import InkstitchException, format_uncaught_exception -if getattr(sys, 'frozen', None) is None: - # When running in development mode, we want to use the inkex installed by - # pip install, not the one bundled with Inkscape which is not new enough. - if sys.platform == "darwin": - extensions_path = "/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions" - else: - extensions_path = "/usr/share/inkscape/extensions" - - sys.path.remove(extensions_path) - sys.path.append(extensions_path) - from inkex import errormsg from lxml.etree import XMLSyntaxError @@ -31,55 +86,95 @@ from lib import extensions from lib.i18n import _ from lib.utils import restore_stderr, save_stderr -# ignore warnings in releases -if getattr(sys, 'frozen', None): +# file DEBUG exists next to inkstitch.py - enabling debug mode depends on value of debug_type in DEBUG file +if debug_type != 'none': + debug.enable(debug_type) + # check if debugger is really activated + debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) + +# ignore warnings in releases - see warnings.warn() +if running_as_frozen or not debug_active: import warnings warnings.filterwarnings('ignore') -logger = logging.getLogger('shapely.geos') +# set logger for shapely +logger = logging.getLogger('shapely.geos') # attach logger of shapely, from ver 2.0.0 all logs are exceptions logger.setLevel(logging.DEBUG) -shapely_errors = StringIO() +shapely_errors = StringIO() # in memory file to store shapely errors ch = logging.StreamHandler(shapely_errors) ch.setLevel(logging.DEBUG) formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) +# pop '--extension' from arguments and generate extension class name from extension name parser = ArgumentParser() parser.add_argument("--extension") my_args, remaining_args = parser.parse_known_args() -if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "DEBUG")): - debug.enable() - -profiler = None -if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "PROFILE")): - profiler = cProfile.Profile() - profiler.enable() - extension_name = my_args.extension # example: foo_bar_baz -> FooBarBaz extension_class_name = extension_name.title().replace("_", "") extension_class = getattr(extensions, extension_class_name) -extension = extension_class() +extension = extension_class() # create instance of extension class - call __init__ method + +# extension run(), but we differentiate between debug and normal mode +# - in debug or profile mode we run extension or profile extension +# - in normal mode we run extension in try/except block to catch all exceptions +if debug_active or profile_type != "none": # if debug or profile mode + print(f"Extension:'{extension_name}' Debug active:{debug_active} type:'{debug_type}' " + f"Profile type:'{profile_type}'", file=sys.stderr) + profile_path = SCRIPTDIR / "profile_stats" + + if profile_type == 'none': + extension.run(args=remaining_args) + elif profile_type == 'cprofile': + import cProfile + import pstats + profiler = cProfile.Profile() -if (hasattr(sys, 'gettrace') and sys.gettrace()) or profiler is not None: - extension.run(args=remaining_args) - if profiler: - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "profile_stats") + profiler.enable() + extension.run(args=remaining_args) profiler.disable() - profiler.dump_stats(path + ".prof") - with open(path, 'w') as stats_file: + profiler.dump_stats(profile_path.with_suffix(".prof")) # can be read by 'snakeviz -s' or 'pyprof2calltree' + with open(profile_path, 'w') as stats_file: stats = pstats.Stats(profiler, stream=stats_file) stats.sort_stats(pstats.SortKey.CUMULATIVE) stats.print_stats() + print(f"profiling stats written to '{profile_path.name}' and '{profile_path.name}.prof'", file=sys.stderr) + + elif profile_type == 'profile': + import profile + import pstats + profiler = profile.Profile() + + profiler.run('extension.run(args=remaining_args)') + + profiler.dump_stats(profile_path.with_suffix(".prof")) # can be read by 'snakeviz' or 'pyprof2calltree' - seems broken + with open(profile_path, 'w') as stats_file: + stats = pstats.Stats(profiler, stream=stats_file) + stats.sort_stats(pstats.SortKey.CUMULATIVE) + stats.print_stats() + print(f"profiling stats written to '{profile_path.name}'", file=sys.stderr) + + elif profile_type == 'pyinstrument': + import pyinstrument + profiler = pyinstrument.Profiler() + + profiler.start() + extension.run(args=remaining_args) + profiler.stop() + + profile_path = SCRIPTDIR / "profile_stats.html" + with open(profile_path, 'w') as stats_file: + stats_file.write(profiler.output_html()) + print(f"profiling stats written to '{profile_path.name}'", file=sys.stderr) - print(f"profiling stats written to {path} and {path}.prof", file=sys.stderr) -else: - save_stderr() +else: # if not debug nor profile mode + save_stderr() # hide GTK spam exception = None try: extension.run(args=remaining_args) diff --git a/lib/debug_utils.py b/lib/debug_utils.py new file mode 100644 index 00000000..6555b083 --- /dev/null +++ b/lib/debug_utils.py @@ -0,0 +1,84 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import os +import sys + +# DEBUG file format: +# - first non-comment line is debugger type +# - valid values are: +# "vscode" or "vscode-script" - for debugging with vscode +# "pycharm" or "pycharm-script" - for debugging with pycharm +# "pydev" or "pydev-script" - for debugging with pydev +# "none" or empty file - for no debugging +# - for offline debugging without inkscape, set debugger name to +# as "vscode-script" or "pycharm-script" or "pydev-script" +# - in that case running from inkscape will not start debugger +# but prepare script for offline debugging from console +# - backward compatibilty is broken due to confusion +# debug_type = 'pydev' # default debugger backwards compatibility +# if 'PYCHARM_REMOTE_DEBUG' in os.environ: # backwards compatibility +# debug_type = 'pycharm' + +# PROFILE file format: +# - first non-comment line is profiler type +# - valid values are: +# "cprofile" - for cProfile +# "pyinstrument" - for pyinstrument +# "profile" - for profile +# "none" - for no profiling + + +def parse_file(filename): + # parse DEBUG or PROFILE file for type + # - return first noncomment and nonempty line from file + value_type = 'none' + with open(filename, 'r') as f: + for line in f: + line = line.strip().lower() + if line.startswith("#") or line == "": # skip comments and empty lines + continue + value_type = line # first non-comment line is type + break + return value_type + +def write_offline_debug_script(SCRIPTDIR): + # prepare script for offline debugging from console + # - only tested on linux + import shutil + ink_file = os.path.join(SCRIPTDIR, ".ink.sh") + with open(ink_file, 'w') as f: + f.write(f"#!/usr/bin/env bash\n\n") + f.write(f"# 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 + + # python module path + f.write(f"# python sys.path:\n") + for p in sys.path: + f.write(f"# {p}\n") + + # print PYTHONPATH one per line + f.write(f"# PYTHONPATH:\n") + for p in os.environ.get('PYTHONPATH', '').split(os.pathsep): + f.write(f"# {p}\n") + + # take argument that not start with '-' as file name + svg_file = " ".join([arg for arg in sys.argv[1:] if not arg.startswith('-')]) + f.write(f"# copy {svg_file} to .ink.svg\n") + # check if filer are not the same + if svg_file != '.ink.svg': + shutil.copy(svg_file, f'{SCRIPTDIR}/.ink.svg') # copy file to .ink.svg + myargs = myargs.replace(svg_file, '.ink.svg') # replace file name with .ink.svg + + # export INK*|PYTHON* environment variables + for k, v in sorted(os.environ.items()): + if k.startswith('INK') or k.startswith('PYTHON'): + f.write(f'export {k}="{v}"\n') + + # f.write(f"# python3 -m debugpy --listen 5678 --wait-for-client inkstitch.py {myargs}\n") + f.write(f"python3 inkstitch.py {myargs}\n") + os.chmod(ink_file, 0o0755) # make file executable -- cgit v1.2.3 From 746768d849dfb72c7f0b2b4a264f153fd3b6e242 Mon Sep 17 00:00:00 2001 From: karnigen Date: Sun, 17 Dec 2023 23:08:39 +0100 Subject: extending debug --- lib/debug.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/lib/debug.py b/lib/debug.py index 4751e6af..6934d6ed 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -62,12 +62,17 @@ class Debug(object): """ def __init__(self): + self.debugger = None self.enabled = False self.last_log_time = None self.current_layer = None self.group_stack = [] - def enable(self): + + def enable(self, debug_type): + if debug_type == 'none': + return + self.debugger = debug_type self.enabled = True self.init_log() self.init_debugger() @@ -140,11 +145,45 @@ 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 + # 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 somewhere: + # "configurations": [ ... + # { + # "name": "Python: Attach", + # "type": "python", + # "request": "attach", + # "connect": { + # "host": "localhost", + # "port": 5678 + # } + # } + # ] + # 3. Touch a file named "DEBUG" at the top of your git repo, as above. + # containing "vscode" or "vscode-script" see parse_file() in debug_mode.py for details + # 4. Start the debug server in VS Code by clicking on the debug icon in the left pane + # select "Python: Attach" from the dropdown menu and click on the green arrow + # + # Notes: + # to see flask server url routes: + # - comment out the line self.disable_logging() in run() of lib/api/server.py + + try: - if 'PYCHARM_REMOTE_DEBUG' in os.environ: + if self.debugger == 'vscode': + import debugpy + elif self.debugger == 'pycharm': import pydevd_pycharm - else: + elif self.debugger == 'pydev': import pydevd + else: + raise ValueError(f"unknown debugger: '{self.debugger}'") except ImportError: self.log("importing pydevd failed (debugger disabled)") @@ -155,17 +194,24 @@ class Debug(object): sys.stderr = devnull try: - if 'PYCHARM_REMOTE_DEBUG' in os.environ: + if self.debugger == 'vscode': + debugpy.listen(('localhost', 5678)) + print("Waiting for debugger attach") + debugpy.wait_for_client() # wait for debugger to attach + debugpy.breakpoint() # stop here to start normal debugging + elif self.debugger == 'pycharm': pydevd_pycharm.settrace('localhost', port=5678, stdoutToServer=True, stderrToServer=True) - else: + elif self.debugger == 'pydev': pydevd.settrace() + else: + raise ValueError(f"unknown debugger: '{self.debugger}'") except socket.error as error: self.log("Debugging: connection to pydevd failed: %s", error) - self.log("Be sure to run 'Start debugging server' in PyDev to enable debugging.") + self.log(f"Be sure to run 'Start debugging server' in {self.debugger} to enable debugging.") else: - self.log("Enabled PyDev debugger.") + self.log(f"Enabled '{self.debugger}' debugger.") sys.stderr = stderr @@ -314,5 +360,5 @@ class Debug(object): debug = Debug() -def enable(): - debug.enable() +def enable(debug_type): + debug.enable(debug_type) -- cgit v1.2.3 From fe323375e42253f00fd378fbaff1a3373385fccc Mon Sep 17 00:00:00 2001 From: karnigen Date: Mon, 25 Dec 2023 19:54:52 +0100 Subject: env update from inkscape, bash names for script --- inkstitch.py | 114 +++++++++++++++++++++++++++++------------------------ lib/debug_utils.py | 36 ++++++++++------- 2 files changed, 84 insertions(+), 66 deletions(-) diff --git a/inkstitch.py b/inkstitch.py index e5ef3d7a..e0c968b6 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -11,11 +11,17 @@ from pathlib import Path SCRIPTDIR = Path(__file__).parent.absolute() if len(sys.argv) < 2: - exit(1) # no arguments - prevent uncidentally running this script + exit(1) # no arguments - prevent accidentally running this script + +prefere_pip_inkex = True # prefer pip installed inkex over inkscape bundled inkex + +# define names of files used by offline Bash script +bash_name = ".ink.sh" +bash_svg = ".ink.svg" running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle -running_from_inkscape = '.ink.svg' not in sys.argv # inkscape never starts extension with .ink.svg file in args -# running_from_inkscape = True # for testing +# we assume that if arguments contain svg file (=.ink.svg) then we are running not from inkscape +running_from_inkscape = bash_svg not in sys.argv debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) # check if debugger is active on startup debug_file = SCRIPTDIR / "DEBUG" @@ -24,53 +30,59 @@ debug_type = 'none' profile_file = SCRIPTDIR / "PROFILE" profile_type = 'none' -# print(f"debug_type:'{debug_type}' profile_type:'{profile_type}'", file=sys.stderr) # for testing - -# if script was already started from debugger then don't read debug file -if not running_as_frozen and not debug_active and os.path.exists(debug_file): - debug_type = debug_utils.parse_file(debug_file) # read type of debugger from debug_file DEBUG - if debug_type == 'none': # for better backward compatibility - print(f"Debug file exists but no debugger type found in '{debug_file.name}'", file=sys.stderr) - -if os.path.exists(profile_file): - profile_type = debug_utils.parse_file(profile_file) # read type of profiler from profile_file PROFILE - if profile_type == 'none': # for better backward compatibility - print(f"Profile file exists but no profiler type found in '{profile_file.name}'", file=sys.stderr) - -if running_from_inkscape: - if debug_type.endswith('-script'): # if offline debugging just create script for later debugging - debug_utils.write_offline_debug_script(SCRIPTDIR) - debug_type = 'none' # do not start debugger when running from inkscape -else: # not running from inkscape - if debug_type.endswith('-script'): # remove '-script' to propely initialize debugger packages for each editor - debug_type = debug_type.replace('-script', '') - -if not running_as_frozen: - # When running in development mode, we prefer inkex installed by pip, not the one bundled with Inkscape. - # - move inkscape extensions path to the end of sys.path - # - we compare PYTHONPATH with sys.path and move PYTHONPATH to the end of sys.path - # - also user inkscape extensions path is moved to the end of sys.path - may cause problems? - # - path for deprecated-simple are removed from sys.path, will be added later by importing inkex - - # PYTHONPATH to list - pythonpath = os.environ.get('PYTHONPATH', '').split(os.pathsep) - # remove pythonpath from sys.path - sys.path = [p for p in sys.path if p not in pythonpath] - # remove deprecated-simple, it will be added later by importing inkex - pythonpath = [p for p in pythonpath if not p.endswith('deprecated-simple')] - # add pythonpath to the end of sys.path - sys.path.extend(pythonpath) - - # >> should be removed after previous code was tested << - # if sys.platform == "darwin": - # extensions_path = "/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions" # Mac - # else: - # extensions_path = "/usr/share/inkscape/extensions" # Linux - # # windows not solved - # move inkscape extensions path to the end of sys.path - # sys.path.remove(extensions_path) - # sys.path.append(extensions_path) - # >> ------------------------------------------------- << +if not running_as_frozen: # debugging/profiling only in development mode + # parse debug file + # - if script was already started from debugger then don't read debug file + if not debug_active and os.path.exists(debug_file): + debug_type = debug_utils.parse_file(debug_file) # read type of debugger from debug_file DEBUG + if debug_type == 'none': # for better backward compatibility + print(f"Debug file exists but no debugger type found in '{debug_file.name}'", file=sys.stderr) + + # parse profile file + if os.path.exists(profile_file): + profile_type = debug_utils.parse_file(profile_file) # read type of profiler from profile_file PROFILE + if profile_type == 'none': # for better backward compatibility + print(f"Profile file exists but no profiler type found in '{profile_file.name}'", file=sys.stderr) + + # process creation of the Bash script + if running_from_inkscape: + if debug_type.endswith('-script'): # if offline debugging just create script for later debugging + debug_utils.write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg) + debug_type = 'none' # do not start debugger when running from inkscape + else: # not running from inkscape + if debug_type.endswith('-script'): # remove '-script' to propely initialize debugger packages for each editor + debug_type = debug_type.replace('-script', '') + + if prefere_pip_inkex and 'PYTHONPATH' in os.environ: + # see static void set_extensions_env() in inkscape/src/inkscape-main.cpp + + # When running in development mode, we prefer inkex installed by pip, not the one bundled with Inkscape. + # - move inkscape extensions path to the end of sys.path + # - we compare PYTHONPATH with sys.path and move PYTHONPATH to the end of sys.path + # - also user inkscape extensions path is moved to the end of sys.path - may cause problems? + # - path for deprecated-simple are removed from sys.path, will be added later by importing inkex + + # PYTHONPATH to list + pythonpath = os.environ.get('PYTHONPATH', '').split(os.pathsep) + # remove pythonpath from sys.path + sys.path = [p for p in sys.path if p not in pythonpath] + # remove deprecated-simple, it will be added later by importing inkex + pythonpath = [p for p in pythonpath if not p.endswith('deprecated-simple')] + # remove nonexisting paths + pythonpath = [p for p in pythonpath if os.path.exists(p)] + # add pythonpath to the end of sys.path + sys.path.extend(pythonpath) + + # >> should be removed after previous code was tested << + # if sys.platform == "darwin": + # extensions_path = "/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions" # Mac + # else: + # extensions_path = "/usr/share/inkscape/extensions" # Linux + # # windows ? + # move inkscape extensions path to the end of sys.path + # sys.path.remove(extensions_path) + # sys.path.append(extensions_path) + # >> ------------------------------------------------- << import logging from argparse import ArgumentParser @@ -122,7 +134,7 @@ extension = extension_class() # create instance of extension class - call __ini # extension run(), but we differentiate between debug and normal mode # - in debug or profile mode we run extension or profile extension -# - in normal mode we run extension in try/except block to catch all exceptions +# - in normal mode we run extension in try/except block to catch all exceptions and hide GTK spam if debug_active or profile_type != "none": # if debug or profile mode print(f"Extension:'{extension_name}' Debug active:{debug_active} type:'{debug_type}' " f"Profile type:'{profile_type}'", file=sys.stderr) diff --git a/lib/debug_utils.py b/lib/debug_utils.py index 6555b083..b5d7aa6a 100644 --- a/lib/debug_utils.py +++ b/lib/debug_utils.py @@ -17,6 +17,7 @@ import sys # as "vscode-script" or "pycharm-script" or "pydev-script" # - in that case running from inkscape will not start debugger # but prepare script for offline debugging from console +# - valid for "none-script" too # - backward compatibilty is broken due to confusion # debug_type = 'pydev' # default debugger backwards compatibility # if 'PYCHARM_REMOTE_DEBUG' in os.environ: # backwards compatibility @@ -44,14 +45,14 @@ def parse_file(filename): break return value_type -def write_offline_debug_script(SCRIPTDIR): +def write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg): # prepare script for offline debugging from console # - only tested on linux import shutil - ink_file = os.path.join(SCRIPTDIR, ".ink.sh") + ink_file = os.path.join(SCRIPTDIR, bash_name) with open(ink_file, 'w') as f: f.write(f"#!/usr/bin/env bash\n\n") - f.write(f"# version: {sys.version}\n") # python version + 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 @@ -61,24 +62,29 @@ def write_offline_debug_script(SCRIPTDIR): for p in sys.path: f.write(f"# {p}\n") - # print PYTHONPATH one per line + # 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): + for p in os.environ.get('PYTHONPATH', '').split(os.pathsep): # PYTHONPATH to list f.write(f"# {p}\n") # take argument that not start with '-' as file name svg_file = " ".join([arg for arg in sys.argv[1:] if not arg.startswith('-')]) - f.write(f"# copy {svg_file} to .ink.svg\n") - # check if filer are not the same - if svg_file != '.ink.svg': - shutil.copy(svg_file, f'{SCRIPTDIR}/.ink.svg') # copy file to .ink.svg - myargs = myargs.replace(svg_file, '.ink.svg') # replace file name with .ink.svg + f.write(f"# copy {svg_file} to {bash_svg}\n") + # check if files are not the same + if svg_file != bash_svg: + shutil.copy(svg_file, SCRIPTDIR / bash_svg) # copy file to bash_svg + myargs = myargs.replace(svg_file, bash_svg) # replace file name with bash_svg - # export INK*|PYTHON* environment variables - for k, v in sorted(os.environ.items()): - if k.startswith('INK') or k.startswith('PYTHON'): - f.write(f'export {k}="{v}"\n') + # see void Extension::set_environment() in inkscape/src/extension/extension.cpp + 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: + f.write(f'# export {k}="{os.environ[k]}"\n') + for k in exported: + if k in os.environ: + f.write(f'export {k}="{os.environ[k]}"\n') - # f.write(f"# python3 -m debugpy --listen 5678 --wait-for-client inkstitch.py {myargs}\n") f.write(f"python3 inkstitch.py {myargs}\n") os.chmod(ink_file, 0o0755) # make file executable -- cgit v1.2.3 From f1f9d275a1ffaeb538a72e3643fb98231323337a Mon Sep 17 00:00:00 2001 From: karnigen Date: Fri, 29 Dec 2023 16:25:17 +0100 Subject: replace DEBUG,PROFILE by DEVEL.ini --- .gitignore | 22 +++++++++-------- DEVEL_template.ini | 35 ++++++++++++++++++++++++++ inkstitch.py | 72 ++++++++++++++++++++++++++++++------------------------ lib/debug.py | 21 +++++++++------- lib/debug_utils.py | 36 +++++++++++++++++++++------ 5 files changed, 127 insertions(+), 59 deletions(-) create mode 100644 DEVEL_template.ini diff --git a/.gitignore b/.gitignore index 2287a056..b84ff90d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,33 @@ +__pycache__ *.swp *.kate-swp *.pyc *.spec *.zip *.tar.gz +*.po dist/ build/ locales/ /inx/ -*.po -/DEBUG .pydevproject .project -/debug.log -/debug.svg /.idea +/.vscode /VERSION /src/ .DS_STORE .DS_Store +flaskserverport.json +electron/yarn.lock + +# debug and profile files +/DEVEL.ini +/DEBUG /PROFILE +/debug* +/.debug* +# old profile files /profile_stats /profile_stats.html /profile_stats.prof -/.vscode -__pycache__ -flaskserverport.json -electron/yarn.lock -.ink.sh -.ink.svg diff --git a/DEVEL_template.ini b/DEVEL_template.ini new file mode 100644 index 00000000..1bbc5c1e --- /dev/null +++ b/DEVEL_template.ini @@ -0,0 +1,35 @@ +[LIBRARY] +;;; use the pip installed version of inkex.py, default: True +; prefer_pip_inkex = False + +[DEBUG] +;;; select one active debugger, default: none +; debugger = vscode +; debugger = pycharm +; debugger = pydev + +;;; disable debugger when calling from inkscape, default: False +; disable_from_inkscape = True + +;;; wait for debugger to attach (vscode), default: True +; wait_attach = False + +;;; debug log file, default: debug.log +; debug_file = debug.log + +;;; creation of bash script, default: False +; create_bash_script = True + +;;; base name for bash script, default: debug_inkstitch +; bash_file_base = debug_inkstitch + +[PROFILE] +;;; select one active profiler, default: none +; profiler = cprofile +; profiler = profile +; profiler = pyinstrument + +;;; base name for profile output files, default: debug_profile +; profile_file_base = debug_profile + + diff --git a/inkstitch.py b/inkstitch.py index e0c968b6..bff8e8d1 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -5,53 +5,60 @@ import os import sys -import lib.debug_utils as debug_utils from pathlib import Path +import configparser +import lib.debug_utils as debug_utils SCRIPTDIR = Path(__file__).parent.absolute() if len(sys.argv) < 2: - exit(1) # no arguments - prevent accidentally running this script + # no arguments - prevent accidentally running this script + print("No arguments given, continue without arguments?") + answer = input("Continue? [y/N] ") + if answer.lower() != 'y': + exit(1) -prefere_pip_inkex = True # prefer pip installed inkex over inkscape bundled inkex +running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle -# define names of files used by offline Bash script -bash_name = ".ink.sh" -bash_svg = ".ink.svg" +ini = configparser.ConfigParser() +ini.read(SCRIPTDIR / "DEVEL.ini") # read DEVEL.ini file if exists -running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle -# we assume that if arguments contain svg file (=.ink.svg) then we are running not from inkscape -running_from_inkscape = bash_svg not in sys.argv +# prefer pip installed inkex over inkscape bundled inkex, pip version is bundled with Inkstitch +prefere_pip_inkex = ini.getboolean("LIBRARY","prefer_pip_inkex", fallback=True) + +# check if running from inkscape, given by environment variable +if os.environ.get('INKSTITCH_OFFLINE_SCRIPT', '').lower() in ['true', '1', 'yes', 'y']: + running_from_inkscape = False +else: + running_from_inkscape = True debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) # check if debugger is active on startup -debug_file = SCRIPTDIR / "DEBUG" debug_type = 'none' - -profile_file = SCRIPTDIR / "PROFILE" profile_type = 'none' if not running_as_frozen: # debugging/profiling only in development mode - # parse debug file + # define names of files used by offline Bash script + 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 + + # specify debugger type # - if script was already started from debugger then don't read debug file - if not debug_active and os.path.exists(debug_file): - debug_type = debug_utils.parse_file(debug_file) # read type of debugger from debug_file DEBUG - if debug_type == 'none': # for better backward compatibility - print(f"Debug file exists but no debugger type found in '{debug_file.name}'", file=sys.stderr) + if not debug_active: + debug_type = ini.get("DEBUG","debugger", fallback="none") # debugger type vscode, pycharm, pydevd, none - # parse profile file - if os.path.exists(profile_file): - profile_type = debug_utils.parse_file(profile_file) # read type of profiler from profile_file PROFILE - if profile_type == 'none': # for better backward compatibility - print(f"Profile file exists but no profiler type found in '{profile_file.name}'", file=sys.stderr) + # specify profiler type + profile_type = ini.get("PROFILE","profiler", fallback="none") # profiler type cprofile, profile, pyinstrument, none # process creation of the Bash script if running_from_inkscape: - if debug_type.endswith('-script'): # if offline debugging just create script for later debugging + if ini.getboolean("DEBUG","create_bash_script", fallback=False): # create script only if enabled in DEVEL.ini debug_utils.write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg) + + # disable debugger when running from inkscape + 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 - else: # not running from inkscape - if debug_type.endswith('-script'): # remove '-script' to propely initialize debugger packages for each editor - debug_type = debug_type.replace('-script', '') if prefere_pip_inkex and 'PYTHONPATH' in os.environ: # see static void set_extensions_env() in inkscape/src/inkscape-main.cpp @@ -100,7 +107,9 @@ from lib.utils import restore_stderr, save_stderr # file DEBUG exists next to inkstitch.py - enabling debug mode depends on value of debug_type in DEBUG file if debug_type != 'none': - debug.enable(debug_type) + debug_file = ini.get("DEBUG","debug_file", fallback="debug.log") + wait_attach = ini.getboolean("DEBUG","wait_attach", fallback=True) # currently only for vscode + debug.enable(debug_type, debug_file, wait_attach) # check if debugger is really activated debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) @@ -136,9 +145,8 @@ extension = extension_class() # create instance of extension class - call __ini # - in debug or profile mode we run extension or profile extension # - in normal mode we run extension in try/except block to catch all exceptions and hide GTK spam if debug_active or profile_type != "none": # if debug or profile mode - print(f"Extension:'{extension_name}' Debug active:{debug_active} type:'{debug_type}' " - f"Profile type:'{profile_type}'", file=sys.stderr) - profile_path = SCRIPTDIR / "profile_stats" + profile_file_base = ini.get("PROFILE","profile_file_base", fallback="debug_profile") + profile_path = SCRIPTDIR / profile_file_base # Path object if profile_type == 'none': extension.run(args=remaining_args) @@ -156,7 +164,7 @@ if debug_active or profile_type != "none": # if debug or profile mode stats = pstats.Stats(profiler, stream=stats_file) stats.sort_stats(pstats.SortKey.CUMULATIVE) stats.print_stats() - print(f"profiling stats written to '{profile_path.name}' and '{profile_path.name}.prof'", file=sys.stderr) + print(f"profiling stats written to '{profile_path.name}' and '{profile_path.name}.prof'. Use snakeviz to see it.", file=sys.stderr) elif profile_type == 'profile': import profile @@ -183,7 +191,7 @@ if debug_active or profile_type != "none": # if debug or profile mode profile_path = SCRIPTDIR / "profile_stats.html" with open(profile_path, 'w') as stats_file: stats_file.write(profiler.output_html()) - print(f"profiling stats written to '{profile_path.name}'", file=sys.stderr) + print(f"profiling stats written to '{profile_path.name}'. Use browser to see it.", file=sys.stderr) else: # if not debug nor profile mode save_stderr() # hide GTK spam diff --git a/lib/debug.py b/lib/debug.py index 6934d6ed..a256bc0a 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -63,23 +63,25 @@ class Debug(object): def __init__(self): self.debugger = None + self.wait_attach = True self.enabled = False self.last_log_time = None self.current_layer = None self.group_stack = [] - def enable(self, debug_type): + def enable(self, debug_type, debug_file, wait_attach): if debug_type == 'none': return self.debugger = debug_type + self.wait_attach = wait_attach self.enabled = True - self.init_log() + self.init_log(debug_file) self.init_debugger() self.init_svg() - def init_log(self): - self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.log") + def init_log(self, debug_file): + self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), debug_file) # delete old content with open(self.log_file, "w"): pass @@ -196,9 +198,10 @@ class Debug(object): try: if self.debugger == 'vscode': debugpy.listen(('localhost', 5678)) - print("Waiting for debugger attach") - debugpy.wait_for_client() # wait for debugger to attach - debugpy.breakpoint() # stop here to start normal debugging + if self.wait_attach: + print("Waiting for debugger attach") + debugpy.wait_for_client() # wait for debugger to attach + debugpy.breakpoint() # stop here to start normal debugging elif self.debugger == 'pycharm': pydevd_pycharm.settrace('localhost', port=5678, stdoutToServer=True, stderrToServer=True) @@ -360,5 +363,5 @@ class Debug(object): debug = Debug() -def enable(debug_type): - debug.enable(debug_type) +def enable(debug_type, debug_file, wait_attach): + debug.enable(debug_type, debug_file, wait_attach) diff --git a/lib/debug_utils.py b/lib/debug_utils.py index b5d7aa6a..183a44f8 100644 --- a/lib/debug_utils.py +++ b/lib/debug_utils.py @@ -5,6 +5,12 @@ import os import sys +from pathlib import Path + +# this file is without: import inkex +# - so we can modify sys.path before importing inkex + +# DEBUG and PROFILE are in DEVEL.ini file # DEBUG file format: # - first non-comment line is debugger type @@ -45,12 +51,24 @@ def parse_file(filename): break return value_type -def write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg): +def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Path): # prepare script for offline debugging from console - # - only tested on linux + + # 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('-')] + if len(svgs) != 1: + print(f"WARN: {len(svgs)} svg files found, expected 1, [{svgs}]. No script created in write debug script.", file=sys.stderr) + return + + 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) + return + import shutil - ink_file = os.path.join(SCRIPTDIR, bash_name) - with open(ink_file, 'w') as f: + bash_file = SCRIPTDIR / 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\n") f.write(f"# python version: {sys.version}\n") # python version @@ -67,13 +85,11 @@ def write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg): for p in os.environ.get('PYTHONPATH', '').split(os.pathsep): # PYTHONPATH to list f.write(f"# {p}\n") - # take argument that not start with '-' as file name - svg_file = " ".join([arg for arg in sys.argv[1:] if not arg.startswith('-')]) f.write(f"# copy {svg_file} to {bash_svg}\n") # check if files are not the same if svg_file != bash_svg: shutil.copy(svg_file, SCRIPTDIR / bash_svg) # copy file to bash_svg - myargs = myargs.replace(svg_file, 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 notexported = ["SELF_CALL"] # if an extension calls inkscape itself @@ -86,5 +102,9 @@ def write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg): if k in os.environ: 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('# call inkstitch\n') f.write(f"python3 inkstitch.py {myargs}\n") - os.chmod(ink_file, 0o0755) # make file executable + bash_file.chmod(0o0755) # make file executable -- cgit v1.2.3 From b4f50b1ed9fa6ac50bcce7bc7b78dd9c7ef6138e Mon Sep 17 00:00:00 2001 From: karnigen Date: Fri, 5 Jan 2024 17:05:22 +0100 Subject: simplification, cleanup, docs, startup dialog, DEBUG.ini --- .gitignore | 12 ++-- DEBUG_template.ini | 39 +++++++++++ DEVEL_template.ini | 35 ---------- inkstitch.py | 192 ++++++++++++++++++----------------------------------- lib/debug.py | 92 ++++++++++++++----------- lib/debug_utils.py | 177 +++++++++++++++++++++++++++++++++--------------- 6 files changed, 290 insertions(+), 257 deletions(-) create mode 100644 DEBUG_template.ini delete mode 100644 DEVEL_template.ini diff --git a/.gitignore b/.gitignore index b84ff90d..7a465710 100644 --- a/.gitignore +++ b/.gitignore @@ -22,12 +22,10 @@ flaskserverport.json electron/yarn.lock # debug and profile files -/DEVEL.ini -/DEBUG -/PROFILE +/DEBUG.ini /debug* /.debug* -# old profile files -/profile_stats -/profile_stats.html -/profile_stats.prof +# old debug files +/DEBUG +/PROFILE +/profile* diff --git a/DEBUG_template.ini b/DEBUG_template.ini new file mode 100644 index 00000000..e04d78b0 --- /dev/null +++ b/DEBUG_template.ini @@ -0,0 +1,39 @@ +[LIBRARY] +;;; use the pip installed version of inkex.py, default: True +; prefer_pip_inkex = False + +[DEBUG] +;;; select one active debugger, default: none +; debugger = vscode +; debugger = pycharm +; debugger = pydev +; debugger = file + +;;; disable debugger when calling from inkscape, default: False +; disable_from_inkscape = True + +;;; wait for debugger to attach (vscode), default: True +; wait_attach = False + +;;; debug log file, default: debug.log +; debug_log_file = debug.log + +;;; debug file for graph related things, default: debug.svg +; debug_svg_file = debug.svg + +;;; creation of bash script, default: False +; create_bash_script = True + +;;; base name for bash script, default: debug_inkstitch +; bash_file_base = debug_inkstitch + +[PROFILE] +;;; select one active profiler, default: none +; profiler = cprofile +; profiler = profile +; profiler = pyinstrument + +;;; base name for profile output files, default: debug_profile +; profile_file_base = debug_profile + + diff --git a/DEVEL_template.ini b/DEVEL_template.ini deleted file mode 100644 index 1bbc5c1e..00000000 --- a/DEVEL_template.ini +++ /dev/null @@ -1,35 +0,0 @@ -[LIBRARY] -;;; use the pip installed version of inkex.py, default: True -; prefer_pip_inkex = False - -[DEBUG] -;;; select one active debugger, default: none -; debugger = vscode -; debugger = pycharm -; debugger = pydev - -;;; disable debugger when calling from inkscape, default: False -; disable_from_inkscape = True - -;;; wait for debugger to attach (vscode), default: True -; wait_attach = False - -;;; debug log file, default: debug.log -; debug_file = debug.log - -;;; creation of bash script, default: False -; create_bash_script = True - -;;; base name for bash script, default: debug_inkstitch -; bash_file_base = debug_inkstitch - -[PROFILE] -;;; select one active profiler, default: none -; profiler = cprofile -; profiler = profile -; profiler = pyinstrument - -;;; base name for profile output files, default: debug_profile -; profile_file_base = debug_profile - - diff --git a/inkstitch.py b/inkstitch.py index bff8e8d1..514d42cd 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -5,26 +5,33 @@ import os import sys -from pathlib import Path -import configparser -import lib.debug_utils as debug_utils +from pathlib import Path # to work with paths as objects +import configparser # to read DEBUG.ini + +import lib.debug_utils as debug_utils SCRIPTDIR = Path(__file__).parent.absolute() +running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle + if len(sys.argv) < 2: # no arguments - prevent accidentally running this script - print("No arguments given, continue without arguments?") - answer = input("Continue? [y/N] ") - if answer.lower() != 'y': - exit(1) - -running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle + 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() + dlg = wx.MessageDialog(None, msg, "Inkstitch", wx.OK | wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + except ImportError: + print(msg) + else: + print(msg) + exit(1) ini = configparser.ConfigParser() -ini.read(SCRIPTDIR / "DEVEL.ini") # read DEVEL.ini file if exists - -# prefer pip installed inkex over inkscape bundled inkex, pip version is bundled with Inkstitch -prefere_pip_inkex = ini.getboolean("LIBRARY","prefer_pip_inkex", fallback=True) +ini.read(SCRIPTDIR / "DEBUG.ini") # read DEBUG.ini file if exists # check if running from inkscape, given by environment variable if os.environ.get('INKSTITCH_OFFLINE_SCRIPT', '').lower() in ['true', '1', 'yes', 'y']: @@ -37,98 +44,74 @@ debug_type = 'none' profile_type = 'none' if not running_as_frozen: # debugging/profiling only in development mode - # define names of files used by offline Bash script - 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 - # specify debugger type - # - if script was already started from debugger then don't read debug file + # - if script was already started from debugger then don't read debug type from ini file if not debug_active: - debug_type = ini.get("DEBUG","debugger", fallback="none") # debugger type vscode, pycharm, pydevd, none + debug_type = ini.get("DEBUG","debugger", fallback="none") # debugger type vscode, pycharm, pydevd, file # specify profiler type - profile_type = ini.get("PROFILE","profiler", fallback="none") # profiler type cprofile, profile, pyinstrument, none + profile_type = ini.get("PROFILE","profiler", fallback="none") # profiler type cprofile, profile, pyinstrument - # process creation of the Bash script if running_from_inkscape: - if ini.getboolean("DEBUG","create_bash_script", fallback=False): # create script only if enabled in DEVEL.ini - debug_utils.write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg) + # 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 + 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) 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) if prefere_pip_inkex and 'PYTHONPATH' in os.environ: - # see static void set_extensions_env() in inkscape/src/inkscape-main.cpp - - # When running in development mode, we prefer inkex installed by pip, not the one bundled with Inkscape. - # - move inkscape extensions path to the end of sys.path - # - we compare PYTHONPATH with sys.path and move PYTHONPATH to the end of sys.path - # - also user inkscape extensions path is moved to the end of sys.path - may cause problems? - # - path for deprecated-simple are removed from sys.path, will be added later by importing inkex - - # PYTHONPATH to list - pythonpath = os.environ.get('PYTHONPATH', '').split(os.pathsep) - # remove pythonpath from sys.path - sys.path = [p for p in sys.path if p not in pythonpath] - # remove deprecated-simple, it will be added later by importing inkex - pythonpath = [p for p in pythonpath if not p.endswith('deprecated-simple')] - # remove nonexisting paths - pythonpath = [p for p in pythonpath if os.path.exists(p)] - # add pythonpath to the end of sys.path - sys.path.extend(pythonpath) - - # >> should be removed after previous code was tested << - # if sys.platform == "darwin": - # extensions_path = "/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions" # Mac - # else: - # extensions_path = "/usr/share/inkscape/extensions" # Linux - # # windows ? - # move inkscape extensions path to the end of sys.path - # sys.path.remove(extensions_path) - # sys.path.append(extensions_path) - # >> ------------------------------------------------- << - -import logging -from argparse import ArgumentParser -from io import StringIO + 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 -from lxml.etree import XMLSyntaxError +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 -import lib.debug as debug -from lib import extensions -from lib.i18n import _ -from lib.utils import restore_stderr, save_stderr +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 -# file DEBUG exists next to inkstitch.py - enabling debug mode depends on value of debug_type in DEBUG file +# enabling of debug depends on value of debug_type in DEBUG.ini file if debug_type != 'none': - debug_file = ini.get("DEBUG","debug_file", fallback="debug.log") - wait_attach = ini.getboolean("DEBUG","wait_attach", fallback=True) # currently only for vscode - debug.enable(debug_type, debug_file, wait_attach) + debug.enable(debug_type, SCRIPTDIR, ini) # check if debugger is really activated debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) -# ignore warnings in releases - see warnings.warn() +# warnings are used by some modules, we want to ignore them all in release +# - see warnings.warn() if running_as_frozen or not debug_active: import warnings warnings.filterwarnings('ignore') -# set logger for shapely -logger = logging.getLogger('shapely.geos') # attach logger of shapely, from ver 2.0.0 all logs are exceptions -logger.setLevel(logging.DEBUG) -shapely_errors = StringIO() # in memory file to store shapely errors -ch = logging.StreamHandler(shapely_errors) -ch.setLevel(logging.DEBUG) -formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') -ch.setFormatter(formatter) -logger.addHandler(ch) +# TODO - check if this is still for shapely needed, apparently, shapely uses only exceptions instead of io. +# all logs were removed from version 2.0.0, if we ensure that shapely is always >= 2.0.0 + +# ---- plan to remove this in future ---- +# set logger for shapely - for old versions of shapely +# logger = logging.getLogger('shapely.geos') # attach logger of shapely +# logger.setLevel(logging.DEBUG) +# shapely_errors = StringIO() # in memory file to store shapely errors +# ch = logging.StreamHandler(shapely_errors) +# ch.setLevel(logging.DEBUG) +# formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') +# ch.setFormatter(formatter) +# logger.addHandler(ch) +# ---- plan to remove this in future ---- # pop '--extension' from arguments and generate extension class name from extension name +# example: --extension=params will instantiate Params() class from lib.extensions. parser = ArgumentParser() parser.add_argument("--extension") my_args, remaining_args = parser.parse_known_args() @@ -141,57 +124,14 @@ extension_class_name = extension_name.title().replace("_", "") extension_class = getattr(extensions, extension_class_name) extension = extension_class() # create instance of extension class - call __init__ method -# extension run(), but we differentiate between debug and normal mode -# - in debug or profile mode we run extension or profile extension -# - in normal mode we run extension in try/except block to catch all exceptions and hide GTK spam +# extension run(), we differentiate between debug and normal mode +# - in debug or profile mode we debug or profile extension.run() method +# - in normal mode we run extension.run() in try/except block to catch all exceptions and hide GTK spam if debug_active or profile_type != "none": # if debug or profile mode - profile_file_base = ini.get("PROFILE","profile_file_base", fallback="debug_profile") - profile_path = SCRIPTDIR / profile_file_base # Path object - - if profile_type == 'none': + if profile_type == 'none': # only debugging extension.run(args=remaining_args) - elif profile_type == 'cprofile': - import cProfile - import pstats - profiler = cProfile.Profile() - - profiler.enable() - extension.run(args=remaining_args) - profiler.disable() - - profiler.dump_stats(profile_path.with_suffix(".prof")) # can be read by 'snakeviz -s' or 'pyprof2calltree' - with open(profile_path, 'w') as stats_file: - stats = pstats.Stats(profiler, stream=stats_file) - stats.sort_stats(pstats.SortKey.CUMULATIVE) - stats.print_stats() - print(f"profiling stats written to '{profile_path.name}' and '{profile_path.name}.prof'. Use snakeviz to see it.", file=sys.stderr) - - elif profile_type == 'profile': - import profile - import pstats - profiler = profile.Profile() - - profiler.run('extension.run(args=remaining_args)') - - profiler.dump_stats(profile_path.with_suffix(".prof")) # can be read by 'snakeviz' or 'pyprof2calltree' - seems broken - with open(profile_path, 'w') as stats_file: - stats = pstats.Stats(profiler, stream=stats_file) - stats.sort_stats(pstats.SortKey.CUMULATIVE) - stats.print_stats() - print(f"profiling stats written to '{profile_path.name}'", file=sys.stderr) - - elif profile_type == 'pyinstrument': - import pyinstrument - profiler = pyinstrument.Profiler() - - profiler.start() - extension.run(args=remaining_args) - profiler.stop() - - profile_path = SCRIPTDIR / "profile_stats.html" - with open(profile_path, 'w') as stats_file: - stats_file.write(profiler.output_html()) - print(f"profiling stats written to '{profile_path.name}'. Use browser to see it.", file=sys.stderr) + else: # do profiling + debug_utils.profile(profile_type, SCRIPTDIR, ini, extension, remaining_args) else: # if not debug nor profile mode save_stderr() # hide GTK spam diff --git a/lib/debug.py b/lib/debug.py index a256bc0a..d3c19429 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -3,21 +3,25 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import atexit import os -import socket import sys -import time -from contextlib import contextmanager -from datetime import datetime +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 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 import inkex -from lxml import etree +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): def decorated(self, *args, **kwargs): if self.enabled: @@ -26,13 +30,17 @@ def check_enabled(func): return decorated +# unwrapping = provision for functions as arguments +# - if argument is callable then it is called and return value is used as argument +# otherwise argument is returned as is def _unwrap(arg): if callable(arg): return 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): def decorated(self, *args, **kwargs): unwrapped_args = [_unwrap(arg) for arg in args] @@ -69,21 +77,26 @@ class Debug(object): self.current_layer = None self.group_stack = [] + 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 - def enable(self, debug_type, debug_file, wait_attach): if debug_type == 'none': return + self.debugger = debug_type - self.wait_attach = wait_attach self.enabled = True - self.init_log(debug_file) + self.init_log() self.init_debugger() self.init_svg() - def init_log(self, debug_file): - self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), debug_file) + def init_log(self): + self.log_file = self.debug_dir / self.debug_log_file # delete old content - with open(self.log_file, "w"): + with self.log_file.open("w"): pass self.log("Debug logging enabled.") @@ -93,9 +106,14 @@ class Debug(object): # 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 # * follow the "Note:" to enable the debug server menu item - # 3. Create a file named "DEBUG" next to inkstitch.py in your git clone. + # 3. Copy and edit a file named "DEBUG.ini" from "DEBUG_template.ini" next to inkstitch.py in your git clone. # 4. Run any extension and PyDev will start debugging. + # debugger = vscode - 'debugpy' for vscode editor + # debugger = pycharm - 'pydevd-pycharm' for pycharm editor + # debugger = pydev - 'pydevd' for eclipse editor + # debugger = file - no debugger, only debug.log, debug.svg are used + ### # To debug with PyCharm: @@ -119,7 +137,7 @@ class Debug(object): # configuration. Set "IDE host name:" to "localhost" and "Port:" to 5678. # You can leave the default settings for all other choices. # - # 3. Touch a file named "DEBUG" at the top of your git repo, as above. + # 3. Touch a file named "DEBUG.ini" at the top of your git repo, as above. # # 4. Create a symbolic link in the Inkscape extensions directory to the # top-level directory of your git repo. On a mac, for example: @@ -132,16 +150,11 @@ class Debug(object): # extensions directory, or you'll see duplicate entries in the Ink/Stitch # extensions menu in Inkscape. # - # 5. In the execution env for Inkscape, set the environment variable - # PYCHARM_REMOTE_DEBUG to any value, and launch Inkscape. If you're starting - # Inkscape from the PyCharm Terminal pane, you can do: - # export PYCHARM_REMOTE_DEBUG=true;inkscape - # - # 6. In Pycharm, either click on the green "bug" icon if visible in the upper + # 5. In Pycharm, either click on the green "bug" icon if visible in the upper # right or press Ctrl-D to start debugging.The PyCharm debugger pane will # display the message "Waiting for process connection..." # - # 7. Do some action in Inkscape which invokes Ink/Stitch extension code, and the + # 6. Do some action in Inkscape which invokes Ink/Stitch extension code, and the # debugger will be triggered. If you've left "Suspend after connect" checked # in the Run configuration, PyCharm will pause in the "self.log("Enabled # PyDev debugger.)" statement, below. Uncheck the box to have it continue @@ -155,7 +168,7 @@ class Debug(object): # # 1. Install the Python extension for VS Code # pip install debugpy - # 2. create .vscode/launch.json containing somewhere: + # 2. create .vscode/launch.json containing: # "configurations": [ ... # { # "name": "Python: Attach", @@ -167,10 +180,11 @@ class Debug(object): # } # } # ] - # 3. Touch a file named "DEBUG" at the top of your git repo, as above. - # containing "vscode" or "vscode-script" see parse_file() in debug_mode.py for details + # 3. Touch a file named "DEBUG.ini" at the top of your git repo, as above. # 4. Start the debug server in VS Code by clicking on the debug icon in the left pane - # select "Python: Attach" from the dropdown menu and click on the green arrow + # select "Python: Attach" from the dropdown menu and click on the green arrow. + # The debug server will start and connect to already running python processes, + # but immediately exit if no python processes are running. # # Notes: # to see flask server url routes: @@ -184,11 +198,13 @@ class Debug(object): import pydevd_pycharm elif self.debugger == 'pydev': import pydevd + elif self.debugger == 'file': + pass else: raise ValueError(f"unknown debugger: '{self.debugger}'") except ImportError: - self.log("importing pydevd failed (debugger disabled)") + self.log(f"importing debugger failed (debugger disabled) for {self.debugger}") # pydevd likes to shout about errors to stderr whether I want it to or not with open(os.devnull, 'w') as devnull: @@ -207,6 +223,8 @@ class Debug(object): stderrToServer=True) elif self.debugger == 'pydev': pydevd.settrace() + elif self.debugger == 'file': + pass else: raise ValueError(f"unknown debugger: '{self.debugger}'") @@ -224,7 +242,7 @@ class Debug(object): def save_svg(self): tree = etree.ElementTree(self.svg) - debug_svg = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.svg") + debug_svg = self.debug_dir / self.debug_svg_file tree.write(debug_svg) @check_enabled @@ -267,20 +285,21 @@ class Debug(object): timestamp = now.isoformat() self.last_log_time = now - with open(self.log_file, "a") as logfile: + with self.log_file.open("a") as logfile: print(timestamp, message % args, file=logfile) logfile.flush() + # decorator to measure time of function def time(self, func): def decorated(*args, **kwargs): if self.enabled: self.raw_log("entering %s()", func.__name__) - start = time.time() + start = time.monotonic() result = func(*args, **kwargs) if self.enabled: - end = time.time() + end = time.monotonic() self.raw_log("leaving %s(), duration = %s", func.__name__, round(end - start, 6)) return result @@ -348,20 +367,19 @@ class Debug(object): INKSCAPE_LABEL: name })) + # decorator to measure time of with block @contextmanager def time_this(self, label="code block"): if self.enabled: - start = time.time() + start = time.monotonic() self.raw_log("begin %s", label) yield if self.enabled: - self.raw_log("completed %s, duration = %s", label, time.time() - start) + self.raw_log("completed %s, duration = %s", label, time.monotonic() - start) +# global debug object debug = Debug() - -def enable(debug_type, debug_file, wait_attach): - debug.enable(debug_type, debug_file, wait_attach) diff --git a/lib/debug_utils.py b/lib/debug_utils.py index 183a44f8..298a7b80 100644 --- a/lib/debug_utils.py +++ b/lib/debug_utils.py @@ -5,54 +5,27 @@ import os import sys -from pathlib import Path +from pathlib import Path # to work with paths as objects +import configparser # to read DEBUG.ini # this file is without: import inkex -# - so we can modify sys.path before importing inkex - -# DEBUG and PROFILE are in DEVEL.ini file - -# DEBUG file format: -# - first non-comment line is debugger type -# - valid values are: -# "vscode" or "vscode-script" - for debugging with vscode -# "pycharm" or "pycharm-script" - for debugging with pycharm -# "pydev" or "pydev-script" - for debugging with pydev -# "none" or empty file - for no debugging -# - for offline debugging without inkscape, set debugger name to -# as "vscode-script" or "pycharm-script" or "pydev-script" -# - in that case running from inkscape will not start debugger -# but prepare script for offline debugging from console -# - valid for "none-script" too -# - backward compatibilty is broken due to confusion -# debug_type = 'pydev' # default debugger backwards compatibility -# if 'PYCHARM_REMOTE_DEBUG' in os.environ: # backwards compatibility -# debug_type = 'pycharm' - -# PROFILE file format: -# - first non-comment line is profiler type -# - valid values are: -# "cprofile" - for cProfile -# "pyinstrument" - for pyinstrument -# "profile" - for profile -# "none" - for no profiling - - -def parse_file(filename): - # parse DEBUG or PROFILE file for type - # - return first noncomment and nonempty line from file - value_type = 'none' - with open(filename, 'r') as f: - for line in f: - line = line.strip().lower() - if line.startswith("#") or line == "": # skip comments and empty lines - continue - value_type = line # first non-comment line is type - break - return value_type - -def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Path): - # prepare script for offline debugging from console +# - we need dump argv and sys.path as is on startup from inkscape +# - 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): + ''' + prepare Bash script for offline debugging from console + arguments: + - debug_script_dir - Path object, absolute path to directory of inkstitch.py + - ini - see DEBUG.ini + ''' + + # define names of files used by offline Bash script + 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 # 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('-')] @@ -65,8 +38,8 @@ def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Pa print(f"WARN: input svg file is same as output svg file. No script created in write debug script.", file=sys.stderr) return - import shutil - bash_file = SCRIPTDIR / bash_name + 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\n") @@ -86,9 +59,7 @@ def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Pa f.write(f"# {p}\n") f.write(f"# copy {svg_file} to {bash_svg}\n") - # check if files are not the same - if svg_file != bash_svg: - shutil.copy(svg_file, SCRIPTDIR / bash_svg) # copy file to bash_svg + 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 # see void Extension::set_environment() in inkscape/src/extension/extension.cpp @@ -107,4 +78,106 @@ def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Pa f.write('# call inkstitch\n') f.write(f"python3 inkstitch.py {myargs}\n") - bash_file.chmod(0o0755) # make file executable + bash_file.chmod(0o0755) # make file executable, hopefully ignored on Windows + + +def reorder_sys_path(): + ''' + change sys.path to prefer pip installed inkex over inkscape bundled inkex + ''' + + # see static void set_extensions_env() in inkscape/src/inkscape-main.cpp + # what we do: + # - move inkscape extensions path to the end of sys.path + # - we compare PYTHONPATH with sys.path and move PYTHONPATH to the end of sys.path + # - also user inkscape extensions path is moved to the end of sys.path - may cause problems? + # - path for deprecated-simple are removed from sys.path, will be added later by importing inkex + + # PYTHONPATH to list + pythonpath = os.environ.get('PYTHONPATH', '').split(os.pathsep) + # remove pythonpath from sys.path + sys.path = [p for p in sys.path if p not in pythonpath] + # remove deprecated-simple, it will be added later by importing inkex + pythonpath = [p for p in pythonpath if not p.endswith('deprecated-simple')] + # remove nonexisting paths + pythonpath = [p for p in pythonpath if os.path.exists(p)] + # add pythonpath to the end of sys.path + sys.path.extend(pythonpath) + +# ----------------------------------------------------------------------------- +# Profilers: +# currently supported profilers: +# - cProfile - standard python profiler +# - profile - standard python profiler +# - pyinstrument - profiler with nice html output + + +def profile(profile_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_path = profile_dir / profile_file_base # Path object + + if profile_type == 'cprofile': + with_cprofile(extension, remaining_args, profile_file_path) + elif profile_type == 'profile': + with_profile(extension, remaining_args, profile_file_path) + elif profile_type == 'pyinstrument': + with_pyinstrument(extension, remaining_args, profile_file_path) + else: + raise ValueError(f"unknown profiler type: '{profile_type}'") + +def with_cprofile(extension, remaining_args, profile_file_path): + ''' + profile with cProfile + ''' + import cProfile + import pstats + profiler = cProfile.Profile() + + profiler.enable() + extension.run(args=remaining_args) + profiler.disable() + + profiler.dump_stats(profile_file_path.with_suffix(".prof")) # can be read by 'snakeviz -s' or 'pyprof2calltree' + with open(profile_file_path, 'w') as stats_file: + 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.", + file=sys.stderr) + +def with_profile(extension, remaining_args, profile_file_path): + ''' + profile with profile + ''' + import profile + import pstats + profiler = profile.Profile() + + profiler.run('extension.run(args=remaining_args)') + + profiler.dump_stats(profile_file_path.with_suffix(".prof")) # can be read by 'snakeviz' or 'pyprof2calltree' - seems broken + with open(profile_file_path, 'w') as stats_file: + 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.", + file=sys.stderr) + +def with_pyinstrument(extension, remaining_args, profile_file_path): + ''' + profile with pyinstrument + ''' + import pyinstrument + profiler = pyinstrument.Profiler() + + profiler.start() + extension.run(args=remaining_args) + profiler.stop() + + profile_file_path = profile_file_path.with_suffix(".html") + 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) -- cgit v1.2.3 From 0f404bb737fd5879ce8171cafb73f10ac1ec15ed Mon Sep 17 00:00:00 2001 From: karnigen Date: Thu, 11 Jan 2024 14:19:16 +0100 Subject: extending bash with cmd line args: -d -p --- DEBUG_template.ini | 27 +++++++++++++-------- inkstitch.py | 40 ++++++++++++++++++++++-------- lib/debug_utils.py | 71 +++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 106 insertions(+), 32 deletions(-) diff --git a/DEBUG_template.ini b/DEBUG_template.ini index e04d78b0..41d06796 100644 --- a/DEBUG_template.ini +++ b/DEBUG_template.ini @@ -3,11 +3,16 @@ ; prefer_pip_inkex = False [DEBUG] -;;; select one active debugger, default: none -; debugger = vscode -; debugger = pycharm -; debugger = pydev -; debugger = file +;;; select one active debug_type, default: none +; debug_type = vscode +; debug_type = pycharm +; debug_type = pydev + +;;; enable debugger, see cmd line arg -d, default: False +; debug_enable = True + +;;; debug log output to file even if debugger is not enabled, default: False +; debug_to_file = True ;;; disable debugger when calling from inkscape, default: False ; disable_from_inkscape = True @@ -28,12 +33,14 @@ ; bash_file_base = debug_inkstitch [PROFILE] -;;; select one active profiler, default: none -; profiler = cprofile -; profiler = profile -; profiler = pyinstrument +;;; select one active profiler_type, default: none +; profiler_type = cprofile +; profiler_type = profile +; profiler_type = pyinstrument + +;;; enable profiler, see cmd line arg -p, default: False +; profile_enable = True ;;; base name for profile output files, default: debug_profile ; profile_file_base = debug_profile - diff --git a/inkstitch.py b/inkstitch.py index 514d42cd..c00fcc04 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -41,16 +41,36 @@ else: debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) # check if debugger is active on startup debug_type = 'none' -profile_type = 'none' +profiler_type = 'none' 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 + # - if script was already started from debugger then don't read debug type from ini file or cmd line if not debug_active: - debug_type = ini.get("DEBUG","debugger", fallback="none") # debugger type vscode, pycharm, pydevd, file + # enable/disable debugger + 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' + + # enbale/disable profiling + 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 - profile_type = ini.get("PROFILE","profiler", fallback="none") # profiler type cprofile, profile, pyinstrument + profiler_type = ini.get("PROFILE","profiler_type", fallback="none") # profiler type cprofile, profile, pyinstrument + if not profile_enable: + profiler_type = 'none' if running_from_inkscape: # process creation of the Bash script - should be done before sys.path is modified, see below in prefere_pip_inkex @@ -96,7 +116,7 @@ if running_as_frozen or not debug_active: warnings.filterwarnings('ignore') # TODO - check if this is still for shapely needed, apparently, shapely uses only exceptions instead of io. -# all logs were removed from version 2.0.0, if we ensure that shapely is always >= 2.0.0 +# all logs were removed from version 2.0.0, ensure that shapely is always >= 2.0.0 # ---- plan to remove this in future ---- # set logger for shapely - for old versions of shapely @@ -127,11 +147,11 @@ extension = extension_class() # create instance of extension class - call __ini # extension run(), we differentiate between debug and normal mode # - in debug or profile mode we debug or profile extension.run() method # - in normal mode we run extension.run() in try/except block to catch all exceptions and hide GTK spam -if debug_active or profile_type != "none": # if debug or profile mode - if profile_type == 'none': # only debugging +if debug_active or profiler_type != "none": # if debug or profile mode + if profiler_type == 'none': # only debugging extension.run(args=remaining_args) else: # do profiling - debug_utils.profile(profile_type, SCRIPTDIR, ini, extension, remaining_args) + debug_utils.profile(profiler_type, SCRIPTDIR, ini, extension, remaining_args) else: # if not debug nor profile mode save_stderr() # hide GTK spam @@ -154,7 +174,7 @@ else: # if not debug nor profile mode finally: restore_stderr() - if shapely_errors.tell(): - errormsg(shapely_errors.getvalue()) + # if shapely_errors.tell(): + # errormsg(shapely_errors.getvalue()) sys.exit(0) diff --git a/lib/debug_utils.py b/lib/debug_utils.py index 298a7b80..ab2a6ca9 100644 --- a/lib/debug_utils.py +++ b/lib/debug_utils.py @@ -13,7 +13,6 @@ 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): ''' prepare Bash script for offline debugging from console @@ -42,30 +41,40 @@ def write_offline_debug_script(debug_script_dir : Path, ini : configparser.Confi 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\n") - f.write(f"# python version: {sys.version}\n") # python version + f.write(f'#!/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 + # environment PATH + f.write(f'# PATH:\n') + f.write(f'# {os.environ["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(f'# python sys.path:\n') for p in sys.path: - f.write(f"# {p}\n") + f.write(f'# {p}\n') # see static void set_extensions_env() in inkscape/src/inkscape-main.cpp - f.write(f"# PYTHONPATH:\n") + f.write(f'# PYTHONPATH:\n') for p in os.environ.get('PYTHONPATH', '').split(os.pathsep): # PYTHONPATH to list - f.write(f"# {p}\n") + f.write(f'# {p}\n') - f.write(f"# copy {svg_file} to {bash_svg}\n") + f.write(f'# copy {svg_file} to {bash_svg}\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 # see void Extension::set_environment() in inkscape/src/extension/extension.cpp - notexported = ["SELF_CALL"] # if an extension calls inkscape itself - exported = ["INKEX_GETTEXT_DOMAIN", "INKEX_GETTEXT_DIRECTORY", - "INKSCAPE_PROFILE_DIR", "DOCUMENT_PATH", "PYTHONPATH"] + 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: f.write(f'# export {k}="{os.environ[k]}"\n') @@ -77,10 +86,48 @@ def write_offline_debug_script(debug_script_dir : Path, ini : configparser.Confi f.write(f'export INKSTITCH_OFFLINE_SCRIPT="True"\n') f.write('# call inkstitch\n') - f.write(f"python3 inkstitch.py {myargs}\n") + f.write(f'python3 inkstitch.py {myargs}\n') bash_file.chmod(0o0755) # make file executable, hopefully ignored on Windows +def bash_parser(): + return ''' +set -e # exit on error + +# parse cmd line arguments: +# -d enable debugging +# -p enable profiling +# ":..." - silent error reporting +while getopts ":dp" opt; do + case $opt in + d) + arg_d="true" + ;; + p) + arg_p="true" + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; + 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 + +''' + + def reorder_sys_path(): ''' change sys.path to prefer pip installed inkex over inkscape bundled inkex -- cgit v1.2.3 From 0e08a263116c4c5cda0fd81da122ae384a13b52a Mon Sep 17 00:00:00 2001 From: karnigen Date: Thu, 11 Jan 2024 17:48:11 +0100 Subject: updated decription --- inkstitch.py | 10 +++++----- lib/debug.py | 56 +++++++++++++++++++++++++++++++++++++++++++----------- lib/debug_utils.py | 17 +++++++++-------- 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/inkstitch.py b/inkstitch.py index c00fcc04..d85eaba4 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -47,7 +47,7 @@ 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 if not debug_active: - # enable/disable debugger + # enable/disable debugger from bash: -d if os.environ.get('INKSTITCH_DEBUG_ENABLE', '').lower() in ['true', '1', 'yes', 'y']: debug_enable = True else: @@ -61,7 +61,7 @@ if not running_as_frozen: # debugging/profiling only in development mode if debug_to_file and debug_type == 'none': debug_type = 'file' - # enbale/disable profiling + # enbale/disable profiling from bash: -p if os.environ.get('INKSTITCH_PROFILE_ENABLE', '').lower() in ['true', '1', 'yes', 'y']: profile_enable = True else: @@ -115,8 +115,8 @@ if running_as_frozen or not debug_active: import warnings warnings.filterwarnings('ignore') -# TODO - check if this is still for shapely needed, apparently, shapely uses only exceptions instead of io. -# all logs were removed from version 2.0.0, ensure that shapely is always >= 2.0.0 +# 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 ---- # set logger for shapely - for old versions of shapely @@ -174,7 +174,7 @@ else: # if not debug nor profile mode finally: restore_stderr() - # if shapely_errors.tell(): + # if shapely_errors.tell(): # see above plan to remove this in future for shapely # errormsg(shapely_errors.getvalue()) sys.exit(0) diff --git a/lib/debug.py b/lib/debug.py index d3c19429..93bb9629 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -101,22 +101,54 @@ class Debug(object): self.log("Debug logging enabled.") def init_debugger(self): - # How to debug Ink/Stitch with LiClipse: + ### 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 + # 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 + # 1. set debug_type to one of - vscode, pycharm, pydev, see below for details + # 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 + # 1. set profiler_type to one of - cprofile, profile, pyinstrument + # profiler_type = cprofile - 'cProfile' profiler + # profiler_type = profile - 'profile' profiler + # profiler_type = pyinstrument- 'pyinstrument' profiler + # 2. set profile_enable = True in DEBUG.ini + # 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: + # - 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: # # 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 # * follow the "Note:" to enable the debug server menu item - # 3. Copy and edit a file named "DEBUG.ini" from "DEBUG_template.ini" next to inkstitch.py in your git clone. + # 3. Copy and edit a file named "DEBUG.ini" from "DEBUG_template.ini" next to inkstitch.py in your git clone + # and set debug_type = pydev # 4. Run any extension and PyDev will start debugging. - # debugger = vscode - 'debugpy' for vscode editor - # debugger = pycharm - 'pydevd-pycharm' for pycharm editor - # debugger = pydev - 'pydevd' for eclipse editor - # debugger = file - no debugger, only debug.log, debug.svg are used - ### - # 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. @@ -137,7 +169,8 @@ class Debug(object): # configuration. Set "IDE host name:" to "localhost" and "Port:" to 5678. # You can leave the default settings for all other choices. # - # 3. Touch a file named "DEBUG.ini" at the top of your git repo, as above. + # 3. Touch a file named "DEBUG.ini" at the top of your git repo, as above + # set debug_type = pycharm # # 4. Create a symbolic link in the Inkscape extensions directory to the # top-level directory of your git repo. On a mac, for example: @@ -162,7 +195,7 @@ class Debug(object): ### - # 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 # @@ -180,7 +213,8 @@ class Debug(object): # } # } # ] - # 3. Touch a file named "DEBUG.ini" at the top of your git repo, as above. + # 3. Touch a file named "DEBUG.ini" at the top of your git repo, as above + # set debug_type = vscode # 4. Start the debug server in VS Code by clicking on the debug icon in the left pane # select "Python: Attach" from the dropdown menu and click on the green arrow. # The debug server will start and connect to already running python processes, diff --git a/lib/debug_utils.py b/lib/debug_utils.py index ab2a6ca9..169fa4c5 100644 --- a/lib/debug_utils.py +++ b/lib/debug_utils.py @@ -53,7 +53,7 @@ def write_offline_debug_script(debug_script_dir : Path, ini : configparser.Confi # environment PATH f.write(f'# PATH:\n') - f.write(f'# {os.environ["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') @@ -67,17 +67,18 @@ def write_offline_debug_script(debug_script_dir : Path, ini : configparser.Confi 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') + 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 # 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', 'INKSCAPE_PROFILE_DIR', 'DOCUMENT_PATH', 'PYTHONPATH'] for k in notexported: if k in os.environ: - f.write(f'# export {k}="{os.environ[k]}"\n') + f.write(f'# export {k}="{os.environ[k]}"\n') for k in exported: if k in os.environ: f.write(f'export {k}="{os.environ[k]}"\n') @@ -159,21 +160,21 @@ def reorder_sys_path(): # - pyinstrument - profiler with nice html output -def profile(profile_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_path = profile_dir / profile_file_base # Path object - if profile_type == 'cprofile': + if profiler_type == 'cprofile': with_cprofile(extension, remaining_args, profile_file_path) - elif profile_type == 'profile': + elif profiler_type == 'profile': with_profile(extension, remaining_args, profile_file_path) - elif profile_type == 'pyinstrument': + elif profiler_type == 'pyinstrument': with_pyinstrument(extension, remaining_args, profile_file_path) else: - raise ValueError(f"unknown profiler type: '{profile_type}'") + raise ValueError(f"unknown profiler type: '{profiler_type}'") def with_cprofile(extension, remaining_args, profile_file_path): ''' -- cgit v1.2.3 From 55145a568a69d3f838ca0554aa883ad7e7c41e8d Mon Sep 17 00:00:00 2001 From: karnigen Date: Fri, 12 Jan 2024 01:34:20 +0100 Subject: added support for lxml <5.0.0 in write --- lib/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/debug.py b/lib/debug.py index 93bb9629..e407b54c 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -277,7 +277,7 @@ class Debug(object): def save_svg(self): tree = etree.ElementTree(self.svg) debug_svg = self.debug_dir / self.debug_svg_file - tree.write(debug_svg) + tree.write(str(debug_svg)) # xlml <5.0.0 does not support Path objects @check_enabled @unwrap_arguments -- cgit v1.2.3 From 78a3c93fe3b75f9aeb315f13f6ccd925230672c2 Mon Sep 17 00:00:00 2001 From: karnigen Date: Fri, 12 Jan 2024 01:47:11 +0100 Subject: just typo --- lib/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/debug.py b/lib/debug.py index e407b54c..eb49005a 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -277,7 +277,7 @@ class Debug(object): def save_svg(self): tree = etree.ElementTree(self.svg) debug_svg = self.debug_dir / self.debug_svg_file - tree.write(str(debug_svg)) # xlml <5.0.0 does not support Path objects + tree.write(str(debug_svg)) # lxml <5.0.0 does not support Path objects @check_enabled @unwrap_arguments -- cgit v1.2.3