# 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["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') 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 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(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)