summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorkarnigen <karnigen@gmail.com>2024-01-05 17:05:22 +0100
committerkarnigen <karnigen@gmail.com>2024-01-05 17:05:22 +0100
commitb4f50b1ed9fa6ac50bcce7bc7b78dd9c7ef6138e (patch)
tree5e1f65d459e834fbf1387318ec3b1816ef7f22bb /lib
parentf1f9d275a1ffaeb538a72e3643fb98231323337a (diff)
simplification, cleanup, docs, startup dialog, DEBUG.ini
Diffstat (limited to 'lib')
-rw-r--r--lib/debug.py92
-rw-r--r--lib/debug_utils.py177
2 files changed, 180 insertions, 89 deletions
diff --git a/lib/debug.py b/lib/debug.py
index a256bc0a..d3c19429 100644
--- a/lib/debug.py
+++ b/lib/debug.py
@@ -3,21 +3,25 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-import atexit
import os
-import socket
import sys
-import time
-from contextlib import contextmanager
-from datetime import datetime
+import atexit # to save svg file on exit
+import socket # to check if debugger is running
+import time # to measure time of code block, use time.monotonic() instead of time.time()
+from datetime import datetime
+
+from contextlib import contextmanager # to measure time of with block
+import configparser # to read DEBUG.ini
+from pathlib import Path # to work with paths as objects
import inkex
-from lxml import etree
+from lxml import etree # to create svg file
from .svg import line_strings_to_path
from .svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL
-
+# decorator to check if debugging is enabled
+# - if debug is not enabled then decorated function is not called
def check_enabled(func):
def decorated(self, *args, **kwargs):
if self.enabled:
@@ -26,13 +30,17 @@ def check_enabled(func):
return decorated
+# unwrapping = provision for functions as arguments
+# - if argument is callable then it is called and return value is used as argument
+# otherwise argument is returned as is
def _unwrap(arg):
if callable(arg):
return arg()
else:
return arg
-
+# decorator to unwrap arguments if they are callable
+# eg: if argument is lambda function then it is called and return value is used as argument
def unwrap_arguments(func):
def decorated(self, *args, **kwargs):
unwrapped_args = [_unwrap(arg) for arg in args]
@@ -69,21 +77,26 @@ class Debug(object):
self.current_layer = None
self.group_stack = []
+ def enable(self, debug_type, debug_dir : Path, ini : configparser.ConfigParser):
+ # initilize file names and other parameters from DEBUG.ini file
+ self.debug_dir = debug_dir # directory where debug files are stored
+ self.debug_log_file = ini.get("DEBUG","debug_log_file", fallback="debug.log")
+ self.debug_svg_file = ini.get("DEBUG","debug_svg_file", fallback="debug.svg")
+ self.wait_attach = ini.getboolean("DEBUG","wait_attach", fallback=True) # currently only for vscode
- def enable(self, debug_type, debug_file, wait_attach):
if debug_type == 'none':
return
+
self.debugger = debug_type
- self.wait_attach = wait_attach
self.enabled = True
- self.init_log(debug_file)
+ self.init_log()
self.init_debugger()
self.init_svg()
- def init_log(self, debug_file):
- self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), debug_file)
+ def init_log(self):
+ self.log_file = self.debug_dir / self.debug_log_file
# delete old content
- with open(self.log_file, "w"):
+ with self.log_file.open("w"):
pass
self.log("Debug logging enabled.")
@@ -93,9 +106,14 @@ class Debug(object):
# 1. Install LiClipse (liclipse.com) -- no need to install Eclipse first
# 2. Start debug server as described here: http://www.pydev.org/manual_adv_remote_debugger.html
# * follow the "Note:" to enable the debug server menu item
- # 3. Create a file named "DEBUG" next to inkstitch.py in your git clone.
+ # 3. Copy and edit a file named "DEBUG.ini" from "DEBUG_template.ini" next to inkstitch.py in your git clone.
# 4. Run any extension and PyDev will start debugging.
+ # debugger = vscode - 'debugpy' for vscode editor
+ # debugger = pycharm - 'pydevd-pycharm' for pycharm editor
+ # debugger = pydev - 'pydevd' for eclipse editor
+ # debugger = file - no debugger, only debug.log, debug.svg are used
+
###
# To debug with PyCharm:
@@ -119,7 +137,7 @@ class Debug(object):
# configuration. Set "IDE host name:" to "localhost" and "Port:" to 5678.
# You can leave the default settings for all other choices.
#
- # 3. Touch a file named "DEBUG" at the top of your git repo, as above.
+ # 3. Touch a file named "DEBUG.ini" at the top of your git repo, as above.
#
# 4. Create a symbolic link in the Inkscape extensions directory to the
# top-level directory of your git repo. On a mac, for example:
@@ -132,16 +150,11 @@ class Debug(object):
# extensions directory, or you'll see duplicate entries in the Ink/Stitch
# extensions menu in Inkscape.
#
- # 5. In the execution env for Inkscape, set the environment variable
- # PYCHARM_REMOTE_DEBUG to any value, and launch Inkscape. If you're starting
- # Inkscape from the PyCharm Terminal pane, you can do:
- # export PYCHARM_REMOTE_DEBUG=true;inkscape
- #
- # 6. In Pycharm, either click on the green "bug" icon if visible in the upper
+ # 5. In Pycharm, either click on the green "bug" icon if visible in the upper
# right or press Ctrl-D to start debugging.The PyCharm debugger pane will
# display the message "Waiting for process connection..."
#
- # 7. Do some action in Inkscape which invokes Ink/Stitch extension code, and the
+ # 6. Do some action in Inkscape which invokes Ink/Stitch extension code, and the
# debugger will be triggered. If you've left "Suspend after connect" checked
# in the Run configuration, PyCharm will pause in the "self.log("Enabled
# PyDev debugger.)" statement, below. Uncheck the box to have it continue
@@ -155,7 +168,7 @@ class Debug(object):
#
# 1. Install the Python extension for VS Code
# pip install debugpy
- # 2. create .vscode/launch.json containing somewhere:
+ # 2. create .vscode/launch.json containing:
# "configurations": [ ...
# {
# "name": "Python: Attach",
@@ -167,10 +180,11 @@ class Debug(object):
# }
# }
# ]
- # 3. Touch a file named "DEBUG" at the top of your git repo, as above.
- # containing "vscode" or "vscode-script" see parse_file() in debug_mode.py for details
+ # 3. Touch a file named "DEBUG.ini" at the top of your git repo, as above.
# 4. Start the debug server in VS Code by clicking on the debug icon in the left pane
- # select "Python: Attach" from the dropdown menu and click on the green arrow
+ # select "Python: Attach" from the dropdown menu and click on the green arrow.
+ # The debug server will start and connect to already running python processes,
+ # but immediately exit if no python processes are running.
#
# Notes:
# to see flask server url routes:
@@ -184,11 +198,13 @@ class Debug(object):
import pydevd_pycharm
elif self.debugger == 'pydev':
import pydevd
+ elif self.debugger == 'file':
+ pass
else:
raise ValueError(f"unknown debugger: '{self.debugger}'")
except ImportError:
- self.log("importing pydevd failed (debugger disabled)")
+ self.log(f"importing debugger failed (debugger disabled) for {self.debugger}")
# pydevd likes to shout about errors to stderr whether I want it to or not
with open(os.devnull, 'w') as devnull:
@@ -207,6 +223,8 @@ class Debug(object):
stderrToServer=True)
elif self.debugger == 'pydev':
pydevd.settrace()
+ elif self.debugger == 'file':
+ pass
else:
raise ValueError(f"unknown debugger: '{self.debugger}'")
@@ -224,7 +242,7 @@ class Debug(object):
def save_svg(self):
tree = etree.ElementTree(self.svg)
- debug_svg = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.svg")
+ debug_svg = self.debug_dir / self.debug_svg_file
tree.write(debug_svg)
@check_enabled
@@ -267,20 +285,21 @@ class Debug(object):
timestamp = now.isoformat()
self.last_log_time = now
- with open(self.log_file, "a") as logfile:
+ with self.log_file.open("a") as logfile:
print(timestamp, message % args, file=logfile)
logfile.flush()
+ # decorator to measure time of function
def time(self, func):
def decorated(*args, **kwargs):
if self.enabled:
self.raw_log("entering %s()", func.__name__)
- start = time.time()
+ start = time.monotonic()
result = func(*args, **kwargs)
if self.enabled:
- end = time.time()
+ end = time.monotonic()
self.raw_log("leaving %s(), duration = %s", func.__name__, round(end - start, 6))
return result
@@ -348,20 +367,19 @@ class Debug(object):
INKSCAPE_LABEL: name
}))
+ # decorator to measure time of with block
@contextmanager
def time_this(self, label="code block"):
if self.enabled:
- start = time.time()
+ start = time.monotonic()
self.raw_log("begin %s", label)
yield
if self.enabled:
- self.raw_log("completed %s, duration = %s", label, time.time() - start)
+ self.raw_log("completed %s, duration = %s", label, time.monotonic() - start)
+# global debug object
debug = Debug()
-
-def enable(debug_type, debug_file, wait_attach):
- debug.enable(debug_type, debug_file, wait_attach)
diff --git a/lib/debug_utils.py b/lib/debug_utils.py
index 183a44f8..298a7b80 100644
--- a/lib/debug_utils.py
+++ b/lib/debug_utils.py
@@ -5,54 +5,27 @@
import os
import sys
-from pathlib import Path
+from pathlib import Path # to work with paths as objects
+import configparser # to read DEBUG.ini
# this file is without: import inkex
-# - so we can modify sys.path before importing inkex
-
-# DEBUG and PROFILE are in DEVEL.ini file
-
-# DEBUG file format:
-# - first non-comment line is debugger type
-# - valid values are:
-# "vscode" or "vscode-script" - for debugging with vscode
-# "pycharm" or "pycharm-script" - for debugging with pycharm
-# "pydev" or "pydev-script" - for debugging with pydev
-# "none" or empty file - for no debugging
-# - for offline debugging without inkscape, set debugger name to
-# as "vscode-script" or "pycharm-script" or "pydev-script"
-# - in that case running from inkscape will not start debugger
-# but prepare script for offline debugging from console
-# - valid for "none-script" too
-# - backward compatibilty is broken due to confusion
-# debug_type = 'pydev' # default debugger backwards compatibility
-# if 'PYCHARM_REMOTE_DEBUG' in os.environ: # backwards compatibility
-# debug_type = 'pycharm'
-
-# PROFILE file format:
-# - first non-comment line is profiler type
-# - valid values are:
-# "cprofile" - for cProfile
-# "pyinstrument" - for pyinstrument
-# "profile" - for profile
-# "none" - for no profiling
-
-
-def parse_file(filename):
- # parse DEBUG or PROFILE file for type
- # - return first noncomment and nonempty line from file
- value_type = 'none'
- with open(filename, 'r') as f:
- for line in f:
- line = line.strip().lower()
- if line.startswith("#") or line == "": # skip comments and empty lines
- continue
- value_type = line # first non-comment line is type
- break
- return value_type
-
-def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Path):
- # prepare script for offline debugging from console
+# - we need dump argv and sys.path as is on startup from inkscape
+# - later sys.path may be modified that influences importing inkex (see prefere_pip_inkex)
+
+
+
+def write_offline_debug_script(debug_script_dir : Path, ini : configparser.ConfigParser):
+ '''
+ prepare Bash script for offline debugging from console
+ arguments:
+ - debug_script_dir - Path object, absolute path to directory of inkstitch.py
+ - ini - see DEBUG.ini
+ '''
+
+ # define names of files used by offline Bash script
+ bash_file_base = ini.get("DEBUG","bash_file_base", fallback="debug_inkstitch")
+ bash_name = Path(bash_file_base).with_suffix(".sh") # Path object
+ bash_svg = Path(bash_file_base).with_suffix(".svg") # Path object
# check if input svg file exists in arguments, take argument that not start with '-' as file name
svgs = [arg for arg in sys.argv[1:] if not arg.startswith('-')]
@@ -65,8 +38,8 @@ def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Pa
print(f"WARN: input svg file is same as output svg file. No script created in write debug script.", file=sys.stderr)
return
- import shutil
- bash_file = SCRIPTDIR / bash_name
+ import shutil # to copy svg file
+ bash_file = debug_script_dir / bash_name
with open(bash_file, 'w') as f: # "w" text mode, automatic conversion of \n to os.linesep
f.write(f"#!/usr/bin/env bash\n\n")
@@ -86,9 +59,7 @@ def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Pa
f.write(f"# {p}\n")
f.write(f"# copy {svg_file} to {bash_svg}\n")
- # check if files are not the same
- if svg_file != bash_svg:
- shutil.copy(svg_file, SCRIPTDIR / bash_svg) # copy file to bash_svg
+ shutil.copy(svg_file, debug_script_dir / bash_svg) # copy file to bash_svg
myargs = myargs.replace(str(svg_file), str(bash_svg)) # replace file name with bash_svg
# see void Extension::set_environment() in inkscape/src/extension/extension.cpp
@@ -107,4 +78,106 @@ def write_offline_debug_script(SCRIPTDIR : Path, bash_name : Path, bash_svg : Pa
f.write('# call inkstitch\n')
f.write(f"python3 inkstitch.py {myargs}\n")
- bash_file.chmod(0o0755) # make file executable
+ bash_file.chmod(0o0755) # make file executable, hopefully ignored on Windows
+
+
+def reorder_sys_path():
+ '''
+ change sys.path to prefer pip installed inkex over inkscape bundled inkex
+ '''
+
+ # see static void set_extensions_env() in inkscape/src/inkscape-main.cpp
+ # what we do:
+ # - move inkscape extensions path to the end of sys.path
+ # - we compare PYTHONPATH with sys.path and move PYTHONPATH to the end of sys.path
+ # - also user inkscape extensions path is moved to the end of sys.path - may cause problems?
+ # - path for deprecated-simple are removed from sys.path, will be added later by importing inkex
+
+ # PYTHONPATH to list
+ pythonpath = os.environ.get('PYTHONPATH', '').split(os.pathsep)
+ # remove pythonpath from sys.path
+ sys.path = [p for p in sys.path if p not in pythonpath]
+ # remove deprecated-simple, it will be added later by importing inkex
+ pythonpath = [p for p in pythonpath if not p.endswith('deprecated-simple')]
+ # remove nonexisting paths
+ pythonpath = [p for p in pythonpath if os.path.exists(p)]
+ # add pythonpath to the end of sys.path
+ sys.path.extend(pythonpath)
+
+# -----------------------------------------------------------------------------
+# Profilers:
+# currently supported profilers:
+# - cProfile - standard python profiler
+# - profile - standard python profiler
+# - pyinstrument - profiler with nice html output
+
+
+def profile(profile_type, profile_dir : Path, ini : configparser.ConfigParser, extension, remaining_args):
+ '''
+ profile with cProfile, profile or pyinstrument
+ '''
+ profile_file_base = ini.get("PROFILE","profile_file_base", fallback="debug_profile")
+ profile_file_path = profile_dir / profile_file_base # Path object
+
+ if profile_type == 'cprofile':
+ with_cprofile(extension, remaining_args, profile_file_path)
+ elif profile_type == 'profile':
+ with_profile(extension, remaining_args, profile_file_path)
+ elif profile_type == 'pyinstrument':
+ with_pyinstrument(extension, remaining_args, profile_file_path)
+ else:
+ raise ValueError(f"unknown profiler type: '{profile_type}'")
+
+def with_cprofile(extension, remaining_args, profile_file_path):
+ '''
+ profile with cProfile
+ '''
+ import cProfile
+ import pstats
+ profiler = cProfile.Profile()
+
+ profiler.enable()
+ extension.run(args=remaining_args)
+ profiler.disable()
+
+ profiler.dump_stats(profile_file_path.with_suffix(".prof")) # can be read by 'snakeviz -s' or 'pyprof2calltree'
+ with open(profile_file_path, 'w') as stats_file:
+ stats = pstats.Stats(profiler, stream=stats_file)
+ stats.sort_stats(pstats.SortKey.CUMULATIVE)
+ stats.print_stats()
+ print(f"Profiler: cprofile, stats written to '{profile_file_path.name}' and '{profile_file_path.name}.prof'. Use snakeviz to see it.",
+ file=sys.stderr)
+
+def with_profile(extension, remaining_args, profile_file_path):
+ '''
+ profile with profile
+ '''
+ import profile
+ import pstats
+ profiler = profile.Profile()
+
+ profiler.run('extension.run(args=remaining_args)')
+
+ profiler.dump_stats(profile_file_path.with_suffix(".prof")) # can be read by 'snakeviz' or 'pyprof2calltree' - seems broken
+ with open(profile_file_path, 'w') as stats_file:
+ stats = pstats.Stats(profiler, stream=stats_file)
+ stats.sort_stats(pstats.SortKey.CUMULATIVE)
+ stats.print_stats()
+ print(f"'Profiler: profile, stats written to '{profile_file_path.name}' and '{profile_file_path.name}.prof'. Use of snakeviz is broken.",
+ file=sys.stderr)
+
+def with_pyinstrument(extension, remaining_args, profile_file_path):
+ '''
+ profile with pyinstrument
+ '''
+ import pyinstrument
+ profiler = pyinstrument.Profiler()
+
+ profiler.start()
+ extension.run(args=remaining_args)
+ profiler.stop()
+
+ profile_file_path = profile_file_path.with_suffix(".html")
+ with open(profile_file_path, 'w') as stats_file:
+ stats_file.write(profiler.output_html())
+ print(f"Profiler: pyinstrument, stats written to '{profile_file_path.name}'. Use browser to see it.", file=sys.stderr)