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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
# 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()
import traceback
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
from .utils import safe_get
from ..utils.paths import get_ini
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'
sew_stack_enabled = safe_get(get_ini(), "DEBUG", "enable_sew_stack", default=False)
# --------------------------------------------------------------------------------------------
# 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)
def log_exception(self):
if self.enabled:
self.raw_log(traceback.format_exc())
@contextmanager
def log_exceptions(self):
try:
yield
except Exception:
self.log_exception()
raise
# global debug object
debug = Debug()
|