karnigen 2024-04-22 08:13:04 -04:00 zatwierdzone przez GitHub
commit 7fcc69bbca
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
15 zmienionych plików z 981 dodań i 549 usunięć

10
.gitignore vendored
Wyświetl plik

@ -6,6 +6,7 @@ __pycache__
*.zip
*.tar.gz
*.po
*.log
dist/
build/
locales/
@ -22,10 +23,15 @@ flaskserverport.json
electron/yarn.lock
# debug and profile files
/DEBUG.ini
logs/
/DEBUG.toml
/LOGGING.toml
/LOGGING[0-9]*.toml
/debug*
/.debug*
# old debug files
# old debug files - to be removed
/DEBUG*.ini
/DEBUG
/PROFILE
/profile*

Wyświetl plik

@ -1,46 +0,0 @@
[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

Wyświetl plik

@ -0,0 +1,51 @@
### customize this file and save as DEBUG.toml
[LIBRARY]
### use the pip installed version of inkex.py, default: true
# prefer_pip_inkex = false
[LOGGING]
### logging configuration file, default: none - use implicit DEBUG logging to inkstitch.log
### we may have multiple configurations: LOGGING.toml, LOGGING1.toml, LOGGING2.toml, etc.
# log_config_file = "LOGGING.toml"
### disable globally logging: default: false - using logging.disable()
### reenable logging.disable(0)
# disable_logging = true
[DEBUG]
### simulate frozen mode, overwrite running_as_frozen in inkstitch.py, default: false
# force_frozen = true
### 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
### disable debugger when calling from inkscape, default: false
# disable_from_inkscape = true
### wait for debugger to attach (vscode), default: true
# wait_attach = false
### 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 = "logs/debug_profile"

Wyświetl plik

@ -0,0 +1,69 @@
### customize this file and save as LOGGING.toml
### logging/warning template for inkstitch
### format: toml
### enable config file in DEBUG.toml: log_config_file
### warnings.simplefilter(action), default: "default"
### - possible values: "error", "ignore", "always", "default", "module", "once"
warnings_action = "default"
### logging.captureWarnings() default: true
### - possible values: true, false
warnings_capture = true
### mandatory, must be an integer and 1
version = 1
### disable loggers activated before the configuration is loaded
disable_existing_loggers = false
### define the loggers, handlers, formatters, filters
[filters]
[formatters.simple]
format = "%(asctime)s [%(levelname)s]: %(filename)s.%(funcName)s: %(message)s"
[formatters.debug]
format = "%(asctime)s %(message)s"
[handlers.file_inkstitch]
class = "logging.FileHandler"
formatter = "simple"
filename = "%(SCRIPTDIR)s/logs/inkstitch.log"
mode = "w"
[handlers.file_inkstitch_debug]
class = "logging.FileHandler"
formatter = "debug"
filename = "%(SCRIPTDIR)s/logs/inkstitch_debug.log"
mode = "w"
[handlers.file_root]
class = "logging.FileHandler"
formatter = "simple"
filename = "%(SCRIPTDIR)s/logs/inkstitch_root.log"
mode = "w"
### used for: logger = logging.getLogger("inkstitch")
### logger = logging.getLogger("inkstitch.xxx") where xxx is not specified in this config file
### - highest level logger for all 'inkstitch.*' loggers
[loggers.inkstitch]
level = "DEBUG"
handlers = [ "file_inkstitch",]
propagate = false
### used for: logger = logging.getLogger("inkstitch.debug")
### - use quotes for the logger name with dots, otherwise it will be treated as a table subsection
### - [loggers.inkstitch.debug] is not the same as [loggers.'inkstitch.debug']
[loggers.'inkstitch.debug']
level = "DEBUG" # to enable the logger, seems to be the default
# level = "CRITICAL" # to disable the logger
handlers = [ "file_inkstitch_debug",]
propagate = false
### root - loggers not specified in this config file will be managed by this logger
[loggers.root]
level = "DEBUG"
handlers = [ "file_root",]

Wyświetl plik

@ -6,15 +6,46 @@
import os
import sys
from pathlib import Path # to work with paths as objects
import configparser # to read DEBUG.ini
from argparse import ArgumentParser # to parse arguments and remove --extension
import lib.debug_utils as debug_utils
if sys.version_info >= (3, 11):
import tomllib # built-in in Python 3.11+
else:
import tomli as tomllib
import logging
import lib.debug.utils as debug_utils
import lib.debug.logging as debug_logging
from lib.debug.utils import safe_get # mimic get method of dict with default value
# --------------------------------------------------------------------------------------------
SCRIPTDIR = Path(__file__).parent.absolute()
logger = logging.getLogger("inkstitch") # create module logger with name 'inkstitch'
# TODO --- temporary --- catch old DEBUG.ini file and inform user to reformat it to DEBUG.toml
old_debug_ini = SCRIPTDIR / "DEBUG.ini"
if old_debug_ini.exists():
print("ERROR: old DEBUG.ini exists, please reformat it to DEBUG.toml and remove DEBUG.ini file", file=sys.stderr)
exit(1)
# --- end of temporary ---
debug_toml = SCRIPTDIR / "DEBUG.toml"
if debug_toml.exists():
with debug_toml.open("rb") as f:
ini = tomllib.load(f) # read DEBUG.toml file if exists, otherwise use default values in ini object
else:
ini = {}
# --------------------------------------------------------------------------------------------
running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle
if not running_as_frozen: # override running_as_frozen from DEBUG.toml - for testing
if safe_get(ini, "DEBUG", "force_frozen", default=False):
running_as_frozen = True
if len(sys.argv) < 2:
# no arguments - prevent accidentally running this script
msg = "No arguments given, exiting!" # without gettext localization see _()
@ -26,13 +57,14 @@ if len(sys.argv) < 2:
dlg.ShowModal()
dlg.Destroy()
except ImportError:
print(msg)
print(msg, file=sys.stderr)
else:
print(msg)
print(msg, file=sys.stderr)
exit(1)
ini = configparser.ConfigParser()
ini.read(SCRIPTDIR / "DEBUG.ini") # read DEBUG.ini file if exists
# activate logging - must be done before any logging is done
debug_logging.activate_logging(running_as_frozen, ini, SCRIPTDIR)
# --------------------------------------------------------------------------------------------
# check if running from inkscape, given by environment variable
if os.environ.get('INKSTITCH_OFFLINE_SCRIPT', '').lower() in ['true', '1', 'yes', 'y']:
@ -40,6 +72,7 @@ if os.environ.get('INKSTITCH_OFFLINE_SCRIPT', '').lower() in ['true', '1', 'yes'
else:
running_from_inkscape = True
# initialize debug and profiler type
debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace()) # check if debugger is active on startup
debug_type = 'none'
profiler_type = 'none'
@ -48,54 +81,42 @@ if not running_as_frozen: # debugging/profiling only in development mode
# specify debugger type
# but if script was already started from debugger then don't read debug type from ini file or cmd line
if not debug_active:
debug_type = debug_utils.resole_debug_type(ini) # read debug type from ini file or cmd line
debug_type = debug_utils.resolve_debug_type(ini) # read debug type from ini file or cmd line
profile_type = debug_utils.resole_profile_type(ini) # read profile type from ini file or cmd line
profiler_type = debug_utils.resolve_profiler_type(ini) # read profile type from ini file or cmd line
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
if safe_get(ini, "DEBUG", "create_bash_script", default=False): # create script only if enabled in DEBUG.toml
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)
disable_from_inkscape = safe_get(ini, "DEBUG", "disable_from_inkscape", default=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)
prefere_pip_inkex = safe_get(ini, "LIBRARY", "prefer_pip_inkex", default=True)
if prefere_pip_inkex and 'PYTHONPATH' in os.environ:
debug_utils.reorder_sys_path()
# enabling of debug depends on value of debug_type in DEBUG.ini file
# enabling of debug depends on value of debug_type in DEBUG.toml file
if debug_type != 'none':
from lib.debug import debug # import global variable debug - don't import whole module
debug.enable(debug_type, SCRIPTDIR, ini)
from lib.debug.debugger import init_debugger
init_debugger(debug_type, ini)
# check if debugger is really activated
debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace())
# 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')
# activate logging for svg
# we need to import only after possible modification of sys.path, we disable here flake8 E402
from lib.debug import debug # noqa: E402 # import global variable debug - don't import whole module
debug.enable() # perhaps it would be better to find a more relevant name; in fact, it's about logging and svg creation.
# 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 ----
# import logging # to set logger for shapely
# from io import StringIO # to store shapely errors
# 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 ----
# log startup info
debug_logging.startup_info(logger, SCRIPTDIR, running_as_frozen, running_from_inkscape, debug_active, debug_type, profiler_type)
# --------------------------------------------------------------------------------------------
# pop '--extension' from arguments and generate extension class name from extension name
# example: --extension=params will instantiate Params() class from lib.extensions.
@ -151,7 +172,4 @@ else: # if not debug nor profile mode
finally:
restore_stderr()
# if shapely_errors.tell(): # see above plan to remove this in future for shapely
# errormsg(shapely_errors.getvalue())
sys.exit(0)

Wyświetl plik

@ -4,7 +4,6 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import errno
import logging
import socket
import sys
import time
@ -66,9 +65,6 @@ class APIServer(Thread):
self.flask_server.shutdown()
self.server_thread.join()
def disable_logging(self):
logging.getLogger('werkzeug').setLevel(logging.ERROR)
# https://github.com/aluo-x/Learning_Neural_Acoustic_Fields/blob/master/train.py
# https://github.com/pytorch/pytorch/issues/71029
def find_free_port(self):
@ -77,8 +73,6 @@ class APIServer(Thread):
return s.getsockname()[1]
def run(self):
self.disable_logging()
self.host = "127.0.0.1"
self.port = self.find_free_port()
self.flask_server = make_server(self.host, self.port, self.app)

Wyświetl plik

@ -1,421 +0,0 @@
# 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
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 # 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:
return func(self, *args, **kwargs)
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]
unwrapped_kwargs = {name: _unwrap(value) for name, value in kwargs.items()}
return func(self, *unwrapped_args, **unwrapped_kwargs)
return decorated
class Debug(object):
"""Tools to help debug Ink/Stitch
This class contains methods to log strings and SVG elements. Strings are
logged to debug.log, and SVG elements are stored in debug.svg to aid in
debugging stitch algorithms.
All functionality is gated by self.enabled. If debugging is not enabled,
then debug calls will consume very few resources. Any method argument
can be a callable, in which case it is called and the return value is
logged instead. This way one can log potentially expensive expressions
by wrapping them in a lambda:
debug.log(lambda: some_expensive_function(some_argument))
The lambda is only called if debugging is enabled.
"""
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, 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 = self.debug_dir / self.debug_log_file
# delete old content
with self.log_file.open("w"):
pass
self.log("Debug logging enabled.")
# we intentionally disable flakes C901 - function is too complex, beacuse it is used only for debugging
# currently complexity is set 10 see 'make style', this means that function can have max 10 nested blocks, here we have more
# flake8: noqa: C901
def init_debugger(self):
# ### 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. 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:
# You must use the PyCharm Professional Edition and _not_ the Community
# Edition. Jetbrains has chosen to make remote debugging a Pro feature.
# To debug Inkscape python extensions, the extension code and Inkscape run
# independently of PyCharm, and only communicate with the debugger via a
# TCP socket. Thus, debugging is "remote," even if it's on the same machine,
# connected via the loopback interface.
#
# 1. pip install pydev_pycharm
#
# pydev_pycharm is versioned frequently. Jetbrains suggests installing
# a version at least compatible with the current build. For example, if your
# PyCharm build, as found in menu PyCharm -> About Pycharm is 223.8617.48,
# you could do:
# pip install pydevd-pycharm~=223.8617.48
#
# 2. From the Pycharm "Run" menu, choose "Edit Configurations..." and create a new
# 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.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:
# cd ~/Library/Application\ Support/org.inkscape.Inkscape/config/inkscape/extensions/
# ln -s <full path to the top level of your Ink/Stitch git repo>
# On other architectures it may be:
# cd ~/.config/inkscape/extensions
# ln -s <full path to the top level of your Ink/Stitch git repo>
# Remove any other Ink/Stitch files or references to Ink/Stitch from the
# extensions directory, or you'll see duplicate entries in the Ink/Stitch
# extensions menu in Inkscape.
#
# 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..."
#
# 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 self.debugger == 'vscode':
import debugpy
elif self.debugger == 'pycharm':
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(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:
stderr = sys.stderr
sys.stderr = devnull
try:
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)
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(f"Be sure to run 'Start debugging server' in {self.debugger} to enable debugging.")
else:
self.log(f"Enabled '{self.debugger}' debugger.")
sys.stderr = stderr
def init_svg(self):
self.svg = etree.Element("svg", nsmap=inkex.NSS)
atexit.register(self.save_svg)
def save_svg(self):
tree = etree.ElementTree(self.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
def add_layer(self, name="Debug"):
layer = etree.Element("g", {
INKSCAPE_GROUPMODE: "layer",
INKSCAPE_LABEL: name,
"style": "display: none"
})
self.svg.append(layer)
self.current_layer = layer
@check_enabled
@unwrap_arguments
def open_group(self, name="Group"):
group = etree.Element("g", {
INKSCAPE_LABEL: name
})
self.log_svg_element(group)
self.group_stack.append(group)
@check_enabled
@unwrap_arguments
def close_group(self):
if self.group_stack:
self.group_stack.pop()
@check_enabled
@unwrap_arguments
def log(self, message, *args):
if self.last_log_time:
message = "(+%s) %s" % (datetime.now() - self.last_log_time, message)
self.raw_log(message, *args)
def raw_log(self, message, *args):
now = datetime.now()
timestamp = now.isoformat()
self.last_log_time = now
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.monotonic()
result = func(*args, **kwargs)
if self.enabled:
end = time.monotonic()
self.raw_log("leaving %s(), duration = %s", func.__name__, round(end - start, 6))
return result
return decorated
@check_enabled
@unwrap_arguments
def log_svg_element(self, element):
if self.current_layer is None:
self.add_layer()
if self.group_stack:
self.group_stack[-1].append(element)
else:
self.current_layer.append(element)
@check_enabled
@unwrap_arguments
def log_line_string(self, line_string, name=None, color=None):
"""Add a Shapely LineString to the SVG log."""
self.log_line_strings([line_string], name, color)
@check_enabled
@unwrap_arguments
def log_line_strings(self, line_strings, name=None, color=None):
path = line_strings_to_path(line_strings)
path.set('style', str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3", "fill": None})))
if name is not None:
path.set(INKSCAPE_LABEL, name)
self.log_svg_element(path)
@check_enabled
@unwrap_arguments
def log_line(self, start, end, name="line", color=None):
self.log_svg_element(etree.Element("path", {
"d": "M%s,%s %s,%s" % (start + end),
"style": str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3", "fill": None})),
INKSCAPE_LABEL: name
}))
@check_enabled
@unwrap_arguments
def log_point(self, point, name="point", color=None):
self.log_svg_element(etree.Element("circle", {
"cx": str(point.x),
"cy": str(point.y),
"r": "1",
"style": str(inkex.Style({"fill": "#000000"})),
}))
@check_enabled
@unwrap_arguments
def log_graph(self, graph, name="Graph", color=None):
d = ""
for edge in graph.edges:
d += "M%s,%s %s,%s" % (edge[0] + edge[1])
self.log_svg_element(etree.Element("path", {
"d": d,
"style": str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3", "fill": None})),
INKSCAPE_LABEL: name
}))
# decorator to measure time of with block
@contextmanager
def time_this(self, label="code block"):
if self.enabled:
start = time.monotonic()
self.raw_log("begin %s", label)
yield
if self.enabled:
self.raw_log("completed %s, duration = %s", label, time.monotonic() - start)
# global debug object
debug = Debug()

Wyświetl plik

@ -0,0 +1,11 @@
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
# this enable:
# from .debug import debug
# from ..debug import debug
from .debug import debug

251
lib/debug/debug.py 100644
Wyświetl plik

@ -0,0 +1,251 @@
# 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 atexit # to save svg file on exit
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
from pathlib import Path # to work with paths as objects
import inkex
from lxml import etree # to create svg file
from ..svg import line_strings_to_path
from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL
import logging
logger = logging.getLogger("inkstitch.debug") # create module logger with name 'inkstitch.debug'
# to log messages if previous debug logger is not enabled
logger_inkstich = logging.getLogger("inkstitch") # create module logger with name 'inkstitch'
# --------------------------------------------------------------------------------------------
# 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:
return func(self, *args, **kwargs)
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]
unwrapped_kwargs = {name: _unwrap(value) for name, value in kwargs.items()}
return func(self, *unwrapped_args, **unwrapped_kwargs)
return decorated
class Debug(object):
"""Tools to help debug Ink/Stitch
This class contains methods to log strings and SVG elements. Strings are
logged to debug.log, and SVG elements are stored in debug.svg to aid in
debugging stitch algorithms.
All functionality is gated by self.enabled. If debugging is not enabled,
then debug calls will consume very few resources. Any method argument
can be a callable, in which case it is called and the return value is
logged instead. This way one can log potentially expensive expressions
by wrapping them in a lambda:
debug.log(lambda: some_expensive_function(some_argument))
The lambda is only called if debugging is enabled.
"""
def __init__(self):
self.enabled = False
self.last_log_time = None
self.current_layer = None
self.group_stack = []
self.svg_filename = None
def enable(self):
# determine svg filename from logger
if len(logger.handlers) > 0 and isinstance(logger.handlers[0], logging.FileHandler):
# determine filename of svg file from logger
filename = Path(logger.handlers[0].baseFilename)
self.svg_filename = filename.with_suffix(".svg")
self.svg_filename.unlink(missing_ok=True) # remove existing svg file
# self.log is activated by active logger
# - enabled only if logger first handler is FileHandler
# to disable "inkstitch.debug" simply set logging level to CRITICAL
if logger.isEnabledFor(logging.INFO) and self.svg_filename is not None:
self.enabled = True
self.log(f"Logging enabled with svg file: {self.svg_filename}")
self.init_svg()
else:
# use alternative logger to log message if logger has no handlers
logger_inkstich.info("No handlers in logger, cannot enable logging and svg file creation")
def init_svg(self):
self.svg = etree.Element("svg", nsmap=inkex.NSS)
atexit.register(self.save_svg)
def save_svg(self):
if self.enabled and self.svg_filename is not None:
self.log(f"Writing svg file: {self.svg_filename}")
tree = etree.ElementTree(self.svg)
tree.write(str(self.svg_filename)) # lxml <5.0.0 does not support Path objects, requires string
else:
# use alternative logger to log message if logger has no handlers
logger_inkstich.info(f"Saving to svg file is not activated {self.svg_filename=}")
@check_enabled
@unwrap_arguments
def add_layer(self, name="Debug"):
layer = etree.Element("g", {
INKSCAPE_GROUPMODE: "layer",
INKSCAPE_LABEL: name,
"style": "display: none"
})
self.svg.append(layer)
self.current_layer = layer
@check_enabled
@unwrap_arguments
def open_group(self, name="Group"):
group = etree.Element("g", {
INKSCAPE_LABEL: name
})
self.log_svg_element(group)
self.group_stack.append(group)
@check_enabled
@unwrap_arguments
def close_group(self):
if self.group_stack:
self.group_stack.pop()
@check_enabled
@unwrap_arguments
def log(self, message, *args):
if self.last_log_time:
message = "(+%s) %s" % (datetime.now() - self.last_log_time, message)
self.raw_log(message, *args)
def raw_log(self, message, *args):
now = datetime.now()
self.last_log_time = now
msg = message % args
logger.info(msg)
# 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.monotonic()
result = func(*args, **kwargs)
if self.enabled:
end = time.monotonic()
self.raw_log("leaving %s(), duration = %s", func.__name__, round(end - start, 6))
return result
return decorated
@check_enabled
@unwrap_arguments
def log_svg_element(self, element):
if self.current_layer is None:
self.add_layer()
if self.group_stack:
self.group_stack[-1].append(element)
else:
self.current_layer.append(element)
@check_enabled
@unwrap_arguments
def log_line_string(self, line_string, name=None, color=None):
"""Add a Shapely LineString to the SVG log."""
self.log_line_strings([line_string], name, color)
@check_enabled
@unwrap_arguments
def log_line_strings(self, line_strings, name=None, color=None):
path = line_strings_to_path(line_strings)
path.set('style', str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3", "fill": None})))
if name is not None:
path.set(INKSCAPE_LABEL, name)
self.log_svg_element(path)
@check_enabled
@unwrap_arguments
def log_line(self, start, end, name="line", color=None):
self.log_svg_element(etree.Element("path", {
"d": "M%s,%s %s,%s" % (start + end),
"style": str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3", "fill": None})),
INKSCAPE_LABEL: name
}))
@check_enabled
@unwrap_arguments
def log_point(self, point, name="point", color=None):
self.log_svg_element(etree.Element("circle", {
"cx": str(point.x),
"cy": str(point.y),
"r": "1",
"style": str(inkex.Style({"fill": "#000000"})),
}))
@check_enabled
@unwrap_arguments
def log_graph(self, graph, name="Graph", color=None):
d = ""
for edge in graph.edges:
d += "M%s,%s %s,%s" % (edge[0] + edge[1])
self.log_svg_element(etree.Element("path", {
"d": d,
"style": str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3", "fill": None})),
INKSCAPE_LABEL: name
}))
# decorator to measure time of with block
@contextmanager
def time_this(self, label="code block"):
if self.enabled:
start = time.monotonic()
self.raw_log("begin %s", label)
yield
if self.enabled:
self.raw_log("completed %s, duration = %s", label, time.monotonic() - start)
# global debug object
debug = Debug()

Wyświetl plik

@ -0,0 +1,192 @@
# Authors: see git history
#
# Copyright (c) 2024 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
# ### General debugging notes:
# 1. to enable debugging or profiling copy DEBUG_template.toml to DEBUG.toml and edit it
# ### How create bash script for offline debugging from console
# 1. in DEBUG.toml set create_bash_script = true
# 2. call inkstitch.py extension from inkscape to create bash script named by bash_file_base in DEBUG.toml
# 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.toml
# 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.toml
# 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.toml
# - to change various output file names see DEBUG.toml
# - to disable waiting for debugger to attach (vscode editor) set wait_attach = false in DEBUG.toml
# - to prefer inkscape version of inkex module over pip version set prefer_pip_inkex = false in DEBUG.toml
# ###
# ### 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. Copy and edit a file named "DEBUG.toml" from "DEBUG_template.toml" 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:
# You must use the PyCharm Professional Edition and _not_ the Community
# Edition. Jetbrains has chosen to make remote debugging a Pro feature.
# To debug Inkscape python extensions, the extension code and Inkscape run
# independently of PyCharm, and only communicate with the debugger via a
# TCP socket. Thus, debugging is "remote," even if it's on the same machine,
# connected via the loopback interface.
#
# 1. pip install pydev_pycharm
#
# pydev_pycharm is versioned frequently. Jetbrains suggests installing
# a version at least compatible with the current build. For example, if your
# PyCharm build, as found in menu PyCharm -> About Pycharm is 223.8617.48,
# you could do:
# pip install pydevd-pycharm~=223.8617.48
#
# 2. From the Pycharm "Run" menu, choose "Edit Configurations..." and create a new
# 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.toml" 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:
# cd ~/Library/Application\ Support/org.inkscape.Inkscape/config/inkscape/extensions/
# ln -s <full path to the top level of your Ink/Stitch git repo>
# On other architectures it may be:
# cd ~/.config/inkscape/extensions
# ln -s <full path to the top level of your Ink/Stitch git repo>
# Remove any other Ink/Stitch files or references to Ink/Stitch from the
# extensions directory, or you'll see duplicate entries in the Ink/Stitch
# extensions menu in Inkscape.
#
# 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..."
#
# 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.toml" 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
import os
import sys
import socket # to check if debugger is running
from .utils import safe_get # mimic get method of dict with default value
import logging
logger = logging.getLogger("inkstitch")
# we intentionally disable flakes C901 - function is too complex, beacuse it is used only for debugging
# currently complexity is set 10 see 'make style', this means that function can have max 10 nested blocks, here we have more
# flake8: noqa: C901
def init_debugger(debug_type:str, ini: dict):
if debug_type == 'none':
return
wait_attach = safe_get(ini, "DEBUG", "wait_attach", default=True) # currently only for vscode
debugger = debug_type
try:
if debugger == 'vscode':
import debugpy
elif debugger == 'pycharm':
import pydevd_pycharm
elif debugger == 'pydev':
import pydevd
elif debugger == 'file':
pass
else:
raise ValueError(f"unknown debugger: '{debugger}'")
except ImportError:
logger.info(f"importing debugger failed (debugger disabled) for {debugger}")
# pydevd likes to shout about errors to stderr whether I want it to or not
with open(os.devnull, 'w') as devnull:
stderr = sys.stderr
sys.stderr = devnull
try:
if debugger == 'vscode':
debugpy.listen(('localhost', 5678))
if wait_attach:
print("Waiting for debugger attach", file=sys.stderr)
debugpy.wait_for_client() # wait for debugger to attach
debugpy.breakpoint() # stop here to start normal debugging
elif debugger == 'pycharm':
pydevd_pycharm.settrace('localhost', port=5678, stdoutToServer=True,
stderrToServer=True)
elif debugger == 'pydev':
pydevd.settrace()
elif debugger == 'file':
pass
else:
raise ValueError(f"unknown debugger: '{debugger}'")
except socket.error as error:
logger.info("Debugging: connection to pydevd failed: %s", error)
logger.info(f"Be sure to run 'Start debugging server' in {debugger} to enable debugging.")
else:
logger.info(f"Enabled '{debugger}' debugger.")
sys.stderr = stderr

Wyświetl plik

@ -0,0 +1,294 @@
# Authors: see git history
#
# Copyright (c) 2024 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
# basic info for inkstitch logging:
# ---------------------------------
# some idea can be found in: Modern Python logging - https://www.youtube.com/watch?v=9L77QExPmI0
#
# logging vs warnings
# -------------------
# warnings - see https://docs.python.org/3/library/warnings.html
# logging - see https://docs.python.org/3/library/logging.html
#
# In simplified terms, use "warning" to alert that a specific function is deprecated, and in all other cases, use "logging".
# Warnings are primarily intended for libraries where there's a newer solution available, but for backward compatibility
# reasons, the old functionality is retained.
#
# In complex applications like Inkstitch, it might be sensible to exclusively use one method, namely "logging",
# to unify and simplify the messaging system of such a system.
#
#
# root logger:
# ------------
# - The primary logger orchestrates all other loggers through logging.xxx() calls.
# - It should only be utilized at the application's highest level to manage the logging of all loggers.
# - It can easily disable all loggers by invoking logging.disable() and channel all warnings to logging
# by setting logging.captureWarnings(True) with the level WARNING.
# - The configuration of all loggers can be achieved via a file, and logging.config.dictConfig(logging_dict).
#
# module logger:
# --------------
# - Instantiate the logger by invoking logger=getLogger(name).
# Avoid using __name__ as the name, as it generates numerous loggers per application.
# The logger name persists globally throughout the application.
# - Avoid configuring the module logger within the module itself;
# instead, utilize the top-level application configuration with logging.config.
# This allows users of the application to customize it according to their requirements.
# example of module logger:
# -------------------------
# import logging
# logger = logging.getLogger('inkstitch') # create module logger with name 'inkstitch', but configure it at top level of app
# ...
# logger.debug('debug message') # example of using module logger
# ...
# top level of the application:
# ----------------------------
# - configure root and other loggers
# - best practice is to configure from a file: eg logging.config.fileConfig('logging.conf')
# - consider toml format for logging configuration (json, yaml, xml, dict are also possible)
#
# list of loggers in inkstitch (not complete):
# -------------------------------------------
# - root - main logger that controls all other loggers
# - inkstitch - suggested name for inkstitch
# - inkstitch.debug - uses in debug module with svg file saving
#
# third-party loggers:
# --------------------
# - werkzeug - is used by flask
# - shapely.geos - was used by shapely but currently replaced by exceptions and warnings
#
# --------------------------------------------------------------------------------------------
import os
import sys
from pathlib import Path
if sys.version_info >= (3, 11):
import tomllib # built-in in Python 3.11+
else:
import tomli as tomllib
import warnings # to control python warnings
import logging # to configure logging
import logging.config # to configure logging from dict
from .utils import safe_get # mimic get method of dict with default value
logger = logging.getLogger('inkstitch')
# --------------------------------------------------------------------------------------------
# activate_logging - configure logging for inkstitch application
def activate_logging(running_as_frozen: bool, ini: dict, SCRIPTDIR: Path):
if running_as_frozen: # in release mode
activate_for_frozen()
else: # in development
activate_for_development(ini, SCRIPTDIR)
# Configure logging in frozen (release) mode of application:
# in release mode normally we want to ignore all warnings and logging, but we can enable it by setting environment variables
# - INKSTITCH_LOGLEVEL - logging level:
# 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
# - PYTHONWARNINGS, -W - warnings action controlled by python
# actions: 'error', 'ignore', 'always', 'default', 'module', 'once'
def activate_for_frozen():
loglevel = os.environ.get('INKSTITCH_LOGLEVEL') # read log level from environment variable or None
docpath = os.environ.get('DOCUMENT_PATH') # read document path from environment variable (set by inkscape) or None
if docpath is not None and loglevel is not None and loglevel.upper() in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
# The end user enabled logging and warnings are redirected to the input_svg.inkstitch.log file.
vars = {
'loglevel': loglevel.upper(),
'logfilename': Path(docpath).with_suffix('.inkstitch.log') # log file is created in document path
}
config = expand_variables(frozen_config, vars)
# dictConfig has access to top level variables, dict contains: ext://__main__.var
# - restriction: variable must be last token in string - very limited functionality, avoid using it
# After this operation, logging will be activated, so we can use the logger.
logging.config.dictConfig(config) # configure root logger from dict
logging.captureWarnings(True) # capture all warnings to log file with level WARNING
else:
logging.disable() # globally disable all logging of all loggers
warnings.simplefilter('ignore') # ignore all warnings
# in development mode we want to use configuration from some LOGGING.toml file
def activate_for_development(ini: dict, SCRIPTDIR: Path):
logging_config_file = safe_get(ini, "LOGGING", "log_config_file", default=None)
vars = {'SCRIPTDIR': SCRIPTDIR} # dynamic data for logging configuration
if logging_config_file is not None:
logging_config_file = Path(logging_config_file)
if logging_config_file.exists():
with open(logging_config_file, "rb") as f:
devel_config = tomllib.load(f) # -> dict
else:
raise FileNotFoundError(f"{logging_config_file} file not found")
else: # if LOGGING.toml file does not exist, use default logging configuration
vars['loglevel'] = 'DEBUG' # set log level to DEBUG
vars['logfilename'] = SCRIPTDIR / "inkstitch.log" # log file is created in current directory
devel_config = development_config # get TOML configuration from module
configure_logging(devel_config, ini, vars) # initialize and activate logging configuration
logger.info("Running in development mode")
logger.info(f"Using logging configuration from file: {logging_config_file}")
logger.debug(f"Logging configuration: {devel_config = }")
# --------------------------------------------------------------------------------------------
# configure logging from dictionary:
# - capture all warnings to log file with level WARNING - depends on warnings_capture
# - set action for warnings: 'error', 'ignore', 'always', 'default', 'module', 'once' - depends on warnings_action
def configure_logging(config: dict, ini: dict, vars: dict):
config = expand_variables(config, vars)
# After this operation, logging will be activated, so we can use the logger.
logging.config.dictConfig(config) # configure loggers from dict - using loglevel, logfilename
warnings_capture = config.get('warnings_capture', True)
logging.captureWarnings(warnings_capture) # capture warnings to log file with level WARNING
warnings_action = config.get('warnings_action', 'default').lower()
warnings.simplefilter(warnings_action) # set action for warnings: 'error', 'ignore', 'always', ...
disable_logging = safe_get(ini, "LOGGING", "disable_logging", default=False)
if disable_logging:
logger.warning(f"Logging is disabled by configuration in ini file. {disable_logging = }")
logging.disable() # globally disable all logging of all loggers
# Evaluate evaluation of variables in logging configuration:
# "handlers": {
# "file": {
# "filename": "%(SCRIPTDIR)s/xxx.log", # <--- replace %(SCRIPTDIR)s -> script path
# "%(logfilename)s", # <--- replace %(logfilename)s -> log file name
# ...
# "loggers": {
# "inkstitch": {
# "level": "%(loglevel)s", # <--- replace %(loglevel)s -> log level
# ...
# - for external configuration file (eg. LOGGING.toml) we cannot pass parameters such as the current directory.
# - we do: filename = "%(SCRIPTDIR)s/inkstitch.log" -> filename = "path/inkstitch.log"
# - safety: we can use only predefined variables in myvars, otherwise we get KeyError
# - return modified configuration
# - create logging directory if not exists, directory cannot end with ":" to avoid error with ext:// keys
def expand_variables(cfg: dict, vars: dict):
for k, v in cfg.get('loggers', {}).items():
if 'level' in v: # replace level in logger
cfg['loggers'][k]['level'] = v['level'] % vars
for k, v in cfg.get('handlers', {}).items():
if 'filename' in v: # replace filename in handler
orig_filename = v['filename'] # original filename for later comparison
cfg['handlers'][k]['filename'] = v['filename'] % vars
# create logging directory only if substitution was done, we need to avoid ext:// cfg:// keys
if orig_filename != cfg['handlers'][k]['filename']:
dirname = Path(cfg['handlers'][k]['filename']).parent
if not dirname.exists():
# inform user about creating logging directory, otherwise it is silent, logging is not yet active
print(f"DEBUG: Creating logging directory: {dirname} ", file=sys.stderr)
dirname.mkdir(parents=True, exist_ok=True)
return cfg
def startup_info(logger: logging.Logger, SCRIPTDIR: Path, running_as_frozen: bool, running_from_inkscape: bool,
debug_active: bool, debug_type: str, profiler_type: str):
logger.info(f"Running as frozen: {running_as_frozen}")
logger.info(f"Running from inkscape: {running_from_inkscape}")
logger.info(f"Debugger active: {debug_active}")
logger.info(f"Debugger type: {debug_type!r}")
logger.info(f"Profiler type: {profiler_type!r}")
# log Python version, platform, command line arguments, sys.path
import sys
import platform
logger.info(f"Python version: {sys.version}")
logger.info(f"Platform: {platform.platform()}")
logger.info(f"Command line arguments: {sys.argv}")
logger.debug(f"sys.path: {sys.path}")
# example of logger configuration for release mode:
# ------------------------------------------------
# - logger suitable for release mode, where we assume that the directory of the input SVG file allows writing the log file.
# - in inkstitch.py we check release mode and environment variable INKSTITCH_LOGLEVEL
# - this config redirect all loggers to file svg_file.inkstitch.log to directory of svg file
# - set loglevel and logfilename in inkstitch.py before calling logging.config.dictConfig(frozen_config)
frozen_config = {
"version": 1, # mandatory key and value (int) is 1
"disable_existing_loggers": False, # false enable all loggers not defined here, true disable
"filters": {}, # no filters
"formatters": {
"simple": { # formatter name (https://docs.python.org/3/library/logging.html#logging.LogRecord)
"format": '%(asctime)s [%(levelname)s]: %(filename)s.%(funcName)s: %(message)s' # format string
}
},
"handlers": {
"file": {
"class": "logging.FileHandler", # type - file output
"formatter": "simple", # use formatter 'simple' for handler 'file'
"filename": "%(logfilename)s", # access variable logfilename
"mode": "w" # create new file
}
},
"loggers": {
"root": { # top level logger
"level": "%(loglevel)s", # access variable loglevel
"handlers": ["file"], # use handler 'file' for logger
}
},
}
# ---------------------------------------------------
# example of implicit developer logger configuration:
# ---------------------------------------------------
# - configured two loggers: root and inkstitch loggers
# - output is redirected to file 'logfilename' in the directory of inkstitch.py
# - this configuration uses only one log level 'loglevel for both the root and inkstitch loggers.
# - set loglevel and logfilename in inkstitch.py before calling logging.config.dictConfig(development_config)
development_config = {
"warnings_action": "default", # dafault action for warnings
"warnings_capture": True, # capture warnings to log file with level WARNING
"version": 1, # mandatory key and value (int) is 1
"disable_existing_loggers": False, # false enable all loggers not defined here, true disable
"filters": {}, # no filters
"formatters": {
"simple": { # formatter name (https://docs.python.org/3/library/logging.html#logging.LogRecord)
"format": '%(asctime)s [%(levelname)s]: %(filename)s.%(funcName)s: %(message)s' # format string
}
},
"handlers": {
"file": {
"class": "logging.FileHandler", # type - file output
"formatter": "simple", # use formatter 'simple' for handler 'file'
"filename": "%(logfilename)s", # ext: --> access variable logfilename
"mode": "w" # create new file
},
},
"loggers": { # configure loggers
"inkstitch": { # specific logger to inkstitch application
"level": "%(loglevel)s", # ext: --> access variable loglevel
"handlers": ["file"], # use handler 'file' for logger
"propagate": False, # don't propagate to root logger - otherwise all will be logged twice
},
"root": { # top level logger
"level": "%(loglevel)s", # ext: --> access variable loglevel
"handlers": ["file"], # use handler 'file' for logger
}
},
}

Wyświetl plik

@ -1,28 +1,42 @@
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Copyright (c) 2024 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)
import os
import sys
from pathlib import Path # to work with paths as objects
import logging
def write_offline_debug_script(debug_script_dir: Path, ini: configparser.ConfigParser):
logger = logging.getLogger("inkstitch")
# safe_get - get value from nested dictionary, return default if key does not exist
# - to read nested values from dict - mimic get method of dict with default value
# example: safe_get({'a': {'b': 1}}, 'a', 'b') -> 1
# safe_get({'a': {'b': 1}}, 'a', 'c', default=2) -> 2
def safe_get(dictionary: dict, *keys, default=None):
for key in keys:
if key not in dictionary:
return default
dictionary = dictionary[key]
return dictionary
def write_offline_debug_script(debug_script_dir: Path, ini: dict):
'''
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
- ini - see DEBUG.toml
'''
# define names of files used by offline Bash script
bash_file_base = ini.get("DEBUG", "bash_file_base", fallback="debug_inkstitch")
bash_file_base = safe_get(ini, "DEBUG", "bash_file_base", default="debug_inkstitch")
bash_name = Path(bash_file_base).with_suffix(".sh") # Path object
bash_svg = Path(bash_file_base).with_suffix(".svg") # Path object
@ -125,7 +139,6 @@ 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
@ -147,34 +160,30 @@ def reorder_sys_path():
# -----------------------------------------------------------------------------
# try to resolve debugger type from ini file or cmd line of bash
def resole_debug_type(ini: configparser.ConfigParser):
def resolve_debug_type(ini: dict):
# 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_enable = safe_get(ini, "DEBUG", "debug_enable", default=False) # enable debugger on startup from ini
debug_type = ini.get("DEBUG", "debug_type", fallback="none") # debugger type vscode, pycharm, pydevd
debug_type = safe_get(ini, "DEBUG", "debug_type", default="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):
def resolve_profiler_type(ini: dict):
# 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
profile_enable = safe_get(ini, "PROFILE", "profile_enable", default=False) # read from ini
# specify profiler type
profiler_type = ini.get("PROFILE", "profiler_type", fallback="none") # profiler type cprofile, profile, pyinstrument
profiler_type = safe_get(ini, "PROFILE", "profiler_type", default="none") # profiler type cprofile, profile, pyinstrument
if not profile_enable:
profiler_type = 'none'
@ -189,13 +198,19 @@ def resole_profile_type(ini: configparser.ConfigParser):
# - pyinstrument - profiler with nice html output
def profile(profiler_type, profile_dir: Path, ini: configparser.ConfigParser, extension, remaining_args):
def profile(profiler_type, profile_dir: Path, ini: dict, extension, remaining_args):
'''
profile with cProfile, profile or pyinstrument
'''
profile_file_base = ini.get("PROFILE", "profile_file_base", fallback="debug_profile")
profile_file_base = safe_get(ini, "PROFILE", "profile_file_base", default="debug_profile")
profile_file_path = profile_dir / profile_file_base # Path object
# create directory if not exists
dirname = profile_file_path.parent
if not dirname.exists():
logger.debug(f"Creating directory for profile output: {dirname}")
dirname.mkdir(parents=True, exist_ok=True)
if profiler_type == 'cprofile':
with_cprofile(extension, remaining_args, profile_file_path)
elif profiler_type == 'profile':
@ -206,7 +221,7 @@ def profile(profiler_type, profile_dir: Path, ini: configparser.ConfigParser, ex
raise ValueError(f"unknown profiler type: '{profiler_type}'")
def with_cprofile(extension, remaining_args, profile_file_path):
def with_cprofile(extension, remaining_args, profile_file_path: Path):
'''
profile with cProfile
'''
@ -227,7 +242,7 @@ def with_cprofile(extension, remaining_args, profile_file_path):
file=sys.stderr)
def with_profile(extension, remaining_args, profile_file_path):
def with_profile(extension, remaining_args, profile_file_path: Path):
'''
profile with profile
'''
@ -246,7 +261,7 @@ def with_profile(extension, remaining_args, profile_file_path):
file=sys.stderr)
def with_pyinstrument(extension, remaining_args, profile_file_path):
def with_pyinstrument(extension, remaining_args, profile_file_path: Path):
'''
profile with pyinstrument
'''

Wyświetl plik

@ -3,7 +3,6 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import logging
from copy import copy
import inkex
@ -92,12 +91,7 @@ class BreakApart(InkstitchExtension):
return polygons
def geom_is_valid(self, geom):
# Don't complain about invalid shapes, we just want to know
logger = logging.getLogger('shapely.geos')
level = logger.level
logger.setLevel(logging.CRITICAL)
valid = geom.is_valid
logger.setLevel(level)
return valid
def ensure_minimum_size(self, polygons, size):

Wyświetl plik

@ -4,7 +4,6 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import json
import logging
import os
import socket
import sys
@ -159,9 +158,6 @@ class PrintPreviewServer(Thread):
self.flask_server.shutdown()
self.server_thread.join()
def disable_logging(self):
logging.getLogger('werkzeug').setLevel(logging.ERROR)
# https://github.com/aluo-x/Learning_Neural_Acoustic_Fields/blob/master/train.py
# https://github.com/pytorch/pytorch/issues/71029
def find_free_port(self):
@ -170,8 +166,6 @@ class PrintPreviewServer(Thread):
return s.getsockname()[1]
def run(self):
self.disable_logging()
self.host = "127.0.0.1"
self.port = self.find_free_port()
# exporting the port number for languages to work in electron vuejs part of inkstitch

Wyświetl plik

@ -1,8 +1,14 @@
# Assuming pyembroidery is installed in parent directory of inkstitch.
# Maybe it would be better to install PyEmbroidery as a submodule and automatically update it,
# but the .gitignore file should be updated to ignore the build directory.
./pyembroidery
# get up to date inkex version (Febuary 10, 2024)
inkex @ git+https://gitlab.com/inkscape/extensions.git@8d51d7449d73096382c2f39e726eddc4f9bbcfc4
# for linux user it may be tricky to install wxPython from sources
# prebuilt packages: https://wxpython.org/pages/downloads/index.html
# https://extras.wxpython.org/wxPython4/extras/linux/gtk3/
wxPython>=4.1.1
backports.functools_lru_cache
@ -14,6 +20,10 @@ numpy
jinja2>2.9
requests
# toml release 0.10.2 still buggy for heterogenous arrays
# tomli is built as tomllib in python 3.11 and higher
tomli
# colormath - last official release: 3.0.0
# we need already submitted fixes - so let's grab them from the github repository
colormath @ git+https://github.com/gtaylor/python-colormath.git@4a076831fd5136f685aa7143db81eba27b2cd19a