django-explain-errors 0.11.1__tar.gz → 0.12.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.11.1/django_explain_errors.egg-info → django_explain_errors-0.12.0}/PKG-INFO +9 -10
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/README.md +8 -9
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0/django_explain_errors.egg-info}/PKG-INFO +9 -10
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/middleware.py +1 -1
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/setup.py +1 -1
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_debug_page.py +1 -1
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_eval_run.py +264 -3
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_middleware.py +2 -2
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_print_stdout.py +3 -3
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/LICENSE +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/MANIFEST.in +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/django_explain_errors.egg-info/SOURCES.txt +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/django_explain_errors.egg-info/dependency_links.txt +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/django_explain_errors.egg-info/requires.txt +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/django_explain_errors.egg-info/top_level.txt +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/__init__.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/client.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/debug_page.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/management/__init__.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/management/commands/__init__.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/management/commands/build_error_index.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/rag/__init__.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/rag/indexer.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/rag/retriever.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/rag/store.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/sanitize.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/throttle.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/tracebacks.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/setup.cfg +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_async_marking.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_build_error_index.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_client.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_dedup.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_eval_fixtures.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_eval_judge.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_language.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_local_embeddings.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_process_exception_split.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_rag.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_sanitize.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_signals.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_throttle.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_token_param_fallback.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_tracebacks.py +0 -0
- {django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_truncation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-explain-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -68,8 +68,8 @@ Explains Django exceptions on the debug page, pointing at the real fix in your o
|
|
|
68
68
|
</p>
|
|
69
69
|
|
|
70
70
|
*The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
|
|
71
|
-
failing function (`clone_latest_post`) and gives the one-line fix. Real
|
|
72
|
-
the
|
|
71
|
+
failing function (`clone_latest_post`) and gives the one-line fix. Real output from the eval
|
|
72
|
+
harness, generated with `gpt-4o-mini` (the default before `gpt-6-luna`).*
|
|
73
73
|
|
|
74
74
|
- **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
|
|
75
75
|
- Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
|
|
@@ -196,7 +196,7 @@ python manage.py check --deploy --fail-level WARNING
|
|
|
196
196
|
In the terminal, the explanation is printed with the model that produced it:
|
|
197
197
|
|
|
198
198
|
```
|
|
199
|
-
Error explanation (gpt-
|
|
199
|
+
Error explanation (gpt-6-luna):
|
|
200
200
|
The view raised a ValueError because ...
|
|
201
201
|
```
|
|
202
202
|
|
|
@@ -282,7 +282,7 @@ and keeps using that for later calls. Other providers keep receiving `max_tokens
|
|
|
282
282
|
| ------------------ | -------- | ----------- |
|
|
283
283
|
| `OPENAI_API_KEY` (env or settings) | Yes, unless `OPENAI_BASE_URL` points at an endpoint that does not authenticate | API key used to authenticate with the configured endpoint. Read first from the environment, then from `settings`. |
|
|
284
284
|
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
285
|
-
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini
|
|
285
|
+
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-6-luna` (was `gpt-4o-mini`). If you set `OPENAI_MODEL` yourself, the default change does not affect you, but OpenAI retires `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, and `gpt-4.1-mini` on 2027-01-14, after which calls to them fail. |
|
|
286
286
|
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000`; scales up automatically when `EXPLAIN_ERRORS_LANGUAGE` is set (see below), unless you set this explicitly, which always overrides the scaling. |
|
|
287
287
|
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for model API calls. Defaults to `10`. |
|
|
288
288
|
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Total character budget for the traceback sent to the model. Application frames (your own code, as opposed to Django, the standard library, or installed packages) are always kept; library frames fill whatever budget remains, nearest the raise point first, with an `... N library frames omitted ...` line where frames are dropped. If the application frames alone exceed the budget, falls back to keeping the last N characters of the raw traceback. Defaults to `3000`. |
|
|
@@ -327,7 +327,7 @@ OPENAI_API_KEY = "your_anthropic_api_key_here"
|
|
|
327
327
|
OPENAI_MODEL = "..." # see Anthropic's current model list below
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
`OPENAI_MODEL` is mandatory here: the default (`gpt-
|
|
330
|
+
`OPENAI_MODEL` is mandatory here: the default (`gpt-6-luna`) doesn't exist on Anthropic's API
|
|
331
331
|
and will 404. Use one of the model names from
|
|
332
332
|
[Anthropic's model overview](https://platform.claude.com/docs/en/about-claude/models/overview);
|
|
333
333
|
model names are versioned and retired over time, so check that page rather than relying on a
|
|
@@ -571,9 +571,8 @@ generated, so the extra headroom costs nothing if unused. Setting `OPENAI_MAX_TO
|
|
|
571
571
|
explicitly always overrides this scaling, at any value, including one lower than the
|
|
572
572
|
unscaled 1,000 default. The multiplier itself is deliberately generous rather than
|
|
573
573
|
precise: the underlying tokens-per-word figures are estimates, not direct
|
|
574
|
-
measurements, and
|
|
575
|
-
|
|
576
|
-
these estimates lean on.
|
|
574
|
+
measurements, and tokenizers differ across models and providers, so the
|
|
575
|
+
multiplier is not calibrated to any one of them.
|
|
577
576
|
|
|
578
577
|
## Codebase-aware explanations (RAG)
|
|
579
578
|
|
|
@@ -796,7 +795,7 @@ Most of the gap comes from two checks, measured on the 10 fixtures whose cause i
|
|
|
796
795
|
- **Points to the right file and function:** RAG-on 26, RAG-off 13
|
|
797
796
|
- **Avoids invented details:** RAG-on 26, RAG-off 17
|
|
798
797
|
|
|
799
|
-
Without your source, gpt-4o-mini tends to invent a plausible function name or parameter rather than admit it doesn't know. With the real code in front of it, it mostly doesn't.
|
|
798
|
+
Without your source, `gpt-4o-mini` (the generator in these runs, the default before `gpt-6-luna`) tends to invent a plausible function name or parameter rather than admit it doesn't know. With the real code in front of it, it mostly doesn't.
|
|
800
799
|
|
|
801
800
|
**Limitations:**
|
|
802
801
|
|
|
@@ -16,8 +16,8 @@ Explains Django exceptions on the debug page, pointing at the real fix in your o
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
*The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
|
|
19
|
-
failing function (`clone_latest_post`) and gives the one-line fix. Real
|
|
20
|
-
the
|
|
19
|
+
failing function (`clone_latest_post`) and gives the one-line fix. Real output from the eval
|
|
20
|
+
harness, generated with `gpt-4o-mini` (the default before `gpt-6-luna`).*
|
|
21
21
|
|
|
22
22
|
- **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
|
|
23
23
|
- Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
|
|
@@ -144,7 +144,7 @@ python manage.py check --deploy --fail-level WARNING
|
|
|
144
144
|
In the terminal, the explanation is printed with the model that produced it:
|
|
145
145
|
|
|
146
146
|
```
|
|
147
|
-
Error explanation (gpt-
|
|
147
|
+
Error explanation (gpt-6-luna):
|
|
148
148
|
The view raised a ValueError because ...
|
|
149
149
|
```
|
|
150
150
|
|
|
@@ -230,7 +230,7 @@ and keeps using that for later calls. Other providers keep receiving `max_tokens
|
|
|
230
230
|
| ------------------ | -------- | ----------- |
|
|
231
231
|
| `OPENAI_API_KEY` (env or settings) | Yes, unless `OPENAI_BASE_URL` points at an endpoint that does not authenticate | API key used to authenticate with the configured endpoint. Read first from the environment, then from `settings`. |
|
|
232
232
|
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
233
|
-
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini
|
|
233
|
+
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-6-luna` (was `gpt-4o-mini`). If you set `OPENAI_MODEL` yourself, the default change does not affect you, but OpenAI retires `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, and `gpt-4.1-mini` on 2027-01-14, after which calls to them fail. |
|
|
234
234
|
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000`; scales up automatically when `EXPLAIN_ERRORS_LANGUAGE` is set (see below), unless you set this explicitly, which always overrides the scaling. |
|
|
235
235
|
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for model API calls. Defaults to `10`. |
|
|
236
236
|
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Total character budget for the traceback sent to the model. Application frames (your own code, as opposed to Django, the standard library, or installed packages) are always kept; library frames fill whatever budget remains, nearest the raise point first, with an `... N library frames omitted ...` line where frames are dropped. If the application frames alone exceed the budget, falls back to keeping the last N characters of the raw traceback. Defaults to `3000`. |
|
|
@@ -275,7 +275,7 @@ OPENAI_API_KEY = "your_anthropic_api_key_here"
|
|
|
275
275
|
OPENAI_MODEL = "..." # see Anthropic's current model list below
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
-
`OPENAI_MODEL` is mandatory here: the default (`gpt-
|
|
278
|
+
`OPENAI_MODEL` is mandatory here: the default (`gpt-6-luna`) doesn't exist on Anthropic's API
|
|
279
279
|
and will 404. Use one of the model names from
|
|
280
280
|
[Anthropic's model overview](https://platform.claude.com/docs/en/about-claude/models/overview);
|
|
281
281
|
model names are versioned and retired over time, so check that page rather than relying on a
|
|
@@ -519,9 +519,8 @@ generated, so the extra headroom costs nothing if unused. Setting `OPENAI_MAX_TO
|
|
|
519
519
|
explicitly always overrides this scaling, at any value, including one lower than the
|
|
520
520
|
unscaled 1,000 default. The multiplier itself is deliberately generous rather than
|
|
521
521
|
precise: the underlying tokens-per-word figures are estimates, not direct
|
|
522
|
-
measurements, and
|
|
523
|
-
|
|
524
|
-
these estimates lean on.
|
|
522
|
+
measurements, and tokenizers differ across models and providers, so the
|
|
523
|
+
multiplier is not calibrated to any one of them.
|
|
525
524
|
|
|
526
525
|
## Codebase-aware explanations (RAG)
|
|
527
526
|
|
|
@@ -744,7 +743,7 @@ Most of the gap comes from two checks, measured on the 10 fixtures whose cause i
|
|
|
744
743
|
- **Points to the right file and function:** RAG-on 26, RAG-off 13
|
|
745
744
|
- **Avoids invented details:** RAG-on 26, RAG-off 17
|
|
746
745
|
|
|
747
|
-
Without your source, gpt-4o-mini tends to invent a plausible function name or parameter rather than admit it doesn't know. With the real code in front of it, it mostly doesn't.
|
|
746
|
+
Without your source, `gpt-4o-mini` (the generator in these runs, the default before `gpt-6-luna`) tends to invent a plausible function name or parameter rather than admit it doesn't know. With the real code in front of it, it mostly doesn't.
|
|
748
747
|
|
|
749
748
|
**Limitations:**
|
|
750
749
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-explain-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -68,8 +68,8 @@ Explains Django exceptions on the debug page, pointing at the real fix in your o
|
|
|
68
68
|
</p>
|
|
69
69
|
|
|
70
70
|
*The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
|
|
71
|
-
failing function (`clone_latest_post`) and gives the one-line fix. Real
|
|
72
|
-
the
|
|
71
|
+
failing function (`clone_latest_post`) and gives the one-line fix. Real output from the eval
|
|
72
|
+
harness, generated with `gpt-4o-mini` (the default before `gpt-6-luna`).*
|
|
73
73
|
|
|
74
74
|
- **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
|
|
75
75
|
- Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
|
|
@@ -196,7 +196,7 @@ python manage.py check --deploy --fail-level WARNING
|
|
|
196
196
|
In the terminal, the explanation is printed with the model that produced it:
|
|
197
197
|
|
|
198
198
|
```
|
|
199
|
-
Error explanation (gpt-
|
|
199
|
+
Error explanation (gpt-6-luna):
|
|
200
200
|
The view raised a ValueError because ...
|
|
201
201
|
```
|
|
202
202
|
|
|
@@ -282,7 +282,7 @@ and keeps using that for later calls. Other providers keep receiving `max_tokens
|
|
|
282
282
|
| ------------------ | -------- | ----------- |
|
|
283
283
|
| `OPENAI_API_KEY` (env or settings) | Yes, unless `OPENAI_BASE_URL` points at an endpoint that does not authenticate | API key used to authenticate with the configured endpoint. Read first from the environment, then from `settings`. |
|
|
284
284
|
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
285
|
-
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini
|
|
285
|
+
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-6-luna` (was `gpt-4o-mini`). If you set `OPENAI_MODEL` yourself, the default change does not affect you, but OpenAI retires `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, and `gpt-4.1-mini` on 2027-01-14, after which calls to them fail. |
|
|
286
286
|
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000`; scales up automatically when `EXPLAIN_ERRORS_LANGUAGE` is set (see below), unless you set this explicitly, which always overrides the scaling. |
|
|
287
287
|
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for model API calls. Defaults to `10`. |
|
|
288
288
|
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Total character budget for the traceback sent to the model. Application frames (your own code, as opposed to Django, the standard library, or installed packages) are always kept; library frames fill whatever budget remains, nearest the raise point first, with an `... N library frames omitted ...` line where frames are dropped. If the application frames alone exceed the budget, falls back to keeping the last N characters of the raw traceback. Defaults to `3000`. |
|
|
@@ -327,7 +327,7 @@ OPENAI_API_KEY = "your_anthropic_api_key_here"
|
|
|
327
327
|
OPENAI_MODEL = "..." # see Anthropic's current model list below
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
`OPENAI_MODEL` is mandatory here: the default (`gpt-
|
|
330
|
+
`OPENAI_MODEL` is mandatory here: the default (`gpt-6-luna`) doesn't exist on Anthropic's API
|
|
331
331
|
and will 404. Use one of the model names from
|
|
332
332
|
[Anthropic's model overview](https://platform.claude.com/docs/en/about-claude/models/overview);
|
|
333
333
|
model names are versioned and retired over time, so check that page rather than relying on a
|
|
@@ -571,9 +571,8 @@ generated, so the extra headroom costs nothing if unused. Setting `OPENAI_MAX_TO
|
|
|
571
571
|
explicitly always overrides this scaling, at any value, including one lower than the
|
|
572
572
|
unscaled 1,000 default. The multiplier itself is deliberately generous rather than
|
|
573
573
|
precise: the underlying tokens-per-word figures are estimates, not direct
|
|
574
|
-
measurements, and
|
|
575
|
-
|
|
576
|
-
these estimates lean on.
|
|
574
|
+
measurements, and tokenizers differ across models and providers, so the
|
|
575
|
+
multiplier is not calibrated to any one of them.
|
|
577
576
|
|
|
578
577
|
## Codebase-aware explanations (RAG)
|
|
579
578
|
|
|
@@ -796,7 +795,7 @@ Most of the gap comes from two checks, measured on the 10 fixtures whose cause i
|
|
|
796
795
|
- **Points to the right file and function:** RAG-on 26, RAG-off 13
|
|
797
796
|
- **Avoids invented details:** RAG-on 26, RAG-off 17
|
|
798
797
|
|
|
799
|
-
Without your source, gpt-4o-mini tends to invent a plausible function name or parameter rather than admit it doesn't know. With the real code in front of it, it mostly doesn't.
|
|
798
|
+
Without your source, `gpt-4o-mini` (the generator in these runs, the default before `gpt-6-luna`) tends to invent a plausible function name or parameter rather than admit it doesn't know. With the real code in front of it, it mostly doesn't.
|
|
800
799
|
|
|
801
800
|
**Limitations:**
|
|
802
801
|
|
|
@@ -97,7 +97,7 @@ class ExplainErrorsMiddleware:
|
|
|
97
97
|
self.throttle = SlidingWindowThrottle(max_calls, window_seconds)
|
|
98
98
|
|
|
99
99
|
# Configurable via settings, with sensible defaults.
|
|
100
|
-
self.model = getattr(settings, "OPENAI_MODEL", "gpt-
|
|
100
|
+
self.model = getattr(settings, "OPENAI_MODEL", "gpt-6-luna")
|
|
101
101
|
self.language = getattr(settings, "EXPLAIN_ERRORS_LANGUAGE", None)
|
|
102
102
|
|
|
103
103
|
configured_max_tokens = getattr(settings, "OPENAI_MAX_TOKENS", None)
|
|
@@ -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.12.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',
|
|
@@ -165,7 +165,7 @@ class DebugPageRequestCycleTest(SimpleTestCase):
|
|
|
165
165
|
content = response.content.decode()
|
|
166
166
|
self.assertIn('id="explain-errors"', content)
|
|
167
167
|
self.assertIn("Mocked explanation.", content)
|
|
168
|
-
mock_print.assert_any_call("Error explanation (gpt-
|
|
168
|
+
mock_print.assert_any_call("Error explanation (gpt-6-luna):\n", "Mocked explanation.")
|
|
169
169
|
|
|
170
170
|
@override_settings(EXPLAIN_ERRORS_INJECT_DEBUG_PAGE=False)
|
|
171
171
|
def test_inject_debug_page_off_no_banner(self):
|
|
@@ -17,8 +17,13 @@ from unittest.mock import patch
|
|
|
17
17
|
|
|
18
18
|
from evals.fixtures import FIXTURES_BY_NAME
|
|
19
19
|
from evals.run import (
|
|
20
|
+
_capture_call,
|
|
20
21
|
estimate_cost_usd,
|
|
22
|
+
is_generation_failure,
|
|
21
23
|
judge_all,
|
|
24
|
+
parse_generation_error,
|
|
25
|
+
run_fixture,
|
|
26
|
+
tally_generation,
|
|
22
27
|
tally_judgments,
|
|
23
28
|
tally_latency,
|
|
24
29
|
tally_retries,
|
|
@@ -422,6 +427,258 @@ class JudgeAllConcurrencyTest(unittest.TestCase):
|
|
|
422
427
|
self.assertLessEqual(active["peak"], 4)
|
|
423
428
|
|
|
424
429
|
|
|
430
|
+
def _skipped(group, sides):
|
|
431
|
+
return {
|
|
432
|
+
"group": group,
|
|
433
|
+
"judge_failure": False,
|
|
434
|
+
"judgment_skipped": True,
|
|
435
|
+
"generation_failed_sides": sides,
|
|
436
|
+
"rag_on_is_a": True,
|
|
437
|
+
"retries": 0,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class _FakeResponse:
|
|
442
|
+
def __init__(self, status_code, body):
|
|
443
|
+
self.status_code = status_code
|
|
444
|
+
self._body = body
|
|
445
|
+
|
|
446
|
+
def json(self):
|
|
447
|
+
if isinstance(self._body, ValueError):
|
|
448
|
+
raise self._body
|
|
449
|
+
return self._body
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class GenerationFailureRecordingTest(unittest.TestCase):
|
|
453
|
+
"""run_fixture flags calls with no usable explanation."""
|
|
454
|
+
|
|
455
|
+
def _run(self, response, generation_error=None):
|
|
456
|
+
fixture = FIXTURES_BY_NAME["none_attribute"]
|
|
457
|
+
captured = {
|
|
458
|
+
"exc_type": AttributeError,
|
|
459
|
+
"exception": None,
|
|
460
|
+
"traceback": "tb",
|
|
461
|
+
"prompt_tokens": None,
|
|
462
|
+
"completion_tokens": None,
|
|
463
|
+
"generation_error": generation_error,
|
|
464
|
+
}
|
|
465
|
+
with patch("evals.run._capture_call", return_value=(response, captured)):
|
|
466
|
+
call, _ = run_fixture(object(), fixture, False, 0)
|
|
467
|
+
return call
|
|
468
|
+
|
|
469
|
+
def test_null_explanation_is_a_failure(self):
|
|
470
|
+
call = self._run(
|
|
471
|
+
_FakeResponse(500, {"message": None}), generation_error="the model returned an empty response"
|
|
472
|
+
)
|
|
473
|
+
self.assertTrue(call["generation_failed"])
|
|
474
|
+
self.assertEqual(call["generation_error"], "the model returned an empty response")
|
|
475
|
+
self.assertIsNone(call["explanation"])
|
|
476
|
+
|
|
477
|
+
def test_whitespace_only_explanation_is_a_failure(self):
|
|
478
|
+
call = self._run(_FakeResponse(500, {"message": " \n\t "}))
|
|
479
|
+
self.assertTrue(call["generation_failed"])
|
|
480
|
+
self.assertIsNone(call["generation_error"])
|
|
481
|
+
|
|
482
|
+
def test_empty_string_and_non_json_bodies_are_failures(self):
|
|
483
|
+
self.assertTrue(self._run(_FakeResponse(500, {"message": ""}))["generation_failed"])
|
|
484
|
+
self.assertTrue(self._run(_FakeResponse(500, ValueError("bad")))["generation_failed"])
|
|
485
|
+
|
|
486
|
+
def test_real_explanation_is_not_a_failure(self):
|
|
487
|
+
call = self._run(_FakeResponse(500, {"message": "Because None has no attribute."}))
|
|
488
|
+
self.assertFalse(call["generation_failed"])
|
|
489
|
+
self.assertIsNone(call["generation_error"])
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class GenerationErrorCaptureTest(unittest.TestCase):
|
|
493
|
+
def test_parses_reason_from_middleware_line(self):
|
|
494
|
+
text = "noise\nFailed to get an explanation from the model API: Error code: 404 - model not found\n"
|
|
495
|
+
self.assertEqual(
|
|
496
|
+
parse_generation_error(text), "Error code: 404 - model not found"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
def test_no_failure_line_is_none(self):
|
|
500
|
+
self.assertIsNone(parse_generation_error("Error explanation (m):\n fine\n"))
|
|
501
|
+
self.assertIsNone(parse_generation_error(""))
|
|
502
|
+
|
|
503
|
+
def test_capture_call_records_reason_and_still_prints(self):
|
|
504
|
+
import contextlib
|
|
505
|
+
import io
|
|
506
|
+
|
|
507
|
+
outer = io.StringIO()
|
|
508
|
+
|
|
509
|
+
def fn():
|
|
510
|
+
print("Failed to get an explanation from the model API:", ValueError("boom"))
|
|
511
|
+
return "ok"
|
|
512
|
+
|
|
513
|
+
with contextlib.redirect_stdout(outer):
|
|
514
|
+
result, captured = _capture_call(fn)
|
|
515
|
+
self.assertEqual(result, "ok")
|
|
516
|
+
self.assertEqual(captured["generation_error"], "boom")
|
|
517
|
+
self.assertIn("boom", outer.getvalue())
|
|
518
|
+
|
|
519
|
+
def test_capture_call_without_failure_leaves_error_null(self):
|
|
520
|
+
_, captured = _capture_call(lambda: None)
|
|
521
|
+
self.assertIsNone(captured["generation_error"])
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class IsGenerationFailureTest(unittest.TestCase):
|
|
525
|
+
def test_flag_wins_when_present(self):
|
|
526
|
+
self.assertTrue(is_generation_failure({"generation_failed": True, "explanation": "x"}))
|
|
527
|
+
self.assertFalse(is_generation_failure({"generation_failed": False, "explanation": None}))
|
|
528
|
+
|
|
529
|
+
def test_older_records_without_the_flag_fall_back_to_the_explanation(self):
|
|
530
|
+
self.assertTrue(is_generation_failure({"explanation": None}))
|
|
531
|
+
self.assertTrue(is_generation_failure({"explanation": " "}))
|
|
532
|
+
self.assertFalse(is_generation_failure({"explanation": "real"}))
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class JudgeSkipTest(unittest.TestCase):
|
|
536
|
+
"""judge_all never calls the judge for a pair with a failed side."""
|
|
537
|
+
|
|
538
|
+
def _judge(self, off_explanation, on_explanation, **extra):
|
|
539
|
+
fixture = FIXTURES_BY_NAME["none_attribute"]
|
|
540
|
+
|
|
541
|
+
def call(rag, explanation):
|
|
542
|
+
record = {
|
|
543
|
+
"fixture": fixture.name,
|
|
544
|
+
"rag_enabled": rag,
|
|
545
|
+
"run_index": 0,
|
|
546
|
+
"traceback": "tb",
|
|
547
|
+
"explanation": explanation,
|
|
548
|
+
}
|
|
549
|
+
record.update(extra.get("on" if rag else "off", {}))
|
|
550
|
+
return record
|
|
551
|
+
|
|
552
|
+
calls = [call(False, off_explanation), call(True, on_explanation)]
|
|
553
|
+
with patch("evals.judge.get_judge_client", return_value=object()), patch(
|
|
554
|
+
"evals.judge.get_judge_model", return_value="m"
|
|
555
|
+
), patch("evals.judge.compare", return_value={"fixture": fixture.name}) as compare, patch(
|
|
556
|
+
"evals.run._build_judge_source", return_value=[]
|
|
557
|
+
):
|
|
558
|
+
judgments = judge_all([fixture], calls, {}, seed=1)
|
|
559
|
+
return judgments, compare
|
|
560
|
+
|
|
561
|
+
def test_neither_fails_judges_as_before(self):
|
|
562
|
+
judgments, compare = self._judge("off text", "on text")
|
|
563
|
+
compare.assert_called_once()
|
|
564
|
+
self.assertEqual(len(judgments), 1)
|
|
565
|
+
self.assertNotIn("judgment_skipped", judgments[0])
|
|
566
|
+
|
|
567
|
+
def test_rag_on_failure_skips_the_judge(self):
|
|
568
|
+
judgments, compare = self._judge("off text", None)
|
|
569
|
+
compare.assert_not_called()
|
|
570
|
+
self.assertTrue(judgments[0]["judgment_skipped"])
|
|
571
|
+
self.assertEqual(judgments[0]["generation_failed_sides"], ["rag_on"])
|
|
572
|
+
self.assertFalse(judgments[0]["judge_failure"])
|
|
573
|
+
|
|
574
|
+
def test_rag_off_failure_skips_the_judge(self):
|
|
575
|
+
judgments, compare = self._judge("", "on text")
|
|
576
|
+
compare.assert_not_called()
|
|
577
|
+
self.assertEqual(judgments[0]["generation_failed_sides"], ["rag_off"])
|
|
578
|
+
|
|
579
|
+
def test_both_failing_skips_and_names_both_sides(self):
|
|
580
|
+
judgments, compare = self._judge(None, None)
|
|
581
|
+
compare.assert_not_called()
|
|
582
|
+
self.assertEqual(
|
|
583
|
+
sorted(judgments[0]["generation_failed_sides"]), ["rag_off", "rag_on"]
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
def test_whitespace_only_explanation_skips_the_judge(self):
|
|
587
|
+
judgments, compare = self._judge(" \n ", "on text")
|
|
588
|
+
compare.assert_not_called()
|
|
589
|
+
self.assertEqual(judgments[0]["generation_failed_sides"], ["rag_off"])
|
|
590
|
+
|
|
591
|
+
def test_recorded_flag_skips_even_with_text_present(self):
|
|
592
|
+
judgments, compare = self._judge("off", "on", on={"generation_failed": True})
|
|
593
|
+
compare.assert_not_called()
|
|
594
|
+
self.assertEqual(judgments[0]["generation_failed_sides"], ["rag_on"])
|
|
595
|
+
|
|
596
|
+
def test_skipped_record_keeps_identifying_fields(self):
|
|
597
|
+
judgments, _ = self._judge(None, "on text")
|
|
598
|
+
j = judgments[0]
|
|
599
|
+
self.assertEqual((j["fixture"], j["group"], j["run_index"]), ("none_attribute", FIXTURES_BY_NAME["none_attribute"].group, 0))
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class SkippedPairTallyTest(unittest.TestCase):
|
|
603
|
+
def test_skipped_pairs_count_toward_no_group(self):
|
|
604
|
+
judgments = [
|
|
605
|
+
_judgment("A", "rag_on", rag_on_is_a=True),
|
|
606
|
+
_skipped("A", ["rag_on"]),
|
|
607
|
+
_skipped("B", ["rag_on", "rag_off"]),
|
|
608
|
+
]
|
|
609
|
+
result = tally_judgments(judgments)
|
|
610
|
+
self.assertEqual(result["skipped_pairs"], 2)
|
|
611
|
+
self.assertEqual(result["judge_failures"], 0)
|
|
612
|
+
self.assertEqual(result["wins_by_group"]["A"], {"rag_on": 1, "rag_off": 0, "tie": 0})
|
|
613
|
+
self.assertEqual(result["wins_by_group"]["B"], {"rag_on": 0, "rag_off": 0, "tie": 0})
|
|
614
|
+
self.assertEqual(
|
|
615
|
+
sum(result["question_yes_counts_by_group"]["B"]["rag_on"].values()), 0
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
def test_no_skips_reports_zero(self):
|
|
619
|
+
self.assertEqual(tally_judgments([_judgment("A", "tie")])["skipped_pairs"], 0)
|
|
620
|
+
|
|
621
|
+
def test_tally_generation_counts_failures(self):
|
|
622
|
+
calls = [
|
|
623
|
+
{"explanation": "ok", "generation_failed": False},
|
|
624
|
+
{"explanation": None, "generation_failed": True},
|
|
625
|
+
{"explanation": " "}, # older record, no flag
|
|
626
|
+
]
|
|
627
|
+
self.assertEqual(tally_generation(calls), {"failures": 2, "calls": 3})
|
|
628
|
+
|
|
629
|
+
def test_summary_prints_generation_lines_and_excludes_skips_from_incomplete(self):
|
|
630
|
+
import contextlib
|
|
631
|
+
import io
|
|
632
|
+
|
|
633
|
+
from evals.run import print_summary
|
|
634
|
+
|
|
635
|
+
calls = [
|
|
636
|
+
{"explanation": None, "generation_failed": True, "rag_enabled": False,
|
|
637
|
+
"latency_seconds": 1.0, "prompt_tokens": None, "completion_tokens": None},
|
|
638
|
+
{"explanation": None, "generation_failed": True, "rag_enabled": True,
|
|
639
|
+
"latency_seconds": 1.0, "prompt_tokens": None, "completion_tokens": None},
|
|
640
|
+
]
|
|
641
|
+
# Skipped pairs stay out of the judge-failure ratio: 1 judged, 0 failed.
|
|
642
|
+
judgments = [_judgment("A", "tie"), _skipped("A", ["rag_on", "rag_off"])]
|
|
643
|
+
out = io.StringIO()
|
|
644
|
+
with contextlib.redirect_stdout(out):
|
|
645
|
+
print_summary([], calls, judgments, "g", "j")
|
|
646
|
+
text = out.getvalue()
|
|
647
|
+
self.assertIn("Generation failures: 2 of 2 calls", text)
|
|
648
|
+
self.assertIn("Skipped pairs (generation failure, not judged): 1", text)
|
|
649
|
+
self.assertNotIn("RUN INCOMPLETE", text)
|
|
650
|
+
|
|
651
|
+
def test_all_pairs_skipped_prints_run_incomplete_without_dividing_by_zero(self):
|
|
652
|
+
import contextlib
|
|
653
|
+
import io
|
|
654
|
+
|
|
655
|
+
from evals.run import print_summary
|
|
656
|
+
|
|
657
|
+
calls = [
|
|
658
|
+
{"explanation": None, "generation_failed": True, "rag_enabled": flag,
|
|
659
|
+
"latency_seconds": 1.0, "prompt_tokens": None, "completion_tokens": None}
|
|
660
|
+
for flag in (False, True)
|
|
661
|
+
]
|
|
662
|
+
judgments = [_skipped("A", ["rag_on", "rag_off"]), _skipped("B", ["rag_on"])]
|
|
663
|
+
out = io.StringIO()
|
|
664
|
+
with contextlib.redirect_stdout(out):
|
|
665
|
+
print_summary([], calls, judgments, "g", "j")
|
|
666
|
+
text = out.getvalue()
|
|
667
|
+
self.assertIn("RUN INCOMPLETE: all 2 pair(s) were skipped", text)
|
|
668
|
+
self.assertNotIn("Wins by group", text)
|
|
669
|
+
|
|
670
|
+
def test_no_judgments_at_all_is_not_flagged_incomplete(self):
|
|
671
|
+
import contextlib
|
|
672
|
+
import io
|
|
673
|
+
|
|
674
|
+
from evals.run import print_summary
|
|
675
|
+
|
|
676
|
+
out = io.StringIO()
|
|
677
|
+
with contextlib.redirect_stdout(out):
|
|
678
|
+
print_summary([], [], [], "g", "j")
|
|
679
|
+
self.assertNotIn("RUN INCOMPLETE", out.getvalue())
|
|
680
|
+
|
|
681
|
+
|
|
425
682
|
class TallyRetriesTest(unittest.TestCase):
|
|
426
683
|
|
|
427
684
|
def test_sums_per_judgment_retries_and_tolerates_missing(self):
|
|
@@ -446,10 +703,10 @@ class FixtureSettingsModelTest(unittest.TestCase):
|
|
|
446
703
|
self.assertEqual(self._model(OPENAI_MODEL="some-other-model"), "some-other-model")
|
|
447
704
|
|
|
448
705
|
def test_unset_falls_back_to_default(self):
|
|
449
|
-
self.assertEqual(self._model(), "gpt-
|
|
706
|
+
self.assertEqual(self._model(), "gpt-6-luna")
|
|
450
707
|
|
|
451
708
|
def test_empty_string_falls_back_to_default(self):
|
|
452
|
-
self.assertEqual(self._model(OPENAI_MODEL=""), "gpt-
|
|
709
|
+
self.assertEqual(self._model(OPENAI_MODEL=""), "gpt-6-luna")
|
|
453
710
|
|
|
454
711
|
|
|
455
712
|
class EvalHarnessEndToEndTest(unittest.TestCase):
|
|
@@ -511,7 +768,7 @@ class EvalHarnessEndToEndTest(unittest.TestCase):
|
|
|
511
768
|
self.assertGreaterEqual(data["timing"]["generator_seconds"], 0)
|
|
512
769
|
self.assertGreaterEqual(data["timing"]["judge_seconds"], 0)
|
|
513
770
|
|
|
514
|
-
self.assertEqual(data["generator_model"], "gpt-
|
|
771
|
+
self.assertEqual(data["generator_model"], "gpt-6-luna")
|
|
515
772
|
self.assertEqual(data["judge_model"], "judge-test-model")
|
|
516
773
|
self.assertEqual(len(data["calls"]), 30) # 15 fixtures x 2 RAG states x 1 run
|
|
517
774
|
self.assertEqual(len(data["judgments"]), 15)
|
|
@@ -530,10 +787,14 @@ class EvalHarnessEndToEndTest(unittest.TestCase):
|
|
|
530
787
|
"prompt_tokens",
|
|
531
788
|
"completion_tokens",
|
|
532
789
|
"status_code",
|
|
790
|
+
"generation_failed",
|
|
791
|
+
"generation_error",
|
|
533
792
|
}
|
|
534
793
|
for call in data["calls"]:
|
|
535
794
|
self.assertTrue(call_keys.issubset(call.keys()), call)
|
|
536
795
|
self.assertEqual(call["status_code"], 500)
|
|
796
|
+
self.assertFalse(call["generation_failed"])
|
|
797
|
+
self.assertIsNone(call["generation_error"])
|
|
537
798
|
self.assertEqual(call["explanation"], "Mocked generator explanation.")
|
|
538
799
|
self.assertEqual(call["prompt_tokens"], 10)
|
|
539
800
|
self.assertEqual(call["completion_tokens"], 5)
|
|
@@ -268,7 +268,7 @@ class ExplainErrorsMiddlewarePreserveDebugPageTest(SimpleTestCase):
|
|
|
268
268
|
with patch("builtins.print") as mock_print:
|
|
269
269
|
with self.assertRaises(ValueError):
|
|
270
270
|
mw(self.factory.get("/"))
|
|
271
|
-
mock_print.assert_any_call("Error explanation (gpt-
|
|
271
|
+
mock_print.assert_any_call("Error explanation (gpt-6-luna):\n", "Mocked explanation.")
|
|
272
272
|
|
|
273
273
|
@override_settings(EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE=False)
|
|
274
274
|
def test_preserve_flag_explicitly_off_returns_json_500(self):
|
|
@@ -321,7 +321,7 @@ class OpenAICallConfigTest(SimpleTestCase):
|
|
|
321
321
|
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
322
322
|
mw.process_exception(self.factory.get("/"), Exception("x"))
|
|
323
323
|
kwargs = self.client.chat.completions.create.call_args.kwargs
|
|
324
|
-
self.assertEqual(kwargs["model"], "gpt-
|
|
324
|
+
self.assertEqual(kwargs["model"], "gpt-6-luna")
|
|
325
325
|
self.assertEqual(kwargs["max_tokens"], DEFAULT_MAX_TOKENS)
|
|
326
326
|
|
|
327
327
|
@override_settings(OPENAI_MODEL="gpt-5-mini", OPENAI_MAX_TOKENS=50)
|
|
@@ -33,7 +33,7 @@ class PrintStdoutRequestCycleTest(SimpleTestCase):
|
|
|
33
33
|
response = client.get("/boom/")
|
|
34
34
|
|
|
35
35
|
self.assertEqual(response.status_code, 500)
|
|
36
|
-
mock_print.assert_any_call("Error explanation (gpt-
|
|
36
|
+
mock_print.assert_any_call("Error explanation (gpt-6-luna):\n", "Mocked explanation.")
|
|
37
37
|
|
|
38
38
|
@override_settings(OPENAI_MODEL="some-model")
|
|
39
39
|
def test_configured_model_appears_in_stdout_prefix(self):
|
|
@@ -52,7 +52,7 @@ class PrintStdoutRequestCycleTest(SimpleTestCase):
|
|
|
52
52
|
|
|
53
53
|
self.assertEqual(response.status_code, 500)
|
|
54
54
|
for call in mock_print.call_args_list:
|
|
55
|
-
self.assertNotEqual(call.args[0], "Error explanation (gpt-
|
|
55
|
+
self.assertNotEqual(call.args[0], "Error explanation (gpt-6-luna):\n")
|
|
56
56
|
self.assertIn('id="explain-errors"', response.content.decode())
|
|
57
57
|
|
|
58
58
|
@override_settings(EXPLAIN_ERRORS_PRINT_STDOUT=False)
|
|
@@ -83,7 +83,7 @@ class PrintStdoutRequestCycleTest(SimpleTestCase):
|
|
|
83
83
|
|
|
84
84
|
self.assertEqual(response.status_code, 500)
|
|
85
85
|
for call in mock_print.call_args_list:
|
|
86
|
-
self.assertNotEqual(call.args[0], "Error explanation (gpt-
|
|
86
|
+
self.assertNotEqual(call.args[0], "Error explanation (gpt-6-luna):\n")
|
|
87
87
|
self.assertIn('id="explain-errors"', response.content.decode())
|
|
88
88
|
|
|
89
89
|
|
|
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
|
{django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/rag/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/explain_errors/rag/retriever.py
RENAMED
|
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.11.1 → django_explain_errors-0.12.0}/tests/test_build_error_index.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_local_embeddings.py
RENAMED
|
File without changes
|
{django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_process_exception_split.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_explain_errors-0.11.1 → django_explain_errors-0.12.0}/tests/test_token_param_fallback.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|