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.
@@ -0,0 +1,61 @@
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
+
22
+ from .monkeypatching import *
23
+ from .tracing import *
24
+ from project_version_finder import get_version
25
+ from packaging.version import Version
26
+
27
+ _full_version_ = Version(get_version('pusher_debug'))
28
+
29
+ __version__ = str(_full_version_)
30
+
31
+ __doc__ = """
32
+ Functions to facillitate debugging:
33
+
34
+ Monkeypatching functions (add_to_class and add_new_class are decorators)
35
+ add_to_class, backup_member, restore_member, backup_new_member, restore_new_member
36
+ monkeypatch member functions and move between original and new versions
37
+
38
+ add_new_class, backup_class, restore_class, backup_new_class, restore_new_class
39
+ do the same for entire classes
40
+
41
+ Tracing functions
42
+ ObjWatch (from objwatch library) and DetailWrapper permit detailled monitoring
43
+ of calls/returns from a class or set of classes
44
+
45
+ fn_name, fn_entry, and fn_exit
46
+ make easy documentation of function enter/exit with more focus that ObjWatch
47
+
48
+ """
49
+
50
+ __all__ = [
51
+ # Class member function manipulation
52
+ 'add_to_class', 'backup_member', 'restore_member',
53
+ 'restore_new_member',
54
+ # Class manipulation
55
+ 'add_new_class', 'backup_class', 'restore_class',
56
+ 'restore_new_class',
57
+ # ObjWatch functions and classes:
58
+ 'ObjWatch', 'DetailWrapper', 'labelest',
59
+ # Debug logging
60
+ 'fn_name', 'fn_entry', 'fn_exit',
61
+ ]
@@ -0,0 +1,32 @@
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
+
22
+ from .monkeypatching import add_to_class, backup_member, restore_member, restore_new_member, add_new_class, backup_class, restore_class, restore_new_class, backup_new_member, backup_new_member
23
+
24
+
25
+ __all__ = [
26
+ # Class member function manipulation
27
+ 'add_to_class', 'backup_member', 'restore_member',
28
+ 'restore_new_member',
29
+ # Class manipulation
30
+ 'add_new_class', 'backup_class', 'restore_class',
31
+ 'restore_new_class',
32
+ ]
@@ -0,0 +1,418 @@
1
+ #!/usr/bin/env python
2
+ from inspect import currentframe, signature, isclass, isfunction, unwrap, getmodule
3
+ import regex as re
4
+ from typing import Callable, overload, Any, Literal, Final
5
+ from types import FrameType, NoneType
6
+
7
+ from ..util import qualname_context, fix_locals_qualname, find_in_dicts
8
+
9
+ def add_to_class(cls:type) -> Any :
10
+ """
11
+ Decorator w/ 1 argument:
12
+ Takes a class (cls), and modifies the following function to
13
+ (1) have an appropriate __qualname__ to be in class cls, and
14
+ (2) add it as a member to cls
15
+
16
+ Works with @classmethod, @staticmethod and @property (and if you
17
+ add a property p, works with @p to add other access functions)
18
+ @add_to_class(MyClass)
19
+ @classmethod
20
+ def MyFn(cls, arg1, arg2):
21
+ # do stuff
22
+ return result
23
+
24
+ will add a new class method to MyClass:
25
+ MyClass.MyFn() which takes two (other) arguments.
26
+ """
27
+ assert(isclass(cls))
28
+ @overload
29
+ def decorator(_fn_: Callable[...,Any|None]) -> Callable[...,Any|None]: ...
30
+ @overload
31
+ def decorator(_fn_: property) -> property: ...
32
+ def decorator(_fn_: Callable[...,Any|None]|property) -> Callable[...,Any|None]|property:
33
+ """
34
+ internal decorator function to actually decorate _fn_
35
+ """
36
+ #
37
+ # Can probaby add properties since we now use setattr
38
+ #
39
+ # sanity checks
40
+ cleanup = True
41
+ frame = currentframe()
42
+ assert(isinstance(frame, FrameType) and isinstance(frame.f_back, FrameType))
43
+ local = frame.f_back.f_locals
44
+ cls_qualname = fix_locals_qualname(cls.__qualname__)
45
+ if isinstance(_fn_, property) :
46
+ cleanup = False
47
+ check_fns = [_fn_.fget, _fn_.fset, _fn_.fdel]
48
+ is_property = True
49
+ for f in check_fns :
50
+ if f :
51
+ fn_name = fix_locals_qualname(f.__name__)
52
+ break
53
+ pass
54
+ for f in check_fns :
55
+ if f :
56
+ f.__qualname__ = f'{cls_qualname}.{fn_name}'
57
+ f.__name__ = f'{fn_name}'
58
+ pass
59
+ pass
60
+ if fn_name in cls.__dict__ : # are we changing or replacing or new
61
+ c_prop = cls.__dict__[fn_name]
62
+ same = True
63
+ for f_old, f in zip([c_prop.fget, c_prop.fset, c_prop.fdel], check_fns) :
64
+ if f_old and f_old != f :
65
+ same = False
66
+ break
67
+ pass
68
+ if same :
69
+ save = False # adding an access function
70
+ else:
71
+ save = True # different property in local
72
+ pass
73
+ else :
74
+ save = False # nothing to save
75
+ pass
76
+ else :
77
+ check_fns = [_fn_]
78
+ fn_name = fix_locals_qualname(_fn_.__name__)
79
+ is_property = False
80
+ pass
81
+ if isinstance(_fn_, classmethod) :
82
+ for fn in check_fns : # type: ignore[unreachable]
83
+ if fn :
84
+ _fn_unwrap_ = unwrap(fn)
85
+ assert(list(signature(_fn_unwrap_).parameters.keys())[0] == 'cls')
86
+ assert(isfunction(_fn_unwrap_))
87
+ pass
88
+ pass
89
+ pass
90
+ elif isinstance(_fn_, staticmethod):
91
+ for fn in check_fns :
92
+ if fn :
93
+ _fn_unwrap_ = unwrap(fn)
94
+ assert(isfunction(_fn_unwrap_))
95
+ cleanup = False
96
+ pass
97
+ pass
98
+ pass
99
+ else :
100
+ for fn in check_fns :
101
+ if fn :
102
+ assert(list(signature(fn).parameters.keys())[0] == 'self')
103
+ pass
104
+ pass
105
+ pass
106
+ for fn in check_fns :
107
+ if fn :
108
+ fn.__qualname__ = f'{cls_qualname}.{fn_name}'
109
+ pass
110
+ pass
111
+ orig_name = "orig_"+fn_name
112
+ new_name = "new_"+fn_name
113
+ base_name = fn_name
114
+ if fn_name[0:4] == 'new_' :
115
+ base_name = fn_name[4:]
116
+ orig_name = 'orig_' + base_name
117
+ new_name = fn_name
118
+ pass
119
+ if (base_name in cls.__dict__
120
+ and (orig_name not in cls.__dict__
121
+ or (is_property and save))):
122
+ setattr(cls, orig_name, cls.__dict__[base_name])
123
+ pass
124
+ setattr(cls, new_name, _fn_)
125
+ setattr(cls, base_name, _fn_)
126
+ if _fn_.__doc__ :
127
+ if not re.search('monkeypatched',_fn_.__doc__) :
128
+ _fn_.__doc__ += f'\nmonkeypatched into {cls.__name__}'
129
+ pass
130
+ elif (orig_name in cls.__dict__
131
+ and cls.__dict__[orig_name].__doc__) :
132
+ _fn_.__doc__ = cls.__dict__[orig_name].__doc__ + '\nmonkeypatched'
133
+ else :
134
+ _fn_.__doc__ = f'monkeypatched into {cls.__name__}'
135
+ pass
136
+ if orig_name in cls.__dict__ :
137
+ # No longer the same as fn, so fix names
138
+ if is_property:
139
+ cls.__dict__[orig_name].__set_name__(cls,orig_name)
140
+ pass
141
+ else:
142
+ cls.__dict__[orig_name].__qualname__ = f"{cls_qualname}.{orig_name}"
143
+ cls.__dict__[orig_name].__name__ = orig_name
144
+ pass
145
+ pass
146
+ if is_property :
147
+ # for property, add property w/ expected name to locals
148
+ local[fn_name] = _fn_
149
+ pass
150
+ return _fn_
151
+
152
+ return decorator
153
+
154
+ # @overload
155
+ # def mv_cls(cls:type, *, saving:bool=False, saving_new:bool=False, restoring:bool=False, restoring_new:bool=False, new_class:Literal[True | False]) -> Callable: ...
156
+ # @overload
157
+ # def mv_cls(cls:type, *, saving:bool=False, saving_new:bool=False, restoring:bool=False, restoring_new:bool=False, new_class:None) -> None: ...
158
+ # @overload
159
+ # def mv_cls(cls:type, *, saving:bool=False, saving_new:bool=False, restoring:bool=False, restoring_new:bool=False, new_class:type) -> type: ...
160
+ def mv_cls(cls:type, *, saving:bool=False, saving_new:bool=False, restoring:bool=False, restoring_new:bool=False, new_class:type|None|bool = None) -> Any:
161
+ """
162
+ Function which does the work of class monkey patching
163
+ We have (at most) two versions of classes: new and orig
164
+ The base function will be one of these, but we adjust the cls.__name__
165
+ as appropriate
166
+ """
167
+ if (int(saving) + int(saving_new) + int(restoring) + int(restoring_new) + bool(new_class)) != 1 :
168
+ raise ValueError("You must set exactly 1 of saving, saving_new, restoring, "
169
+ "restoring_new, new_class to True (or a class in the last case)")
170
+ if (not isinstance(new_class, NoneType)
171
+ and not isinstance(new_class, bool)
172
+ and not isclass(new_class)) :
173
+ raise ValueError("new_class needs to be a class")
174
+
175
+ assert(isclass(cls))
176
+ cls_name = cls.__name__
177
+ if cls_name[0:4] == 'new_' :
178
+ base_name = cls_name[4:]
179
+ new_name = cls_name
180
+ orig_name = 'orig_' + base_name
181
+ elif cls_name[0:5] == 'orig_' :
182
+ base_name = cls_name[5:]
183
+ new_name = 'new_' + base_name
184
+ orig_name = cls_name
185
+ else:
186
+ base_name = cls_name
187
+ new_name = 'new_' + base_name
188
+ orig_name = 'orig_' + base_name
189
+ pass
190
+ module = getmodule(cls)
191
+ qualpart = '.'.join(cls.__qualname__.split('.')[:-1])
192
+ d = module.__dict__
193
+ if saving and orig_name not in d :
194
+ d[orig_name] = cls
195
+ if new_name in d :
196
+ d[new_name].__name__ = new_name
197
+ d[new_name].__qualname__ = qualpart + '.' + new_name
198
+ pass
199
+ return None
200
+ elif saving_new and new_name not in d :
201
+ d[new_name] = cls
202
+ if orig_name in d :
203
+ d[orig_name].__name__ = orig_name
204
+ d[orig_name].__qualname__ = qualpart + '.' + orig_name
205
+ pass
206
+ return None
207
+ elif restoring and orig_name in d :
208
+ d[base_name] = d[orig_name]
209
+ d[base_name].__name__ = base_name
210
+ d[base_name].__qualname__ = qualpart + '.' + base_name
211
+ if new_name in d :
212
+ d[new_name].__name__ = new_name
213
+ d[new_name].__qualname__ = qualpart + '.' + new_name
214
+ pass
215
+ return None
216
+ elif restoring and new_name in d :
217
+ d[base_name] = d[new_name]
218
+ d[base_name].__name__ = base_name
219
+ d[base_name].__qualname__ = qualpart + '.' + base_name
220
+ if orig_name in d :
221
+ d[orig_name].__name__ = orig_name
222
+ d[orig_name].__qualname__ = qualpart + '.' + orig_name
223
+ pass
224
+ return None
225
+ elif new_class :
226
+ def decorator(new_cls:type) -> type :
227
+ if new_cls.__doc__ :
228
+ new_cls.__doc__ += f'\nmonkeypatched into {module}'
229
+ elif base_name in d and d[base_name].__doc__:
230
+ new_cls.__doc__ = d[base_name].__doc__ + f'\nmonkeypatched into {module}'
231
+ elif orig_name in d and d[orig_name].__doc__:
232
+ new_cls.__doc__ = d[orig_name].__doc__ + f'\nmonkeypatched into {module}'
233
+ else:
234
+ new_cls.__doc__ = f'monkeypatched into {module}'
235
+ pass
236
+ if base_name in d :
237
+ mv_cls(d[base_name], saving=True)
238
+ pass
239
+ #b_name = name
240
+ d[orig_name] = d[base_name]
241
+ d[orig_name].__name__ = orig_name
242
+ d[orig_name].__qualname__ = qualpart + '.' + orig_name
243
+ d[new_name] = new_cls
244
+ d[new_name].__name__ = base_name
245
+ d[new_name].__qualname__ = qualpart + '.' + base_name
246
+ d[base_name] = d[new_name]
247
+ return new_cls
248
+ if isinstance(new_class, bool) and new_class :
249
+ return decorator
250
+ else :
251
+ return decorator(new_class)
252
+ else :
253
+ pass # exactly 1 previous case known to be true
254
+ pass # This branch has returned by now
255
+
256
+ def backup_class(cls:type) -> None :
257
+ """
258
+ cls should be class
259
+ this will make orig_class = class
260
+ (checks first if orig_class already exists)
261
+ """
262
+ mv_cls(cls, saving=True)
263
+ return
264
+
265
+ def backup_new_class(cls:type) -> None :
266
+ """
267
+ cls should be class
268
+ this will make new_class = class
269
+ (checks first if new_class already exists)
270
+ """
271
+ mv_cls(cls, saving_new=True)
272
+ return
273
+
274
+ def restore_class(cls:type) -> None :
275
+ """
276
+ cls should be class
277
+ Checks first if orig_class exists, and if so
278
+ this will make class = orig_class
279
+ """
280
+ mv_cls(cls, restoring=True)
281
+ return
282
+
283
+ def restore_new_class(cls:type) -> None :
284
+ """
285
+ cls should be class
286
+ Checks first if new_class exists, and if so
287
+ this will make class = new_class
288
+ """
289
+ mv_cls(cls, restoring_new=True)
290
+ return
291
+
292
+ def add_new_class(cls:type, new_cls:type|None=None) -> type|Callable :
293
+ """
294
+ as a function:
295
+ cls should be class, as should new_cls
296
+ this will make new_class = new_cls (backing up class
297
+ first as orig_class if it already exists), and then
298
+ copy new_class to class.
299
+
300
+ With only one argument, it can be a decorator for
301
+ a class delcaration
302
+ """
303
+ if not new_cls :
304
+ return mv_cls(cls, new_class=True)
305
+ else :
306
+ return mv_cls(cls, new_class=new_cls)
307
+
308
+ def fix_names_mv_fn(fn:Callable|property, *, saving:bool=False, saving_new:bool=False, restoring:bool=False, restoring_new:bool=False) -> None:
309
+ """
310
+ Function which does the work of the member function monkeypatching
311
+ We have (at most) two versions of functions: new and orig
312
+ The base function will be one of these, but we adjust the fn.__name__
313
+ to be correct. E.g.
314
+ if we have original
315
+ C.__init__ == C.orig___init__
316
+ and C.__init__.__name__ == '__init__'
317
+ and C.new___init__.__name__ == 'new___init__'
318
+ OR
319
+ C.__init__ == C.new___init__
320
+ and C.__init__.__name__ == '__init__'
321
+ and C.orig___init__.__name__ == 'orig___init__'
322
+ """
323
+ if (int(saving) + int(saving_new) + int(restoring) + int(restoring_new)) != 1 :
324
+ raise ValueError("You must set exactly 1 of saving, saving_new, restoring, "
325
+ "restoring_new to True")
326
+ if isinstance(fn, property) :
327
+ for f in [fn.fget, fn.fset, fn.fdel]:
328
+ if f :
329
+ base_name = f.__name__
330
+ fn_qname = f.__qualname__
331
+ break
332
+ pass
333
+ pass
334
+ else :
335
+ base_name = fn.__name__
336
+ fn_qname = fn.__qualname__
337
+ pass
338
+ orig_name = 'orig_' + base_name
339
+ new_name = 'new_' + base_name
340
+ split_name = fn_qname.split('.')
341
+ cls_qname = '.'.join(split_name[:-1])
342
+ # find out where cls may be defined
343
+ _, [g, l], [lg, ll], d1, d2, frm = qualname_context(fn_qname)
344
+ # prefer local, just up stack frame, and lastly global
345
+ cls = find_in_dicts(split_name[-2], [ll, l, d2, d1, lg, g])
346
+ # cls.__qualname__ differs from cls_qname in that the first
347
+ # may have blah.blah.<locals>. prepended
348
+ if saving : # base -> orig
349
+ if not orig_name in cls.__dict__ :
350
+ setattr(cls, orig_name, fn)
351
+ # don't change names in fn, as it is still base fn
352
+ else:
353
+ pass # nothing to do
354
+ elif saving_new : # base -> orig
355
+ if not new_name in cls.__dict__ :
356
+ setattr(cls, new_name, fn)
357
+ # don't change names in fn, as it is still base fn
358
+ else:
359
+ pass # nothing to do
360
+ elif restoring and orig_name in cls.__dict__ : # orig -> base
361
+ if (new_name in cls.__dict__
362
+ and cls.__dict__[base_name] == cls.__dict__[new_name]) :
363
+ # Do change names of new, since we replace
364
+ new_fn = cls.__dict__[new_name]
365
+ new_fn.__name__ = new_name
366
+ new_fn.__qualname__ = f"{cls_qname}.{new_name}"
367
+ pass
368
+ setattr(cls, base_name, cls.__dict__[orig_name])
369
+ base_fn = cls.__dict__[base_name]
370
+ base_fn.__name__ = base_name
371
+ base_fn.__qualname__ = f"{cls_qname}.{base_name}"
372
+ elif restoring_new and new_name in cls.__dict__ : # new -> base
373
+ if (orig_name in cls.__dict__
374
+ and cls.__dict__[base_name] == cls.__dict__[orig_name]) :
375
+ # Do change names of orig, since we replace
376
+ orig_fn = cls.__dict__[orig_name]
377
+ orig_fn.__name__ = orig_name
378
+ orig_fn.__qualname__ = f"{cls_qname}.{orig_name}"
379
+ pass
380
+ setattr(cls, base_name, cls.__dict__[new_name])
381
+ base_fn = cls.__dict__[base_name]
382
+ base_fn.__name__ = base_name
383
+ base_fn.__qualname__ = f"{cls_qname}.{base_name}"
384
+ else:
385
+ pass # all cases enumerated above or we raised ValueError
386
+ return
387
+
388
+ def backup_member(member:Any) -> None :
389
+ """
390
+ member should be Class.Function
391
+ this will make Class.orig_Function = Class.Function
392
+ (checks first if orig_Function already exists)
393
+ """
394
+ return fix_names_mv_fn(member, saving=True)
395
+
396
+ def backup_new_member(member:Any) -> None :
397
+ """
398
+ member should be Class.Function
399
+ this will make Class.new_Function = Class.Function
400
+ (checks first if new_Function already exists)
401
+ """
402
+ return fix_names_mv_fn(member, saving_new=True)
403
+
404
+ def restore_member(member:Any) -> None :
405
+ """
406
+ member should be Class.Function
407
+ Checks first if orig_Function exists, and if so
408
+ this will make Class.Function = Class.orig_Function
409
+ """
410
+ return fix_names_mv_fn(member, restoring=True)
411
+
412
+ def restore_new_member(member:Any) -> None :
413
+ """
414
+ member should be Class.Function
415
+ Checks first if new_Function exists, and if so
416
+ this will make Class.Function = Class.new_Function
417
+ """
418
+ return fix_names_mv_fn(member, restoring_new=True)
pusher_debug/py.typed ADDED
File without changes
@@ -0,0 +1,30 @@
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
+
22
+ from objwatch import ObjWatch
23
+ from .tracing import DetailWrapper, labelest, fn_name, fn_entry, fn_exit
24
+
25
+ __all__ = [
26
+ # ObjWatch functions and classes:
27
+ 'ObjWatch', 'DetailWrapper', 'labelest',
28
+ # Debug logging
29
+ 'fn_name', 'fn_entry', 'fn_exit',
30
+ ]