1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
# 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
import configparser
import lib.debug_utils as debug_utils
SCRIPTDIR = Path(__file__).parent.absolute()
if len(sys.argv) < 2:
# no arguments - prevent accidentally running this script
print("No arguments given, continue without arguments?")
answer = input("Continue? [y/N] ")
if answer.lower() != 'y':
exit(1)
running_as_frozen = getattr(sys, 'frozen', None) is not None # check if running from pyinstaller bundle
ini = configparser.ConfigParser()
ini.read(SCRIPTDIR / "DEVEL.ini") # read DEVEL.ini file if exists
# prefer pip installed inkex over inkscape bundled inkex, pip version is bundled with Inkstitch
prefere_pip_inkex = ini.getboolean("LIBRARY","prefer_pip_inkex", fallback=True)
# 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'
profile_type = 'none'
if not running_as_frozen: # debugging/profiling only in development mode
# 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
# specify debugger type
# - if script was already started from debugger then don't read debug file
if not debug_active:
debug_type = ini.get("DEBUG","debugger", fallback="none") # debugger type vscode, pycharm, pydevd, none
# specify profiler type
profile_type = ini.get("PROFILE","profiler", fallback="none") # profiler type cprofile, profile, pyinstrument, none
# process creation of the Bash script
if running_from_inkscape:
if ini.getboolean("DEBUG","create_bash_script", fallback=False): # create script only if enabled in DEVEL.ini
debug_utils.write_offline_debug_script(SCRIPTDIR, bash_name, bash_svg)
# 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
if prefere_pip_inkex and 'PYTHONPATH' in os.environ:
# see static void set_extensions_env() in inkscape/src/inkscape-main.cpp
# When running in development mode, we prefer inkex installed by pip, not the one bundled with Inkscape.
# - 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)
# >> should be removed after previous code was tested <<
# if sys.platform == "darwin":
# extensions_path = "/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions" # Mac
# else:
# extensions_path = "/usr/share/inkscape/extensions" # Linux
# # windows ?
# move inkscape extensions path to the end of sys.path
# sys.path.remove(extensions_path)
# sys.path.append(extensions_path)
# >> ------------------------------------------------- <<
import logging
from argparse import ArgumentParser
from io import StringIO
from lib.exceptions import InkstitchException, format_uncaught_exception
from inkex import errormsg
from lxml.etree import XMLSyntaxError
import lib.debug as debug
from lib import extensions
from lib.i18n import _
from lib.utils import restore_stderr, save_stderr
# file DEBUG exists next to inkstitch.py - enabling debug mode depends on value of debug_type in DEBUG file
if debug_type != 'none':
debug_file = ini.get("DEBUG","debug_file", fallback="debug.log")
wait_attach = ini.getboolean("DEBUG","wait_attach", fallback=True) # currently only for vscode
debug.enable(debug_type, debug_file, wait_attach)
# check if debugger is really activated
debug_active = bool((gettrace := getattr(sys, 'gettrace')) and gettrace())
# ignore warnings in releases - see warnings.warn()
if running_as_frozen or not debug_active:
import warnings
warnings.filterwarnings('ignore')
# set logger for shapely
logger = logging.getLogger('shapely.geos') # attach logger of shapely, from ver 2.0.0 all logs are exceptions
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)
# pop '--extension' from arguments and generate extension class name from extension name
parser = ArgumentParser()
parser.add_argument("--extension")
my_args, remaining_args = parser.parse_known_args()
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() # create instance of extension class - call __init__ method
# extension run(), but we differentiate between debug and normal mode
# - in debug or profile mode we run extension or profile extension
# - in normal mode we run extension in try/except block to catch all exceptions and hide GTK spam
if debug_active or profile_type != "none": # if debug or profile mode
profile_file_base = ini.get("PROFILE","profile_file_base", fallback="debug_profile")
profile_path = SCRIPTDIR / profile_file_base # Path object
if profile_type == 'none':
extension.run(args=remaining_args)
elif profile_type == 'cprofile':
import cProfile
import pstats
profiler = cProfile.Profile()
profiler.enable()
extension.run(args=remaining_args)
profiler.disable()
profiler.dump_stats(profile_path.with_suffix(".prof")) # can be read by 'snakeviz -s' or 'pyprof2calltree'
with open(profile_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 '{profile_path.name}' and '{profile_path.name}.prof'. Use snakeviz to see it.", file=sys.stderr)
elif profile_type == 'profile':
import profile
import pstats
profiler = profile.Profile()
profiler.run('extension.run(args=remaining_args)')
profiler.dump_stats(profile_path.with_suffix(".prof")) # can be read by 'snakeviz' or 'pyprof2calltree' - seems broken
with open(profile_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 '{profile_path.name}'", file=sys.stderr)
elif profile_type == 'pyinstrument':
import pyinstrument
profiler = pyinstrument.Profiler()
profiler.start()
extension.run(args=remaining_args)
profiler.stop()
profile_path = SCRIPTDIR / "profile_stats.html"
with open(profile_path, 'w') as stats_file:
stats_file.write(profiler.output_html())
print(f"profiling stats written to '{profile_path.name}'. Use browser to see it.", file=sys.stderr)
else: # if not debug nor profile mode
save_stderr() # hide GTK spam
exception = None
try:
extension.run(args=remaining_args)
except (SystemExit, KeyboardInterrupt):
raise
except XMLSyntaxError:
msg = _("Ink/Stitch cannot read your SVG file. "
"This is often the case when you use a file which has been created with Adobe Illustrator.")
msg += "\n\n"
msg += _("Try to import the file into Inkscape through 'File > Import...' (Ctrl+I)")
errormsg(msg)
except InkstitchException as exc:
errormsg(str(exc))
except Exception:
errormsg(format_uncaught_exception())
sys.exit(1)
finally:
restore_stderr()
if shapely_errors.tell():
errormsg(shapely_errors.getvalue())
sys.exit(0)
|