py4web-debug-tools 1.3.0__tar.gz → 1.3.2__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 (21) hide show
  1. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/CHANGELOG.md +12 -0
  2. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/PKG-INFO +2 -1
  3. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/pyproject.toml +2 -1
  4. py4web_debug_tools-1.3.2/src/py4web_debug/__about__.py +1 -0
  5. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/dumping.py +118 -22
  6. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/internals.py +3 -4
  7. py4web_debug_tools-1.3.0/src/py4web_debug/__about__.py +0 -1
  8. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/.gitignore +0 -0
  9. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/README.md +0 -0
  10. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/__init__.py +0 -0
  11. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/_internals.py +0 -0
  12. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/core.py +0 -0
  13. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/debugbar.py +0 -0
  14. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/env.py +0 -0
  15. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/py.typed +0 -0
  16. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/templates/debugbar.html +0 -0
  17. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/templates/dumpdie.html +0 -0
  18. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/templates/error_default.html +0 -0
  19. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/templates/fancy_dumpdie.html +0 -0
  20. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/types.py +0 -0
  21. {py4web_debug_tools-1.3.0 → py4web_debug_tools-1.3.2}/src/py4web_debug/wsgi.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v1.3.2 (2025-12-12)
6
+
7
+ ### Fix
8
+
9
+ * Use existing debug-tools `dump` instead of py4web's `dumps`, hopefully to prevent circular import error ([`0d5afad`](https://github.com/trialandsuccess/py4web-debug-tools/commit/0d5afad2904834cf80d9252b89816b7ae360249a))
10
+
11
+ ## v1.3.1 (2025-04-01)
12
+
13
+ ### Fix
14
+
15
+ * **dbg:** Detect more variable names ([`a80556c`](https://github.com/trialandsuccess/py4web-debug-tools/commit/a80556c2d35f89a5c0f900518074b7800dfeb844))
16
+
5
17
  ## v1.3.0 (2025-04-01)
6
18
 
7
19
  ### Feature
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py4web-debug-tools
3
- Version: 1.3.0
3
+ Version: 1.3.2
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
@@ -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'
@@ -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.2"
@@ -1,8 +1,9 @@
1
1
  import ast
2
+ import inspect
2
3
  import json
3
4
  import sys
4
- import traceback
5
5
  import typing
6
+ from types import FrameType
6
7
 
7
8
  import pydal.objects
8
9
  from configurablejson import ConfigurableJsonEncoder, JSONRule
@@ -159,35 +160,130 @@ def dd(*data: typing.Any, fancy: bool = True, api: bool = False) -> None:
159
160
  T = typing.TypeVar("T")
160
161
 
161
162
 
162
- def dbg(value: T) -> T:
163
+ def _extract_dbg_variable_name(frame: FrameType) -> str:
164
+ # Read the full source of the caller file
165
+ with open(frame.f_code.co_filename, "r", encoding="utf-8") as f:
166
+ source_code = f.readlines()
167
+
168
+ # Extract the relevant lines (handle multiline statements)
169
+ full_source = ""
170
+ line_number = frame.f_lineno - 1
171
+ while line_number < len(source_code):
172
+ full_source += source_code[line_number].strip()
173
+ if full_source.count("(") == full_source.count(")"): # Ensure all parentheses are closed
174
+ break
175
+ line_number += 1
176
+
177
+ # Parse the AST
178
+ tree = ast.parse(full_source)
179
+
180
+ # Find the relevant call in the AST
181
+ var_name = ""
182
+ for node in ast.walk(tree):
183
+ if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
184
+ call = node.value
185
+ while isinstance(call, ast.Call): # Handle nested function calls
186
+ if hasattr(call.func, "id") and call.func.id == "dbg":
187
+ var_name = ast.unparse(call.args[0])
188
+ break
189
+ elif isinstance(call.func, ast.Attribute) and call.func.attr == "dbg":
190
+ var_name = ast.unparse(call.args[0])
191
+ break
192
+ call = call.args[0] if call.args else None
193
+ elif isinstance(node, ast.Assign) and isinstance(node.value, ast.Call):
194
+ call = node.value
195
+ while isinstance(call, ast.Call): # Handle assignments with nested calls
196
+ if hasattr(call.func, "id") and call.func.id == "dbg":
197
+ var_name = ast.unparse(call.args[0])
198
+ break
199
+ elif isinstance(call.func, ast.Attribute) and call.func.attr == "dbg":
200
+ var_name = ast.unparse(call.args[0])
201
+ break
202
+ call = call.args[0] if call.args else None
203
+ elif isinstance(node, ast.Return) and isinstance(node.value, ast.Call):
204
+ call = node.value
205
+ while isinstance(call, ast.Call): # Handle return dbg(x)
206
+ if hasattr(call.func, "id") and call.func.id == "dbg":
207
+ var_name = ast.unparse(call.args[0])
208
+ break
209
+ elif isinstance(call.func, ast.Attribute) and call.func.attr == "dbg":
210
+ var_name = ast.unparse(call.args[0])
211
+ break
212
+ call = call.args[0] if call.args else None
213
+ elif isinstance(node, ast.keyword) and isinstance(node.value, ast.Call):
214
+ # Handle function keyword arguments like some_func(arg=dbg("arg"))
215
+ call = node.value
216
+ if hasattr(call.func, "id") and call.func.id == "dbg":
217
+ var_name = ast.unparse(call.args[0])
218
+ break
219
+ elif isinstance(call.func, ast.Attribute) and call.func.attr == "dbg":
220
+ var_name = ast.unparse(call.args[0])
221
+ break
222
+
223
+ return var_name
224
+
225
+
226
+ def extract_dbg_variable_name(frame: FrameType) -> typing.Optional[str]:
227
+ """
228
+ Extracts the variable name passed to dbg() from the source code.
229
+ Handles multiline statements, assignments, function arguments, and return values.
163
230
  """
164
- Helper inspired by Rust's `dbg!`.
231
+ try:
232
+ return _extract_dbg_variable_name(frame)
233
+ except Exception:
234
+ return None
235
+
165
236
 
166
- Prints out the file, line, variable name, type and value.
167
- Also returns the original value.
237
+ def dbg(value: T) -> T:
168
238
  """
169
- value_type = type(value).__name__
239
+ Improved Rust-like dbg function that captures variable names even in assignments,
240
+ namespaced calls, and nested function calls.
241
+ Handles multiline calls, function arguments, and return statements.
170
242
 
171
- frame = traceback.extract_stack()[-2] # Get caller frame info
172
- file_name = frame.filename.split("/")[-1]
173
- line_number = frame.lineno
243
+ Examples:
174
244
 
175
- try:
176
- with open(frame.filename, "r") as f:
177
- source_code = f.readlines()
245
+ ```
246
+ def handle(_):
247
+ ...
178
248
 
179
- line = source_code[line_number - 1].strip()
180
- tree = ast.parse(line)
249
+ def my_decorator(fn): return fn
181
250
 
182
- var_name = None
183
- if isinstance(tree, ast.Module) and isinstance(tree.body[0], ast.Expr):
184
- call = tree.body[0].value
185
- if isinstance(call, ast.Call) and call.func.id == "dbg":
186
- var_name = ast.unparse(call.args[0]) # Extract the first argument as a string
251
+ @my_decorator
252
+ def some_func(arg):
253
+ return dbg("hello")
187
254
 
188
- except Exception:
189
- var_name = "?"
190
255
 
191
- print(f"[{file_name}:{line_number}] {value_type}({var_name}) = {value}", file=sys.stderr)
256
+ dbg('hi')
257
+ y = dbg('hi')
258
+ py4web_debug.dbg('hi')
259
+ z = py4web_debug.dbg('hi')
192
260
 
261
+ handle(dbg('hi'))
262
+ handle(py4web_debug.dbg('hi'))
263
+
264
+ some_long_expression = " 3 + 123"
265
+
266
+ handle(
267
+ dbg(
268
+ (
269
+ some_long_expression +
270
+ some_long_expression
271
+ )
272
+ )
273
+ )
274
+
275
+ some_func(arg=dbg("arg1"))
276
+ ```
277
+ """
278
+ value_type = type(value).__name__
279
+
280
+ # Get the caller's frame
281
+ frame = inspect.currentframe().f_back
282
+ frame_info = inspect.getframeinfo(frame)
283
+ file_name = frame_info.filename.split("/")[-1]
284
+ line_number = frame_info.lineno
285
+
286
+ var_name = extract_dbg_variable_name(frame) or "?"
287
+
288
+ print(f"[{file_name}:{line_number}] {value_type}({var_name}) = {value}", file=sys.stderr)
193
289
  return value
@@ -1,6 +1,5 @@
1
1
  import functools
2
2
  import http.client
3
- import json
4
3
  import logging
5
4
  import os
6
5
  import re
@@ -14,7 +13,6 @@ from py4web import HTTP, action, response
14
13
  from py4web.core import (
15
14
  REGEX_APPJSON,
16
15
  Template,
17
- dumps,
18
16
  error_logger,
19
17
  get_error_snapshot,
20
18
  request,
@@ -22,6 +20,7 @@ from py4web.core import (
22
20
  from typing_extensions import NotRequired
23
21
  from yatl import XML
24
22
 
23
+ from .dumping import dump
25
24
  from .types import ErrorSnapshot
26
25
 
27
26
 
@@ -79,7 +78,7 @@ def custom_error_page(
79
78
  # if client accepts 'application/json' - return json
80
79
  if re.search(REGEX_APPJSON, request.headers.get("accept", "")):
81
80
  response.status = code
82
- return json.dumps(context)
81
+ return dump(context)
83
82
  # else - return html error-page
84
83
 
85
84
  if renderer:
@@ -142,7 +141,7 @@ class patch_py4:
142
141
  ret = func(*func_args, **func_kwargs)
143
142
  if isinstance(ret, dict):
144
143
  response.headers["Content-Type"] = "application/json"
145
- ret = dumps(ret)
144
+ ret = dump(ret)
146
145
  return ret
147
146
  except HTTP as http:
148
147
  response.status = http.status
@@ -1 +0,0 @@
1
- __version__ = "1.3.0"