jc-debug 0.1.0__tar.gz
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.
- jc_debug-0.1.0/LICENSE +21 -0
- jc_debug-0.1.0/PKG-INFO +47 -0
- jc_debug-0.1.0/README.md +7 -0
- jc_debug-0.1.0/pyproject.toml +32 -0
- jc_debug-0.1.0/setup.cfg +4 -0
- jc_debug-0.1.0/src/debug/__init__.py +560 -0
- jc_debug-0.1.0/src/jc_debug.egg-info/PKG-INFO +47 -0
- jc_debug-0.1.0/src/jc_debug.egg-info/SOURCES.txt +8 -0
- jc_debug-0.1.0/src/jc_debug.egg-info/dependency_links.txt +1 -0
- jc_debug-0.1.0/src/jc_debug.egg-info/top_level.txt +1 -0
jc_debug-0.1.0/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Jeff Clough
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
jc_debug-0.1.0/PKG-INFO
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: jc-debug
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A collection of useful Python classes and functions.
|
5
|
+
Author-email: Jeff Clough <jeff@cloughcottage.com>
|
6
|
+
License: MIT License
|
7
|
+
|
8
|
+
Copyright (c) 2025 Jeff Clough
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
|
28
|
+
Project-URL: Homepage, https://github.com/jc-handy/debug
|
29
|
+
Keywords: utility,toolkit,convenience
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
31
|
+
Classifier: Intended Audience :: Developers
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
35
|
+
Classifier: Operating System :: OS Independent
|
36
|
+
Requires-Python: >=3.11
|
37
|
+
Description-Content-Type: text/markdown
|
38
|
+
License-File: LICENSE
|
39
|
+
Dynamic: license-file
|
40
|
+
|
41
|
+
# Debug
|
42
|
+
|
43
|
+
## Description
|
44
|
+
This package simplifies debug output and makes it more powerful.
|
45
|
+
|
46
|
+
## Installation
|
47
|
+
Run `python3 -m pip install jc-debug` to install it. This will install the package named "debug" in your site-packages.
|
jc_debug-0.1.0/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools>=61.0.0"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "jc-debug"
|
7
|
+
version = "0.1.0" # Consider using a proper versioning scheme
|
8
|
+
authors = [
|
9
|
+
{ name="Jeff Clough", email="jeff@cloughcottage.com" },
|
10
|
+
]
|
11
|
+
description = "A collection of useful Python classes and functions."
|
12
|
+
readme = "README.md"
|
13
|
+
license = { file="LICENSE" }
|
14
|
+
requires-python = ">=3.11"
|
15
|
+
# Add your dependencies here, for example:
|
16
|
+
# dependencies = [
|
17
|
+
# "requests>=2.28",
|
18
|
+
# "numpy>=1.24",
|
19
|
+
# ]
|
20
|
+
keywords = ["utility", "toolkit", "convenience"]
|
21
|
+
classifiers = [
|
22
|
+
"Development Status :: 3 - Alpha", # Adjust as your package matures
|
23
|
+
"Intended Audience :: Developers",
|
24
|
+
"License :: OSI Approved :: MIT License", # Update if you choose a different license
|
25
|
+
"Programming Language :: Python :: 3",
|
26
|
+
"Programming Language :: Python :: 3.11",
|
27
|
+
"Operating System :: OS Independent",
|
28
|
+
]
|
29
|
+
|
30
|
+
[project.urls]
|
31
|
+
"Homepage" = "https://github.com/jc-handy/debug"
|
32
|
+
#"Bug Tracker" = "https://your-bug-tracker-url.example.com/issues" # If you have one
|
jc_debug-0.1.0/setup.cfg
ADDED
@@ -0,0 +1,560 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
This debug module a class named DebugChannel, instances of which are
|
5
|
+
useful for adding temporary or conditional debug output to CLI scripts.
|
6
|
+
|
7
|
+
The minimal boilerplate is pretty simple:
|
8
|
+
|
9
|
+
from debug import DebugChannel
|
10
|
+
dc=DebugChannel(True)
|
11
|
+
|
12
|
+
By default, output is sent to stdandard error and formatted as:
|
13
|
+
|
14
|
+
'{label}: [{pid}] {basename}:{line}:{function}: {indent}{message}\\n'
|
15
|
+
|
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.
|
30
|
+
|
31
|
+
So, for example, if you want to see how your variables are behaving in a
|
32
|
+
loop, you might do something like this:
|
33
|
+
|
34
|
+
from debug import DebugChannel
|
35
|
+
|
36
|
+
dc=DebugChannel(
|
37
|
+
True,
|
38
|
+
fmt="{label}: {line:3}: {indent}{message}\\n"
|
39
|
+
)
|
40
|
+
|
41
|
+
dc("Entering loop ...").indent()
|
42
|
+
for i in range(5):
|
43
|
+
dc(f"i={i}").indent()
|
44
|
+
for j in range(3):
|
45
|
+
dc(f"j={j}")
|
46
|
+
dc.undent()("Done with j loop.")
|
47
|
+
dc.undent()("Done with i loop.")
|
48
|
+
|
49
|
+
That gives you this necely indented output. The indent() and undent()
|
50
|
+
methods are one thing that makes DebugChannels so nice to work with.
|
51
|
+
|
52
|
+
DC: 8: Entering loop ...
|
53
|
+
DC: 10: i=0
|
54
|
+
DC: 12: j=0
|
55
|
+
DC: 12: j=1
|
56
|
+
DC: 12: j=2
|
57
|
+
DC: 13: Done with j loop.
|
58
|
+
DC: 10: i=1
|
59
|
+
DC: 12: j=0
|
60
|
+
DC: 12: j=1
|
61
|
+
DC: 12: j=2
|
62
|
+
DC: 13: Done with j loop.
|
63
|
+
DC: 10: i=2
|
64
|
+
DC: 12: j=0
|
65
|
+
DC: 12: j=1
|
66
|
+
DC: 12: j=2
|
67
|
+
DC: 13: Done with j loop.
|
68
|
+
DC: 10: i=3
|
69
|
+
DC: 12: j=0
|
70
|
+
DC: 12: j=1
|
71
|
+
DC: 12: j=2
|
72
|
+
DC: 13: Done with j loop.
|
73
|
+
DC: 10: i=4
|
74
|
+
DC: 12: j=0
|
75
|
+
DC: 12: j=1
|
76
|
+
DC: 12: j=2
|
77
|
+
DC: 13: Done with j loop.
|
78
|
+
DC: 14: Done with i loop.
|
79
|
+
|
80
|
+
That's a simple example, but you might be starting to get an idea of
|
81
|
+
how versatile DebugChannel instances can be.
|
82
|
+
|
83
|
+
A DebugChannel can also be used as a function decorator:
|
84
|
+
|
85
|
+
import time
|
86
|
+
from debug import DebugChannel
|
87
|
+
|
88
|
+
def delay(**kwargs):
|
89
|
+
time.sleep(1)
|
90
|
+
return True
|
91
|
+
|
92
|
+
dc=DebugChannel(True,callback=delay)
|
93
|
+
dc.setFormat("{label}: {function}: {indent}{message}\\n")
|
94
|
+
|
95
|
+
@dc
|
96
|
+
def example1(msg):
|
97
|
+
print(msg)
|
98
|
+
|
99
|
+
@dc
|
100
|
+
def example2(msg,count):
|
101
|
+
for i in range(count):
|
102
|
+
example1(f"{i+1}: {msg}")
|
103
|
+
|
104
|
+
example2("First test",3)
|
105
|
+
example2("Second test",2)
|
106
|
+
|
107
|
+
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.
|
132
|
+
"""
|
133
|
+
|
134
|
+
__all__=[
|
135
|
+
'DebugChannel',
|
136
|
+
'gmtime',
|
137
|
+
'localtime',
|
138
|
+
]
|
139
|
+
__version__='0.1.0'
|
140
|
+
|
141
|
+
import inspect,os,sys,traceback
|
142
|
+
# Because I need "time" to be a local variable in DebugChannel.write() ...
|
143
|
+
from time import gmtime,localtime,strftime,time as get_time
|
144
|
+
|
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(
|
174
|
+
True,
|
175
|
+
stream=LogStream(facility='user'),
|
176
|
+
label='D',
|
177
|
+
fmt='{label}: {basename}({line}): {indent}{message}\\n'
|
178
|
+
)
|
179
|
+
d('Testing')
|
180
|
+
|
181
|
+
The output in /var/log/user.log (which might be a different path on
|
182
|
+
your system) might look like this:
|
183
|
+
|
184
|
+
Aug 16 22:58:16 pi4 x[18478] D: x(12): Testing
|
185
|
+
|
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.
|
189
|
+
|
190
|
+
Run this module directly with
|
191
|
+
|
192
|
+
python3 -m debug
|
193
|
+
|
194
|
+
to see a demonstration of indenture. The example code for that demo
|
195
|
+
is at the bottom of the debug.py source file.
|
196
|
+
|
197
|
+
IGNORING MODULES:
|
198
|
+
DebugChannel.write() goes to some length to ensure the filename and
|
199
|
+
line number reported in its output are something helpful to the
|
200
|
+
caller. For instance, the source line shouldn't be anything in this
|
201
|
+
DebugChannel class.
|
202
|
+
|
203
|
+
Use the ignoreModule() method to tell the DebugChannel object ignore
|
204
|
+
other modules, and optionally, specific functions within that
|
205
|
+
module."""
|
206
|
+
|
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
|
218
|
+
):
|
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
|
+
|
272
|
+
d=DebugChannel(opt.debug)
|
273
|
+
.
|
274
|
+
.
|
275
|
+
.
|
276
|
+
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
|
+
|
452
|
+
debug=DebugChannel(opt.debug)
|
453
|
+
debug('Testing')
|
454
|
+
|
455
|
+
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
|
560
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: jc-debug
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A collection of useful Python classes and functions.
|
5
|
+
Author-email: Jeff Clough <jeff@cloughcottage.com>
|
6
|
+
License: MIT License
|
7
|
+
|
8
|
+
Copyright (c) 2025 Jeff Clough
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
|
28
|
+
Project-URL: Homepage, https://github.com/jc-handy/debug
|
29
|
+
Keywords: utility,toolkit,convenience
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
31
|
+
Classifier: Intended Audience :: Developers
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
35
|
+
Classifier: Operating System :: OS Independent
|
36
|
+
Requires-Python: >=3.11
|
37
|
+
Description-Content-Type: text/markdown
|
38
|
+
License-File: LICENSE
|
39
|
+
Dynamic: license-file
|
40
|
+
|
41
|
+
# Debug
|
42
|
+
|
43
|
+
## Description
|
44
|
+
This package simplifies debug output and makes it more powerful.
|
45
|
+
|
46
|
+
## Installation
|
47
|
+
Run `python3 -m pip install jc-debug` to install it. This will install the package named "debug" in your site-packages.
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
debug
|