Merge pull request #2653 from inkstitch/kgn/debug_profile_extend_vscode

Kgn/debug profile extend vscode
pull/2684/head
karnigen 2024-01-12 19:01:22 +01:00 zatwierdzone przez GitHub
commit bc991aaa25
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
5 zmienionych plików z 559 dodań i 103 usunięć

21
.gitignore vendored
Wyświetl plik

@ -1,28 +1,31 @@
__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
/PROFILE
/profile_stats
/profile_stats.prof
/.vscode
__pycache__
flaskserverport.json
electron/yarn.lock
# debug and profile files
/DEBUG.ini
/debug*
/.debug*
# old debug files
/DEBUG
/PROFILE
/profile*

46
DEBUG_template.ini 100644
Wyświetl plik

@ -0,0 +1,46 @@
[LIBRARY]
;;; use the pip installed version of inkex.py, default: True
; prefer_pip_inkex = False
[DEBUG]
;;; 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
;;; 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_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

Wyświetl plik

@ -2,84 +2,159 @@
#
# 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
from argparse import ArgumentParser
from io import StringIO
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
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 / "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']:
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_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 or cmd line
if not debug_active:
# enable/disable debugger from bash: -d
if os.environ.get('INKSTITCH_DEBUG_ENABLE', '').lower() in ['true', '1', 'yes', 'y']:
debug_enable = True
else:
debug_enable = ini.getboolean("DEBUG","debug_enable", fallback=False) # enable debugger on startup from ini
debug_type = ini.get("DEBUG","debug_type", fallback="none") # debugger type vscode, pycharm, pydevd
if not debug_enable:
debug_type = 'none'
debug_to_file = ini.getboolean("DEBUG","debug_to_file", fallback=False) # write debug output to file
if debug_to_file and debug_type == 'none':
debug_type = 'file'
# enbale/disable profiling from bash: -p
if os.environ.get('INKSTITCH_PROFILE_ENABLE', '').lower() in ['true', '1', 'yes', 'y']:
profile_enable = True
else:
profile_enable = ini.getboolean("PROFILE","profile_enable", fallback=False) # read from ini
# specify profiler type
profiler_type = ini.get("PROFILE","profiler_type", fallback="none") # profiler type cprofile, profile, pyinstrument
if not profile_enable:
profiler_type = 'none'
if running_from_inkscape:
# process creation of the Bash script - should be done before sys.path is modified, see below in prefere_pip_inkex
if ini.getboolean("DEBUG","create_bash_script", fallback=False): # create script only if enabled in DEBUG.ini
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:
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
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"
from inkex import errormsg # to show error message in inkscape
from lxml.etree import XMLSyntaxError # to catch XMLSyntaxError from inkex
sys.path.remove(extensions_path)
sys.path.append(extensions_path)
from lib.debug import debug # import global variable debug - don't import whole module
from inkex import errormsg
from lxml.etree import XMLSyntaxError
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
import lib.debug as debug
from lib import extensions
from lib.i18n import _
from lib.utils import restore_stderr, save_stderr
# enabling of debug depends on value of debug_type in DEBUG.ini file
if debug_type != 'none':
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
if getattr(sys, 'frozen', None):
# 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')
logger = logging.getLogger('shapely.geos')
logger.setLevel(logging.DEBUG)
shapely_errors = StringIO()
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 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
# 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()
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
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.disable()
profiler.dump_stats(path + ".prof")
# 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 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(profiler_type, SCRIPTDIR, ini, extension, remaining_args)
with open(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 {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)
@ -99,7 +174,7 @@ else:
finally:
restore_stderr()
if shapely_errors.tell():
errormsg(shapely_errors.getvalue())
# if shapely_errors.tell(): # see above plan to remove this in future for shapely
# errormsg(shapely_errors.getvalue())
sys.exit(0)

Wyświetl plik

@ -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]
@ -62,36 +70,85 @@ 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):
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
if debug_type == 'none':
return
self.debugger = debug_type
self.enabled = True
self.init_log()
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")
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.")
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. 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
# and set debug_type = pydev
# 4. Run any extension and PyDev will start debugging.
###
# To debug with PyCharm:
### To debug with PyCharm:
# You must use the PyCharm Professional Edition and _not_ the Community
# Edition. Jetbrains has chosen to make remote debugging a Pro feature.
@ -112,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" 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:
@ -125,29 +183,62 @@ 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
# 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:
# "configurations": [ ...
# {
# "name": "Python: Attach",
# "type": "python",
# "request": "attach",
# "connect": {
# "host": "localhost",
# "port": 5678
# }
# }
# ]
# 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,
# but immediately exit if no python processes are running.
#
# 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
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:
@ -155,17 +246,27 @@ class Debug(object):
sys.stderr = devnull
try:
if 'PYCHARM_REMOTE_DEBUG' in os.environ:
if self.debugger == 'vscode':
debugpy.listen(('localhost', 5678))
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)
else:
elif self.debugger == 'pydev':
pydevd.settrace()
elif self.debugger == 'file':
pass
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
@ -175,8 +276,8 @@ 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")
tree.write(debug_svg)
debug_svg = self.debug_dir / self.debug_svg_file
tree.write(str(debug_svg)) # lxml <5.0.0 does not support Path objects
@check_enabled
@unwrap_arguments
@ -218,20 +319,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
@ -299,20 +401,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.enable()

231
lib/debug_utils.py 100644
Wyświetl plik

@ -0,0 +1,231 @@
# 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
from pathlib import Path # to work with paths as objects
import configparser # to read DEBUG.ini
# this file is without: import 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)
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('-')]
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 # 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')
# 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.get("PATH","")}\n')
# for p in os.environ.get("PATH", '').split(os.pathsep): # PATH to list
# f.write(f'# {p}\n')
# python module path
f.write(f'# python sys.path:\n')
for p in sys.path:
f.write(f'# {p}\n')
# see static void set_extensions_env() in inkscape/src/inkscape-main.cpp
f.write(f'# PYTHONPATH:\n')
for p in os.environ.get('PYTHONPATH', '').split(os.pathsep): # PYTHONPATH to list
f.write(f'# {p}\n')
f.write(f'# copy {svg_file} to {bash_svg}\n#\n')
shutil.copy(svg_file, debug_script_dir / bash_svg) # copy file to bash_svg
myargs = myargs.replace(str(svg_file), str(bash_svg)) # replace file name with bash_svg
# 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')
for k in exported:
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')
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
'''
# 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(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 profiler_type == 'cprofile':
with_cprofile(extension, remaining_args, profile_file_path)
elif profiler_type == 'profile':
with_profile(extension, remaining_args, profile_file_path)
elif profiler_type == 'pyinstrument':
with_pyinstrument(extension, remaining_args, profile_file_path)
else:
raise ValueError(f"unknown profiler type: '{profiler_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)