cs-debug 20250325__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.
- cs_debug-20250325/PKG-INFO +162 -0
- cs_debug-20250325/pyproject.toml +187 -0
- cs_debug-20250325/src/cs/debug.py +890 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cs-debug
|
|
3
|
+
Version: 20250325
|
|
4
|
+
Summary: Assorted debugging facilities.
|
|
5
|
+
Keywords: python2,python3
|
|
6
|
+
Author-email: Cameron Simpson <cs@cskk.id.au>
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Programming Language :: Python
|
|
9
|
+
Classifier: Programming Language :: Python :: 2
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
16
|
+
Requires-Dist: cs.deco>=20250306
|
|
17
|
+
Requires-Dist: cs.fs>=20250325
|
|
18
|
+
Requires-Dist: cs.lex>=20250323
|
|
19
|
+
Requires-Dist: cs.logutils>=20250323
|
|
20
|
+
Requires-Dist: cs.obj>=20250306
|
|
21
|
+
Requires-Dist: cs.pfx>=20250308
|
|
22
|
+
Requires-Dist: cs.py.func>=20240630
|
|
23
|
+
Requires-Dist: cs.py.stack>=20250306
|
|
24
|
+
Requires-Dist: cs.py3>=20220523
|
|
25
|
+
Requires-Dist: cs.seq>=20250306
|
|
26
|
+
Requires-Dist: cs.upd>=20240630
|
|
27
|
+
Requires-Dist: cs.x>=20240630
|
|
28
|
+
Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
|
|
29
|
+
Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
|
|
30
|
+
Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
|
|
31
|
+
Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/debug.py
|
|
32
|
+
|
|
33
|
+
Assorted debugging facilities.
|
|
34
|
+
|
|
35
|
+
*Latest release 20250325*:
|
|
36
|
+
* stack_dump: stack may also be a traceback object or an exception.
|
|
37
|
+
* stack_dump: move the logic to obtain the stack into cs.py.stack.frames().
|
|
38
|
+
|
|
39
|
+
If the environment variable `$CS_DEBUG_BUILTINS` is set to a comma
|
|
40
|
+
separated list of names then the `builtins` module will be monkey
|
|
41
|
+
patched with those names, enabling trite debug use of those names
|
|
42
|
+
anywhere in the code provided this module has been imported somewhere.
|
|
43
|
+
|
|
44
|
+
The allowed names are the list `cs.debug.__all__` and include:
|
|
45
|
+
* `X`: `cs.x.X`
|
|
46
|
+
* `abrk`: a decorator to call `breakpoint()` on an `AssertionError`
|
|
47
|
+
* `pformat`: `pprint.pformat`
|
|
48
|
+
* `pprint`: `pprint.pprint`
|
|
49
|
+
* `print`: `cs.upd.print`
|
|
50
|
+
* `r`: `cs.lex.r`
|
|
51
|
+
* `redirect_stdout`: `contextlib.redirect_stdout`
|
|
52
|
+
* `s`: `cs.lex.s`
|
|
53
|
+
* `stack_dump`: dump current `Thread`'s call stack
|
|
54
|
+
* `thread_dump` dump the active `Thread`s with their call stacks
|
|
55
|
+
* `trace`: the `@trace` decorator
|
|
56
|
+
`$CS_DEBUG_BUILTINS` can also be set to `"1"` to install all of
|
|
57
|
+
`__all__` in the builtins.
|
|
58
|
+
|
|
59
|
+
Module contents:
|
|
60
|
+
- <a name="abrk"></a>`abrk(*da, **dkw)`: A decorator to intercept certain exceptions
|
|
61
|
+
(by default `AssertionError`, `NameError`, `RuntimeError`)
|
|
62
|
+
and call `breakpoint()`.
|
|
63
|
+
The breakpoint frame contains:
|
|
64
|
+
- `func`: the wrapper function
|
|
65
|
+
- `func_a`, `func_kw`: the function positional and keyword arguments
|
|
66
|
+
- <a name="stack_dump"></a>`stack_dump(stack=None, limit=None, logger=None, log_level=None)`: Dump a stack trace to a logger.
|
|
67
|
+
|
|
68
|
+
Parameters:
|
|
69
|
+
* `stack`: a stack list as returned by `traceback.extract_stack`.
|
|
70
|
+
If missing or `None`, use the result of `traceback.extract_stack()`.
|
|
71
|
+
If `stack` has a `.tb_frame` or `.__traceback__` attribute,
|
|
72
|
+
extract the stack from that (this covers traceback objects and exceptions).
|
|
73
|
+
* `limit`: a limit to the number of stack entries to dump.
|
|
74
|
+
If missing or `None`, dump all entries.
|
|
75
|
+
* `logger`: a `logger.Logger` ducktype or the name of a logger.
|
|
76
|
+
If missing or `None`, obtain a logger from `logging.getLogger()`.
|
|
77
|
+
* `log_level`: the logging level for the dump.
|
|
78
|
+
If missing or `None`, use `cs.logutils.loginfo.level`.
|
|
79
|
+
- <a name="thread_dump"></a>`thread_dump(Ts=None, fp=None)`: Write thread identifiers and stack traces to the file `fp`.
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
* `Ts`: the `Thread`s to dump; if unspecified use `threading.enumerate()`.
|
|
83
|
+
* `fp`: the file to which to write; if unspecified use `sys.stderr`.
|
|
84
|
+
- <a name="TimingOutLock"></a>`Class `TimingOutLock`: A `Lock` replacement which times out, used for locating deadlock points.
|
|
85
|
+
- <a name="trace"></a>`trace(*da, **dkw)`: Decorator to report the call and return of a function.
|
|
86
|
+
|
|
87
|
+
Decorator parameters:
|
|
88
|
+
* `call`: trace the call, default `True`
|
|
89
|
+
* `retval`: trace the return, default `False`
|
|
90
|
+
* `exception`: trace raised exceptions, default `True`
|
|
91
|
+
* `use_pformat`: present the return value using
|
|
92
|
+
`pformat` instead of `repr`, default `False`
|
|
93
|
+
* `with_caller`: include the caller if this function, default `False`
|
|
94
|
+
* `with_pfx`: include the current `Pfx` prefix, default `False`
|
|
95
|
+
|
|
96
|
+
# Release Log
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
*Release 20250325*:
|
|
101
|
+
* stack_dump: stack may also be a traceback object or an exception.
|
|
102
|
+
* stack_dump: move the logic to obtain the stack into cs.py.stack.frames().
|
|
103
|
+
|
|
104
|
+
*Release 20241005*:
|
|
105
|
+
* New log_via_print(msg, *args[, file=stdout]) function to use cs.upd.print as a logging call.
|
|
106
|
+
* @trace: new $CS_DEBUG_TRACE envvar which may be "print" or "warning" or "X".
|
|
107
|
+
* New @abrk decorator to intercept AssertionError, NameError and RuntimeError and call breakpoint.
|
|
108
|
+
|
|
109
|
+
*Release 20240630*:
|
|
110
|
+
Assorted updates.
|
|
111
|
+
|
|
112
|
+
*Release 20240519*:
|
|
113
|
+
trace_caller: access frame.name instead of frame.funcname.
|
|
114
|
+
|
|
115
|
+
*Release 20240423*:
|
|
116
|
+
* Support "import *" by populating __all__ with X, r, s, TimingOutLock, thread_dump, stack_dump, trace.
|
|
117
|
+
* @trace: include the elapsed time on the return/exception log message.
|
|
118
|
+
|
|
119
|
+
*Release 20230613.1*:
|
|
120
|
+
Bugfix builtins monkey patch.
|
|
121
|
+
|
|
122
|
+
*Release 20230613*:
|
|
123
|
+
Honour $CS_DEBUG_BUILTINS envvar to monkey patch the builtins module, constraints via a white list.
|
|
124
|
+
|
|
125
|
+
*Release 20230610*:
|
|
126
|
+
* DebuggingRLock fixes.
|
|
127
|
+
* Move @trace from cs.py.func to cs.debug.
|
|
128
|
+
* Drop Lock and RLock alias factories - importers should just use the debugging lock classes directly.
|
|
129
|
+
* Rename threading.Thread to threading_Thread.
|
|
130
|
+
* Simplify the debugging lock classes.
|
|
131
|
+
|
|
132
|
+
*Release 20221118*:
|
|
133
|
+
stack_dump: cope when cs.logutils.setup_logging not run yet.
|
|
134
|
+
|
|
135
|
+
*Release 20211208*:
|
|
136
|
+
@trace moved to cs.pyfunc, other minor changes.
|
|
137
|
+
|
|
138
|
+
*Release 20200318*:
|
|
139
|
+
Remove use of cs.obj.O, universally supplanted by types.SimpleNamespace.
|
|
140
|
+
|
|
141
|
+
*Release 20181231*:
|
|
142
|
+
* New TimingOutLock for locating deadlock points, grew from debugging cs.vt.index.
|
|
143
|
+
* Other minor changes.
|
|
144
|
+
|
|
145
|
+
*Release 20171231*:
|
|
146
|
+
* Update imports for recentchanges.
|
|
147
|
+
* New context manager TraceSuite to trace start and end of a code suite.
|
|
148
|
+
|
|
149
|
+
*Release 20160918*:
|
|
150
|
+
selftest(): fix parameter ordering to match unittest.
|
|
151
|
+
|
|
152
|
+
*Release 20160828*:
|
|
153
|
+
Update metadata with "install_requires" instead of "requires".
|
|
154
|
+
|
|
155
|
+
*Release 20160827*:
|
|
156
|
+
* New openfiles() to return selected pathnames of open files via lsof(8).
|
|
157
|
+
* New selftest() to invoke unittests with benefits.
|
|
158
|
+
* DebugShell, a cmd.Cmd subclass for debugging - current use case calls this with self.__dict__ in a test case tearDwon.
|
|
159
|
+
* debug_object_shell: convenience wrapper for DebugShell to call it on an object's attributes.
|
|
160
|
+
|
|
161
|
+
*Release 20150116*:
|
|
162
|
+
PyPI prep.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cs-debug"
|
|
3
|
+
description = "Assorted debugging facilities."
|
|
4
|
+
authors = [
|
|
5
|
+
{ name = "Cameron Simpson", email = "cs@cskk.id.au" },
|
|
6
|
+
]
|
|
7
|
+
keywords = [
|
|
8
|
+
"python2",
|
|
9
|
+
"python3",
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"cs.deco>=20250306",
|
|
13
|
+
"cs.fs>=20250325",
|
|
14
|
+
"cs.lex>=20250323",
|
|
15
|
+
"cs.logutils>=20250323",
|
|
16
|
+
"cs.obj>=20250306",
|
|
17
|
+
"cs.pfx>=20250308",
|
|
18
|
+
"cs.py.func>=20240630",
|
|
19
|
+
"cs.py.stack>=20250306",
|
|
20
|
+
"cs.py3>=20220523",
|
|
21
|
+
"cs.seq>=20250306",
|
|
22
|
+
"cs.upd>=20240630",
|
|
23
|
+
"cs.x>=20240630",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Programming Language :: Python",
|
|
27
|
+
"Programming Language :: Python :: 2",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Development Status :: 4 - Beta",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"Operating System :: OS Independent",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
34
|
+
]
|
|
35
|
+
version = "20250325"
|
|
36
|
+
|
|
37
|
+
[project.license]
|
|
38
|
+
text = "GNU General Public License v3 or later (GPLv3+)"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
"Monorepo Hg/Mercurial Mirror" = "https://hg.sr.ht/~cameron-simpson/css"
|
|
42
|
+
"Monorepo Git Mirror" = "https://github.com/cameron-simpson/css"
|
|
43
|
+
"MonoRepo Commits" = "https://bitbucket.org/cameron_simpson/css/commits/branch/main"
|
|
44
|
+
Source = "https://github.com/cameron-simpson/css/blob/main/lib/python/cs/debug.py"
|
|
45
|
+
|
|
46
|
+
[project.readme]
|
|
47
|
+
text = """
|
|
48
|
+
Assorted debugging facilities.
|
|
49
|
+
|
|
50
|
+
*Latest release 20250325*:
|
|
51
|
+
* stack_dump: stack may also be a traceback object or an exception.
|
|
52
|
+
* stack_dump: move the logic to obtain the stack into cs.py.stack.frames().
|
|
53
|
+
|
|
54
|
+
If the environment variable `$CS_DEBUG_BUILTINS` is set to a comma
|
|
55
|
+
separated list of names then the `builtins` module will be monkey
|
|
56
|
+
patched with those names, enabling trite debug use of those names
|
|
57
|
+
anywhere in the code provided this module has been imported somewhere.
|
|
58
|
+
|
|
59
|
+
The allowed names are the list `cs.debug.__all__` and include:
|
|
60
|
+
* `X`: `cs.x.X`
|
|
61
|
+
* `abrk`: a decorator to call `breakpoint()` on an `AssertionError`
|
|
62
|
+
* `pformat`: `pprint.pformat`
|
|
63
|
+
* `pprint`: `pprint.pprint`
|
|
64
|
+
* `print`: `cs.upd.print`
|
|
65
|
+
* `r`: `cs.lex.r`
|
|
66
|
+
* `redirect_stdout`: `contextlib.redirect_stdout`
|
|
67
|
+
* `s`: `cs.lex.s`
|
|
68
|
+
* `stack_dump`: dump current `Thread`'s call stack
|
|
69
|
+
* `thread_dump` dump the active `Thread`s with their call stacks
|
|
70
|
+
* `trace`: the `@trace` decorator
|
|
71
|
+
`$CS_DEBUG_BUILTINS` can also be set to `\"1\"` to install all of
|
|
72
|
+
`__all__` in the builtins.
|
|
73
|
+
|
|
74
|
+
Module contents:
|
|
75
|
+
- <a name=\"abrk\"></a>`abrk(*da, **dkw)`: A decorator to intercept certain exceptions
|
|
76
|
+
(by default `AssertionError`, `NameError`, `RuntimeError`)
|
|
77
|
+
and call `breakpoint()`.
|
|
78
|
+
The breakpoint frame contains:
|
|
79
|
+
- `func`: the wrapper function
|
|
80
|
+
- `func_a`, `func_kw`: the function positional and keyword arguments
|
|
81
|
+
- <a name=\"stack_dump\"></a>`stack_dump(stack=None, limit=None, logger=None, log_level=None)`: Dump a stack trace to a logger.
|
|
82
|
+
|
|
83
|
+
Parameters:
|
|
84
|
+
* `stack`: a stack list as returned by `traceback.extract_stack`.
|
|
85
|
+
If missing or `None`, use the result of `traceback.extract_stack()`.
|
|
86
|
+
If `stack` has a `.tb_frame` or `.__traceback__` attribute,
|
|
87
|
+
extract the stack from that (this covers traceback objects and exceptions).
|
|
88
|
+
* `limit`: a limit to the number of stack entries to dump.
|
|
89
|
+
If missing or `None`, dump all entries.
|
|
90
|
+
* `logger`: a `logger.Logger` ducktype or the name of a logger.
|
|
91
|
+
If missing or `None`, obtain a logger from `logging.getLogger()`.
|
|
92
|
+
* `log_level`: the logging level for the dump.
|
|
93
|
+
If missing or `None`, use `cs.logutils.loginfo.level`.
|
|
94
|
+
- <a name=\"thread_dump\"></a>`thread_dump(Ts=None, fp=None)`: Write thread identifiers and stack traces to the file `fp`.
|
|
95
|
+
|
|
96
|
+
Parameters:
|
|
97
|
+
* `Ts`: the `Thread`s to dump; if unspecified use `threading.enumerate()`.
|
|
98
|
+
* `fp`: the file to which to write; if unspecified use `sys.stderr`.
|
|
99
|
+
- <a name=\"TimingOutLock\"></a>`Class `TimingOutLock`: A `Lock` replacement which times out, used for locating deadlock points.
|
|
100
|
+
- <a name=\"trace\"></a>`trace(*da, **dkw)`: Decorator to report the call and return of a function.
|
|
101
|
+
|
|
102
|
+
Decorator parameters:
|
|
103
|
+
* `call`: trace the call, default `True`
|
|
104
|
+
* `retval`: trace the return, default `False`
|
|
105
|
+
* `exception`: trace raised exceptions, default `True`
|
|
106
|
+
* `use_pformat`: present the return value using
|
|
107
|
+
`pformat` instead of `repr`, default `False`
|
|
108
|
+
* `with_caller`: include the caller if this function, default `False`
|
|
109
|
+
* `with_pfx`: include the current `Pfx` prefix, default `False`
|
|
110
|
+
|
|
111
|
+
# Release Log
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
*Release 20250325*:
|
|
116
|
+
* stack_dump: stack may also be a traceback object or an exception.
|
|
117
|
+
* stack_dump: move the logic to obtain the stack into cs.py.stack.frames().
|
|
118
|
+
|
|
119
|
+
*Release 20241005*:
|
|
120
|
+
* New log_via_print(msg, *args[, file=stdout]) function to use cs.upd.print as a logging call.
|
|
121
|
+
* @trace: new $CS_DEBUG_TRACE envvar which may be \"print\" or \"warning\" or \"X\".
|
|
122
|
+
* New @abrk decorator to intercept AssertionError, NameError and RuntimeError and call breakpoint.
|
|
123
|
+
|
|
124
|
+
*Release 20240630*:
|
|
125
|
+
Assorted updates.
|
|
126
|
+
|
|
127
|
+
*Release 20240519*:
|
|
128
|
+
trace_caller: access frame.name instead of frame.funcname.
|
|
129
|
+
|
|
130
|
+
*Release 20240423*:
|
|
131
|
+
* Support \"import *\" by populating __all__ with X, r, s, TimingOutLock, thread_dump, stack_dump, trace.
|
|
132
|
+
* @trace: include the elapsed time on the return/exception log message.
|
|
133
|
+
|
|
134
|
+
*Release 20230613.1*:
|
|
135
|
+
Bugfix builtins monkey patch.
|
|
136
|
+
|
|
137
|
+
*Release 20230613*:
|
|
138
|
+
Honour $CS_DEBUG_BUILTINS envvar to monkey patch the builtins module, constraints via a white list.
|
|
139
|
+
|
|
140
|
+
*Release 20230610*:
|
|
141
|
+
* DebuggingRLock fixes.
|
|
142
|
+
* Move @trace from cs.py.func to cs.debug.
|
|
143
|
+
* Drop Lock and RLock alias factories - importers should just use the debugging lock classes directly.
|
|
144
|
+
* Rename threading.Thread to threading_Thread.
|
|
145
|
+
* Simplify the debugging lock classes.
|
|
146
|
+
|
|
147
|
+
*Release 20221118*:
|
|
148
|
+
stack_dump: cope when cs.logutils.setup_logging not run yet.
|
|
149
|
+
|
|
150
|
+
*Release 20211208*:
|
|
151
|
+
@trace moved to cs.pyfunc, other minor changes.
|
|
152
|
+
|
|
153
|
+
*Release 20200318*:
|
|
154
|
+
Remove use of cs.obj.O, universally supplanted by types.SimpleNamespace.
|
|
155
|
+
|
|
156
|
+
*Release 20181231*:
|
|
157
|
+
* New TimingOutLock for locating deadlock points, grew from debugging cs.vt.index.
|
|
158
|
+
* Other minor changes.
|
|
159
|
+
|
|
160
|
+
*Release 20171231*:
|
|
161
|
+
* Update imports for recentchanges.
|
|
162
|
+
* New context manager TraceSuite to trace start and end of a code suite.
|
|
163
|
+
|
|
164
|
+
*Release 20160918*:
|
|
165
|
+
selftest(): fix parameter ordering to match unittest.
|
|
166
|
+
|
|
167
|
+
*Release 20160828*:
|
|
168
|
+
Update metadata with \"install_requires\" instead of \"requires\".
|
|
169
|
+
|
|
170
|
+
*Release 20160827*:
|
|
171
|
+
* New openfiles() to return selected pathnames of open files via lsof(8).
|
|
172
|
+
* New selftest() to invoke unittests with benefits.
|
|
173
|
+
* DebugShell, a cmd.Cmd subclass for debugging - current use case calls this with self.__dict__ in a test case tearDwon.
|
|
174
|
+
* debug_object_shell: convenience wrapper for DebugShell to call it on an object's attributes.
|
|
175
|
+
|
|
176
|
+
*Release 20150116*:
|
|
177
|
+
PyPI prep."""
|
|
178
|
+
content-type = "text/markdown"
|
|
179
|
+
|
|
180
|
+
[build-system]
|
|
181
|
+
build-backend = "flit_core.buildapi"
|
|
182
|
+
requires = [
|
|
183
|
+
"flit_core >=3.2,<4",
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
[tool.flit.module]
|
|
187
|
+
name = "cs.debug"
|
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
#
|
|
3
|
+
# Assorted debugging facilities.
|
|
4
|
+
# - Cameron Simpson <cs@cskk.id.au> 20apr2013
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
r'''
|
|
8
|
+
Assorted debugging facilities.
|
|
9
|
+
|
|
10
|
+
If the environment variable `$CS_DEBUG_BUILTINS` is set to a comma
|
|
11
|
+
separated list of names then the `builtins` module will be monkey
|
|
12
|
+
patched with those names, enabling trite debug use of those names
|
|
13
|
+
anywhere in the code provided this module has been imported somewhere.
|
|
14
|
+
|
|
15
|
+
The allowed names are the list `cs.debug.__all__` and include:
|
|
16
|
+
* `X`: `cs.x.X`
|
|
17
|
+
* `abrk`: a decorator to call `breakpoint()` on an `AssertionError`
|
|
18
|
+
* `pformat`: `pprint.pformat`
|
|
19
|
+
* `pprint`: `pprint.pprint`
|
|
20
|
+
* `print`: `cs.upd.print`
|
|
21
|
+
* `r`: `cs.lex.r`
|
|
22
|
+
* `redirect_stdout`: `contextlib.redirect_stdout`
|
|
23
|
+
* `s`: `cs.lex.s`
|
|
24
|
+
* `stack_dump`: dump current `Thread`'s call stack
|
|
25
|
+
* `thread_dump` dump the active `Thread`s with their call stacks
|
|
26
|
+
* `trace`: the `@trace` decorator
|
|
27
|
+
`$CS_DEBUG_BUILTINS` can also be set to `"1"` to install all of
|
|
28
|
+
`__all__` in the builtins.
|
|
29
|
+
'''
|
|
30
|
+
|
|
31
|
+
from __future__ import print_function
|
|
32
|
+
from cmd import Cmd
|
|
33
|
+
from contextlib import redirect_stdout
|
|
34
|
+
import inspect
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
from pprint import pformat, pprint # pylint: disable=unused-import
|
|
38
|
+
from subprocess import Popen, PIPE
|
|
39
|
+
import sys
|
|
40
|
+
from threading import (
|
|
41
|
+
enumerate as enumerate_threads,
|
|
42
|
+
Lock as threading_Lock,
|
|
43
|
+
RLock as threading_RLock,
|
|
44
|
+
Thread as threading_Thread,
|
|
45
|
+
)
|
|
46
|
+
import time
|
|
47
|
+
import traceback
|
|
48
|
+
from types import SimpleNamespace as NS
|
|
49
|
+
|
|
50
|
+
from cs.deco import ALL, decorator
|
|
51
|
+
from cs.fs import shortpath
|
|
52
|
+
from cs.lex import s, r, is_identifier, is_dotted_identifier # pylint: disable=unused-import
|
|
53
|
+
import cs.logutils
|
|
54
|
+
from cs.logutils import debug, error, warning, D, ifdebug, loginfo
|
|
55
|
+
from cs.obj import Proxy
|
|
56
|
+
from cs.pfx import Pfx
|
|
57
|
+
from cs.py.func import funccite, funcname, func_a_kw_fmt
|
|
58
|
+
from cs.py.stack import caller, frames
|
|
59
|
+
from cs.py3 import Queue, Queue_Empty, exec_code
|
|
60
|
+
from cs.seq import seq
|
|
61
|
+
from cs.threads import ThreadState
|
|
62
|
+
from cs.upd import print # pylint: disable=redefined-builtin
|
|
63
|
+
from cs.x import X
|
|
64
|
+
|
|
65
|
+
__version__ = '20250325'
|
|
66
|
+
|
|
67
|
+
DISTINFO = {
|
|
68
|
+
'keywords': ["python2", "python3"],
|
|
69
|
+
'classifiers': [
|
|
70
|
+
"Programming Language :: Python",
|
|
71
|
+
"Programming Language :: Python :: 2",
|
|
72
|
+
"Programming Language :: Python :: 3",
|
|
73
|
+
],
|
|
74
|
+
'install_requires': [
|
|
75
|
+
'cs.deco',
|
|
76
|
+
'cs.fs',
|
|
77
|
+
'cs.lex',
|
|
78
|
+
'cs.logutils',
|
|
79
|
+
'cs.obj',
|
|
80
|
+
'cs.pfx',
|
|
81
|
+
'cs.py.func',
|
|
82
|
+
'cs.py.stack',
|
|
83
|
+
'cs.py3',
|
|
84
|
+
'cs.seq',
|
|
85
|
+
'cs.upd',
|
|
86
|
+
'cs.x',
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
__all__ = ['X', 'pformat', 'pprint', 'print', 'r', 'redirect_stdout', 's']
|
|
91
|
+
|
|
92
|
+
# environment variable specifying names to become built in
|
|
93
|
+
CS_DEBUG_BUILTINS_ENVVAR = 'CS_DEBUG_BUILTINS'
|
|
94
|
+
|
|
95
|
+
# white list of allowed builtin names
|
|
96
|
+
CS_DEBUG_BUILTINS_NAMES = ('X', 'pformat', 'pprint', 's', 'r', 'trace')
|
|
97
|
+
|
|
98
|
+
# @DEBUG dispatches a thread to monitor function elapsed time.
|
|
99
|
+
# This is how often it polls for function completion.
|
|
100
|
+
DEBUG_POLL_RATE = 0.25
|
|
101
|
+
|
|
102
|
+
@ALL
|
|
103
|
+
class TimingOutLock(object):
|
|
104
|
+
''' A `Lock` replacement which times out, used for locating deadlock points.
|
|
105
|
+
'''
|
|
106
|
+
|
|
107
|
+
def __init__(self, deadlock_timeout=20.0, recursive=False):
|
|
108
|
+
self._lock = threading_RLock() if recursive else threading_Lock()
|
|
109
|
+
self._deadlock_timeout = deadlock_timeout
|
|
110
|
+
|
|
111
|
+
def acquire(self, blocking=True, timeout=-1, name=None):
|
|
112
|
+
if timeout < 0:
|
|
113
|
+
timeout = self._deadlock_timeout
|
|
114
|
+
else:
|
|
115
|
+
timeout = min(timeout, self._deadlock_timeout)
|
|
116
|
+
ok = (
|
|
117
|
+
self._lock.acquire(timeout=timeout)
|
|
118
|
+
if blocking else self._lock.acquire(blocking=blocking)
|
|
119
|
+
)
|
|
120
|
+
if not ok:
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
"TIMEOUT acquiring lock held by %s:%r" %
|
|
123
|
+
(self.owner, self.owner_name)
|
|
124
|
+
)
|
|
125
|
+
self.owner = caller()
|
|
126
|
+
self.owner_name = name
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def release(self):
|
|
130
|
+
return self._lock.release()
|
|
131
|
+
|
|
132
|
+
def __enter__(self):
|
|
133
|
+
self.acquire()
|
|
134
|
+
self.owner = caller()
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def __exit__(self, *a):
|
|
138
|
+
return self._lock.__exit__(*a)
|
|
139
|
+
|
|
140
|
+
class TraceSuite(object):
|
|
141
|
+
''' Context manager to trace start and end of a code suite.
|
|
142
|
+
'''
|
|
143
|
+
|
|
144
|
+
def __init__(self, msg, *a):
|
|
145
|
+
if a:
|
|
146
|
+
msg = msg % a
|
|
147
|
+
self.msg = msg
|
|
148
|
+
|
|
149
|
+
def __enter__(self):
|
|
150
|
+
X("TraceSuite ENTER %s", self.msg)
|
|
151
|
+
|
|
152
|
+
def __exit__(self, exc_type, exc_value, exc_tb):
|
|
153
|
+
X("TraceSuite LEAVE %s: exc_value=%s", self.msg, exc_value)
|
|
154
|
+
|
|
155
|
+
def Thread(*a, **kw):
|
|
156
|
+
if not ifdebug():
|
|
157
|
+
return threading_Thread(*a, **kw)
|
|
158
|
+
filename, lineno = inspect.stack()[1][1:3]
|
|
159
|
+
return DebuggingThread({'filename': filename, 'lineno': lineno}, *a, **kw)
|
|
160
|
+
|
|
161
|
+
@ALL
|
|
162
|
+
def thread_dump(Ts=None, fp=None):
|
|
163
|
+
''' Write thread identifiers and stack traces to the file `fp`.
|
|
164
|
+
|
|
165
|
+
Parameters:
|
|
166
|
+
* `Ts`: the `Thread`s to dump; if unspecified use `threading.enumerate()`.
|
|
167
|
+
* `fp`: the file to which to write; if unspecified use `sys.stderr`.
|
|
168
|
+
'''
|
|
169
|
+
if Ts is None:
|
|
170
|
+
Ts = enumerate_threads()
|
|
171
|
+
if fp is None:
|
|
172
|
+
fp = sys.stderr
|
|
173
|
+
with Pfx("thread_dump"):
|
|
174
|
+
frames = sys._current_frames()
|
|
175
|
+
for T in Ts:
|
|
176
|
+
try:
|
|
177
|
+
frame = frames[T.ident]
|
|
178
|
+
except KeyError:
|
|
179
|
+
warning("no frame for Thread.ident=%s", T.ident)
|
|
180
|
+
continue
|
|
181
|
+
print("Thread", T.ident, T.name, T, file=fp)
|
|
182
|
+
traceback.print_stack(frame, None, fp)
|
|
183
|
+
print(file=fp)
|
|
184
|
+
|
|
185
|
+
@ALL
|
|
186
|
+
def stack_dump(stack=None, limit=None, logger=None, log_level=None):
|
|
187
|
+
''' Dump a stack trace to a logger.
|
|
188
|
+
|
|
189
|
+
Parameters:
|
|
190
|
+
* `stack`: a stack list as returned by `traceback.extract_stack`.
|
|
191
|
+
If missing or `None`, use the result of `traceback.extract_stack()`.
|
|
192
|
+
If `stack` has a `.tb_frame` or `.__traceback__` attribute,
|
|
193
|
+
extract the stack from that (this covers traceback objects and exceptions).
|
|
194
|
+
* `limit`: a limit to the number of stack entries to dump.
|
|
195
|
+
If missing or `None`, dump all entries.
|
|
196
|
+
* `logger`: a `logger.Logger` ducktype or the name of a logger.
|
|
197
|
+
If missing or `None`, obtain a logger from `logging.getLogger()`.
|
|
198
|
+
* `log_level`: the logging level for the dump.
|
|
199
|
+
If missing or `None`, use `cs.logutils.loginfo.level`.
|
|
200
|
+
'''
|
|
201
|
+
stack = frames(stack, limit=limit)
|
|
202
|
+
if logger is None:
|
|
203
|
+
logger = logging.getLogger()
|
|
204
|
+
elif isinstance(logger, str):
|
|
205
|
+
logger = logging.getLogger(logger)
|
|
206
|
+
if log_level is None:
|
|
207
|
+
log_level = getattr(loginfo, 'level', logging.WARNING)
|
|
208
|
+
for text in traceback.format_list(stack):
|
|
209
|
+
for line in text.splitlines():
|
|
210
|
+
logger.log(log_level, line.rstrip())
|
|
211
|
+
|
|
212
|
+
def DEBUG(f, force=False):
|
|
213
|
+
''' Decorator to wrap functions in timing and value debuggers.
|
|
214
|
+
'''
|
|
215
|
+
from cs.result import Result
|
|
216
|
+
|
|
217
|
+
def inner(*a, **kw):
|
|
218
|
+
if not force and not ifdebug():
|
|
219
|
+
return f(*a, **kw)
|
|
220
|
+
filename, lineno = inspect.stack()[1][1:3]
|
|
221
|
+
n = seq()
|
|
222
|
+
R = Result()
|
|
223
|
+
T = threading_Thread(
|
|
224
|
+
target=_debug_watcher, args=(filename, lineno, n, f.__name__, R)
|
|
225
|
+
)
|
|
226
|
+
T.daemon = True
|
|
227
|
+
T.start()
|
|
228
|
+
debug(
|
|
229
|
+
"%s:%d: [%d] call %s(*%r, **%r)", filename, lineno, n, f.__name__, a,
|
|
230
|
+
kw
|
|
231
|
+
)
|
|
232
|
+
start = time.time()
|
|
233
|
+
try:
|
|
234
|
+
retval = f(*a, **kw)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
error("EXCEPTION from %s(*%s, **%s): %s", f, a, kw, e)
|
|
237
|
+
raise
|
|
238
|
+
end = time.time()
|
|
239
|
+
debug(
|
|
240
|
+
"%s:%d: [%d] called %s, elapsed %gs, got %r", filename, lineno, n,
|
|
241
|
+
f.__name__, end - start, retval
|
|
242
|
+
)
|
|
243
|
+
R.put(retval)
|
|
244
|
+
return retval
|
|
245
|
+
|
|
246
|
+
return inner
|
|
247
|
+
|
|
248
|
+
def _debug_watcher(filename, lineno, n, funcname, R):
|
|
249
|
+
slow = 2
|
|
250
|
+
sofar = 0
|
|
251
|
+
slowness = 0
|
|
252
|
+
while not R.ready:
|
|
253
|
+
if slowness >= slow:
|
|
254
|
+
debug(
|
|
255
|
+
"%s:%d: [%d] calling %s, %gs elapsed so far...", filename, lineno, n,
|
|
256
|
+
funcname, sofar
|
|
257
|
+
)
|
|
258
|
+
# reset report time and complain more slowly next time
|
|
259
|
+
slowness = 0
|
|
260
|
+
slow += 1
|
|
261
|
+
time.sleep(DEBUG_POLL_RATE)
|
|
262
|
+
sofar += DEBUG_POLL_RATE
|
|
263
|
+
slowness += DEBUG_POLL_RATE
|
|
264
|
+
|
|
265
|
+
def DF(func, *a, **kw):
|
|
266
|
+
''' Wrapper for a function call to debug its use.
|
|
267
|
+
|
|
268
|
+
This requires rewriting the call from `f(*a,*kw)` to `DF(f,*a,**kw)`.
|
|
269
|
+
Alternatively one could rewrite as `DEBUG(f)(*a,**kw)`.
|
|
270
|
+
'''
|
|
271
|
+
return DEBUG(func, force=True)(*a, **kw)
|
|
272
|
+
|
|
273
|
+
class DebugWrapper(NS):
|
|
274
|
+
''' Base class for classes presenting debugging wrappers.
|
|
275
|
+
'''
|
|
276
|
+
|
|
277
|
+
def debug(self, msg, *a):
|
|
278
|
+
if a:
|
|
279
|
+
msg = msg % a
|
|
280
|
+
cs.logutils.debug(': '.join((self.debug_label, msg)))
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def debug_label(self):
|
|
284
|
+
info = '%s:%d' % (self.filename, self.lineno)
|
|
285
|
+
try:
|
|
286
|
+
context = self.context
|
|
287
|
+
except AttributeError:
|
|
288
|
+
pass
|
|
289
|
+
else:
|
|
290
|
+
info = ':'.join(info, str(context))
|
|
291
|
+
label = '%s-%d[%s]' % (self.__class__.__name__, id(self), info)
|
|
292
|
+
return label
|
|
293
|
+
|
|
294
|
+
class DebuggingLock(DebugWrapper):
|
|
295
|
+
''' Wrapper class for `threading.Lock` to trace creation and use.
|
|
296
|
+
|
|
297
|
+
`cs.threads.Lock()` returns one of these in debug mode or a raw
|
|
298
|
+
`threading.Lock` otherwise.
|
|
299
|
+
'''
|
|
300
|
+
|
|
301
|
+
def __init__(self, *, slow=2, **dkw):
|
|
302
|
+
DebugWrapper.__init__(self, **dkw)
|
|
303
|
+
self.debug("__init__(slow=%r)", slow)
|
|
304
|
+
if slow <= 0:
|
|
305
|
+
raise ValueError("slow must be positive, received: %r" % (slow,))
|
|
306
|
+
self.slow = slow
|
|
307
|
+
self.lock = threading_Lock()
|
|
308
|
+
self.held = None
|
|
309
|
+
|
|
310
|
+
def __enter__(self):
|
|
311
|
+
##self.lock.__enter__()
|
|
312
|
+
self.acquire()
|
|
313
|
+
return self
|
|
314
|
+
|
|
315
|
+
def __exit__(self, *a):
|
|
316
|
+
##return self.lock.__exit__(*a)
|
|
317
|
+
self.release()
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
def acquire(self, *a):
|
|
321
|
+
''' Acquire the lock.
|
|
322
|
+
'''
|
|
323
|
+
# quietly support Python 3 arguments after blocking parameter
|
|
324
|
+
blocking = True
|
|
325
|
+
if a:
|
|
326
|
+
blocking = a[0]
|
|
327
|
+
a = a[1:]
|
|
328
|
+
filename, lineno = inspect.stack()[1][1:3]
|
|
329
|
+
debug("%s:%d: acquire(blocking=%s)", filename, lineno, blocking)
|
|
330
|
+
if blocking:
|
|
331
|
+
# blocking
|
|
332
|
+
# try non-blocking first
|
|
333
|
+
# if successful, good
|
|
334
|
+
# otherwise spawn a monitoring thread to report on slow acquisition
|
|
335
|
+
# and block
|
|
336
|
+
taken = self.lock.acquire(False)
|
|
337
|
+
if not taken:
|
|
338
|
+
Q = Queue()
|
|
339
|
+
T = Thread(target=self._timed_acquire, args=(Q, filename, lineno))
|
|
340
|
+
T.daemon = True
|
|
341
|
+
T.start()
|
|
342
|
+
taken = self.lock.acquire(blocking, *a)
|
|
343
|
+
Q.put(taken)
|
|
344
|
+
else:
|
|
345
|
+
# non-blocking: do ordinary lock acquisition
|
|
346
|
+
taken = self.lock.acquire(blocking, *a)
|
|
347
|
+
if taken:
|
|
348
|
+
self.held = (filename, lineno)
|
|
349
|
+
return taken
|
|
350
|
+
|
|
351
|
+
def release(self):
|
|
352
|
+
''' Release the lock.
|
|
353
|
+
'''
|
|
354
|
+
filename, lineno = inspect.stack()[0][1:3]
|
|
355
|
+
debug("%s:%d: release()", filename, lineno)
|
|
356
|
+
self.held = None
|
|
357
|
+
self.lock.release()
|
|
358
|
+
|
|
359
|
+
def _timed_acquire(self, Q, filename, lineno):
|
|
360
|
+
''' Block waiting for lock acquisition.
|
|
361
|
+
Report slow acquisition.
|
|
362
|
+
|
|
363
|
+
This would be inline above except that Python 2 `Lock`s do
|
|
364
|
+
not have a timeout parameter, hence this thread.
|
|
365
|
+
This probably scales VERY badly if there is a lot of `Lock`
|
|
366
|
+
contention.
|
|
367
|
+
'''
|
|
368
|
+
slow = self.slow
|
|
369
|
+
sofar = 0
|
|
370
|
+
slowness = 0
|
|
371
|
+
while True:
|
|
372
|
+
# block until lock acquired
|
|
373
|
+
try:
|
|
374
|
+
Q.get(True, 1)
|
|
375
|
+
except Queue_Empty:
|
|
376
|
+
sofar += 1
|
|
377
|
+
slowness += 1
|
|
378
|
+
if slowness >= slow:
|
|
379
|
+
self.debug(
|
|
380
|
+
"from %s:%d: acquire: after %gs, held by %s", filename, lineno,
|
|
381
|
+
sofar, self.held
|
|
382
|
+
)
|
|
383
|
+
# complain more slowly next time
|
|
384
|
+
slowness = 0
|
|
385
|
+
slow += 1
|
|
386
|
+
else:
|
|
387
|
+
break
|
|
388
|
+
|
|
389
|
+
class DebuggingRLock(DebugWrapper):
|
|
390
|
+
''' Wrapper class for threading.RLock to trace creation and use.
|
|
391
|
+
|
|
392
|
+
`cs.threads.RLock()` returns on of these in debug mode or a raw
|
|
393
|
+
`threading.RLock` otherwise.
|
|
394
|
+
'''
|
|
395
|
+
|
|
396
|
+
def __init__(self, owner=None, **dkw):
|
|
397
|
+
if owner is None:
|
|
398
|
+
owner = caller()
|
|
399
|
+
DebugWrapper.__init__(
|
|
400
|
+
self, filename=owner.filename, lineno=owner.lineno, **dkw
|
|
401
|
+
)
|
|
402
|
+
self.debug('__init__')
|
|
403
|
+
self.lock = threading_RLock()
|
|
404
|
+
self.stack = []
|
|
405
|
+
|
|
406
|
+
def __str__(self):
|
|
407
|
+
return "%s[%s:%s]%s" % (
|
|
408
|
+
type(self).__name__,
|
|
409
|
+
shortpath(self.filename),
|
|
410
|
+
self.lineno,
|
|
411
|
+
"->".join(
|
|
412
|
+
["%s:%s" % filename_lineno for filename_lineno in self.stack]
|
|
413
|
+
),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def __enter__(self, locker=None):
|
|
417
|
+
if locker is None:
|
|
418
|
+
locker = caller()
|
|
419
|
+
filename_lineno = locker.filename, locker.lineno
|
|
420
|
+
self.debug('from %s:%d: __enter__ ...', locker.filename, locker.lineno)
|
|
421
|
+
entry = self.lock.__enter__()
|
|
422
|
+
self.stack.append(filename_lineno)
|
|
423
|
+
return entry
|
|
424
|
+
|
|
425
|
+
def __exit__(self, *a, exiter=None):
|
|
426
|
+
if exiter is None:
|
|
427
|
+
exiter = caller()
|
|
428
|
+
self.debug('%s:%d: __exit__(*%s) ...', exiter.filename, exiter.lineno, a)
|
|
429
|
+
exited = self.lock.__exit__(*a)
|
|
430
|
+
self.stack.pop()
|
|
431
|
+
return exited
|
|
432
|
+
|
|
433
|
+
def acquire(self, blocking=True, timeout=-1, acquirer=None):
|
|
434
|
+
if acquirer is None:
|
|
435
|
+
acquirer = caller()
|
|
436
|
+
self.debug(
|
|
437
|
+
'%s:%d: acquire(blocking=%s)', acquirer.filename, acquirer.lineno,
|
|
438
|
+
blocking
|
|
439
|
+
)
|
|
440
|
+
if timeout < 0:
|
|
441
|
+
ret = self.lock.acquire(blocking)
|
|
442
|
+
else:
|
|
443
|
+
ret = self.lock.acquire(blocking, timeout)
|
|
444
|
+
if ret:
|
|
445
|
+
self.stack.append((acquirer.filename, acquirer.lineno))
|
|
446
|
+
return ret
|
|
447
|
+
|
|
448
|
+
def release(self, releaser=None):
|
|
449
|
+
if releaser is None:
|
|
450
|
+
releaser = caller()
|
|
451
|
+
self.debug('%s:%d: release()', releaser.filename, releaser.lineno)
|
|
452
|
+
self.lock.release()
|
|
453
|
+
self.stack.pop()
|
|
454
|
+
|
|
455
|
+
Lock = DebuggingLock
|
|
456
|
+
RLock = DebuggingRLock
|
|
457
|
+
|
|
458
|
+
_debug_threads = set()
|
|
459
|
+
|
|
460
|
+
def dump_debug_threads():
|
|
461
|
+
D("dump_debug_threads:")
|
|
462
|
+
for T in _debug_threads:
|
|
463
|
+
D("dump_debug_threads: thread %r: %r", T.name, T.debug_label)
|
|
464
|
+
D("dump_debug_threads done")
|
|
465
|
+
|
|
466
|
+
class DebuggingThread(threading_Thread, DebugWrapper):
|
|
467
|
+
|
|
468
|
+
def __init__(self, dkw, *a, **kw):
|
|
469
|
+
DebugWrapper.__init__(self, **dkw)
|
|
470
|
+
self.debug("NEW THREAD(*%r, **%r)", a, kw)
|
|
471
|
+
_debug_threads.add(self)
|
|
472
|
+
threading_Thread.__init__(self, *a, **kw)
|
|
473
|
+
|
|
474
|
+
@DEBUG
|
|
475
|
+
def join(self, timeout=None):
|
|
476
|
+
self.debug("join(timeout=%r)...", timeout)
|
|
477
|
+
retval = threading_Thread.join(self, timeout=timeout)
|
|
478
|
+
self.debug("join(timeout=%r) completed", timeout)
|
|
479
|
+
_debug_threads.discard(self)
|
|
480
|
+
return retval
|
|
481
|
+
|
|
482
|
+
def trace_caller(func):
|
|
483
|
+
''' Decorator to report the caller of a function when called.
|
|
484
|
+
'''
|
|
485
|
+
|
|
486
|
+
def subfunc(*a, **kw):
|
|
487
|
+
frame = caller()
|
|
488
|
+
D(
|
|
489
|
+
"CALL %s()<%s:%d> FROM %s()<%s:%d>",
|
|
490
|
+
func.__name__,
|
|
491
|
+
func.__code__.co_filename,
|
|
492
|
+
func.__code__.co_firstlineno,
|
|
493
|
+
frame.name,
|
|
494
|
+
frame.filename,
|
|
495
|
+
frame.lineno,
|
|
496
|
+
)
|
|
497
|
+
return func(*a, **kw)
|
|
498
|
+
|
|
499
|
+
subfunc.__name__ = "trace_caller/subfunc/" + func.__name__
|
|
500
|
+
return subfunc
|
|
501
|
+
|
|
502
|
+
class TracingObject(Proxy):
|
|
503
|
+
|
|
504
|
+
def __init__(self, other):
|
|
505
|
+
Proxy.__init__(self, other)
|
|
506
|
+
self.__attr_map = {}
|
|
507
|
+
|
|
508
|
+
def __getattribute__(self, attr):
|
|
509
|
+
X("TracingObject.__getattribute__(attr=%r)", attr)
|
|
510
|
+
_proxied = Proxy.__getattribute__(self, '_proxied')
|
|
511
|
+
try:
|
|
512
|
+
value = object.__getattribute__(_proxied, attr)
|
|
513
|
+
except AttributeError:
|
|
514
|
+
X("no .%s attribute", attr)
|
|
515
|
+
raise
|
|
516
|
+
else:
|
|
517
|
+
X("getattr .%s", attr)
|
|
518
|
+
return TracingObject(value)
|
|
519
|
+
|
|
520
|
+
def __call__(self, *a, **kw):
|
|
521
|
+
_proxied = Proxy.__getattribute__(self, '_proxied')
|
|
522
|
+
X("call %s(*%r, **%r)", _proxied, a, kw)
|
|
523
|
+
return _proxied(*a, **kw)
|
|
524
|
+
|
|
525
|
+
class DummyMap(object):
|
|
526
|
+
|
|
527
|
+
def __init__(self, label, d=None):
|
|
528
|
+
X("new DummyMap labelled %r, d=%r", label, d)
|
|
529
|
+
self.__label = label
|
|
530
|
+
self.__map = {}
|
|
531
|
+
if d:
|
|
532
|
+
self.__map.update(d)
|
|
533
|
+
|
|
534
|
+
def __str__(self):
|
|
535
|
+
return self.__label
|
|
536
|
+
|
|
537
|
+
def items(self):
|
|
538
|
+
X("%s.items", self)
|
|
539
|
+
return []
|
|
540
|
+
|
|
541
|
+
def __getitem__(self, key):
|
|
542
|
+
v = self.__map.get(key)
|
|
543
|
+
X("%s[%r] => %r", self, key, v)
|
|
544
|
+
return v
|
|
545
|
+
|
|
546
|
+
def openfiles(substr=None, pid=None):
|
|
547
|
+
''' Run lsof(8) against process `pid`
|
|
548
|
+
returning paths of open files whose paths contain `substr`.
|
|
549
|
+
|
|
550
|
+
Parameters:
|
|
551
|
+
* `substr`: default substring to select by; default returns all paths.
|
|
552
|
+
* `pid`: process to examine; default from `os.getpid()`.
|
|
553
|
+
'''
|
|
554
|
+
if pid is None:
|
|
555
|
+
pid = os.getpid()
|
|
556
|
+
paths = []
|
|
557
|
+
P = Popen(['lsof', '-p', str(pid)], stdout=PIPE)
|
|
558
|
+
for lsof in P.stdout:
|
|
559
|
+
lsof = lsof.decode()
|
|
560
|
+
fields = lsof.split()
|
|
561
|
+
if len(fields) >= 9:
|
|
562
|
+
if fields[4] == 'REG':
|
|
563
|
+
if substr is None or substr in fields[8]:
|
|
564
|
+
paths.append(fields[8])
|
|
565
|
+
P.wait()
|
|
566
|
+
return paths
|
|
567
|
+
|
|
568
|
+
class DebugShell(Cmd):
|
|
569
|
+
''' An interactive prompt for python statements, attached to `/dev/tty` by default.
|
|
570
|
+
'''
|
|
571
|
+
|
|
572
|
+
def __init__(self, var_dict, stdin=None, stdout=None):
|
|
573
|
+
if stdin is None:
|
|
574
|
+
stdin = open('/dev/tty', 'r')
|
|
575
|
+
if stdout is None:
|
|
576
|
+
stdout = open('/dev/tty', 'a')
|
|
577
|
+
self.stdin = stdin
|
|
578
|
+
self.stdout = stdout
|
|
579
|
+
Cmd.__init__(self, stdin=stdin, stdout=stdout)
|
|
580
|
+
self.vars = var_dict
|
|
581
|
+
|
|
582
|
+
def default(self, line):
|
|
583
|
+
''' Default command action.
|
|
584
|
+
'''
|
|
585
|
+
if line == 'EOF':
|
|
586
|
+
return True
|
|
587
|
+
try:
|
|
588
|
+
exec_code(line, globals(), self.vars)
|
|
589
|
+
except Exception as e:
|
|
590
|
+
X("Exception: %s", e)
|
|
591
|
+
self.stdout.flush()
|
|
592
|
+
return False
|
|
593
|
+
|
|
594
|
+
def debug_object_shell(o, prompt=None):
|
|
595
|
+
''' Interactive prompt for inspecting variables.
|
|
596
|
+
'''
|
|
597
|
+
if prompt is None:
|
|
598
|
+
prompt = str(o) + '> '
|
|
599
|
+
v = o.__dict__
|
|
600
|
+
C = DebugShell(v)
|
|
601
|
+
intro = '\n\n'
|
|
602
|
+
for k in sorted(v.keys()):
|
|
603
|
+
intro += '\n %s = %r' % (k, v[k])
|
|
604
|
+
intro += '\n'
|
|
605
|
+
C.prompt = prompt
|
|
606
|
+
C.cmdloop(intro)
|
|
607
|
+
|
|
608
|
+
_trace_state = ThreadState(indent='')
|
|
609
|
+
|
|
610
|
+
def log_via_print(msg, *a, file=None):
|
|
611
|
+
''' Logging style message using `cs.upd.print`.
|
|
612
|
+
'''
|
|
613
|
+
if a:
|
|
614
|
+
msg = msg % a
|
|
615
|
+
if file is None:
|
|
616
|
+
file = sys.stdout
|
|
617
|
+
print(msg, file=file, flush=True)
|
|
618
|
+
|
|
619
|
+
@ALL
|
|
620
|
+
@decorator
|
|
621
|
+
def abrk(func, exceptions=(AssertionError, NameError, RuntimeError)):
|
|
622
|
+
''' A decorator to intercept certain exceptions
|
|
623
|
+
(by default `AssertionError`, `NameError`, `RuntimeError`)
|
|
624
|
+
and call `breakpoint()`.
|
|
625
|
+
The breakpoint frame contains:
|
|
626
|
+
- `func`: the wrapper function
|
|
627
|
+
- `func_a`, `func_kw`: the function positional and keyword arguments
|
|
628
|
+
'''
|
|
629
|
+
|
|
630
|
+
def cs_debug_abrk_wrapper(*func_a, **func_kw):
|
|
631
|
+
try:
|
|
632
|
+
return func(*func_a, **func_kw)
|
|
633
|
+
except exceptions as e:
|
|
634
|
+
warning(
|
|
635
|
+
"%s: %s\n func = %s\n func_a = %r\nfunc_kw = %r",
|
|
636
|
+
funccite(func),
|
|
637
|
+
e,
|
|
638
|
+
funccite(func),
|
|
639
|
+
func_a,
|
|
640
|
+
func_kw,
|
|
641
|
+
)
|
|
642
|
+
breakpoint()
|
|
643
|
+
raise
|
|
644
|
+
|
|
645
|
+
return cs_debug_abrk_wrapper
|
|
646
|
+
|
|
647
|
+
@ALL
|
|
648
|
+
@decorator
|
|
649
|
+
# pylint: disable=too-many-arguments
|
|
650
|
+
def trace(
|
|
651
|
+
func,
|
|
652
|
+
call=True,
|
|
653
|
+
retval=False,
|
|
654
|
+
exception=True,
|
|
655
|
+
use_pformat=False,
|
|
656
|
+
with_caller=False,
|
|
657
|
+
with_pfx=False,
|
|
658
|
+
xlog=None,
|
|
659
|
+
):
|
|
660
|
+
''' Decorator to report the call and return of a function.
|
|
661
|
+
|
|
662
|
+
Decorator parameters:
|
|
663
|
+
* `call`: trace the call, default `True`
|
|
664
|
+
* `retval`: trace the return, default `False`
|
|
665
|
+
* `exception`: trace raised exceptions, default `True`
|
|
666
|
+
* `use_pformat`: present the return value using
|
|
667
|
+
`pformat` instead of `repr`, default `False`
|
|
668
|
+
* `with_caller`: include the caller if this function, default `False`
|
|
669
|
+
* `with_pfx`: include the current `Pfx` prefix, default `False`
|
|
670
|
+
'''
|
|
671
|
+
|
|
672
|
+
citation = funcname(func) ## funccite(func)
|
|
673
|
+
|
|
674
|
+
def traced_function_wrapper(*a, **kw):
|
|
675
|
+
''' Wrapper for `func` to trace call and return.
|
|
676
|
+
'''
|
|
677
|
+
global _trace_state # pylint: disable=global-statement
|
|
678
|
+
if with_pfx:
|
|
679
|
+
# late import so that we can use this in modules we import
|
|
680
|
+
# pylint: disable=import-outside-toplevel
|
|
681
|
+
try:
|
|
682
|
+
from cs.pfx import XP as xlog
|
|
683
|
+
except ImportError:
|
|
684
|
+
xlog = X
|
|
685
|
+
else:
|
|
686
|
+
xlog = X
|
|
687
|
+
log_cite = citation
|
|
688
|
+
if with_caller:
|
|
689
|
+
log_cite = log_cite + "from[%s]" % (caller(),)
|
|
690
|
+
if call:
|
|
691
|
+
fmt, av = func_a_kw_fmt(log_cite, *a, **kw)
|
|
692
|
+
xlog("%sCALL " + fmt, _trace_state.indent, *av)
|
|
693
|
+
old_indent = _trace_state.indent
|
|
694
|
+
_trace_state.indent += ' '
|
|
695
|
+
start_time = time.time()
|
|
696
|
+
try:
|
|
697
|
+
result = func(*a, **kw)
|
|
698
|
+
except Exception as e:
|
|
699
|
+
end_time = time.time()
|
|
700
|
+
if exception:
|
|
701
|
+
xlog_kw = {}
|
|
702
|
+
if xlog is X:
|
|
703
|
+
xlog_kw['colour'] = 'red'
|
|
704
|
+
xlog(
|
|
705
|
+
"%sCALL %s %gs RAISE %r",
|
|
706
|
+
_trace_state.indent,
|
|
707
|
+
log_cite,
|
|
708
|
+
end_time - start_time,
|
|
709
|
+
e,
|
|
710
|
+
**xlog_kw,
|
|
711
|
+
)
|
|
712
|
+
_trace_state.indent = old_indent
|
|
713
|
+
raise
|
|
714
|
+
else:
|
|
715
|
+
end_time = time.time()
|
|
716
|
+
if retval:
|
|
717
|
+
xlog(
|
|
718
|
+
"%sCALL %s %gs RETURN %s",
|
|
719
|
+
_trace_state.indent,
|
|
720
|
+
log_cite,
|
|
721
|
+
end_time - start_time,
|
|
722
|
+
(pformat if use_pformat else repr)(result),
|
|
723
|
+
)
|
|
724
|
+
if inspect.isgeneratorfunction(func):
|
|
725
|
+
iterator = result
|
|
726
|
+
|
|
727
|
+
def traced_generator():
|
|
728
|
+
while True:
|
|
729
|
+
next_time = time.time()
|
|
730
|
+
if call:
|
|
731
|
+
xlog(
|
|
732
|
+
"%sNEXT %s %gs ...",
|
|
733
|
+
_trace_state.indent,
|
|
734
|
+
log_cite,
|
|
735
|
+
next_time - start_time,
|
|
736
|
+
)
|
|
737
|
+
try:
|
|
738
|
+
item = next(iterator)
|
|
739
|
+
except StopIteration:
|
|
740
|
+
yield_time = time.time()
|
|
741
|
+
xlog(
|
|
742
|
+
"%sDONE %s %gs ...",
|
|
743
|
+
_trace_state.indent,
|
|
744
|
+
log_cite,
|
|
745
|
+
yield_time - next_time,
|
|
746
|
+
)
|
|
747
|
+
break
|
|
748
|
+
except Exception as e:
|
|
749
|
+
end_time = time.time()
|
|
750
|
+
if exception:
|
|
751
|
+
xlog_kw = {}
|
|
752
|
+
if xlog is X:
|
|
753
|
+
xlog_kw['colour'] = 'red'
|
|
754
|
+
xlog(
|
|
755
|
+
"%sCALL %s %gs RAISE %r",
|
|
756
|
+
_trace_state.indent,
|
|
757
|
+
log_cite,
|
|
758
|
+
end_time - start_time,
|
|
759
|
+
e,
|
|
760
|
+
**xlog_kw,
|
|
761
|
+
)
|
|
762
|
+
_trace_state.indent = old_indent
|
|
763
|
+
raise
|
|
764
|
+
else:
|
|
765
|
+
yield_time = time.time()
|
|
766
|
+
xlog(
|
|
767
|
+
"%sYIELD %gs %s <= %s",
|
|
768
|
+
_trace_state.indent,
|
|
769
|
+
yield_time - next_time,
|
|
770
|
+
s(item),
|
|
771
|
+
log_cite,
|
|
772
|
+
)
|
|
773
|
+
yield item
|
|
774
|
+
|
|
775
|
+
result = traced_generator()
|
|
776
|
+
else:
|
|
777
|
+
##xlog("%sRETURN %s <= %s", _trace_state.indent, type(result), log_cite)
|
|
778
|
+
if retval:
|
|
779
|
+
xlog(
|
|
780
|
+
"%sRETURN %gs %s <= %s",
|
|
781
|
+
_trace_state.indent,
|
|
782
|
+
end_time - start_time,
|
|
783
|
+
s(result),
|
|
784
|
+
log_cite,
|
|
785
|
+
)
|
|
786
|
+
_trace_state.indent = old_indent
|
|
787
|
+
return result
|
|
788
|
+
|
|
789
|
+
traced_function_wrapper.__name__ = "@trace(%s)" % (citation,)
|
|
790
|
+
traced_function_wrapper.__doc__ = "@trace(%s)\n\n" + (func.__doc__ or '')
|
|
791
|
+
return traced_function_wrapper
|
|
792
|
+
|
|
793
|
+
def trace_DEBUG(debug_spec=None):
|
|
794
|
+
''' Apply the `@trace` decorator to functions specified by `debug_spec`,
|
|
795
|
+
default from the environment variable `$DEBUG`.
|
|
796
|
+
'''
|
|
797
|
+
with Pfx("trace_DEBUG"):
|
|
798
|
+
try:
|
|
799
|
+
import importlib
|
|
800
|
+
except ImportError as e:
|
|
801
|
+
warning("trace_DEBUG: cannot import importlib, no applying: %s", e)
|
|
802
|
+
return
|
|
803
|
+
if debug_spec is None:
|
|
804
|
+
debug_spec = os.environ.get('DEBUG', '')
|
|
805
|
+
if isinstance(debug_spec, str):
|
|
806
|
+
debug_spec = debug_spec.split(',')
|
|
807
|
+
with Pfx("%r", debug_spec):
|
|
808
|
+
module_names = []
|
|
809
|
+
function_names = []
|
|
810
|
+
for spec in debug_spec:
|
|
811
|
+
with Pfx(spec):
|
|
812
|
+
if is_dotted_identifier(spec):
|
|
813
|
+
module_names.append(spec)
|
|
814
|
+
elif ':' in spec:
|
|
815
|
+
# module:funcname
|
|
816
|
+
module_name, func_name = spec.split(':', 1)
|
|
817
|
+
if (is_dotted_identifier(module_name)
|
|
818
|
+
and is_dotted_identifier(func_name)):
|
|
819
|
+
function_names.append((module_name, func_name))
|
|
820
|
+
for module_name in module_names:
|
|
821
|
+
with Pfx("module %s", module_name):
|
|
822
|
+
try:
|
|
823
|
+
M = importlib.import_module(module_name)
|
|
824
|
+
except ImportError as e:
|
|
825
|
+
warning("cannot import: %s", e)
|
|
826
|
+
continue
|
|
827
|
+
M.DEBUG = True
|
|
828
|
+
for module_name, func_name in function_names:
|
|
829
|
+
with Pfx("function %s:%s", module_name, func_name):
|
|
830
|
+
try:
|
|
831
|
+
M = importlib.import_module(module_name)
|
|
832
|
+
except ImportError as e:
|
|
833
|
+
warning("cannot import: %s", e)
|
|
834
|
+
continue
|
|
835
|
+
try:
|
|
836
|
+
F = getattr(M, func_name)
|
|
837
|
+
except AttributeError as e:
|
|
838
|
+
warning("function %s not found: %s", e)
|
|
839
|
+
continue
|
|
840
|
+
if callable(F):
|
|
841
|
+
setattr(M, func_name, trace(F))
|
|
842
|
+
|
|
843
|
+
def selftest(module_name, defaultTest=None, argv=None):
|
|
844
|
+
''' Called by my unit tests.
|
|
845
|
+
'''
|
|
846
|
+
# pylint: disable=import-outside-toplevel
|
|
847
|
+
if argv is None:
|
|
848
|
+
argv = sys.argv
|
|
849
|
+
import importlib
|
|
850
|
+
importlib.import_module(module_name)
|
|
851
|
+
import signal
|
|
852
|
+
signal.signal(signal.SIGHUP, lambda sig, frame: thread_dump())
|
|
853
|
+
signal.signal(signal.SIGINT, lambda sig, frame: sys.exit(thread_dump()))
|
|
854
|
+
import unittest
|
|
855
|
+
return unittest.main(module=module_name, defaultTest=defaultTest, argv=argv)
|
|
856
|
+
|
|
857
|
+
builtin_names_s = os.environ.get(CS_DEBUG_BUILTINS_ENVVAR, '')
|
|
858
|
+
if builtin_names_s:
|
|
859
|
+
try:
|
|
860
|
+
import builtins # pylint: disable=unused-import
|
|
861
|
+
except ImportError:
|
|
862
|
+
warning(
|
|
863
|
+
"$%s=%r but connot import builtins for monkey patching",
|
|
864
|
+
CS_DEBUG_BUILTINS_ENVVAR, builtin_names_s
|
|
865
|
+
)
|
|
866
|
+
else:
|
|
867
|
+
vs = vars()
|
|
868
|
+
for builtin_name in (__all__ if builtin_names_s == "1" else
|
|
869
|
+
builtin_names_s.split(',')):
|
|
870
|
+
if not builtin_name:
|
|
871
|
+
continue
|
|
872
|
+
if builtin_name not in __all__:
|
|
873
|
+
warning(
|
|
874
|
+
"$%s: ignoring %r, not in cs.debug.__all__:%r",
|
|
875
|
+
CS_DEBUG_BUILTINS_ENVVAR, builtin_name, __all__
|
|
876
|
+
)
|
|
877
|
+
continue
|
|
878
|
+
if builtin_name in ('breakpoint',):
|
|
879
|
+
# breakpoint doesn't work right if wrapped, gets the wrong frame
|
|
880
|
+
continue
|
|
881
|
+
if not is_identifier(builtin_name):
|
|
882
|
+
warning(
|
|
883
|
+
"$%s: ignoring %r, not an identifier", CS_DEBUG_BUILTINS_ENVVAR,
|
|
884
|
+
builtin_name
|
|
885
|
+
)
|
|
886
|
+
continue
|
|
887
|
+
setattr(builtins, builtin_name, vs[builtin_name])
|
|
888
|
+
|
|
889
|
+
# honour the $DEBUG trace flags
|
|
890
|
+
trace_DEBUG()
|