simplification, cleanup, docs, startup dialog, DEBUG.ini

pull/2653/head
karnigen 2024-01-05 17:05:22 +01:00
rodzic f1f9d275a1
commit b4f50b1ed9
5 zmienionych plików z 253 dodań i 220 usunięć

12
.gitignore vendored
Wyświetl plik

@ -22,12 +22,10 @@ flaskserverport.json
electron/yarn.lock electron/yarn.lock
# debug and profile files # debug and profile files
/DEVEL.ini /DEBUG.ini
/DEBUG
/PROFILE
/debug* /debug*
/.debug* /.debug*
# old profile files # old debug files
/profile_stats /DEBUG
/profile_stats.html /PROFILE
/profile_stats.prof /profile*

Wyświetl plik

@ -7,6 +7,7 @@
; debugger = vscode ; debugger = vscode
; debugger = pycharm ; debugger = pycharm
; debugger = pydev ; debugger = pydev
; debugger = file
;;; disable debugger when calling from inkscape, default: False ;;; disable debugger when calling from inkscape, default: False
; disable_from_inkscape = True ; disable_from_inkscape = True
@ -15,7 +16,10 @@
; wait_attach = False ; wait_attach = False
;;; debug log file, default: debug.log ;;; debug log file, default: debug.log
; debug_file = 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 ;;; creation of bash script, default: False
; create_bash_script = True ; create_bash_script = True

Wyświetl plik

@ -5,26 +5,33 @@
import os import os
import sys import sys
from pathlib import Path from pathlib import Path # to work with paths as objects
import configparser import configparser # to read DEBUG.ini
import lib.debug_utils as debug_utils
import lib.debug_utils as debug_utils
SCRIPTDIR = Path(__file__).parent.absolute() SCRIPTDIR = Path(__file__).parent.absolute()
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 running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle
ini = configparser.ConfigParser() if len(sys.argv) < 2:
ini.read(SCRIPTDIR / "DEVEL.ini") # read DEVEL.ini file if exists # 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
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)
# prefer pip installed inkex over inkscape bundled inkex, pip version is bundled with Inkstitch ini = configparser.ConfigParser()
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 # check if running from inkscape, given by environment variable
if os.environ.get('INKSTITCH_OFFLINE_SCRIPT', '').lower() in ['true', '1', 'yes', 'y']: if os.environ.get('INKSTITCH_OFFLINE_SCRIPT', '').lower() in ['true', '1', 'yes', 'y']:
@ -37,98 +44,74 @@ debug_type = 'none'
profile_type = 'none' profile_type = 'none'
if not running_as_frozen: # debugging/profiling only in development mode 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 # 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: 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 # 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 running_from_inkscape:
if ini.getboolean("DEBUG","create_bash_script", fallback=False): # create script only if enabled in DEVEL.ini # process creation of the Bash script - should be done before sys.path is modified, see below in prefere_pip_inkex
debug_utils.write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg) 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 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: if disable_from_inkscape:
debug_type = 'none' # do not start debugger when running 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: if prefere_pip_inkex and 'PYTHONPATH' in os.environ:
# see static void set_extensions_env() in inkscape/src/inkscape-main.cpp debug_utils.reorder_sys_path()
# When running in development mode, we prefer inkex installed by pip, not the one bundled with Inkscape. from argparse import ArgumentParser # to parse arguments and remove --extension
# - move inkscape extensions path to the end of sys.path import logging # to set logger for shapely
# - we compare PYTHONPATH with sys.path and move PYTHONPATH to the end of sys.path from io import StringIO # to store shapely errors
# - 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
from lib.exceptions import InkstitchException, format_uncaught_exception from lib.exceptions import InkstitchException, format_uncaught_exception
from inkex import errormsg from inkex import errormsg # to show error message in inkscape
from lxml.etree import XMLSyntaxError from lxml.etree import XMLSyntaxError # to catch XMLSyntaxError from inkex
import lib.debug as debug from lib.debug import debug # import global variable debug - don't import whole module
from lib import extensions
from lib.i18n import _
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 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': if debug_type != 'none':
debug_file = ini.get("DEBUG","debug_file", fallback="debug.log") debug.enable(debug_type, SCRIPTDIR, ini)
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 # check if debugger is really activated
debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) 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: if running_as_frozen or not debug_active:
import warnings import warnings
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
# set logger for shapely # TODO - check if this is still for shapely needed, apparently, shapely uses only exceptions instead of io.
logger = logging.getLogger('shapely.geos') # attach logger of shapely, from ver 2.0.0 all logs are exceptions # all logs were removed from version 2.0.0, if we ensure that shapely is always >= 2.0.0
logger.setLevel(logging.DEBUG)
shapely_errors = StringIO() # in memory file to store shapely errors # ---- plan to remove this in future ----
ch = logging.StreamHandler(shapely_errors) # set logger for shapely - for old versions of shapely
ch.setLevel(logging.DEBUG) # logger = logging.getLogger('shapely.geos') # attach logger of shapely
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') # logger.setLevel(logging.DEBUG)
ch.setFormatter(formatter) # shapely_errors = StringIO() # in memory file to store shapely errors
logger.addHandler(ch) # 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 # 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 = ArgumentParser()
parser.add_argument("--extension") parser.add_argument("--extension")
my_args, remaining_args = parser.parse_known_args() 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_class = getattr(extensions, extension_class_name)
extension = extension_class() # create instance of extension class - call __init__ method extension = extension_class() # create instance of extension class - call __init__ method
# extension run(), but we differentiate between debug and normal mode # extension run(), we differentiate between debug and normal mode
# - in debug or profile mode we run extension or profile extension # - in debug or profile mode we debug or profile extension.run() method
# - in normal mode we run extension in try/except block to catch all exceptions and hide GTK spam # - 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 debug_active or profile_type != "none": # if debug or profile mode
profile_file_base = ini.get("PROFILE","profile_file_base", fallback="debug_profile") if profile_type == 'none': # only debugging
profile_path = SCRIPTDIR / profile_file_base # Path object
if profile_type == 'none':
extension.run(args=remaining_args) extension.run(args=remaining_args)
elif profile_type == 'cprofile': else: # do profiling
import cProfile debug_utils.profile(profile_type, SCRIPTDIR, ini, extension, remaining_args)
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: # if not debug nor profile mode else: # if not debug nor profile mode
save_stderr() # hide GTK spam save_stderr() # hide GTK spam

Wyświetl plik

@ -3,21 +3,25 @@
# Copyright (c) 2010 Authors # Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import atexit
import os import os
import socket
import sys import sys
import time import atexit # to save svg file on exit
from contextlib import contextmanager import socket # to check if debugger is running
from datetime import datetime 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 import inkex
from lxml import etree from lxml import etree # to create svg file
from .svg import line_strings_to_path from .svg import line_strings_to_path
from .svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL 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 check_enabled(func):
def decorated(self, *args, **kwargs): def decorated(self, *args, **kwargs):
if self.enabled: if self.enabled:
@ -26,13 +30,17 @@ def check_enabled(func):
return decorated 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): def _unwrap(arg):
if callable(arg): if callable(arg):
return arg() return arg()
else: else:
return arg 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 unwrap_arguments(func):
def decorated(self, *args, **kwargs): def decorated(self, *args, **kwargs):
unwrapped_args = [_unwrap(arg) for arg in args] unwrapped_args = [_unwrap(arg) for arg in args]
@ -69,21 +77,26 @@ class Debug(object):
self.current_layer = None self.current_layer = None
self.group_stack = [] 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': if debug_type == 'none':
return return
self.debugger = debug_type self.debugger = debug_type
self.wait_attach = wait_attach
self.enabled = True self.enabled = True
self.init_log(debug_file) self.init_log()
self.init_debugger() self.init_debugger()
self.init_svg() self.init_svg()
def init_log(self, debug_file): def init_log(self):
self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), debug_file) self.log_file = self.debug_dir / self.debug_log_file
# delete old content # delete old content
with open(self.log_file, "w"): with self.log_file.open("w"):
pass pass
self.log("Debug logging enabled.") self.log("Debug logging enabled.")
@ -93,9 +106,14 @@ class Debug(object):
# 1. Install LiClipse (liclipse.com) -- no need to install Eclipse first # 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 # 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 # * 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. # 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:
@ -119,7 +137,7 @@ class Debug(object):
# configuration. Set "IDE host name:" to "localhost" and "Port:" to 5678. # configuration. Set "IDE host name:" to "localhost" and "Port:" to 5678.
# You can leave the default settings for all other choices. # 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 # 4. Create a symbolic link in the Inkscape extensions directory to the
# top-level directory of your git repo. On a mac, for example: # 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 directory, or you'll see duplicate entries in the Ink/Stitch
# extensions menu in Inkscape. # extensions menu in Inkscape.
# #
# 5. In the execution env for Inkscape, set the environment variable # 5. In Pycharm, either click on the green "bug" icon if visible in the upper
# 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
# right or press Ctrl-D to start debugging.The PyCharm debugger pane will # right or press Ctrl-D to start debugging.The PyCharm debugger pane will
# display the message "Waiting for process connection..." # 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 # debugger will be triggered. If you've left "Suspend after connect" checked
# in the Run configuration, PyCharm will pause in the "self.log("Enabled # in the Run configuration, PyCharm will pause in the "self.log("Enabled
# PyDev debugger.)" statement, below. Uncheck the box to have it continue # 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 # 1. Install the Python extension for VS Code
# pip install debugpy # pip install debugpy
# 2. create .vscode/launch.json containing somewhere: # 2. create .vscode/launch.json containing:
# "configurations": [ ... # "configurations": [ ...
# { # {
# "name": "Python: Attach", # "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. # 3. Touch a file named "DEBUG.ini" 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 # 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: # Notes:
# to see flask server url routes: # to see flask server url routes:
@ -184,11 +198,13 @@ class Debug(object):
import pydevd_pycharm import pydevd_pycharm
elif self.debugger == 'pydev': elif self.debugger == 'pydev':
import pydevd import pydevd
elif self.debugger == 'file':
pass
else: else:
raise ValueError(f"unknown debugger: '{self.debugger}'") raise ValueError(f"unknown debugger: '{self.debugger}'")
except ImportError: 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 # pydevd likes to shout about errors to stderr whether I want it to or not
with open(os.devnull, 'w') as devnull: with open(os.devnull, 'w') as devnull:
@ -207,6 +223,8 @@ class Debug(object):
stderrToServer=True) stderrToServer=True)
elif self.debugger == 'pydev': elif self.debugger == 'pydev':
pydevd.settrace() pydevd.settrace()
elif self.debugger == 'file':
pass
else: else:
raise ValueError(f"unknown debugger: '{self.debugger}'") raise ValueError(f"unknown debugger: '{self.debugger}'")
@ -224,7 +242,7 @@ class Debug(object):
def save_svg(self): def save_svg(self):
tree = etree.ElementTree(self.svg) 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) tree.write(debug_svg)
@check_enabled @check_enabled
@ -267,20 +285,21 @@ class Debug(object):
timestamp = now.isoformat() timestamp = now.isoformat()
self.last_log_time = now 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) print(timestamp, message % args, file=logfile)
logfile.flush() logfile.flush()
# decorator to measure time of function
def time(self, func): def time(self, func):
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
if self.enabled: if self.enabled:
self.raw_log("entering %s()", func.__name__) self.raw_log("entering %s()", func.__name__)
start = time.time() start = time.monotonic()
result = func(*args, **kwargs) result = func(*args, **kwargs)
if self.enabled: if self.enabled:
end = time.time() end = time.monotonic()
self.raw_log("leaving %s(), duration = %s", func.__name__, round(end - start, 6)) self.raw_log("leaving %s(), duration = %s", func.__name__, round(end - start, 6))
return result return result
@ -348,20 +367,19 @@ class Debug(object):
INKSCAPE_LABEL: name INKSCAPE_LABEL: name
})) }))
# decorator to measure time of with block
@contextmanager @contextmanager
def time_this(self, label="code block"): def time_this(self, label="code block"):
if self.enabled: if self.enabled:
start = time.time() start = time.monotonic()
self.raw_log("begin %s", label) self.raw_log("begin %s", label)
yield yield
if self.enabled: 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() debug = Debug()
def enable(debug_type, debug_file, wait_attach):
debug.enable(debug_type, debug_file, wait_attach)

Wyświetl plik

@ -5,54 +5,27 @@
import os import os
import sys 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 # this file is without: import inkex
# - so we can modify sys.path before importing inkex # - 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)
# 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): def write_offline_debug_script(debug_script_dir : Path, ini : configparser.ConfigParser):
# prepare script for offline debugging from console '''
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 # 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('-')] 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) print(f"WARN: input svg file is same as output svg file. No script created in write debug script.", file=sys.stderr)
return return
import shutil import shutil # to copy svg file
bash_file = SCRIPTDIR / bash_name bash_file = debug_script_dir / bash_name
with open(bash_file, 'w') as f: # "w" text mode, automatic conversion of \n to os.linesep 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"#!/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"# {p}\n")
f.write(f"# copy {svg_file} to {bash_svg}\n") f.write(f"# copy {svg_file} to {bash_svg}\n")
# check if files are not the same shutil.copy(svg_file, debug_script_dir / bash_svg) # copy file to bash_svg
if svg_file != bash_svg:
shutil.copy(svg_file, SCRIPTDIR / 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 # 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('# 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 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)