django-explain-errors 0.8.0__tar.gz → 0.9.0__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.
- {django_explain_errors-0.8.0/django_explain_errors.egg-info → django_explain_errors-0.9.0}/PKG-INFO +14 -8
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/README.md +13 -7
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0/django_explain_errors.egg-info}/PKG-INFO +14 -8
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/debug_page.py +94 -5
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/setup.py +1 -1
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_debug_page.py +62 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/LICENSE +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/MANIFEST.in +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/django_explain_errors.egg-info/SOURCES.txt +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/django_explain_errors.egg-info/dependency_links.txt +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/django_explain_errors.egg-info/requires.txt +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/django_explain_errors.egg-info/top_level.txt +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/__init__.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/client.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/management/__init__.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/management/commands/__init__.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/management/commands/build_error_index.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/middleware.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/rag/__init__.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/rag/indexer.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/rag/retriever.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/rag/store.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/sanitize.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/throttle.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/tracebacks.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/setup.cfg +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_build_error_index.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_client.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_eval_fixtures.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_eval_judge.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_eval_run.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_language.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_middleware.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_print_stdout.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_rag.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_sanitize.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_signals.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_throttle.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_tracebacks.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/tests/test_truncation.py +0 -0
{django_explain_errors-0.8.0/django_explain_errors.egg-info → django_explain_errors-0.9.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-explain-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.
|
|
5
5
|
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
6
|
Author: topunix
|
|
@@ -44,14 +44,14 @@ Dynamic: summary
|
|
|
44
44
|
|
|
45
45
|
# Django Explain Errors Middleware
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
to a language model
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
out of the box, with Anthropic's Claude models through Anthropic's
|
|
47
|
+
When debug mode is on, this Django middleware sends each unhandled exception
|
|
48
|
+
to a language model and shows its explanation of what went wrong and how to
|
|
49
|
+
fix it on Django's debug page, directly under the exception headline. The
|
|
50
|
+
explanation is also printed to stdout. The middleware works with the OpenAI
|
|
51
|
+
API out of the box, with Anthropic's Claude models through Anthropic's
|
|
52
52
|
OpenAI-compatible endpoint, and with any other OpenAI-compatible endpoint
|
|
53
|
-
(Ollama, LM Studio, Azure, or a corporate gateway) by setting a base URL,
|
|
54
|
-
|
|
53
|
+
(Ollama, LM Studio, Azure, or a corporate gateway) by setting a base URL, so
|
|
54
|
+
explanations can run entirely on a local model if you prefer not to send
|
|
55
55
|
code off your machine.
|
|
56
56
|
|
|
57
57
|
Enabling RAG is strongly recommended. With it, the middleware retrieves the
|
|
@@ -92,6 +92,8 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
|
92
92
|
- Captures Django errors and exceptions
|
|
93
93
|
- Shows the explanation on Django's debug page, directly under the
|
|
94
94
|
exception headline (`EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`, on by default)
|
|
95
|
+
- Copy button on each code block in the explanation, so a suggested fix
|
|
96
|
+
copies in one click
|
|
95
97
|
- Prints the explanation to stdout, for terminal workflows and logs
|
|
96
98
|
(`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
|
|
97
99
|
- Optional JSON 500 response instead of the debug page
|
|
@@ -223,6 +225,10 @@ background) so it reads on Django's page. The explanation is HTML-escaped first,
|
|
|
223
225
|
Markdown subset (inline code, code blocks, bold, lists) is rendered as HTML. Nothing from the
|
|
224
226
|
model is ever marked safe.
|
|
225
227
|
|
|
228
|
+
Each fenced code block gets a "Copy" button that copies the exact code (via
|
|
229
|
+
`navigator.clipboard` on secure contexts such as `localhost`, or a text-selection fallback
|
|
230
|
+
otherwise, for example when `runserver` is reached by IP over plain HTTP).
|
|
231
|
+
|
|
226
232
|
**Fails open.** Any problem building or inserting the banner (a missing request, an
|
|
227
233
|
unexpected debug page layout, anything else) logs one warning and leaves Django's normal
|
|
228
234
|
debug page untouched. This code can never turn a working debug page into a broken one.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Django Explain Errors Middleware
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
to a language model
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
out of the box, with Anthropic's Claude models through Anthropic's
|
|
3
|
+
When debug mode is on, this Django middleware sends each unhandled exception
|
|
4
|
+
to a language model and shows its explanation of what went wrong and how to
|
|
5
|
+
fix it on Django's debug page, directly under the exception headline. The
|
|
6
|
+
explanation is also printed to stdout. The middleware works with the OpenAI
|
|
7
|
+
API out of the box, with Anthropic's Claude models through Anthropic's
|
|
8
8
|
OpenAI-compatible endpoint, and with any other OpenAI-compatible endpoint
|
|
9
|
-
(Ollama, LM Studio, Azure, or a corporate gateway) by setting a base URL,
|
|
10
|
-
|
|
9
|
+
(Ollama, LM Studio, Azure, or a corporate gateway) by setting a base URL, so
|
|
10
|
+
explanations can run entirely on a local model if you prefer not to send
|
|
11
11
|
code off your machine.
|
|
12
12
|
|
|
13
13
|
Enabling RAG is strongly recommended. With it, the middleware retrieves the
|
|
@@ -48,6 +48,8 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
|
48
48
|
- Captures Django errors and exceptions
|
|
49
49
|
- Shows the explanation on Django's debug page, directly under the
|
|
50
50
|
exception headline (`EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`, on by default)
|
|
51
|
+
- Copy button on each code block in the explanation, so a suggested fix
|
|
52
|
+
copies in one click
|
|
51
53
|
- Prints the explanation to stdout, for terminal workflows and logs
|
|
52
54
|
(`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
|
|
53
55
|
- Optional JSON 500 response instead of the debug page
|
|
@@ -179,6 +181,10 @@ background) so it reads on Django's page. The explanation is HTML-escaped first,
|
|
|
179
181
|
Markdown subset (inline code, code blocks, bold, lists) is rendered as HTML. Nothing from the
|
|
180
182
|
model is ever marked safe.
|
|
181
183
|
|
|
184
|
+
Each fenced code block gets a "Copy" button that copies the exact code (via
|
|
185
|
+
`navigator.clipboard` on secure contexts such as `localhost`, or a text-selection fallback
|
|
186
|
+
otherwise, for example when `runserver` is reached by IP over plain HTTP).
|
|
187
|
+
|
|
182
188
|
**Fails open.** Any problem building or inserting the banner (a missing request, an
|
|
183
189
|
unexpected debug page layout, anything else) logs one warning and leaves Django's normal
|
|
184
190
|
debug page untouched. This code can never turn a working debug page into a broken one.
|
{django_explain_errors-0.8.0 → django_explain_errors-0.9.0/django_explain_errors.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-explain-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.
|
|
5
5
|
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
6
|
Author: topunix
|
|
@@ -44,14 +44,14 @@ Dynamic: summary
|
|
|
44
44
|
|
|
45
45
|
# Django Explain Errors Middleware
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
to a language model
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
out of the box, with Anthropic's Claude models through Anthropic's
|
|
47
|
+
When debug mode is on, this Django middleware sends each unhandled exception
|
|
48
|
+
to a language model and shows its explanation of what went wrong and how to
|
|
49
|
+
fix it on Django's debug page, directly under the exception headline. The
|
|
50
|
+
explanation is also printed to stdout. The middleware works with the OpenAI
|
|
51
|
+
API out of the box, with Anthropic's Claude models through Anthropic's
|
|
52
52
|
OpenAI-compatible endpoint, and with any other OpenAI-compatible endpoint
|
|
53
|
-
(Ollama, LM Studio, Azure, or a corporate gateway) by setting a base URL,
|
|
54
|
-
|
|
53
|
+
(Ollama, LM Studio, Azure, or a corporate gateway) by setting a base URL, so
|
|
54
|
+
explanations can run entirely on a local model if you prefer not to send
|
|
55
55
|
code off your machine.
|
|
56
56
|
|
|
57
57
|
Enabling RAG is strongly recommended. With it, the middleware retrieves the
|
|
@@ -92,6 +92,8 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
|
92
92
|
- Captures Django errors and exceptions
|
|
93
93
|
- Shows the explanation on Django's debug page, directly under the
|
|
94
94
|
exception headline (`EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`, on by default)
|
|
95
|
+
- Copy button on each code block in the explanation, so a suggested fix
|
|
96
|
+
copies in one click
|
|
95
97
|
- Prints the explanation to stdout, for terminal workflows and logs
|
|
96
98
|
(`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
|
|
97
99
|
- Optional JSON 500 response instead of the debug page
|
|
@@ -223,6 +225,10 @@ background) so it reads on Django's page. The explanation is HTML-escaped first,
|
|
|
223
225
|
Markdown subset (inline code, code blocks, bold, lists) is rendered as HTML. Nothing from the
|
|
224
226
|
model is ever marked safe.
|
|
225
227
|
|
|
228
|
+
Each fenced code block gets a "Copy" button that copies the exact code (via
|
|
229
|
+
`navigator.clipboard` on secure contexts such as `localhost`, or a text-selection fallback
|
|
230
|
+
otherwise, for example when `runserver` is reached by IP over plain HTTP).
|
|
231
|
+
|
|
226
232
|
**Fails open.** Any problem building or inserting the banner (a missing request, an
|
|
227
233
|
unexpected debug page layout, anything else) logs one warning and leaves Django's normal
|
|
228
234
|
debug page untouched. This code can never turn a working debug page into a broken one.
|
|
@@ -21,10 +21,78 @@ _PRE_STYLE = (
|
|
|
21
21
|
"font-family: monospace; background: #eee; padding: 8px; "
|
|
22
22
|
"overflow-x: auto; white-space: pre-wrap; margin: 4px 0;"
|
|
23
23
|
)
|
|
24
|
+
_PRE_WITH_BUTTON_STYLE = (
|
|
25
|
+
"font-family: monospace; background: #eee; padding: 8px 60px 8px 8px; "
|
|
26
|
+
"overflow-x: auto; white-space: pre-wrap; margin: 0;"
|
|
27
|
+
)
|
|
24
28
|
_P_STYLE = "margin: 6px 0;"
|
|
25
29
|
_UL_STYLE = "margin: 6px 0; padding-inline-start: 1.5em; list-style: disc;"
|
|
26
30
|
_OL_STYLE = "margin: 6px 0; padding-inline-start: 1.5em; list-style: decimal;"
|
|
27
31
|
_LI_STYLE = "margin: 2px 0;"
|
|
32
|
+
_CODE_WRAPPER_STYLE = "position: relative; margin: 4px 0;"
|
|
33
|
+
_COPY_BUTTON_STYLE = (
|
|
34
|
+
"position: absolute; top: 4px; right: 4px; font-family: sans-serif; "
|
|
35
|
+
"font-size: 11px; line-height: 1; padding: 3px 7px; cursor: pointer; "
|
|
36
|
+
"background: #fff; border: 1px solid #999; border-radius: 3px; color: #333;"
|
|
37
|
+
)
|
|
38
|
+
_COPY_BUTTON_CLASS = "explain-errors-copy-btn"
|
|
39
|
+
_CODE_WRAPPER_CLASS = "explain-errors-code-wrapper"
|
|
40
|
+
|
|
41
|
+
_COPY_SCRIPT = """
|
|
42
|
+
<script>
|
|
43
|
+
(function () {{
|
|
44
|
+
var container = document.currentScript && document.currentScript.closest("#explain-errors");
|
|
45
|
+
if (!container) {{
|
|
46
|
+
return;
|
|
47
|
+
}}
|
|
48
|
+
container.addEventListener("click", function (event) {{
|
|
49
|
+
var btn = event.target.closest("button[data-copy]");
|
|
50
|
+
if (!btn) {{
|
|
51
|
+
return;
|
|
52
|
+
}}
|
|
53
|
+
var wrapper = btn.closest(".{wrapper_class}");
|
|
54
|
+
var code = wrapper && wrapper.querySelector("code");
|
|
55
|
+
if (!code) {{
|
|
56
|
+
return;
|
|
57
|
+
}}
|
|
58
|
+
var text = code.textContent;
|
|
59
|
+
var showCopied = function () {{
|
|
60
|
+
if (btn._explainErrorsTimer) {{
|
|
61
|
+
clearTimeout(btn._explainErrorsTimer);
|
|
62
|
+
}}
|
|
63
|
+
btn.textContent = "Copied";
|
|
64
|
+
btn._explainErrorsTimer = setTimeout(function () {{
|
|
65
|
+
btn.textContent = "Copy";
|
|
66
|
+
btn._explainErrorsTimer = null;
|
|
67
|
+
}}, 1500);
|
|
68
|
+
}};
|
|
69
|
+
var fallbackCopy = function () {{
|
|
70
|
+
try {{
|
|
71
|
+
var range = document.createRange();
|
|
72
|
+
range.selectNodeContents(code);
|
|
73
|
+
var selection = window.getSelection();
|
|
74
|
+
selection.removeAllRanges();
|
|
75
|
+
selection.addRange(range);
|
|
76
|
+
var ok = document.execCommand("copy");
|
|
77
|
+
selection.removeAllRanges();
|
|
78
|
+
if (ok) {{
|
|
79
|
+
showCopied();
|
|
80
|
+
}}
|
|
81
|
+
}} catch (err) {{
|
|
82
|
+
/* both copy paths failed; do nothing */
|
|
83
|
+
}}
|
|
84
|
+
}};
|
|
85
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {{
|
|
86
|
+
navigator.clipboard.writeText(text).then(showCopied, fallbackCopy);
|
|
87
|
+
}} else {{
|
|
88
|
+
fallbackCopy();
|
|
89
|
+
}}
|
|
90
|
+
}});
|
|
91
|
+
}})();
|
|
92
|
+
</script>
|
|
93
|
+
""".format(
|
|
94
|
+
wrapper_class=_CODE_WRAPPER_CLASS
|
|
95
|
+
)
|
|
28
96
|
|
|
29
97
|
_LANG_TAG_RE = re.compile(r"^[A-Za-z0-9_+-]*$")
|
|
30
98
|
_INLINE_CODE_RE = re.compile(r"(`[^`\n]*`)")
|
|
@@ -55,12 +123,18 @@ def render_explanation_html(text):
|
|
|
55
123
|
def _render(text):
|
|
56
124
|
parts = text.split("```")
|
|
57
125
|
rendered = []
|
|
126
|
+
has_copy_button = False
|
|
58
127
|
for index, part in enumerate(parts):
|
|
59
128
|
if index % 2 == 1:
|
|
60
|
-
|
|
129
|
+
block_html, has_button = _render_code_block(part)
|
|
130
|
+
rendered.append(block_html)
|
|
131
|
+
has_copy_button = has_copy_button or has_button
|
|
61
132
|
else:
|
|
62
133
|
rendered.append(_render_prose(escape(part)))
|
|
63
|
-
|
|
134
|
+
html = "".join(rendered)
|
|
135
|
+
if has_copy_button:
|
|
136
|
+
html += _COPY_SCRIPT
|
|
137
|
+
return html
|
|
64
138
|
|
|
65
139
|
|
|
66
140
|
def _render_code_block(segment):
|
|
@@ -71,9 +145,24 @@ def _render_code_block(segment):
|
|
|
71
145
|
code = rest
|
|
72
146
|
if code.endswith("\n"):
|
|
73
147
|
code = code[:-1]
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
148
|
+
if not code.strip():
|
|
149
|
+
return (
|
|
150
|
+
'<pre dir="ltr" style="{pre_style}"><code dir="ltr">{code}</code></pre>'
|
|
151
|
+
).format(pre_style=_PRE_STYLE, code=escape(code)), False
|
|
152
|
+
return (
|
|
153
|
+
'<div class="{wrapper_class}" style="{wrapper_style}">'
|
|
154
|
+
'<button type="button" class="{btn_class}" data-copy '
|
|
155
|
+
'aria-label="Copy code" style="{btn_style}">Copy</button>'
|
|
156
|
+
'<pre dir="ltr" style="{pre_style}"><code dir="ltr">{code}</code></pre>'
|
|
157
|
+
"</div>"
|
|
158
|
+
).format(
|
|
159
|
+
wrapper_class=_CODE_WRAPPER_CLASS,
|
|
160
|
+
wrapper_style=_CODE_WRAPPER_STYLE,
|
|
161
|
+
btn_class=_COPY_BUTTON_CLASS,
|
|
162
|
+
btn_style=_COPY_BUTTON_STYLE,
|
|
163
|
+
pre_style=_PRE_WITH_BUTTON_STYLE,
|
|
164
|
+
code=escape(code),
|
|
165
|
+
), True
|
|
77
166
|
|
|
78
167
|
|
|
79
168
|
def _render_prose(escaped_text):
|
|
@@ -10,7 +10,7 @@ with open('README.md', encoding='utf-8') as f:
|
|
|
10
10
|
|
|
11
11
|
setup(
|
|
12
12
|
name='django-explain-errors',
|
|
13
|
-
version='0.
|
|
13
|
+
version='0.9.0',
|
|
14
14
|
packages=find_packages(exclude=['tests', 'tests.*', 'evals', 'evals.*']),
|
|
15
15
|
description='Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.',
|
|
16
16
|
long_description_content_type='text/markdown',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import html as html_module
|
|
2
3
|
from unittest.mock import MagicMock, patch
|
|
3
4
|
|
|
4
5
|
from django.test import (
|
|
@@ -357,6 +358,67 @@ class RenderExplanationHtmlTest(SimpleTestCase):
|
|
|
357
358
|
self.assertNotIn("<a ", html)
|
|
358
359
|
self.assertIn("[text](url)", html)
|
|
359
360
|
|
|
361
|
+
def test_single_code_block_emits_one_copy_button_and_script(self):
|
|
362
|
+
html = render_explanation_html("```\nx = 1\n```")
|
|
363
|
+
|
|
364
|
+
self.assertEqual(html.count("<button"), 1)
|
|
365
|
+
self.assertEqual(html.count("<script>"), 1)
|
|
366
|
+
|
|
367
|
+
def test_multiple_code_blocks_emit_one_button_each_and_exactly_one_script(self):
|
|
368
|
+
html = render_explanation_html("```\nx = 1\n```\ntext\n```\ny = 2\n```")
|
|
369
|
+
|
|
370
|
+
self.assertEqual(html.count("<button"), 2)
|
|
371
|
+
self.assertEqual(html.count("<script>"), 1)
|
|
372
|
+
|
|
373
|
+
def test_copy_button_has_type_button_and_aria_label(self):
|
|
374
|
+
html = render_explanation_html("```\nx = 1\n```")
|
|
375
|
+
|
|
376
|
+
self.assertIn('type="button"', html)
|
|
377
|
+
self.assertIn('aria-label="Copy code"', html)
|
|
378
|
+
|
|
379
|
+
def test_no_code_blocks_emit_no_button_and_no_script(self):
|
|
380
|
+
html = render_explanation_html("Just prose, no code here.")
|
|
381
|
+
|
|
382
|
+
self.assertNotIn("data-copy", html)
|
|
383
|
+
self.assertNotIn("explain-errors-copy-btn", html)
|
|
384
|
+
self.assertNotIn("<script>", html)
|
|
385
|
+
|
|
386
|
+
def test_copy_source_round_trips_html_special_characters(self):
|
|
387
|
+
raw_code = """<script>alert("x & 'y'")</script>"""
|
|
388
|
+
html = render_explanation_html("```\n{}\n```".format(raw_code))
|
|
389
|
+
|
|
390
|
+
start = html.index("<code dir=\"ltr\">") + len('<code dir="ltr">')
|
|
391
|
+
end = html.index("</code>", start)
|
|
392
|
+
escaped_code = html[start:end]
|
|
393
|
+
|
|
394
|
+
self.assertEqual(html_module.unescape(escaped_code), raw_code)
|
|
395
|
+
|
|
396
|
+
def test_empty_code_block_emits_pre_without_button(self):
|
|
397
|
+
html = render_explanation_html("```\n\n```")
|
|
398
|
+
|
|
399
|
+
self.assertIn("<pre", html)
|
|
400
|
+
self.assertNotIn("<button", html)
|
|
401
|
+
self.assertNotIn("explain-errors-code-wrapper", html)
|
|
402
|
+
|
|
403
|
+
def test_whitespace_only_code_block_emits_pre_without_button(self):
|
|
404
|
+
html = render_explanation_html("```\n \n```")
|
|
405
|
+
|
|
406
|
+
self.assertIn("<pre", html)
|
|
407
|
+
self.assertNotIn("<button", html)
|
|
408
|
+
self.assertNotIn("explain-errors-code-wrapper", html)
|
|
409
|
+
|
|
410
|
+
def test_all_empty_code_blocks_emit_no_script(self):
|
|
411
|
+
html = render_explanation_html("```\n\n```\ntext\n```\n \n```")
|
|
412
|
+
|
|
413
|
+
self.assertNotIn("<button", html)
|
|
414
|
+
self.assertNotIn("<script>", html)
|
|
415
|
+
|
|
416
|
+
def test_mix_of_empty_and_nonempty_blocks_emits_button_only_for_nonempty(self):
|
|
417
|
+
html = render_explanation_html("```\n\n```\ntext\n```\nx = 1\n```")
|
|
418
|
+
|
|
419
|
+
self.assertEqual(html.count("<button"), 1)
|
|
420
|
+
self.assertEqual(html.count("<script>"), 1)
|
|
421
|
+
|
|
360
422
|
def test_raising_transform_falls_back_to_escaped_plain_text_and_logs(self):
|
|
361
423
|
with patch(
|
|
362
424
|
"explain_errors.debug_page._render", side_effect=RuntimeError("boom")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_explain_errors-0.8.0 → django_explain_errors-0.9.0}/explain_errors/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|