inkstitch/lib/debug_utils.py

264 wiersze
9.9 KiB
Python

# 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("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('#!/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('# 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('# 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('# 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('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 r'''
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)
export INKSTITCH_DEBUG_ENABLE="True"
;;
p)
export INKSTITCH_PROFILE_ENABLE="True"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
'''
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)
# -----------------------------------------------------------------------------
# try to resolve debugger type from ini file or cmd line of bash
def resole_debug_type(ini: configparser.ConfigParser):
# 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'
return debug_type
# try to resolve profiler type from ini file or cmd line of bash
def resole_profile_type(ini: configparser.ConfigParser):
# enable/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'
return profiler_type
# -----------------------------------------------------------------------------
# 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)