kopia lustrzana https://github.com/inkstitch/inkstitch
				
				
				
			Merge pull request #2653 from inkstitch/kgn/debug_profile_extend_vscode
Kgn/debug profile extend vscodepull/2684/head
						commit
						bc991aaa25
					
				|  | @ -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* | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
							
								
								
									
										185
									
								
								inkstitch.py
								
								
								
								
							
							
						
						
									
										185
									
								
								inkstitch.py
								
								
								
								
							|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										179
									
								
								lib/debug.py
								
								
								
								
							
							
						
						
									
										179
									
								
								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] | ||||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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) | ||||
		Ładowanie…
	
		Reference in New Issue
	
	 karnigen
						karnigen