kopia lustrzana https://github.com/inkstitch/inkstitch
simplification, cleanup, docs, startup dialog, DEBUG.ini
rodzic
f1f9d275a1
commit
b4f50b1ed9
|
@ -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*
|
||||||
|
|
|
@ -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
|
192
inkstitch.py
192
inkstitch.py
|
@ -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
|
||||||
|
|
92
lib/debug.py
92
lib/debug.py
|
@ -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)
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Ładowanie…
Reference in New Issue