jc-debug 0.1.1__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 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
- from debug import DebugChannel
10
- dc=DebugChannel(True)
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
- from debug import DebugChannel
48
+ ```python
49
+ from debug import DebugChannel
35
50
 
36
- dc=DebugChannel(
37
- True,
38
- fmt="{label}: {line:3}: {indent}{message}\\n"
39
- )
51
+ dc=DebugChannel(
52
+ True,
53
+ fmt="{label}: {line:3}: {indent}{message}\\n"
54
+ )
40
55
 
41
- dc("Entering loop ...").indent()
42
- for i in range(5):
43
- dc(f"i={i}").indent()
44
- for j in range(3):
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
- dc.undent()("Done with j loop.")
47
- dc.undent()("Done with i loop.")
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
- import time
86
- from debug import DebugChannel
101
+ ```python
102
+ import time
103
+ from src.debug import DebugChannel
87
104
 
88
- def delay(**kwargs):
89
- time.sleep(1)
90
- return True
105
+ def delay(**kwargs):
106
+ time.sleep(.1)
107
+ return True
91
108
 
92
- dc=DebugChannel(True,callback=delay)
93
- dc.setFormat("{label}: {function}: {indent}{message}\\n")
109
+ dc=DebugChannel(True,callback=delay)
110
+ dc.setFormat("{label}: {function}: {indent}{message}\\n")
94
111
 
95
- @dc
96
- def example1(msg):
97
- print(msg)
112
+ @dc
113
+ def example1(msg):
114
+ print(msg)
98
115
 
99
- @dc
100
- def example2(msg,count):
101
- for i in range(count):
116
+ @dc
117
+ def example2(msg,count):
118
+ for i in range(count):
102
119
  example1(f"{i+1}: {msg}")
103
120
 
104
- example2("First test",3)
105
- example2("Second test",2)
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
- announced in the given DebugChannel's output. If you put that into a
109
- file named foo.py and then run "python3 -m foo", you'll get this:
110
-
111
- DC: __main__: Calling example2('First test',3) ...
112
- DC: example2: Calling example1('1: First test') ...
113
- 1: First test
114
- DC: example2: Function example1 returns None after 0.000069 seconds.
115
- DC: example2: Calling example1('2: First test') ...
116
- 2: First test
117
- DC: example2: Function example1 returns None after 0.000026 seconds.
118
- DC: example2: Calling example1('3: First test') ...
119
- 3: First test
120
- DC: example2: Function example1 returns None after 0.000038 seconds.
121
- DC: __main__: Function example2 returns None after 6.036933 seconds.
122
- DC: __main__: Calling example2('Second test',2) ...
123
- DC: example2: Calling example1('1: Second test') ...
124
- 1: Second test
125
- DC: example2: Function example1 returns None after 0.000039 seconds.
126
- DC: example2: Calling example1('2: Second test') ...
127
- 2: Second test
128
- DC: example2: Function example1 returns None after 0.000023 seconds.
129
- DC: __main__: Function example2 returns None after 4.019719 seconds.
130
-
131
- That's a very general start. See DebugChannel's docs for more.
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
- 'DebugChannel',
136
- 'gmtime',
137
- 'localtime',
154
+ __all__ = [
155
+ "DebugChannel",
156
+ "line_iter",
138
157
  ]
139
- __version__='0.1.0'
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
- def line_iter(s):
146
- """This iterator facilitates stepping through each line of a multi-
147
- line string in place, without having to create a list containing those
148
- lines."""
149
-
150
- i=0
151
- n=len(s)
152
- while i<n:
153
- j=s.find(os.linesep,i)
154
- if j<0:
155
- yield s[i:] # Yield the rest of this string.
156
- j=n
157
- else:
158
- yield s[i:j] # Yield the next line in this string.
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
- d('Testing')
182
+ )
183
+ dc('Testing')
184
+ ```
180
185
 
181
- The output in /var/log/user.log (which might be a different path on
182
- your system) might look like this:
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
- Aug 16 22:58:16 pi4 x[18478] D: x(12): Testing
189
+ Aug 16 22:58:16 pi4 x[18478] DC: dc-log-test(11): Testing
185
190
 
186
- What I really like about this is that the source filename and line
187
- number are included in the log output. The "d('Testing')" call is on
188
- line 12.
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
- Run this module directly with
195
+ Run this module directly with
191
196
 
192
- python3 -m debug
197
+ python3 -m debug
193
198
 
194
- to see a demonstration of indenture. The example code for that demo
195
- is at the bottom of the debug.py source file.
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
- IGNORING MODULES:
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
- def __init__(
208
- self,
209
- enabled=False,
210
- stream=sys.stderr,
211
- label='DC',
212
- indent_with=' ',
213
- fmt='{label}: {basename}:{line}:{function}: {indent}{message}\n',
214
- date_fmt='%Y-%m-%d',
215
- time_fmt='%H:%M:%S',
216
- time_tupler=localtime,
217
- callback=None
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
- """Initialize the stream and on/off state of this new DebugChannel
220
- object. The "enabled" state defaults to False, and the stream
221
- defaults to sys.stderr (though any object with a write() method will
222
- do).
223
-
224
- Arguments:
225
-
226
- enabled True if this DebugChannel object is allowed to output
227
- messages. False if it should be quiet.
228
- stream The stream (or stream-like object) to write messages
229
- to.
230
- label A string indicated what we're doing.
231
- indent_with The string used to indent each level of indenture.
232
- fmt Formatting for our output lines. See setFormat().
233
- date_fmt Date is formatted with strftime() using this string.
234
- time_fmt Time is formatted with strftime() using this string.
235
- time_tupler This is either localtime or gmtime and defaults to
236
- localtime.
237
- callback A function accepting keyword arguments and returning
238
- True if the current message is to be output. The
239
- keyword arguments are all the local variables of
240
- DebugChannel.write(). Of particular interest might be
241
- "stack" and all the variables available for
242
- formatting: label, basename, pid, function, line,
243
- indent, and message.
244
-
245
- """
246
-
247
- assert hasattr(stream,'write'),"DebugChannel REQUIRES a stream object with a write() method."
248
-
249
- self.stream=stream
250
- self.enabled=enabled
251
- self.pid=os.getpid()
252
- self.fmt=fmt
253
- self.indlev=0
254
- self.label=label
255
- self.indstr=indent_with
256
- self._t=0 # The last time we formatted the time.
257
- self.date=None # The last date we formatted.
258
- self.time=None # The last time we formatted.
259
- self.date_fmt=date_fmt
260
- self.time_fmt=time_fmt
261
- self.time_tupler=time_tupler
262
- self.callback=callback
263
- # Do not report functions in this debug module.
264
- self.ignore={}
265
- self.ignoreModule(os.path.normpath(inspect.stack()[0].filename))
266
-
267
- def __bool__(self):
268
- """Return the Enabled state of this DebugChannel object. It is
269
- somtimes necessary to logically test whether our code is in debug
270
- mode at runtime, and this method makes that very simple.
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
- d("Getting diagnostics ...")
278
- diagnostics=get_some_computationally_expensive_data()
279
- d(diagnostics)
280
- """
281
-
282
- return bool(self.enabled)
283
-
284
- def enable(self,state=True):
285
- """Allow this DebugChannel object to write messages if state is
286
- True. Return the previous state as a boolean."""
287
-
288
- prev_state=self.enabled
289
- self.enabled=bool(state)
290
- return prev_state
291
-
292
- def disable(self):
293
- """Inhibit output from this DebugChannel object, and return its
294
- previous "enabled" state."""
295
-
296
- return self.enable(False)
297
-
298
- def ignoreModule(self,name,*args):
299
- """Given the name of a module, e.g. "debug"), ignore any entries in
300
- our call stack from that module. Any subsequent arguments must be
301
- the names of functions to be ignored within that module. If no such
302
- functions are named, all calls from that module will be ignored."""
303
-
304
- if name in sys.modules:
305
- m=str(sys.modules[name])
306
- name=m[m.find(" from '")+7:m.rfind(".py")+3]
307
- if name not in self.ignore:
308
- self.ignore[name]=set([])
309
- self.ignore[name].update(args)
310
-
311
- def setDateFormat(self,fmt):
312
- """Use the formatting rules of strftime() to format the "date"
313
- value to be output in debug messages. Return the previous date
314
- format string."""
315
-
316
- s=self.date_fmt
317
- self.date_fmt=fmt
318
- return s
319
-
320
- def setTimeFormat(self,fmt):
321
- """Use the formatting rules of strftime() to format the "time"
322
- value to be output in debug messages. Return the previous time
323
- format string."""
324
-
325
- s=self.time_fmt
326
- self.time_fmt=fmt
327
- return s
328
-
329
- def setIndentString(self,s):
330
- "Set the string to indent with. Return this DebugChannel object."
331
-
332
- self.indstr=s
333
- return self
334
-
335
- def setFormat(self,fmt):
336
- """Set the format of our debug statements. The format defaults to:
337
-
338
- '{date} {time} {label}: {basename}:{function}:{line}: {indent}{message}\\n'
339
-
340
- Fields:
341
- {date} current date (see setDateFormat())
342
- {time} current time (see setTimeFormat())
343
- {label} what type of thing is getting logged (default: 'DC')
344
- {pid} numeric ID of the current process
345
- {pathname} full path of the calling source file
346
- {basename} base name of the calling source file
347
- {function} name of function debug.write() was called from
348
- {line} number of the calling line of code in its source file
349
- {code} the Python code at the given line of the given file
350
- {indent} indention string multiplied by the indention level
351
- {message} the message to be written
352
-
353
- All non-field text is literal text. The '\\n' at the end is required
354
- if you want a line ending at the end of each message. If your
355
- DebugChannel object is configured to write to a LogStream object
356
- that writes to syslog or something similar, you might want to remove
357
- the {date} and {time} (and maybe {label}) fields from the default
358
- format string to avoid logging these values redundantly."""
359
-
360
- self.fmt=fmt
361
-
362
- def indent(self,indent=1):
363
- """Increase this object's current indenture by this value (which
364
- might be negative. Return this DebugChannel opject with the adjusted
365
- indenture. See write() for how this might be used."""
366
-
367
- self.indlev+=indent
368
- if self.indlev<0:
369
- self.indlev=0
370
- return self
371
-
372
- def undent(self,indent=1):
373
- """Decrease this object's current indenture by this value (which
374
- might be negative. Return this DebugChannel object with the adjusted
375
- indenture. See write() for how this might be used."""
376
-
377
- return self.indent(-indent)
378
-
379
- def writelines(self,seq):
380
- """Just a wrapper around write(), since that method handles
381
- sequences (and other things) just fine. writelines() is only
382
- provided for compatibility with code that expects it to be
383
- supported."""
384
-
385
- return self.write(seq)
386
-
387
- def __call__(self,arg,*args,**kwargs):
388
- """Just a wrapper for the write() method. The message in the arg
389
- argument can be a single-line string, multi-line string, list,
390
- tuple, or dict. See write() for details.
391
- """
392
-
393
- lines=inspect.stack(context=2)[1].code_context
394
- if lines and any(l.lstrip().startswith('@') for l in lines):
395
- # We're being called as a decorator.
396
- def f(*args,**kwargs):
397
- # Record how this function is being called.
398
- sig=','.join([repr(a) for a in args]+[f"{k}={v!r}" for k,v in kwargs.items()])
399
- self.write(f"{arg.__name__}({sig}) ...").indent()
400
- t0=get_time()
401
- # Call the function we're wrapping
402
- ret=arg(*args,**kwargs)
403
- # Record this function's return.
404
- t1=get_time()
405
- sig='...' if sig else ''
406
- dt=t1-t0
407
- d,dt=divmod(dt,86400) # Days
408
- if d:
409
- d=f"{int(d)}d"
410
- else:
411
- d=''
412
- h,dt=divmod(dt,3600) # Hours
413
- if h:
414
- h=f"{int(h)}h"
415
- else:
416
- h='0h' if d else ''
417
- m,dt=divmod(dt,60) # Minutes
418
- if m:
419
- m=f"{int(m)}m"
420
- else:
421
- m='0m' if h else ''
422
- if m: # Seconds
423
- s=f"{int(dt)}s"
424
- else:
425
- if dt>=1: # Fractional seconds
426
- s=f"{dt:0.3f}"
427
- s=s[:4].rstrip('0')+'s'
428
- elif dt>=0.001: # Milliseconds
429
- s=f"{int(dt*1000)}ms"
430
- else: # Microseconds
431
- s=f"{int(dt*1e6)}µs"
432
- self.undent().write(f"{arg.__name__}({sig}) returns {ret!r} after {d}{h}{m}{s}.")
433
- return ret
434
- return f
435
-
436
- # We were called as a regulear method.
437
- return self.write(arg)
438
-
439
- def writeTraceback(self,exc):
440
- """Write the given exception with traceback information to our
441
- output stream."""
442
-
443
- if self.enabled:
444
- for line in traceback.format_exception(exc):
445
- self.write(line.rstrip())
446
-
447
- def write(self,message):
448
- """If our debug state is on (True), write the given message using the
449
- our current format. In any case, return this DebugChannel instance
450
- so that, for example, things like this will work:
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
- debug.write("Entering func(arg=%r)"%(arg,)).indent(1)
457
- for i in range(3):
458
- debug("i=%r"%(i,))
459
- debug.indent(-1).write("Leaving func()")
460
-
461
- This lets the caller decide whether to change indenture before or
462
- after the message is written.
463
-
464
- If message is a single string containing no line endings, that
465
- single value will be outout.
466
-
467
- if message contains at least one newline (the value of os.linesep),
468
- each line is output on a debug line of its own.
469
-
470
- If message is a list or tuple, each item in that sequence will be
471
- output on its own line.
472
-
473
- If message is a dictionary, each key/value pair is written out as
474
-
475
- key: value
476
-
477
- to its own log line. The keys are sorted in ascending order."""
478
-
479
- if self.enabled:
480
- # Update our formatted date and time if necessary.
481
- t=int(get_time()) # Let's truncate at whole seconds.
482
- if self._t!=t:
483
- t=self.time_tupler(t)
484
- self._t=t
485
- self.date=strftime(self.date_fmt,t)
486
- self.time=strftime(self.time_fmt,t)
487
- # Set local variables for date and time so they're available for output.
488
- date=self.date
489
- time=self.time
490
- # Find the first non-ignored stack frame whence we were called.
491
- pathname,basename,line=None,None,None
492
- for i,frame in enumerate(inspect.stack()):
493
- # This is for debugging debug.py. It turns out Python 3.6 has a bug in
494
- # inspect.stack() that can return outrageous values for frame.index.
495
- # (So I'm asking for only one line of context, and I've stopped using the
496
- # frame's untrustworthy index value.)
497
- # print(f"""{i}:
498
- # frame: {frame.frame!r}
499
- # filename: {frame.filename!r}
500
- # lineno: {frame.lineno!r}
501
- # function: {frame.function!r}
502
- # code_context: {frame.code_context!r}
503
- # index: {frame.index!r}""")
504
- p=os.path.normpath(frame.filename)
505
- if p not in self.ignore:
506
- break
507
- if frame.function not in self.ignore[p]:
508
- break
509
- # Set some local variables so they'll be available to our callback
510
- # function and for formatting.
511
- pid=self.pid
512
- pathname=os.path.normpath(frame.filename)
513
- basename=os.path.basename(pathname)
514
- line=frame.lineno
515
- function=frame.function
516
- if str(function)=='<module>':
517
- function='__main__'
518
- code=frame.code_context
519
- if code:
520
- code=code[0].rstrip()
521
- else:
522
- code=None
523
- indent=self.indstr*self.indlev
524
- label=self.label
525
-
526
- # If our caller provided a callback function, call that now.
527
- if self.callback:
528
- if not self.callback(**locals()):
529
- return self # Return without writing any output.
530
-
531
- # Format our message and write it to the debug stream.
532
- if isinstance(message,(list,tuple)):
533
- if isinstance(message,tuple):
534
- left,right='()'
535
- else:
536
- left,right='[]'
537
- messages=message
538
- message=left;self.stream.write(self.fmt.format(**locals()))
539
- for message in messages:
540
- message=self.indstr+message
541
- self.stream.write(self.fmt.format(**locals()))
542
- message=right;self.stream.write(self.fmt.format(**locals()))
543
- elif isinstance(message,dict):
544
- messages=dict(message)
545
- message='{';self.stream.write(self.fmt.format(**locals()))
546
- for k in messages.keys():
547
- message=f"{self.indstr}{k}: {messages[k]}"
548
- self.stream.write(self.fmt.format(**locals()))
549
- message='}';self.stream.write(self.fmt.format(**locals()))
550
- elif isinstance(message,str) and os.linesep in message:
551
- messages=message
552
- for message in line_iter(messages):
553
- self.stream.write(self.fmt.format(**locals()))
554
- else:
555
- self.stream.write(self.fmt.format(**locals()))
556
- self.stream.flush()
557
-
558
- # Let the caller call other methods by using our return value.
559
- return self
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