pusher-debug 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.
- pusher_debug/__init__.py +61 -0
- pusher_debug/monkeypatching/__init__.py +32 -0
- pusher_debug/monkeypatching/monkeypatching.py +418 -0
- pusher_debug/py.typed +0 -0
- pusher_debug/tracing/__init__.py +30 -0
- pusher_debug/tracing/tracing.py +244 -0
- pusher_debug/util/__init__.py +23 -0
- pusher_debug/util/util.py +255 -0
- pusher_debug-1.0.0.dist-info/METADATA +69 -0
- pusher_debug-1.0.0.dist-info/RECORD +12 -0
- pusher_debug-1.0.0.dist-info/WHEEL +4 -0
- pusher_debug-1.0.0.dist-info/licenses/LICENSE.md +675 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from pprint import pformat as pretty
|
|
3
|
+
from inspect import currentframe, signature, formatargvalues, getargvalues, Signature
|
|
4
|
+
from objwatch.wrappers.base_wrapper import BaseWrapper
|
|
5
|
+
from io import TextIOBase
|
|
6
|
+
from typing import Any
|
|
7
|
+
from types import NoneType, FrameType
|
|
8
|
+
|
|
9
|
+
from ..util import fix_locals_qualname
|
|
10
|
+
from ..monkeypatching import add_to_class
|
|
11
|
+
|
|
12
|
+
# Prevent fn_name from infinite recursion (for example if
|
|
13
|
+
# put in __init__, which wants to say what self is (which
|
|
14
|
+
# triggers an __init__ ...). That particular route is blocked
|
|
15
|
+
# in the actual code, but I'm betting I missed another way
|
|
16
|
+
# this problem arises
|
|
17
|
+
_fn_name_recursion_block_:int = 0
|
|
18
|
+
_fn_name_recursion_fn_:str = ""
|
|
19
|
+
def fn_name(frame: FrameType | None = None, caller:bool|str = False) -> str :
|
|
20
|
+
"""
|
|
21
|
+
Returns a string listing the function signature and arguements
|
|
22
|
+
for the calling function (or if frame is provided, for the function
|
|
23
|
+
that frame represents). caller, if provided, should be True (to
|
|
24
|
+
provide info on the caller of the calling function) or 'both'
|
|
25
|
+
(to provide info on calling function AND the function that called it)
|
|
26
|
+
"""
|
|
27
|
+
global _fn_name_recursion_block_
|
|
28
|
+
global _fn_name_recursion_fn_
|
|
29
|
+
if not frame :
|
|
30
|
+
frame = currentframe()
|
|
31
|
+
assert(isinstance(frame, FrameType))
|
|
32
|
+
frame = frame.f_back # frame of caller of fn_name
|
|
33
|
+
pass
|
|
34
|
+
if not isinstance(frame, FrameType) :
|
|
35
|
+
return ''
|
|
36
|
+
caller_frame = frame.f_back
|
|
37
|
+
assert(isinstance(caller_frame, FrameType))
|
|
38
|
+
if _fn_name_recursion_block_ :
|
|
39
|
+
from_name = caller_frame.f_code.co_qualname
|
|
40
|
+
return f'recursion {_fn_name_recursion_block_} called from {from_name} originally {_fn_name_recursion_fn_}'
|
|
41
|
+
else :
|
|
42
|
+
_fn_name_recursion_block_ += 1
|
|
43
|
+
_fn_name_recursion_fn_ = caller_frame.f_code.co_qualname
|
|
44
|
+
pass
|
|
45
|
+
if caller :
|
|
46
|
+
if type(caller) == str and caller == "both" :
|
|
47
|
+
do_caller = True
|
|
48
|
+
call = "Fn: "
|
|
49
|
+
else:
|
|
50
|
+
do_caller = False
|
|
51
|
+
frame = frame.f_back
|
|
52
|
+
assert(isinstance(frame, FrameType))
|
|
53
|
+
call = "Caller: "
|
|
54
|
+
pass
|
|
55
|
+
else:
|
|
56
|
+
do_caller = False
|
|
57
|
+
call = "Fn: "
|
|
58
|
+
pass
|
|
59
|
+
args, varargs, keywords, locals = getargvalues(frame)
|
|
60
|
+
arg_str = formatargvalues(args, varargs,keywords, locals)
|
|
61
|
+
fn_qname = fix_locals_qualname(frame.f_code.co_qualname)
|
|
62
|
+
result = f"{call}{fn_qname}{arg_str}"
|
|
63
|
+
_fn_name_recursion_block_ -= 1
|
|
64
|
+
_fn_name_recursion_fn_ = ''
|
|
65
|
+
if do_caller :
|
|
66
|
+
result += f"\nCaller: {fn_name(caller_frame)[4:]}" #trim "Fn: "
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def labelest(label:str) -> None :
|
|
70
|
+
"""
|
|
71
|
+
Function to watch with ObjWatch, which just notes
|
|
72
|
+
what is going on in the log
|
|
73
|
+
"""
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
class DetailWrapper(BaseWrapper) :
|
|
77
|
+
"""
|
|
78
|
+
ObjWatch 'wrapper' which logs all inputs and output.
|
|
79
|
+
|
|
80
|
+
Can be subclassed as
|
|
81
|
+
class CustWrapper(DetailWrapper, special_classes = [C1, C2, C3])
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
in which case C1, C2, and C3 have an _id_ property added which is
|
|
85
|
+
read only and unique for each instance of the same class. This _id_
|
|
86
|
+
is used in the wrap_return to note which instance is returning the value
|
|
87
|
+
in question.
|
|
88
|
+
"""
|
|
89
|
+
special_classes:list[type] = []
|
|
90
|
+
|
|
91
|
+
def __init_subclass__(cls2, *, special_classes:list[type], **kwargs) :
|
|
92
|
+
cls2.special_classes = special_classes
|
|
93
|
+
for c in special_classes :
|
|
94
|
+
c._i_count_ = 0 # type: ignore[attr-defined]
|
|
95
|
+
@add_to_class(c) # type: ignore[misc]
|
|
96
|
+
@classmethod
|
|
97
|
+
def _inst_count_(cls) -> int :
|
|
98
|
+
cls._i_count_ += 1
|
|
99
|
+
return cls._i_count_
|
|
100
|
+
@add_to_class(c) # type: ignore[misc]
|
|
101
|
+
@property
|
|
102
|
+
def _id_(self) -> str :
|
|
103
|
+
if '_inst_id_' not in self.__dict__ :
|
|
104
|
+
self._inst_id_ = self.__class__._inst_count_()
|
|
105
|
+
pass
|
|
106
|
+
return f'{self._inst_id_:05d}'
|
|
107
|
+
pass
|
|
108
|
+
super().__init_subclass__(**kwargs)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
def inst_id(self, slf:Any) -> str :
|
|
112
|
+
"""
|
|
113
|
+
Find unique instance identifier for class we are monitoring
|
|
114
|
+
"""
|
|
115
|
+
DW = self.__class__ # class of wrapper
|
|
116
|
+
if slf == None :
|
|
117
|
+
return ""
|
|
118
|
+
cls = slf.__class__ # class if instance we are logging
|
|
119
|
+
if cls in DW.special_classes :
|
|
120
|
+
return slf._id_ # we've made certain this property exists
|
|
121
|
+
return ""
|
|
122
|
+
|
|
123
|
+
def wrap_call(self, func_name:str, frame: FrameType) -> str:
|
|
124
|
+
if 'saved_frame' not in self.__dict__ :
|
|
125
|
+
self.saved_frame:list[FrameType] = []
|
|
126
|
+
pass
|
|
127
|
+
self.saved_frame.append(frame)
|
|
128
|
+
g = frame.f_globals
|
|
129
|
+
l = frame.f_locals
|
|
130
|
+
code = frame.f_code
|
|
131
|
+
qname = code.co_qualname # equals func_name I think ...
|
|
132
|
+
simple_report = False
|
|
133
|
+
if frame.f_back :
|
|
134
|
+
caller = f" called from {frame.f_back.f_code.co_qualname}"
|
|
135
|
+
else :
|
|
136
|
+
caller = ""
|
|
137
|
+
pass
|
|
138
|
+
if 'self' in l:
|
|
139
|
+
slf = l['self']
|
|
140
|
+
fn = getattr(slf, code.co_name)
|
|
141
|
+
elif code.co_qualname in g:
|
|
142
|
+
slf = None
|
|
143
|
+
fn = g[code.co_qualname]
|
|
144
|
+
else:
|
|
145
|
+
slf = None
|
|
146
|
+
simple_report = True
|
|
147
|
+
pass
|
|
148
|
+
if simple_report :
|
|
149
|
+
return f"Function {func_name}{caller}: Locals: {str(l)}"
|
|
150
|
+
sig = signature(fn)
|
|
151
|
+
args_str = ""
|
|
152
|
+
return_str = sig.return_annotation
|
|
153
|
+
if return_str == Signature.empty :
|
|
154
|
+
return_str = ":"
|
|
155
|
+
else:
|
|
156
|
+
return_str = f"-> {str(return_str)} :"
|
|
157
|
+
pass
|
|
158
|
+
for p,v in sig.parameters.items() :
|
|
159
|
+
if '=' in str(v) :
|
|
160
|
+
sv = str(v)[0:str(v).index('=')]
|
|
161
|
+
else :
|
|
162
|
+
sv = str(v)
|
|
163
|
+
pass
|
|
164
|
+
try :
|
|
165
|
+
s = f"{sv}={l[p].__repr__()}"
|
|
166
|
+
except :
|
|
167
|
+
s = f"{sv}={l[p]}"
|
|
168
|
+
pass
|
|
169
|
+
if not args_str :
|
|
170
|
+
args_str = s
|
|
171
|
+
else :
|
|
172
|
+
args_str += ", " + s
|
|
173
|
+
pass
|
|
174
|
+
pass
|
|
175
|
+
result = f"{func_name} ({args_str}) {return_str} ({caller})"
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
def wrap_return(self, func_name:str, result: Any) -> str:
|
|
179
|
+
frame = self.saved_frame.pop()
|
|
180
|
+
l = frame.f_locals
|
|
181
|
+
if 'self' in l:
|
|
182
|
+
slf = l['self']
|
|
183
|
+
cls = slf.__class__.__qualname__ + ':'
|
|
184
|
+
inst_id = self.inst_id(slf)
|
|
185
|
+
if inst_id :
|
|
186
|
+
return_str = f"{cls}{func_name} -> {result.__repr__()} <{inst_id}>"
|
|
187
|
+
else :
|
|
188
|
+
return_str = f"{cls}{func_name} -> {result.__repr__()}"
|
|
189
|
+
pass
|
|
190
|
+
else :
|
|
191
|
+
return_str = f"{func_name} -> {result.__repr__()}"
|
|
192
|
+
pass
|
|
193
|
+
return return_str
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def wrap_upd(self, old_value: Any, new_value: Any) -> tuple[str,str]:
|
|
197
|
+
return_strs = (f"{old_value.__repr__()}", f"{new_value.__repr__()}",)
|
|
198
|
+
return return_strs
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def fn_entry(frame: FrameType | None = None, callers:int=0, file:TextIOBase|None = None) -> None:
|
|
202
|
+
'''
|
|
203
|
+
Prints entry info, including caller info if key word callers is set to the
|
|
204
|
+
number of callers to report. If key word file is not set, prints to stdout,
|
|
205
|
+
otherwise to the specified file.
|
|
206
|
+
'''
|
|
207
|
+
if not isinstance(frame, FrameType) :
|
|
208
|
+
frame = currentframe()
|
|
209
|
+
if isinstance(frame, FrameType) :
|
|
210
|
+
frame = frame.f_back
|
|
211
|
+
pass
|
|
212
|
+
pass
|
|
213
|
+
print(fn_name(frame), file=file)
|
|
214
|
+
for _ in range(callers) :
|
|
215
|
+
if isinstance(frame, FrameType) :
|
|
216
|
+
frame = frame.f_back
|
|
217
|
+
print(f'Called by{fn_name(frame)[2:]}', file=file)
|
|
218
|
+
pass
|
|
219
|
+
pass
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
def fn_exit(frame: FrameType | None = None, **kwargs) -> None :
|
|
223
|
+
'''
|
|
224
|
+
Prints exit info, including return value if key word result is defined. If key
|
|
225
|
+
word file is not set, prints to stdout, otherwise to the specified file.
|
|
226
|
+
'''
|
|
227
|
+
if not isinstance(frame, FrameType) :
|
|
228
|
+
frame = currentframe()
|
|
229
|
+
if isinstance(frame, FrameType) :
|
|
230
|
+
frame = frame.f_back
|
|
231
|
+
pass
|
|
232
|
+
pass
|
|
233
|
+
if 'file' in kwargs :
|
|
234
|
+
file = kwargs['file']
|
|
235
|
+
else:
|
|
236
|
+
file = None
|
|
237
|
+
pass
|
|
238
|
+
if 'result' in kwargs :
|
|
239
|
+
result = str(kwargs['result'])
|
|
240
|
+
print(f'{fn_name(frame)} -> {result}')
|
|
241
|
+
else:
|
|
242
|
+
print(f'{fn_name(frame)}')
|
|
243
|
+
pass
|
|
244
|
+
return
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
author = "Douglas Sojourner <doug@sojournings.org>"
|
|
3
|
+
|
|
4
|
+
copyright = f"""
|
|
5
|
+
debug-tools, a few tools to make debugging easier
|
|
6
|
+
Copyright (C) 2026 {author}
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
"""
|
|
21
|
+
from .util import grab_all_namespaces, qualname_context, fix_locals_qualname, find_in_dicts
|
|
22
|
+
|
|
23
|
+
__all__ = ['grab_all_namespaces', 'qualname_context', 'fix_locals_qualname']
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from inspect import currentframe, isclass
|
|
4
|
+
from typing import Any, Callable, overload, Literal
|
|
5
|
+
from types import FrameType, FunctionType, NoneType
|
|
6
|
+
|
|
7
|
+
_local_mark = "<locals>"
|
|
8
|
+
_local_mark2 = f".{_local_mark}."
|
|
9
|
+
|
|
10
|
+
def find_in_dicts(name:str, dicts:list|dict|None) -> Any :
|
|
11
|
+
"""
|
|
12
|
+
searches a list of dictionaries for name, or returns None
|
|
13
|
+
(Note that a successfull lookup can also return None)
|
|
14
|
+
"""
|
|
15
|
+
if isinstance(dicts, dict) :
|
|
16
|
+
ds = [dicts,]
|
|
17
|
+
elif isinstance(dicts, list):
|
|
18
|
+
ds = []
|
|
19
|
+
for item in dicts :
|
|
20
|
+
if isinstance(item, list):
|
|
21
|
+
for i2 in item :
|
|
22
|
+
assert(type(i2) == dict)
|
|
23
|
+
ds.append(i2)
|
|
24
|
+
pass
|
|
25
|
+
pass
|
|
26
|
+
elif isinstance(item, NoneType) :
|
|
27
|
+
pass
|
|
28
|
+
else :
|
|
29
|
+
if type(item) != dict :
|
|
30
|
+
print(f"Expecting dict, got {type(item)} = {str(item)}")
|
|
31
|
+
continue
|
|
32
|
+
assert(type(item) == dict)
|
|
33
|
+
ds.append(item)
|
|
34
|
+
pass
|
|
35
|
+
pass # end each item
|
|
36
|
+
pass # end flattening dicts if needed
|
|
37
|
+
for d in ds :
|
|
38
|
+
if d and name in d : # skip None's
|
|
39
|
+
return d[name]
|
|
40
|
+
pass
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def return_empty_list() -> list :
|
|
44
|
+
"""
|
|
45
|
+
function to replace __dir__ when it isn't found
|
|
46
|
+
"""
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def grab_all_namespaces(o: object, split:Literal[True]) -> list[dict[str,Any]]: ...
|
|
51
|
+
@overload
|
|
52
|
+
def grab_all_namespaces(o: object, split:Literal[False]) -> dict[str,Any]: ...
|
|
53
|
+
@overload
|
|
54
|
+
def grab_all_namespaces(o: object) -> dict[str,Any]: ...
|
|
55
|
+
|
|
56
|
+
def grab_all_namespaces(o: object, split=False) -> dict[str,Any] | list[dict[str,Any]]:
|
|
57
|
+
'''
|
|
58
|
+
find Any namespaces associated with o, and collect them
|
|
59
|
+
together. Should even survive cases where __dir__() and/or
|
|
60
|
+
__dict__ don't exist
|
|
61
|
+
'''
|
|
62
|
+
d = getattr(o, '__dict__', None)
|
|
63
|
+
if not d :
|
|
64
|
+
d = dict()
|
|
65
|
+
else:
|
|
66
|
+
d = dict(d) # incase it was something standing in for a dictionary
|
|
67
|
+
pass
|
|
68
|
+
l = d
|
|
69
|
+
g = dict()
|
|
70
|
+
try :
|
|
71
|
+
dirs = getattr(o,'__dir__', return_empty_list)()
|
|
72
|
+
except TypeError:
|
|
73
|
+
if isclass(o) :
|
|
74
|
+
try :
|
|
75
|
+
o = o() # only works if __init__ has no arguments, or defauts for all
|
|
76
|
+
dirs = getattr(o,'__dir__', return_empty_list)()
|
|
77
|
+
except TypeError:
|
|
78
|
+
dirs = []
|
|
79
|
+
pass
|
|
80
|
+
pass
|
|
81
|
+
else:
|
|
82
|
+
dirs = []
|
|
83
|
+
pass
|
|
84
|
+
pass
|
|
85
|
+
if not dirs :
|
|
86
|
+
pass
|
|
87
|
+
if 'globals' in d :
|
|
88
|
+
g.update(d['globals'])
|
|
89
|
+
pass
|
|
90
|
+
if '__globals__' in d :
|
|
91
|
+
g.update(d['__globals__'])
|
|
92
|
+
pass
|
|
93
|
+
if 'globals' in dirs :
|
|
94
|
+
g.update(getattr(o,'globals'))
|
|
95
|
+
pass
|
|
96
|
+
if '__globals__' in dirs :
|
|
97
|
+
g.update(getattr(o,'__globals__'))
|
|
98
|
+
pass
|
|
99
|
+
if 'locals' in dirs :
|
|
100
|
+
l.update(getattr(o,'locals'))
|
|
101
|
+
pass
|
|
102
|
+
if '__locals__' in dirs :
|
|
103
|
+
l.update(getattr(o,'__locals__'))
|
|
104
|
+
pass
|
|
105
|
+
if 'locals' in d :
|
|
106
|
+
l.update(d['locals'])
|
|
107
|
+
pass
|
|
108
|
+
if '__locals__' in d :
|
|
109
|
+
l.update(d['__locals__'])
|
|
110
|
+
pass
|
|
111
|
+
if split :
|
|
112
|
+
return [dict(g), dict(l)]
|
|
113
|
+
else :
|
|
114
|
+
d2 = dict(g)
|
|
115
|
+
d2.update(l)
|
|
116
|
+
return d2
|
|
117
|
+
pass # type: ignore[unreachable]
|
|
118
|
+
|
|
119
|
+
# We want to get global/local address spaces for function
|
|
120
|
+
# Handling these variations on qualname
|
|
121
|
+
#
|
|
122
|
+
# A) N2.N3.fn vs
|
|
123
|
+
# B) N0.N1.<locals>.N2.N3.fn <Figure out N2, then it is reduced to (A)
|
|
124
|
+
#
|
|
125
|
+
#
|
|
126
|
+
# 1) fn : Go back in stack until fn is in global or local
|
|
127
|
+
# 2) N1.fn : Go Back until N1 is in global or local
|
|
128
|
+
# 3) N1.N2.fn same
|
|
129
|
+
# To get actual function go forward using Ni.__dict__, until
|
|
130
|
+
# you finally find a dictionary you can look fn up in.
|
|
131
|
+
|
|
132
|
+
def qualname_context(item:str|Callable) -> tuple[str,list[dict[str,Any]],list[dict[str,Any]],dict[str,Any],dict[str,Any],FrameType|None] :
|
|
133
|
+
"""
|
|
134
|
+
Given a string with the qualname, or a function (from which we get the
|
|
135
|
+
qualname) find:
|
|
136
|
+
1) "qualname after <locals>"
|
|
137
|
+
2) pair of global and local dictionaries where fn or containing package/class
|
|
138
|
+
are defined
|
|
139
|
+
3) __dict__ of package/class where fn is defined
|
|
140
|
+
4) frame where (non "<locals>") part of qualname was first found
|
|
141
|
+
"""
|
|
142
|
+
if isinstance(item, str) :
|
|
143
|
+
name = item
|
|
144
|
+
else:
|
|
145
|
+
name = item.__qualname__
|
|
146
|
+
pass
|
|
147
|
+
split_name = name.split('.')
|
|
148
|
+
if _local_mark in split_name :
|
|
149
|
+
local_index = split_name.index(_local_mark)
|
|
150
|
+
else:
|
|
151
|
+
local_index = len(split_name) + 1
|
|
152
|
+
pass
|
|
153
|
+
top_name = split_name[0]
|
|
154
|
+
bottom_name = split_name[-1]
|
|
155
|
+
frame = currentframe()
|
|
156
|
+
found_index = -1
|
|
157
|
+
while (isinstance(frame, FrameType)
|
|
158
|
+
and frame.f_back
|
|
159
|
+
and found_index < 0) : # to continue if we find wrong one first
|
|
160
|
+
while (isinstance(frame, FrameType)
|
|
161
|
+
and frame.f_back # No earlier frames
|
|
162
|
+
and (not frame.f_code.co_name in split_name # code not in qualname
|
|
163
|
+
or (local_index # or is, but before local_index
|
|
164
|
+
and split_name.index(frame.f_code.co_name) > local_index)
|
|
165
|
+
)
|
|
166
|
+
and not split_name[0] in frame.f_locals # root of qualname in globals
|
|
167
|
+
and not split_name[0] in frame.f_globals # or locals
|
|
168
|
+
):
|
|
169
|
+
frame = frame.f_back # Then go back a level
|
|
170
|
+
pass # end moving back
|
|
171
|
+
top_dicts = [dict(frame.f_locals), dict(frame.f_globals)]
|
|
172
|
+
obj = None
|
|
173
|
+
found_index = -1
|
|
174
|
+
local_dicts = top_dicts # default value for extra dictionaries returned
|
|
175
|
+
d2:dict[str,Any] = {}
|
|
176
|
+
d1:dict[str,Any] = {}
|
|
177
|
+
for i in range(len(split_name)) :
|
|
178
|
+
if split_name[i] == _local_mark :
|
|
179
|
+
local_dicts = grab_all_namespaces(obj, split=True) # return g and l separately
|
|
180
|
+
local_index = i
|
|
181
|
+
continue
|
|
182
|
+
obj = find_in_dicts(split_name[i], top_dicts)
|
|
183
|
+
if obj :
|
|
184
|
+
found_index = i
|
|
185
|
+
if i == len(split_name) - 3 :
|
|
186
|
+
d1 = grab_all_namespaces(obj, split=False)
|
|
187
|
+
elif i == len(split_name) - 2 :
|
|
188
|
+
d2 = grab_all_namespaces(obj, split=False)
|
|
189
|
+
pass
|
|
190
|
+
for j in range(i+1, len(split_name)) :
|
|
191
|
+
if split_name[j] == _local_mark :
|
|
192
|
+
local_dicts = grab_all_namespaces(obj, split=True)
|
|
193
|
+
local_index = j
|
|
194
|
+
continue
|
|
195
|
+
obj = find_in_dicts(split_name[j], grab_all_namespaces(obj, split=True))
|
|
196
|
+
if not obj :
|
|
197
|
+
found_index = -1
|
|
198
|
+
break
|
|
199
|
+
if j == len(split_name) - 3 :
|
|
200
|
+
d1 = grab_all_namespaces(obj, split=False)
|
|
201
|
+
elif j == len(split_name) - 2 :
|
|
202
|
+
d2 = grab_all_namespaces(obj)
|
|
203
|
+
pass
|
|
204
|
+
if found_index >= 0 :
|
|
205
|
+
break
|
|
206
|
+
pass #end if lookup worked
|
|
207
|
+
pass # end of searching in dicts for parts of name
|
|
208
|
+
if found_index < 0 :
|
|
209
|
+
frame = frame.f_back
|
|
210
|
+
pass
|
|
211
|
+
pass # end searching for frame
|
|
212
|
+
if found_index == len(split_name) - 1:
|
|
213
|
+
lname = split_name[-1]
|
|
214
|
+
elif (local_index < len(split_name)
|
|
215
|
+
and local_index == len(split_name) - 2) :
|
|
216
|
+
lname = split_name[-1]
|
|
217
|
+
elif local_index < len(split_name):
|
|
218
|
+
lname = '.'.join(split_name[max(found_index, local_index+1):])
|
|
219
|
+
else:
|
|
220
|
+
lname = '.'.join(split_name[found_index:])
|
|
221
|
+
pass
|
|
222
|
+
if found_index > len(split_name) :
|
|
223
|
+
raise ValueError("What The Hell?")
|
|
224
|
+
elif found_index == len(split_name)-1 :
|
|
225
|
+
if isinstance(frame, FrameType) and frame.f_back :
|
|
226
|
+
f = frame.f_back
|
|
227
|
+
d2 = dict(f.f_globals)
|
|
228
|
+
d2.update(f.f_locals)
|
|
229
|
+
if f.f_back :
|
|
230
|
+
f = f.f_back
|
|
231
|
+
d1 = dict(f.f_globals)
|
|
232
|
+
d1.update(f.f_locals)
|
|
233
|
+
else :
|
|
234
|
+
d1 = {}
|
|
235
|
+
pass
|
|
236
|
+
else:
|
|
237
|
+
d2 = {}
|
|
238
|
+
d1 = {}
|
|
239
|
+
pass
|
|
240
|
+
# Take care of case where we lack "context" right away
|
|
241
|
+
return (lname, top_dicts, [{}, {}], d2, d1, frame)
|
|
242
|
+
else:
|
|
243
|
+
return (lname, top_dicts, local_dicts, d2, d1, frame)
|
|
244
|
+
pass # type: ignore[unreachable]
|
|
245
|
+
|
|
246
|
+
def fix_locals_qualname(name:str) -> str :
|
|
247
|
+
"""
|
|
248
|
+
returns the portion of qualname after '<locals>', or all
|
|
249
|
+
of the qualname if '<locals>' does not occur
|
|
250
|
+
"""
|
|
251
|
+
if _local_mark in name :
|
|
252
|
+
name = name[name.index(_local_mark2)+len(_local_mark2):]
|
|
253
|
+
pass
|
|
254
|
+
return name
|
|
255
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pusher_debug
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: tools to assist with debug
|
|
5
|
+
Project-URL: repository, https://codeberg.org/Pusher2531/debug_tools.git
|
|
6
|
+
Project-URL: issues, https://codeberg.org/Pusher2531/debug_tools/issues
|
|
7
|
+
Author-email: Doug Sojourner <doug@sojournings.org>
|
|
8
|
+
License: GNU General Public License v3 (GPLv3)
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Keywords: class modification,debug,debuging,decorator,monkeypatching,typed
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: <4.0,>=3.14
|
|
24
|
+
Requires-Dist: objwatch
|
|
25
|
+
Requires-Dist: packaging
|
|
26
|
+
Requires-Dist: project-version-finder
|
|
27
|
+
Requires-Dist: regex
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
### `pusher_debug`
|
|
31
|
+
## `pusher_debug.tracing`
|
|
32
|
+
Tools to aid in adding print statements to trace program flow:
|
|
33
|
+
* `ObjWatch` from the objwatch library
|
|
34
|
+
* `DetailWrapper`, a "wrapper" to use with Objwatch that provides more details on function calls and returns, and can be subclassed easily to provide distinct id strings for different instances of selected classes.
|
|
35
|
+
* `fn_name`, `fn_entry`, and `fn_exit`, to provide manual logging of details of function you enter (caller, parameters, return value, annotations)
|
|
36
|
+
|
|
37
|
+
## `pusher_debug.monkeypatching`
|
|
38
|
+
Tools to aid in monkeypatching. Two decorators:
|
|
39
|
+
```python
|
|
40
|
+
@add_new_class(ExistingClass)
|
|
41
|
+
class MyClass: ...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
will (in the module where `ExistingClass` is defined)
|
|
45
|
+
1. Create `orig_ExistingClass`, as a backup of `ExistingClass`
|
|
46
|
+
2. Create `new_ExistingClass`, as a backup of `MyClass`
|
|
47
|
+
3. Make `ExistingClass` be `new_ExistingClass` (fixing `__name__` and `__qualname__` between `new_ExistingClass` and `orig_ExistingClass` as well)
|
|
48
|
+
|
|
49
|
+
Then `restore_class(ExistingClass)` will restore the original, and `restore_new_class(ExistingClass)` will put back the new version again.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
@add_to_class(ExistingClass)
|
|
53
|
+
def fn():...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
will (in `ExistingClass`) add a method `fn`. If `fn` already exists, the the original is backed up as `orig_fn`. `new_fn` is created and names are fixed as for `@add_new_class`.
|
|
57
|
+
|
|
58
|
+
`@add_to_class` can be combined with one of `@classmethod`, `@staticmethod`, or `@property` (and if property, then `fn` in the local namespace will be the property, so you can then do
|
|
59
|
+
```python
|
|
60
|
+
@add_to_class(ExistingClass)
|
|
61
|
+
@fn@setter
|
|
62
|
+
def fn():...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You should know that monkeypatching will wreack havoc with mypy, since it does stuff you shouldn't do. (It's for __debugging__ !)
|
|
66
|
+
|
|
67
|
+
For examples, use help(...), and/or download the source package and look at the tests. Seeing all the # test: ignore[] stuff in the test program gives you an idea of how much mypy hates this.
|
|
68
|
+
|
|
69
|
+
The library can be built with `hatchling build`, tested with `tests/test_tools.py -v`, and should pass `mypy` (including having annotations for objwatch).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pusher_debug/__init__.py,sha256=1GCNPeim3YNLiAhQEGp9IeiWUrm-4IUmMTTuwZGa47A,2052
|
|
2
|
+
pusher_debug/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
pusher_debug/monkeypatching/__init__.py,sha256=79YSIMcQEg5OZLvmHhjnA46DgaiFmJPZzP6qCytKFgw,1225
|
|
4
|
+
pusher_debug/monkeypatching/monkeypatching.py,sha256=r7j71ITB8wOqbr7S1lGhxcaO3KMCATyHnEjMzrk7RwI,15754
|
|
5
|
+
pusher_debug/tracing/__init__.py,sha256=Ie5HwdQVrdqi6pmOYxttU5pOoAB0LLpMLb_AQ5ip2Yo,1049
|
|
6
|
+
pusher_debug/tracing/tracing.py,sha256=FhjQi0U70d72sQfsm09YyQbQq66j7Vh4U5MPpXpjlhI,8452
|
|
7
|
+
pusher_debug/util/__init__.py,sha256=mGyRpNPxneDM7b94UaoYMBBIPzZwzuOxWpHogSFi71U,959
|
|
8
|
+
pusher_debug/util/util.py,sha256=cwm6peTxaA2DHwtBVplOr-QPQAcAeWtup8Dx-pHe-Wk,8746
|
|
9
|
+
pusher_debug-1.0.0.dist-info/METADATA,sha256=p-zeagKl0RrXD8n3w3cYCcguGYn-yXfF0EoiNpXof9M,3319
|
|
10
|
+
pusher_debug-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
pusher_debug-1.0.0.dist-info/licenses/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
|
|
12
|
+
pusher_debug-1.0.0.dist-info/RECORD,,
|