py4web-debug-tools 1.3.1__tar.gz → 1.3.3__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.3.1 → py4web_debug_tools-1.3.3}/CHANGELOG.md +10 -0
  2. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/PKG-INFO +3 -1
  3. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/pyproject.toml +6 -0
  4. py4web_debug_tools-1.3.3/src/py4web_debug/__about__.py +1 -0
  5. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/dumping.py +13 -15
  6. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/internals.py +3 -4
  7. py4web_debug_tools-1.3.3/tests/test_dumping.py +99 -0
  8. py4web_debug_tools-1.3.1/example.py +0 -29
  9. py4web_debug_tools-1.3.1/src/py4web_debug/__about__.py +0 -1
  10. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/.gitignore +0 -0
  11. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/README.md +0 -0
  12. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/__init__.py +0 -0
  13. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/_internals.py +0 -0
  14. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/core.py +0 -0
  15. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/debugbar.py +0 -0
  16. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/env.py +0 -0
  17. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/py.typed +0 -0
  18. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/templates/debugbar.html +0 -0
  19. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/templates/dumpdie.html +0 -0
  20. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/templates/error_default.html +0 -0
  21. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/templates/fancy_dumpdie.html +0 -0
  22. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/types.py +0 -0
  23. {py4web_debug_tools-1.3.1 → py4web_debug_tools-1.3.3}/src/py4web_debug/wsgi.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v1.3.3 (2026-08-06)
6
+
7
+
8
+
9
+ ## v1.3.2 (2025-12-12)
10
+
11
+ ### Fix
12
+
13
+ * 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))
14
+
5
15
  ## v1.3.1 (2025-04-01)
6
16
 
7
17
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py4web-debug-tools
3
- Version: 1.3.1
3
+ Version: 1.3.3
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
@@ -21,7 +21,9 @@ Requires-Dist: pydal
21
21
  Requires-Dist: typing-extensions
22
22
  Requires-Dist: yatl
23
23
  Provides-Extra: dev
24
+ Requires-Dist: dotmap; extra == 'dev'
24
25
  Requires-Dist: hatch; extra == 'dev'
26
+ Requires-Dist: pytest; extra == 'dev'
25
27
  Requires-Dist: su6[all]; extra == 'dev'
26
28
  Description-Content-Type: text/markdown
27
29
 
@@ -54,6 +54,8 @@ Source = "https://github.com/trialandsuccess/py4web-debug-tools"
54
54
  dev = [
55
55
  "su6[all]",
56
56
  "hatch",
57
+ "pytest",
58
+ "dotmap",
57
59
  ]
58
60
 
59
61
 
@@ -69,6 +71,10 @@ upload_to_release = false
69
71
  build_command = "hatch build"
70
72
 
71
73
 
74
+ [tool.pytest.ini_options]
75
+ testpaths = ["tests"]
76
+ pythonpath = ["."]
77
+
72
78
  [tool.su6]
73
79
  directory = "src"
74
80
  include = []
@@ -0,0 +1 @@
1
+ __version__ = "1.3.3"
@@ -2,7 +2,6 @@ import ast
2
2
  import inspect
3
3
  import json
4
4
  import sys
5
- import traceback
6
5
  import typing
7
6
  from types import FrameType
8
7
 
@@ -38,31 +37,30 @@ class PossiblyAsDict(typing.Protocol):
38
37
  class DDJsonEncoder(ConfigurableJsonEncoder):
39
38
  @staticmethod
40
39
  def _default(o: PossiblyAsDict) -> dict[str, typing.Any] | list[typing.Any] | str:
41
- if hasattr(o, "as_list"):
40
+ # note: getattr with default rather than setattr because setattr might lie and produce None
41
+ if getattr(o, "as_list", None):
42
42
  # note: prefer as_list now as not every Rows may have an id (= default key of as_dict)
43
43
  return o.as_list()
44
- # more as list stuff?
45
-
46
- if hasattr(o, "as_dict"):
44
+ elif getattr(o, "as_dict", None):
47
45
  return o.as_dict()
48
- elif hasattr(o, "asdict"):
46
+ elif getattr(o, "asdict", None):
49
47
  return o.asdict()
50
- elif hasattr(o, "_asdict"):
48
+ elif getattr(o, "_asdict", None):
51
49
  return o._asdict()
52
- elif hasattr(o, "_as_dict"):
50
+ elif getattr(o, "_as_dict", None):
53
51
  return o._as_dict()
54
- elif hasattr(o, "to_dict"):
52
+ elif getattr(o, "to_dict", None):
55
53
  return o.to_dict()
56
- elif hasattr(o, "todict"):
54
+ elif getattr(o, "todict", None):
57
55
  return o.todict()
58
- elif hasattr(o, "_todict"):
56
+ elif getattr(o, "_todict", None):
59
57
  return o._todict()
60
- elif hasattr(o, "_to_dict"):
58
+ elif getattr(o, "_to_dict", None):
61
59
  return o._to_dict()
62
- elif hasattr(o, "__json__"):
60
+ elif getattr(o, "__json__", None):
63
61
  return o.__json__()
64
-
65
- return str(o)
62
+ else:
63
+ return str(o)
66
64
 
67
65
  @staticmethod
68
66
  def is_probably_namedtuple(o: typing.Any) -> bool:
@@ -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
@@ -0,0 +1,99 @@
1
+ from types import SimpleNamespace
2
+
3
+ from dotmap import DotMap
4
+ from pytest import MonkeyPatch
5
+
6
+ from src.py4web_debug.dumping import DDJsonEncoder, DumpDieError, dd, dump
7
+
8
+
9
+ def _sample_dotmap() -> DotMap:
10
+ data = DotMap()
11
+ data.a = 1
12
+ data.b.c = "nested"
13
+ return data
14
+
15
+
16
+ def test_dump_handles_dotmap():
17
+ # dump() should serialize a DotMap without crashing.
18
+ result = dump(_sample_dotmap(), with_headers=False)
19
+
20
+ assert '"a": 1' in result
21
+ assert '"c": "nested"' in result
22
+
23
+
24
+ def test_dd_handles_dotmap(monkeypatch: MonkeyPatch):
25
+ # dd() should not crash on a DotMap; in debug mode it raises a DumpDieError
26
+ # (by design) instead of letting json serialization blow up.
27
+ monkeypatch.setenv("PY4WEB_DEBUG_MODE", "1")
28
+
29
+ try:
30
+ dd(_sample_dotmap(), fancy=False)
31
+ except DumpDieError as e:
32
+ assert '"a": 1' in str(e)
33
+ else:
34
+ raise AssertionError("dd() should raise DumpDieError in debug mode")
35
+
36
+
37
+ def test_default_crashes_on_bare_dotmap():
38
+ """
39
+ DotMap's __getattr__ returns an empty DotMap for ANY missing attribute
40
+ instead of raising AttributeError, so hasattr(dotmap, "as_list") is
41
+ always True even though `as_list` isn't a real method on it.
42
+
43
+ Note: dump()/dd() never hit this for a *bare* DotMap in practice, because
44
+ DotMap is a dict subclass and gets recognized (and recursed into) by
45
+ configurablejson's encoder before _default() is ever called - see
46
+ test_dump_handles_dotmap above, which passes fine. This test instead
47
+ calls DDJsonEncoder._default() directly, to pin the underlying flaw that
48
+ both this and the TypeDAL-model-class crash below share.
49
+
50
+ FAILS until DDJsonEncoder._default() is fixed:
51
+ TypeError: 'DotMap' object is not callable
52
+ """
53
+ d = DotMap({"label": "Dashboard"})
54
+
55
+ DDJsonEncoder._default(d)
56
+
57
+
58
+ class _TableMetaLike(type):
59
+ """
60
+ Reproduces TypeDAL's `TableMeta.__getattr__`: for an unbound model class,
61
+ any attribute that isn't a real column falls through to `return None`
62
+ instead of raising AttributeError (see typedal/tables.py TableMeta.__getattr__).
63
+ """
64
+
65
+ def __getattr__(cls, name: str) -> None:
66
+ return None
67
+
68
+
69
+ class Offering(metaclass=_TableMetaLike):
70
+ """Stand-in for a real TypeDAL model class, e.g. used as menu["model"]."""
71
+
72
+
73
+ def test_dump_reproduces_as_list_crash_on_model_class_value():
74
+ """
75
+ Real-world crash reproduction (whitelabel AdminMenuFixture):
76
+ admin nav menu entries are plain dicts that carry a `"model"` key holding
77
+ the *model class itself* (e.g. `Offering`, not an instance). Wrapping such
78
+ an entry in a DotMap (for dot-access in templates) doesn't cause this —
79
+ a plain dict crashes identically, see test below.
80
+
81
+ FAILS until DDJsonEncoder._default() is fixed: it does
82
+ `if hasattr(o, "as_list"): o.as_list()` without checking it's callable.
83
+ TypeDAL's TableMeta.__getattr__ returns None for unknown attributes on a
84
+ model class instead of raising AttributeError, so hasattr() lies and
85
+ `o.as_list()` becomes `None()`. Matches production traceback exactly:
86
+ dumping.py:42 in _default -> TypeError: 'NoneType' object is not callable.
87
+ """
88
+ entry = DotMap({"label": "Aanbod beheren", "icon": "fas fa-masks-theater", "model": Offering})
89
+
90
+ dump(entry, with_headers=False)
91
+
92
+
93
+ def test_dump_reproduces_as_list_crash_without_dotmap():
94
+ # Same crash with a plain dict: DotMap is incidental, not the cause.
95
+ entry1 = {"label": "Aanbod beheren", "model": Offering}
96
+ dump(entry1, with_headers=False)
97
+
98
+ entry2 = SimpleNamespace(label="Aanbod beheren", model=Offering)
99
+ dump(entry2, with_headers=False)
@@ -1,29 +0,0 @@
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"))
@@ -1 +0,0 @@
1
- __version__ = "1.3.1"