py4web-debug-tools 1.2.2__tar.gz → 1.3.1__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.
Files changed (23) hide show
  1. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/CHANGELOG.md +12 -0
  2. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/PKG-INFO +4 -3
  3. py4web_debug_tools-1.3.1/example.py +29 -0
  4. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/pyproject.toml +2 -1
  5. py4web_debug_tools-1.3.1/src/py4web_debug/__about__.py +1 -0
  6. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/__init__.py +2 -1
  7. py4web_debug_tools-1.3.1/src/py4web_debug/dumping.py +290 -0
  8. py4web_debug_tools-1.2.2/src/py4web_debug/__about__.py +0 -1
  9. py4web_debug_tools-1.2.2/src/py4web_debug/dumping.py +0 -154
  10. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/.gitignore +0 -0
  11. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/README.md +0 -0
  12. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/_internals.py +0 -0
  13. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/core.py +0 -0
  14. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/debugbar.py +0 -0
  15. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/env.py +0 -0
  16. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/internals.py +0 -0
  17. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/py.typed +0 -0
  18. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/templates/debugbar.html +0 -0
  19. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/templates/dumpdie.html +0 -0
  20. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/templates/error_default.html +0 -0
  21. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/templates/fancy_dumpdie.html +0 -0
  22. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/types.py +0 -0
  23. {py4web_debug_tools-1.2.2 → py4web_debug_tools-1.3.1}/src/py4web_debug/wsgi.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v1.3.1 (2025-04-01)
6
+
7
+ ### Fix
8
+
9
+ * **dbg:** Detect more variable names ([`a80556c`](https://github.com/trialandsuccess/py4web-debug-tools/commit/a80556c2d35f89a5c0f900518074b7800dfeb844))
10
+
11
+ ## v1.3.0 (2025-04-01)
12
+
13
+ ### Feature
14
+
15
+ * Add `dbg` helper ([`b7529e3`](https://github.com/trialandsuccess/py4web-debug-tools/commit/b7529e32bcaa88a237fa505d26afe3f013d0bdf2))
16
+
5
17
  ## v1.2.2 (2024-11-20)
6
18
 
7
19
  ### Fix
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: py4web-debug-tools
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: Debug Tools for py4web
5
5
  Project-URL: Documentation, https://github.com/trialandsuccess/py4web-debug-tools#readme
6
6
  Project-URL: Issues, https://github.com/trialandsuccess/py4web-debug-tools/issues
7
7
  Project-URL: Source, https://github.com/trialandsuccess/py4web-debug-tools
8
8
  Author-email: Robin van der Noord <contact@trialandsuccess.nl>
9
- License: MIT
9
+ License-Expression: MIT
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3.10
@@ -18,6 +18,7 @@ Requires-Python: >=3.10
18
18
  Requires-Dist: configurable-json>=1.1
19
19
  Requires-Dist: py4web
20
20
  Requires-Dist: pydal
21
+ Requires-Dist: typing-extensions
21
22
  Requires-Dist: yatl
22
23
  Provides-Extra: dev
23
24
  Requires-Dist: hatch; extra == 'dev'
@@ -0,0 +1,29 @@
1
+ import py4web_debug
2
+ from py4web_debug import dbg
3
+
4
+
5
+ def handle(_): ...
6
+
7
+
8
+ def my_decorator(fn):
9
+ return fn
10
+
11
+
12
+ @my_decorator
13
+ def some_func(arg):
14
+ return dbg("hello")
15
+
16
+
17
+ dbg("hi")
18
+ y = dbg("hi")
19
+ py4web_debug.dbg("hi")
20
+ z = py4web_debug.dbg("hi")
21
+
22
+ handle(dbg("hi"))
23
+ handle(py4web_debug.dbg("hi"))
24
+
25
+ some_long_expression = " 3 + 123"
26
+
27
+ handle(dbg((some_long_expression + some_long_expression)))
28
+
29
+ some_func(arg=dbg("arg1"))
@@ -40,7 +40,8 @@ dependencies = [
40
40
  "py4web",
41
41
  "yatl",
42
42
  "pydal",
43
- "configurable-json >= 1.1"
43
+ "configurable-json >= 1.1",
44
+ "typing-extensions",
44
45
  ]
45
46
 
46
47
 
@@ -0,0 +1 @@
1
+ __version__ = "1.3.1"
@@ -2,13 +2,14 @@
2
2
 
3
3
  from .core import tools
4
4
  from .debugbar import catch
5
- from .dumping import dd, dump
5
+ from .dumping import dbg, dd, dump
6
6
  from .env import is_debug
7
7
  from .wsgi import wsgi
8
8
 
9
9
  __all__ = [
10
10
  "tools",
11
11
  "catch",
12
+ "dbg",
12
13
  "dd",
13
14
  "dump",
14
15
  "is_debug",
@@ -0,0 +1,290 @@
1
+ import ast
2
+ import inspect
3
+ import json
4
+ import sys
5
+ import traceback
6
+ import typing
7
+ from types import FrameType
8
+
9
+ import pydal.objects
10
+ from configurablejson import ConfigurableJsonEncoder, JSONRule
11
+ from py4web import response
12
+
13
+ from .env import is_debug
14
+
15
+
16
+ class PossiblyAsDict(typing.Protocol):
17
+ def as_dict(self) -> dict[str, typing.Any]: ...
18
+
19
+ def asdict(self) -> dict[str, typing.Any]: ...
20
+
21
+ def _asdict(self) -> dict[str, typing.Any]: ...
22
+
23
+ def _as_dict(self) -> dict[str, typing.Any]: ...
24
+
25
+ def to_dict(self) -> dict[str, typing.Any]: ...
26
+
27
+ def todict(self) -> dict[str, typing.Any]: ...
28
+
29
+ def _todict(self) -> dict[str, typing.Any]: ...
30
+
31
+ def _to_dict(self) -> dict[str, typing.Any]: ...
32
+
33
+ def __json__(self) -> dict[str, typing.Any]: ...
34
+
35
+ def as_list(self) -> list[typing.Any]: ...
36
+
37
+
38
+ class DDJsonEncoder(ConfigurableJsonEncoder):
39
+ @staticmethod
40
+ def _default(o: PossiblyAsDict) -> dict[str, typing.Any] | list[typing.Any] | str:
41
+ if hasattr(o, "as_list"):
42
+ # note: prefer as_list now as not every Rows may have an id (= default key of as_dict)
43
+ return o.as_list()
44
+ # more as list stuff?
45
+
46
+ if hasattr(o, "as_dict"):
47
+ return o.as_dict()
48
+ elif hasattr(o, "asdict"):
49
+ return o.asdict()
50
+ elif hasattr(o, "_asdict"):
51
+ return o._asdict()
52
+ elif hasattr(o, "_as_dict"):
53
+ return o._as_dict()
54
+ elif hasattr(o, "to_dict"):
55
+ return o.to_dict()
56
+ elif hasattr(o, "todict"):
57
+ return o.todict()
58
+ elif hasattr(o, "_todict"):
59
+ return o._todict()
60
+ elif hasattr(o, "_to_dict"):
61
+ return o._to_dict()
62
+ elif hasattr(o, "__json__"):
63
+ return o.__json__()
64
+
65
+ return str(o)
66
+
67
+ @staticmethod
68
+ def is_probably_namedtuple(o: typing.Any) -> bool:
69
+ """
70
+ Try to guess if 'o' is a Named Tuple or something else.
71
+ """
72
+ return isinstance(o, tuple) and hasattr(o, "_fields")
73
+
74
+ def rules(self, o: typing.Any, with_default: bool = True) -> JSONRule:
75
+ """
76
+ Custom rules for the DD json: set to list and namedtuple to dict
77
+ # NOTE: with_default is (probably) false anyway!!!!
78
+ """
79
+
80
+ _type = typing.NamedTuple if self.is_probably_namedtuple(o) else type(o)
81
+
82
+ rules: dict[type, JSONRule] = {
83
+ # convert set to list
84
+ set: JSONRule(preprocess=lambda o: list(o)),
85
+ # convert namedtuple to dict
86
+ typing.NamedTuple: JSONRule(preprocess=self._default),
87
+ pydal.objects.Row: JSONRule(preprocess=lambda row: row.as_dict()),
88
+ pydal.objects.Rows: JSONRule(preprocess=lambda row: row.as_list()),
89
+ }
90
+
91
+ if rule := rules.get(typing.cast(type, _type)):
92
+ return rule
93
+ elif rule is None and with_default:
94
+ return JSONRule(transform=self._default)
95
+ else:
96
+ # empty rule:
97
+ return JSONRule()
98
+
99
+
100
+ def dump(data: typing.Iterable[typing.Any], with_headers: bool = True) -> str:
101
+ """
102
+ Helper to json dump some data with custom converter and headers
103
+ """
104
+ if with_headers:
105
+ response.headers["Content-Type"] = "application/json"
106
+
107
+ return json.dumps(data, indent=2, cls=DDJsonEncoder)
108
+ # return json.dumps(data, indent=2, default=DDJsonEncoder._default)
109
+
110
+
111
+ class DumpDieError(Exception):
112
+ """
113
+ Custom Exception used to catch dd() in py4web error page -> regular output
114
+ """
115
+
116
+ pass
117
+
118
+
119
+ class FancyDumpDieError(Exception):
120
+ """
121
+ Custom Exception used to catch dd() in py4web error page -> styled output
122
+ """
123
+
124
+ pass
125
+
126
+
127
+ class ApiDumpDieError(Exception):
128
+ """
129
+ Custom Exception used to catch dd() in py4web error page -> JSON only output
130
+ """
131
+
132
+ pass
133
+
134
+
135
+ def dd(*data: typing.Any, fancy: bool = True, api: bool = False) -> None:
136
+ """
137
+ Dump and Die:
138
+ Show args as JSON an halt the rest of the request
139
+ (useful for debugging fifty levels deep in some helper method)
140
+
141
+ """
142
+ if len(data) == 1:
143
+ data = data[0]
144
+
145
+ if not is_debug():
146
+ # simpler:
147
+ print("dd |", data, file=sys.stderr)
148
+ return
149
+
150
+ # if is_debug: more complex error
151
+ _json = dump(data)
152
+ if api:
153
+ # todo: detect automatically?
154
+ raise ApiDumpDieError(_json)
155
+ elif fancy:
156
+ raise FancyDumpDieError(_json)
157
+ else:
158
+ raise DumpDieError(_json)
159
+
160
+
161
+ T = typing.TypeVar("T")
162
+
163
+
164
+ def _extract_dbg_variable_name(frame: FrameType) -> str:
165
+ # Read the full source of the caller file
166
+ with open(frame.f_code.co_filename, "r", encoding="utf-8") as f:
167
+ source_code = f.readlines()
168
+
169
+ # Extract the relevant lines (handle multiline statements)
170
+ full_source = ""
171
+ line_number = frame.f_lineno - 1
172
+ while line_number < len(source_code):
173
+ full_source += source_code[line_number].strip()
174
+ if full_source.count("(") == full_source.count(")"): # Ensure all parentheses are closed
175
+ break
176
+ line_number += 1
177
+
178
+ # Parse the AST
179
+ tree = ast.parse(full_source)
180
+
181
+ # Find the relevant call in the AST
182
+ var_name = ""
183
+ for node in ast.walk(tree):
184
+ if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
185
+ call = node.value
186
+ while isinstance(call, ast.Call): # Handle nested function calls
187
+ if hasattr(call.func, "id") and call.func.id == "dbg":
188
+ var_name = ast.unparse(call.args[0])
189
+ break
190
+ elif isinstance(call.func, ast.Attribute) and call.func.attr == "dbg":
191
+ var_name = ast.unparse(call.args[0])
192
+ break
193
+ call = call.args[0] if call.args else None
194
+ elif isinstance(node, ast.Assign) and isinstance(node.value, ast.Call):
195
+ call = node.value
196
+ while isinstance(call, ast.Call): # Handle assignments with nested calls
197
+ if hasattr(call.func, "id") and call.func.id == "dbg":
198
+ var_name = ast.unparse(call.args[0])
199
+ break
200
+ elif isinstance(call.func, ast.Attribute) and call.func.attr == "dbg":
201
+ var_name = ast.unparse(call.args[0])
202
+ break
203
+ call = call.args[0] if call.args else None
204
+ elif isinstance(node, ast.Return) and isinstance(node.value, ast.Call):
205
+ call = node.value
206
+ while isinstance(call, ast.Call): # Handle return dbg(x)
207
+ if hasattr(call.func, "id") and call.func.id == "dbg":
208
+ var_name = ast.unparse(call.args[0])
209
+ break
210
+ elif isinstance(call.func, ast.Attribute) and call.func.attr == "dbg":
211
+ var_name = ast.unparse(call.args[0])
212
+ break
213
+ call = call.args[0] if call.args else None
214
+ elif isinstance(node, ast.keyword) and isinstance(node.value, ast.Call):
215
+ # Handle function keyword arguments like some_func(arg=dbg("arg"))
216
+ call = node.value
217
+ if hasattr(call.func, "id") and call.func.id == "dbg":
218
+ var_name = ast.unparse(call.args[0])
219
+ break
220
+ elif isinstance(call.func, ast.Attribute) and call.func.attr == "dbg":
221
+ var_name = ast.unparse(call.args[0])
222
+ break
223
+
224
+ return var_name
225
+
226
+
227
+ def extract_dbg_variable_name(frame: FrameType) -> typing.Optional[str]:
228
+ """
229
+ Extracts the variable name passed to dbg() from the source code.
230
+ Handles multiline statements, assignments, function arguments, and return values.
231
+ """
232
+ try:
233
+ return _extract_dbg_variable_name(frame)
234
+ except Exception:
235
+ return None
236
+
237
+
238
+ def dbg(value: T) -> T:
239
+ """
240
+ Improved Rust-like dbg function that captures variable names even in assignments,
241
+ namespaced calls, and nested function calls.
242
+ Handles multiline calls, function arguments, and return statements.
243
+
244
+ Examples:
245
+
246
+ ```
247
+ def handle(_):
248
+ ...
249
+
250
+ def my_decorator(fn): return fn
251
+
252
+ @my_decorator
253
+ def some_func(arg):
254
+ return dbg("hello")
255
+
256
+
257
+ dbg('hi')
258
+ y = dbg('hi')
259
+ py4web_debug.dbg('hi')
260
+ z = py4web_debug.dbg('hi')
261
+
262
+ handle(dbg('hi'))
263
+ handle(py4web_debug.dbg('hi'))
264
+
265
+ some_long_expression = " 3 + 123"
266
+
267
+ handle(
268
+ dbg(
269
+ (
270
+ some_long_expression +
271
+ some_long_expression
272
+ )
273
+ )
274
+ )
275
+
276
+ some_func(arg=dbg("arg1"))
277
+ ```
278
+ """
279
+ value_type = type(value).__name__
280
+
281
+ # Get the caller's frame
282
+ frame = inspect.currentframe().f_back
283
+ frame_info = inspect.getframeinfo(frame)
284
+ file_name = frame_info.filename.split("/")[-1]
285
+ line_number = frame_info.lineno
286
+
287
+ var_name = extract_dbg_variable_name(frame) or "?"
288
+
289
+ print(f"[{file_name}:{line_number}] {value_type}({var_name}) = {value}", file=sys.stderr)
290
+ return value
@@ -1 +0,0 @@
1
- __version__ = "1.2.2"
@@ -1,154 +0,0 @@
1
- import json
2
- import sys
3
- import typing
4
-
5
- import pydal.objects
6
- from configurablejson import ConfigurableJsonEncoder, JSONRule
7
- from py4web import response
8
-
9
- from .env import is_debug
10
-
11
-
12
- class PossiblyAsDict(typing.Protocol):
13
- def as_dict(self) -> dict[str, typing.Any]: ...
14
-
15
- def asdict(self) -> dict[str, typing.Any]: ...
16
-
17
- def _asdict(self) -> dict[str, typing.Any]: ...
18
-
19
- def _as_dict(self) -> dict[str, typing.Any]: ...
20
-
21
- def to_dict(self) -> dict[str, typing.Any]: ...
22
-
23
- def todict(self) -> dict[str, typing.Any]: ...
24
-
25
- def _todict(self) -> dict[str, typing.Any]: ...
26
-
27
- def _to_dict(self) -> dict[str, typing.Any]: ...
28
-
29
- def __json__(self) -> dict[str, typing.Any]: ...
30
-
31
- def as_list(self) -> list[typing.Any]: ...
32
-
33
-
34
- class DDJsonEncoder(ConfigurableJsonEncoder):
35
- @staticmethod
36
- def _default(o: PossiblyAsDict) -> dict[str, typing.Any] | list[typing.Any] | str:
37
- if hasattr(o, "as_list"):
38
- # note: prefer as_list now as not every Rows may have an id (= default key of as_dict)
39
- return o.as_list()
40
- # more as list stuff?
41
-
42
- if hasattr(o, "as_dict"):
43
- return o.as_dict()
44
- elif hasattr(o, "asdict"):
45
- return o.asdict()
46
- elif hasattr(o, "_asdict"):
47
- return o._asdict()
48
- elif hasattr(o, "_as_dict"):
49
- return o._as_dict()
50
- elif hasattr(o, "to_dict"):
51
- return o.to_dict()
52
- elif hasattr(o, "todict"):
53
- return o.todict()
54
- elif hasattr(o, "_todict"):
55
- return o._todict()
56
- elif hasattr(o, "_to_dict"):
57
- return o._to_dict()
58
- elif hasattr(o, "__json__"):
59
- return o.__json__()
60
-
61
- return str(o)
62
-
63
- @staticmethod
64
- def is_probably_namedtuple(o: typing.Any) -> bool:
65
- """
66
- Try to guess if 'o' is a Named Tuple or something else.
67
- """
68
- return isinstance(o, tuple) and hasattr(o, "_fields")
69
-
70
- def rules(self, o: typing.Any, with_default: bool = True) -> JSONRule:
71
- """
72
- Custom rules for the DD json: set to list and namedtuple to dict
73
- # NOTE: with_default is (probably) false anyway!!!!
74
- """
75
-
76
- _type = typing.NamedTuple if self.is_probably_namedtuple(o) else type(o)
77
-
78
- rules: dict[type, JSONRule] = {
79
- # convert set to list
80
- set: JSONRule(preprocess=lambda o: list(o)),
81
- # convert namedtuple to dict
82
- typing.NamedTuple: JSONRule(preprocess=self._default),
83
- pydal.objects.Row: JSONRule(preprocess=lambda row: row.as_dict()),
84
- pydal.objects.Rows: JSONRule(preprocess=lambda row: row.as_list()),
85
- }
86
-
87
- if rule := rules.get(typing.cast(type, _type)):
88
- return rule
89
- elif rule is None and with_default:
90
- return JSONRule(transform=self._default)
91
- else:
92
- # empty rule:
93
- return JSONRule()
94
-
95
-
96
- def dump(data: typing.Iterable[typing.Any], with_headers: bool = True) -> str:
97
- """
98
- Helper to json dump some data with custom converter and headers
99
- """
100
- if with_headers:
101
- response.headers["Content-Type"] = "application/json"
102
-
103
- return json.dumps(data, indent=2, cls=DDJsonEncoder)
104
- # return json.dumps(data, indent=2, default=DDJsonEncoder._default)
105
-
106
-
107
- class DumpDieError(Exception):
108
- """
109
- Custom Exception used to catch dd() in py4web error page -> regular output
110
- """
111
-
112
- pass
113
-
114
-
115
- class FancyDumpDieError(Exception):
116
- """
117
- Custom Exception used to catch dd() in py4web error page -> styled output
118
- """
119
-
120
- pass
121
-
122
-
123
- class ApiDumpDieError(Exception):
124
- """
125
- Custom Exception used to catch dd() in py4web error page -> JSON only output
126
- """
127
-
128
- pass
129
-
130
-
131
- def dd(*data: typing.Any, fancy: bool = True, api: bool = False) -> None:
132
- """
133
- Dump and Die:
134
- Show args as JSON an halt the rest of the request
135
- (useful for debugging fifty levels deep in some helper method)
136
-
137
- """
138
- if len(data) == 1:
139
- data = data[0]
140
-
141
- if not is_debug():
142
- # simpler:
143
- print("dd |", data, file=sys.stderr)
144
- return
145
-
146
- # if is_debug: more complex error
147
- _json = dump(data)
148
- if api:
149
- # todo: detect automatically?
150
- raise ApiDumpDieError(_json)
151
- elif fancy:
152
- raise FancyDumpDieError(_json)
153
- else:
154
- raise DumpDieError(_json)