jc-debug 0.1.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- debug/__init__.py +501 -460
- jc_debug-1.0.0.dist-info/METADATA +530 -0
- jc_debug-1.0.0.dist-info/RECORD +6 -0
- {jc_debug-0.1.0.dist-info → jc_debug-1.0.0.dist-info}/WHEEL +1 -1
- jc_debug-0.1.0.dist-info/METADATA +0 -47
- jc_debug-0.1.0.dist-info/RECORD +0 -6
- {jc_debug-0.1.0.dist-info → jc_debug-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {jc_debug-0.1.0.dist-info → jc_debug-1.0.0.dist-info}/top_level.txt +0 -0
debug/__init__.py
CHANGED
@@ -6,45 +6,61 @@ useful for adding temporary or conditional debug output to CLI scripts.
|
|
6
6
|
|
7
7
|
The minimal boilerplate is pretty simple:
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
```python
|
10
|
+
from debug import DebugChannel
|
11
|
+
|
12
|
+
dc=DebugChannel(True)
|
13
|
+
```
|
14
|
+
|
15
|
+
By default, DebugChannels are created disabled (the write no output), so
|
16
|
+
the `True` above enables `dc` during its instantiation so it needn't be
|
17
|
+
enabled later.
|
18
|
+
|
19
|
+
A more common way of handling this is ...
|
20
|
+
|
21
|
+
```python
|
22
|
+
from argparse import ArgumentParser
|
23
|
+
from debug import DebugChannel
|
24
|
+
|
25
|
+
dc=DebugChannel()
|
26
|
+
|
27
|
+
ap=ArgumentParser()
|
28
|
+
ap.add_argument('--debug',action='store_true',help="Enable debug output.")
|
29
|
+
opt=ap.parse_args()
|
30
|
+
dc.enable(opt.debug)
|
31
|
+
|
32
|
+
...
|
33
|
+
```
|
34
|
+
|
35
|
+
This enables the `dc` DebugChannel instance only if --debug is given on
|
36
|
+
the script's command line.
|
11
37
|
|
12
38
|
By default, output is sent to stdandard error and formatted as:
|
13
39
|
|
14
40
|
'{label}: [{pid}] {basename}:{line}:{function}: {indent}{message}\\n'
|
15
41
|
|
16
|
-
There are several variables you can include in DebugChannel's output
|
17
|
-
|
18
|
-
date - Current date in "%Y-%m-%d" format by default.
|
19
|
-
time - Current time in "%H:%M:%S" format by default.
|
20
|
-
label - Set in the initializer, defaults to "DC".
|
21
|
-
pid - The current numeric process ID.
|
22
|
-
pathname - The full path to the source file.
|
23
|
-
basename - The filename of the source file.
|
24
|
-
function - The name of the function whence dc(...) was called. This
|
25
|
-
will be "__main__" if called from outside any function.
|
26
|
-
line - The linenumber whence dc(...) was called.
|
27
|
-
code - The text of the line of code that called dc().
|
28
|
-
indent - The string (typically spaces) used to indent the message.
|
29
|
-
message - The message to be output.
|
42
|
+
There are several variables you can include in DebugChannel's output.
|
43
|
+
See the DebugChannel docs below for a list.
|
30
44
|
|
31
45
|
So, for example, if you want to see how your variables are behaving in a
|
32
46
|
loop, you might do something like this:
|
33
47
|
|
34
|
-
|
48
|
+
```python
|
49
|
+
from debug import DebugChannel
|
35
50
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
51
|
+
dc=DebugChannel(
|
52
|
+
True,
|
53
|
+
fmt="{label}: {line:3}: {indent}{message}\\n"
|
54
|
+
)
|
40
55
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
56
|
+
dc("Entering loop ...").indent()
|
57
|
+
for i in range(5):
|
58
|
+
dc(f"i={i}").indent()
|
59
|
+
for j in range(3):
|
45
60
|
dc(f"j={j}")
|
46
|
-
|
47
|
-
|
61
|
+
dc.undent()("Done with j loop.")
|
62
|
+
dc.undent()("Done with i loop.")
|
63
|
+
```
|
48
64
|
|
49
65
|
That gives you this necely indented output. The indent() and undent()
|
50
66
|
methods are one thing that makes DebugChannels so nice to work with.
|
@@ -82,479 +98,504 @@ how versatile DebugChannel instances can be.
|
|
82
98
|
|
83
99
|
A DebugChannel can also be used as a function decorator:
|
84
100
|
|
85
|
-
|
86
|
-
|
101
|
+
```python
|
102
|
+
import time
|
103
|
+
from src.debug import DebugChannel
|
87
104
|
|
88
|
-
|
89
|
-
|
90
|
-
|
105
|
+
def delay(**kwargs):
|
106
|
+
time.sleep(.1)
|
107
|
+
return True
|
91
108
|
|
92
|
-
|
93
|
-
|
109
|
+
dc=DebugChannel(True,callback=delay)
|
110
|
+
dc.setFormat("{label}: {function}: {indent}{message}\\n")
|
94
111
|
|
95
|
-
|
96
|
-
|
97
|
-
|
112
|
+
@dc
|
113
|
+
def example1(msg):
|
114
|
+
print(msg)
|
98
115
|
|
99
|
-
|
100
|
-
|
101
|
-
|
116
|
+
@dc
|
117
|
+
def example2(msg,count):
|
118
|
+
for i in range(count):
|
102
119
|
example1(f"{i+1}: {msg}")
|
103
120
|
|
104
|
-
|
105
|
-
|
121
|
+
example2("First test",3)
|
122
|
+
example2("Second test",2)
|
123
|
+
```
|
106
124
|
|
107
125
|
This causes entry into and exit from the decorated function to be
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
126
|
+
recorded in the given DebugChannel's output. If you put that into a file
|
127
|
+
named foo.py and then run "python3 -m foo", you'll get this:
|
128
|
+
|
129
|
+
```
|
130
|
+
DC: __main__: example2('First test',3) ...
|
131
|
+
DC: example2: example1('1: First test') ...
|
132
|
+
1: First test
|
133
|
+
DC: example2: example1(...) returns None after 45µs.
|
134
|
+
DC: example2: example1('2: First test') ...
|
135
|
+
2: First test
|
136
|
+
DC: example2: example1(...) returns None after 29µs.
|
137
|
+
DC: example2: example1('3: First test') ...
|
138
|
+
3: First test
|
139
|
+
DC: example2: example1(...) returns None after 26µs.
|
140
|
+
DC: __main__: example2(...) returns None after 630ms.
|
141
|
+
DC: __main__: example2('Second test',2) ...
|
142
|
+
DC: example2: example1('1: Second test') ...
|
143
|
+
1: Second test
|
144
|
+
DC: example2: example1(...) returns None after 28µs.
|
145
|
+
DC: example2: example1('2: Second test') ...
|
146
|
+
2: Second test
|
147
|
+
DC: example2: example1(...) returns None after 23µs.
|
148
|
+
DC: __main__: example2(...) returns None after 423ms.
|
149
|
+
```
|
150
|
+
|
151
|
+
That's a very general start. See DebugChannel's class docs for more.
|
132
152
|
"""
|
133
153
|
|
134
|
-
__all__=[
|
135
|
-
|
136
|
-
|
137
|
-
'localtime',
|
154
|
+
__all__ = [
|
155
|
+
"DebugChannel",
|
156
|
+
"line_iter",
|
138
157
|
]
|
139
|
-
__version__=
|
158
|
+
__version__ = "0.1.0"
|
159
|
+
|
160
|
+
import inspect, os, sys, traceback
|
140
161
|
|
141
|
-
import inspect,os,sys,traceback
|
142
162
|
# Because I need "time" to be a local variable in DebugChannel.write() ...
|
143
|
-
from time import gmtime,localtime,strftime,time as get_time
|
163
|
+
from time import gmtime, localtime, strftime, time as get_time
|
144
164
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
j+=1
|
160
|
-
i=j
|
161
|
-
|
162
|
-
class DebugChannel(object):
|
163
|
-
"""Objects of this class are really useful for debugging, and this is
|
164
|
-
even more powerful when combined with loggy.LogStream to write all
|
165
|
-
debug output to some appropriate syslog facility. Here's an example,
|
166
|
-
put into an executable script called x:
|
167
|
-
|
168
|
-
#!/usr/bin/env python
|
169
|
-
|
170
|
-
from debug import DebugChannel
|
171
|
-
from loggy import LogStream
|
172
|
-
|
173
|
-
d=DebugChannel(
|
165
|
+
|
166
|
+
class DebugChannel:
|
167
|
+
"""Objects of this class are useful for debugging, and this is even
|
168
|
+
more powerful when combined with loggy.LogStream to write all debug
|
169
|
+
output to some appropriate syslog facility. Here's an example, put
|
170
|
+
into an executable script called dc-log-test:
|
171
|
+
|
172
|
+
```python
|
173
|
+
#!/usr/bin/env python
|
174
|
+
|
175
|
+
from debug import DebugChannel
|
176
|
+
from loggy import LogStream
|
177
|
+
|
178
|
+
dc=DebugChannel(
|
174
179
|
True,
|
175
180
|
stream=LogStream(facility='user'),
|
176
|
-
label='D',
|
177
181
|
fmt='{label}: {basename}({line}): {indent}{message}\\n'
|
178
|
-
|
179
|
-
|
182
|
+
)
|
183
|
+
dc('Testing')
|
184
|
+
```
|
180
185
|
|
181
|
-
|
182
|
-
|
186
|
+
The output in /var/log/user.log (which might be a different path on
|
187
|
+
your system) might look like this:
|
183
188
|
|
184
|
-
|
189
|
+
Aug 16 22:58:16 pi4 x[18478] DC: dc-log-test(11): Testing
|
185
190
|
|
186
|
-
|
187
|
-
|
188
|
-
|
191
|
+
What I really like about this is that the source filename and line
|
192
|
+
number are included in the log output. The "dc('Testing')" call is on
|
193
|
+
line 11 of dc-log-test.
|
189
194
|
|
190
|
-
|
195
|
+
Run this module directly with
|
191
196
|
|
192
|
-
|
197
|
+
python3 -m debug
|
193
198
|
|
194
|
-
|
195
|
-
|
199
|
+
to see a demonstration of indenture. The example code for that demo
|
200
|
+
is at the bottom of the debug.py source file.
|
196
201
|
|
197
|
-
|
202
|
+
IGNORING MODULES:
|
198
203
|
DebugChannel.write() goes to some length to ensure the filename and
|
199
204
|
line number reported in its output are something helpful to the
|
200
205
|
caller. For instance, the source line shouldn't be anything in this
|
201
|
-
DebugChannel class.
|
206
|
+
DebugChannel class.
|
202
207
|
|
203
208
|
Use the ignoreModule() method to tell the DebugChannel object ignore
|
204
209
|
other modules, and optionally, specific functions within that
|
205
210
|
module."""
|
206
211
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
212
|
+
def __init__(
|
213
|
+
self,
|
214
|
+
enabled=False,
|
215
|
+
stream=sys.stderr,
|
216
|
+
label="DC",
|
217
|
+
indent_with=" ",
|
218
|
+
fmt="{label}: {basename}:{line}:{function}: {indent}{message}\n",
|
219
|
+
date_fmt="%Y-%m-%d",
|
220
|
+
time_fmt="%H:%M:%S",
|
221
|
+
time_tupler=localtime,
|
222
|
+
callback=None,
|
218
223
|
):
|
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
|
-
self
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
224
|
+
"""Initialize this new DebugChannel instance.
|
225
|
+
|
226
|
+
Arguments:
|
227
|
+
|
228
|
+
* enabled: True if this DebugChannel object is allowed to
|
229
|
+
output messages. False if it should be quiet.
|
230
|
+
* stream: The stream (or stream-like object) to write
|
231
|
+
messages to.
|
232
|
+
* label: A string indicated what we're doing.
|
233
|
+
* indent_with: Indenting uses this string value.
|
234
|
+
* fmt: Format of debug output. See setFormat().
|
235
|
+
* date_fmt: strftime() uses this string to format dates.
|
236
|
+
* time_fmt: strftime() uses this string to format times.
|
237
|
+
* time_tupler: This is either time.localtime or time.gmtime and
|
238
|
+
defaults to localtime.
|
239
|
+
* callback: A function accepting keyword arguments and returning
|
240
|
+
True if the current message is to be output. The keyword
|
241
|
+
arguments are all the local variables of DebugChannel.write().
|
242
|
+
Of particular interest might be "stack" and all the variables
|
243
|
+
available for formatting (label, basename, pid, function, line,
|
244
|
+
indent, and message).
|
245
|
+
|
246
|
+
"""
|
247
|
+
|
248
|
+
assert hasattr(
|
249
|
+
stream, "write"
|
250
|
+
), "DebugChannel REQUIRES a stream instance with a 'write' method."
|
251
|
+
|
252
|
+
self.stream = stream
|
253
|
+
self.enabled = enabled
|
254
|
+
self.pid = os.getpid()
|
255
|
+
self.fmt = fmt
|
256
|
+
self.indlev = 0
|
257
|
+
self.label = label
|
258
|
+
self.indstr = indent_with
|
259
|
+
self._t = 0 # The last time we formatted the time.
|
260
|
+
self.date = None # The last date we formatted.
|
261
|
+
self.time = None # The last time we formatted.
|
262
|
+
self.date_fmt = date_fmt
|
263
|
+
self.time_fmt = time_fmt
|
264
|
+
self.time_tupler = time_tupler
|
265
|
+
self.callback = callback
|
266
|
+
# Do not report functions in this debug module.
|
267
|
+
self.ignore = {}
|
268
|
+
self.ignoreModule(os.path.normpath(inspect.stack()[0].filename))
|
269
|
+
|
270
|
+
def __bool__(self):
|
271
|
+
"""Return the Enabled state of this DebugChannel object. It is
|
272
|
+
somtimes necessary to logically test whether our code is in
|
273
|
+
debug mode at runtime, and this method makes that very simple.
|
274
|
+
|
275
|
+
```python
|
272
276
|
d=DebugChannel(opt.debug)
|
273
277
|
.
|
274
278
|
.
|
275
279
|
.
|
276
280
|
if d:
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
281
|
+
d("Getting diagnostics ...")
|
282
|
+
diagnostics=get_some_computationally_expensive_data()
|
283
|
+
d(diagnostics)
|
284
|
+
```
|
285
|
+
"""
|
286
|
+
|
287
|
+
return bool(self.enabled)
|
288
|
+
|
289
|
+
def enable(self, state=True):
|
290
|
+
"""Allow this DebugChannel object to write messages if state is
|
291
|
+
True. Return the previous state as a boolean."""
|
292
|
+
|
293
|
+
prev_state = self.enabled
|
294
|
+
self.enabled = bool(state)
|
295
|
+
return prev_state
|
296
|
+
|
297
|
+
def disable(self):
|
298
|
+
"""Inhibit output from this DebugChannel object, and return its
|
299
|
+
previous "enabled" state."""
|
300
|
+
|
301
|
+
return self.enable(False)
|
302
|
+
|
303
|
+
def ignoreModule(self, name, *args):
|
304
|
+
"""Given the name of a module, e.g. "debug"), ignore any entries
|
305
|
+
in our call stack from that module. Any subsequent arguments
|
306
|
+
must be the names of functions to be ignored within that module.
|
307
|
+
If no such functions are named, all calls from that module will
|
308
|
+
be ignored."""
|
309
|
+
|
310
|
+
if name in sys.modules:
|
311
|
+
m = str(sys.modules[name])
|
312
|
+
name = m[m.find(" from '") + 7 : m.rfind(".py") + 3]
|
313
|
+
if name not in self.ignore:
|
314
|
+
self.ignore[name] = set([])
|
315
|
+
self.ignore[name].update(args)
|
316
|
+
|
317
|
+
def setDateFormat(self, fmt):
|
318
|
+
"""Use the formatting rules of strftime() to format the "date"
|
319
|
+
value to be output in debug messages. Return the previous date
|
320
|
+
format string."""
|
321
|
+
|
322
|
+
s = self.date_fmt
|
323
|
+
self.date_fmt = fmt
|
324
|
+
return s
|
325
|
+
|
326
|
+
def setTimeFormat(self, fmt):
|
327
|
+
"""Use the formatting rules of strftime() to format the "time"
|
328
|
+
value to be output in debug messages. Return the previous time
|
329
|
+
format string."""
|
330
|
+
|
331
|
+
s = self.time_fmt
|
332
|
+
self.time_fmt = fmt
|
333
|
+
return s
|
334
|
+
|
335
|
+
def setIndentString(self, s):
|
336
|
+
"""Set the string to indent string. Return this DebugChannel
|
337
|
+
object. E.g. at indent level 3, the "{indent}" portion of the
|
338
|
+
formatted debug will contain 3 copies of the string you set with
|
339
|
+
this function. So ' ' will indent two spaces per indention
|
340
|
+
level. Another popular choice is '| ' to make longer indention
|
341
|
+
runs easier to follow in the debug output."""
|
342
|
+
|
343
|
+
self.indstr = s
|
344
|
+
return self
|
345
|
+
|
346
|
+
def setFormat(self, fmt):
|
347
|
+
"""Set the format of our debug statements. The format defaults
|
348
|
+
to:
|
349
|
+
|
350
|
+
'{label}: {basename}:{line}:{function}: {indent}{message}\\n'
|
351
|
+
|
352
|
+
Fields:
|
353
|
+
* {date}: current date (see setDateFormat())
|
354
|
+
* {time}: current time (see setTimeFormat())
|
355
|
+
* {pid}: numeric ID of the current process
|
356
|
+
* {label}: what type of thing is getting logged (default: 'DC')
|
357
|
+
* {pathname}: full path of the calling source file
|
358
|
+
* {basename}: base name of the calling source file
|
359
|
+
* {function}: name of function debug.write() was called from
|
360
|
+
* {line}: number of the calling line of code in its source file
|
361
|
+
* {code}: the Python code at the given line of the given file
|
362
|
+
* {indent}: indent string multiplied by the indention level
|
363
|
+
* {message}: the message to be written
|
364
|
+
|
365
|
+
All non-field text is literal text. The '\\n' at the end is
|
366
|
+
required if you want a line ending at the end of each message.
|
367
|
+
If your DebugChannel object is configured to write to a
|
368
|
+
LogStream object that writes to syslog or something similar, you
|
369
|
+
might want to remove the {date} and {time} (and maybe {label})
|
370
|
+
fields from the default format string to avoid logging these
|
371
|
+
values redundantly."""
|
372
|
+
|
373
|
+
self.fmt = fmt
|
374
|
+
|
375
|
+
def indent(self, indent=1):
|
376
|
+
"""Increase this object's current indenture by this value (which
|
377
|
+
might be negative. Return this DebugChannel opject with the
|
378
|
+
adjusted indenture. See write() for how this might be used."""
|
379
|
+
|
380
|
+
self.indlev += indent
|
381
|
+
if self.indlev < 0:
|
382
|
+
self.indlev = 0
|
383
|
+
return self
|
384
|
+
|
385
|
+
def undent(self, indent=1):
|
386
|
+
"""Decrease this object's current indenture by this value (which
|
387
|
+
might be negative. Return this DebugChannel object with the
|
388
|
+
adjusted indenture. See write() for how this might be used."""
|
389
|
+
|
390
|
+
return self.indent(-indent)
|
391
|
+
|
392
|
+
def writelines(self, seq):
|
393
|
+
"""Just a wrapper around write(), since that method handles
|
394
|
+
sequences (and other things) just fine. writelines() is only
|
395
|
+
provided for compatibility with code that expects it to be
|
396
|
+
supported."""
|
397
|
+
|
398
|
+
return self.write(seq)
|
399
|
+
|
400
|
+
def __call__(self, arg, *args, **kwargs):
|
401
|
+
"""If this DebugChannel instance is simply being called, this
|
402
|
+
method is a very simple wrapper around the write(...) emthod. If
|
403
|
+
it is being used as a function decorator, that function entry
|
404
|
+
and exit are recorded to the DebugChannel, and this becomes a
|
405
|
+
more featuresome wrapper around the write(...) method."""
|
406
|
+
|
407
|
+
lines = inspect.stack(context=2)[1].code_context
|
408
|
+
if lines and any(l.lstrip().startswith("@") for l in lines):
|
409
|
+
# We're being called as a decorator.
|
410
|
+
def f(*args, **kwargs):
|
411
|
+
# Record how this function is being called.
|
412
|
+
sig = ",".join(
|
413
|
+
[repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
|
414
|
+
)
|
415
|
+
self.write(f"{arg.__name__}({sig}) ...").indent()
|
416
|
+
t0 = get_time()
|
417
|
+
# Call the function we're wrapping
|
418
|
+
ret = arg(*args, **kwargs)
|
419
|
+
# Record this function's return.
|
420
|
+
t1 = get_time()
|
421
|
+
sig = "..." if sig else ""
|
422
|
+
dt = t1 - t0
|
423
|
+
d, dt = divmod(dt, 86400) # Days
|
424
|
+
if d:
|
425
|
+
d = f"{int(d)}d"
|
426
|
+
else:
|
427
|
+
d = ""
|
428
|
+
h, dt = divmod(dt, 3600) # Hours
|
429
|
+
if h:
|
430
|
+
h = f"{int(h)}h"
|
431
|
+
else:
|
432
|
+
h = "0h" if d else ""
|
433
|
+
m, dt = divmod(dt, 60) # Minutes
|
434
|
+
if m:
|
435
|
+
m = f"{int(m)}m"
|
436
|
+
else:
|
437
|
+
m = "0m" if h else ""
|
438
|
+
if m: # Seconds
|
439
|
+
s = f"{int(dt)}s"
|
440
|
+
else:
|
441
|
+
if dt >= 1: # Fractional seconds
|
442
|
+
s = f"{dt:0.3f}"
|
443
|
+
s = s[:4].rstrip("0") + "s"
|
444
|
+
elif dt >= 0.001: # Milliseconds
|
445
|
+
s = f"{int(dt*1000)}ms"
|
446
|
+
else: # Microseconds
|
447
|
+
s = f"{int(dt*1e6)}µs"
|
448
|
+
self.undent().write(
|
449
|
+
f"{arg.__name__}({sig}) returns {ret!r} after {d}{h}{m}{s}."
|
450
|
+
)
|
451
|
+
return ret
|
452
|
+
|
453
|
+
return f
|
454
|
+
|
455
|
+
# This DebugChannel instance is being called as if it were a function.
|
456
|
+
return self.write(arg)
|
457
|
+
|
458
|
+
def writeTraceback(self, exc):
|
459
|
+
"""Write the given exception with traceback information to our
|
460
|
+
output stream."""
|
461
|
+
|
462
|
+
if self.enabled:
|
463
|
+
for line in traceback.format_exception(exc):
|
464
|
+
self.write(line.rstrip())
|
465
|
+
|
466
|
+
def write(self, message):
|
467
|
+
"""If this debug instance is enabled, write the given message
|
468
|
+
using the our current format. In any case, return this
|
469
|
+
DebugChannel instance so further operations can be performed on
|
470
|
+
it. E.g.:
|
471
|
+
|
472
|
+
```python
|
452
473
|
debug=DebugChannel(opt.debug)
|
453
474
|
debug('Testing')
|
454
475
|
|
455
476
|
def func(arg):
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
#
|
494
|
-
#
|
495
|
-
#
|
496
|
-
#
|
497
|
-
#
|
498
|
-
#
|
499
|
-
#
|
500
|
-
#
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
477
|
+
debug.write("Entering func(arg=%r)"%(arg,)).indent(1)
|
478
|
+
for i in range(3):
|
479
|
+
debug(f"{i=}")
|
480
|
+
debug.indent(-1).write("Leaving func(...) normally")
|
481
|
+
```
|
482
|
+
|
483
|
+
This lets the caller decide whether to change indenture among
|
484
|
+
other things before or after the message is written.
|
485
|
+
|
486
|
+
If message is a single string containing no line endings, that
|
487
|
+
single value will be outout. if message contains at least one
|
488
|
+
newline (the value of os.linesep), each line is output on a
|
489
|
+
debug line of its own.
|
490
|
+
|
491
|
+
If message is a list or tuple, each item in that sequence will
|
492
|
+
be output on its own line.
|
493
|
+
|
494
|
+
If message is a dictionary, each key/value pair is written out
|
495
|
+
as "key: value" to its own log line."""
|
496
|
+
|
497
|
+
if self.enabled:
|
498
|
+
# Update our formatted date and time if necessary.
|
499
|
+
t = int(get_time()) # Let's truncate at whole seconds.
|
500
|
+
if self._t != t:
|
501
|
+
t = self.time_tupler(t)
|
502
|
+
self._t = t
|
503
|
+
self.date = strftime(self.date_fmt, t)
|
504
|
+
self.time = strftime(self.time_fmt, t)
|
505
|
+
# Set local variables for date and time so they're available for output.
|
506
|
+
date = self.date
|
507
|
+
time = self.time
|
508
|
+
# Find the first non-ignored stack frame whence we were called.
|
509
|
+
pathname, basename, line = None, None, None
|
510
|
+
for i, frame in enumerate(inspect.stack()):
|
511
|
+
# This is for debugging debug.py. It turns out Python 3.6 has a bug in
|
512
|
+
# inspect.stack() that can return outrageous values for frame.index.
|
513
|
+
# (So I'm asking for only one line of context, and I've stopped using the
|
514
|
+
# frame's untrustworthy index value.)
|
515
|
+
# print(f"""{i}:
|
516
|
+
# frame: {frame.frame!r}
|
517
|
+
# filename: {frame.filename!r}
|
518
|
+
# lineno: {frame.lineno!r}
|
519
|
+
# function: {frame.function!r}
|
520
|
+
# code_context: {frame.code_context!r}
|
521
|
+
# index: {frame.index!r}""")
|
522
|
+
p = os.path.normpath(frame.filename)
|
523
|
+
if p not in self.ignore:
|
524
|
+
break
|
525
|
+
if frame.function not in self.ignore[p]:
|
526
|
+
break
|
527
|
+
# Set some local variables so they'll be available to our callback
|
528
|
+
# function and for formatting.
|
529
|
+
pid = self.pid
|
530
|
+
pathname = os.path.normpath(frame.filename)
|
531
|
+
basename = os.path.basename(pathname)
|
532
|
+
line = frame.lineno
|
533
|
+
function = frame.function
|
534
|
+
if str(function) == "<module>":
|
535
|
+
function = "__main__"
|
536
|
+
code = frame.code_context
|
537
|
+
if code:
|
538
|
+
code = code[0].rstrip()
|
539
|
+
else:
|
540
|
+
code = None
|
541
|
+
indent = self.indstr * self.indlev
|
542
|
+
label = self.label
|
543
|
+
|
544
|
+
# If our caller provided a callback function, call that now.
|
545
|
+
if self.callback:
|
546
|
+
if not self.callback(**locals()):
|
547
|
+
return self # Return without writing any output.
|
548
|
+
|
549
|
+
# Format our message and write it to the debug stream.
|
550
|
+
if isinstance(message, (list, tuple)):
|
551
|
+
if isinstance(message, tuple):
|
552
|
+
left, right = "()"
|
553
|
+
else:
|
554
|
+
left, right = "[]"
|
555
|
+
messages = message
|
556
|
+
message = left
|
557
|
+
self.stream.write(self.fmt.format(**locals()))
|
558
|
+
for message in messages:
|
559
|
+
message = self.indstr + message
|
560
|
+
self.stream.write(self.fmt.format(**locals()))
|
561
|
+
message = right
|
562
|
+
self.stream.write(self.fmt.format(**locals()))
|
563
|
+
elif isinstance(message, dict):
|
564
|
+
messages = dict(message)
|
565
|
+
message = "{"
|
566
|
+
self.stream.write(self.fmt.format(**locals()))
|
567
|
+
for k in messages.keys():
|
568
|
+
message = f"{self.indstr}{k}: {messages[k]}"
|
569
|
+
self.stream.write(self.fmt.format(**locals()))
|
570
|
+
message = "}"
|
571
|
+
self.stream.write(self.fmt.format(**locals()))
|
572
|
+
elif isinstance(message, str) and os.linesep in message:
|
573
|
+
messages = message
|
574
|
+
for message in line_iter(messages):
|
575
|
+
self.stream.write(self.fmt.format(**locals()))
|
576
|
+
else:
|
577
|
+
self.stream.write(self.fmt.format(**locals()))
|
578
|
+
self.stream.flush()
|
579
|
+
|
580
|
+
# The caller can call other DebugChannel methods on our return value.
|
581
|
+
return self
|
582
|
+
|
560
583
|
|
584
|
+
def line_iter(s):
|
585
|
+
"""This iterator facilitates stepping through each line of a multi-
|
586
|
+
line string in place, without having to create a list containing
|
587
|
+
those lines. This is similar to `str.splitlines()`, but it yields
|
588
|
+
slices of the original string rather than returning a list of copies
|
589
|
+
of segments of the original."""
|
590
|
+
|
591
|
+
i = 0
|
592
|
+
n = len(s)
|
593
|
+
while i < n:
|
594
|
+
j = s.find(os.linesep, i)
|
595
|
+
if j < 0:
|
596
|
+
yield s[i:] # Yield the rest of this string.
|
597
|
+
j = n
|
598
|
+
else:
|
599
|
+
yield s[i:j] # Yield the next line in this string.
|
600
|
+
j += 1
|
601
|
+
i = j
|