summaryrefslogtreecommitdiff
path: root/lib/debug_utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/debug_utils.py')
-rw-r--r--lib/debug_utils.py177
1 files changed, 125 insertions, 52 deletions
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)