plain 0.78.2__tar.gz → 0.79.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.
Potentially problematic release.
This version of plain might be problematic. Click here for more details.
- {plain-0.78.2 → plain-0.79.0}/.gitignore +1 -1
- {plain-0.78.2 → plain-0.79.0}/PKG-INFO +1 -1
- {plain-0.78.2 → plain-0.79.0}/plain/CHANGELOG.md +39 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/changelog.py +2 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/upgrade.py +3 -1
- {plain-0.78.2 → plain-0.79.0}/plain/csrf/middleware.py +10 -29
- {plain-0.78.2 → plain-0.79.0}/plain/http/__init__.py +5 -3
- plain-0.79.0/plain/http/middleware.py +32 -0
- {plain-0.78.2 → plain-0.79.0}/plain/http/response.py +2 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/handlers/base.py +4 -13
- {plain-0.78.2 → plain-0.79.0}/plain/internal/handlers/exception.py +26 -42
- {plain-0.78.2 → plain-0.79.0}/plain/internal/middleware/headers.py +3 -7
- {plain-0.78.2 → plain-0.79.0}/plain/internal/middleware/hosts.py +3 -8
- {plain-0.78.2 → plain-0.79.0}/plain/internal/middleware/https.py +5 -5
- {plain-0.78.2 → plain-0.79.0}/plain/internal/middleware/slash.py +3 -8
- {plain-0.78.2 → plain-0.79.0}/plain/runtime/global_settings.py +0 -3
- {plain-0.78.2 → plain-0.79.0}/plain/signals/__init__.py +0 -1
- {plain-0.78.2 → plain-0.79.0}/plain/test/client.py +7 -28
- {plain-0.78.2 → plain-0.79.0}/plain/views/README.md +7 -18
- {plain-0.78.2 → plain-0.79.0}/pyproject.toml +1 -7
- {plain-0.78.2 → plain-0.79.0}/tests/test_csrf.py +13 -15
- plain-0.78.2/plain/csrf/views.py +0 -34
- plain-0.78.2/plain/logs/utils.py +0 -56
- {plain-0.78.2 → plain-0.79.0}/LICENSE +0 -0
- {plain-0.78.2 → plain-0.79.0}/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/AGENTS.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/__main__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/assets/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/assets/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/assets/compile.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/assets/finders.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/assets/urls.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/assets/views.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/chores/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/chores/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/chores/core.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/chores/registry.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/agent/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/agent/docs.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/agent/llmdocs.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/agent/md.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/agent/prompt.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/agent/request.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/build.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/chores.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/core.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/docs.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/formatting.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/install.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/output.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/preflight.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/print.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/registry.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/runtime.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/scaffold.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/server.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/settings.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/shell.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/startup.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/urls.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/cli/utils.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/csrf/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/debug.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/exceptions.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/forms/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/forms/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/forms/boundfield.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/forms/exceptions.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/forms/fields.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/forms/forms.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/http/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/http/cookie.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/http/multipartparser.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/http/request.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/files/base.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/files/locks.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/files/move.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/files/temp.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/files/uploadedfile.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/files/uploadhandler.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/files/utils.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/handlers/wsgi.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/internal/reloader.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/json.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/logs/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/logs/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/logs/configure.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/logs/debug.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/logs/formatters.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/logs/loggers.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/packages/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/packages/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/packages/config.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/packages/registry.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/paginator.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/preflight/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/preflight/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/preflight/checks.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/preflight/files.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/preflight/registry.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/preflight/results.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/preflight/security.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/preflight/urls.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/runtime/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/runtime/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/runtime/user_settings.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/runtime/utils.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/LICENSE +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/app.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/arbiter.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/config.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/errors.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/glogging.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/http/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/http/body.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/http/errors.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/http/message.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/http/parser.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/http/unreader.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/http/wsgi.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/pidfile.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/sock.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/util.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/workers/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/workers/base.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/workers/sync.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/workers/thread.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/server/workers/workertmp.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/signals/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/signing.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/AGENTS.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/core.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/test/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/test/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/test/encoding.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/test/exceptions.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/urls/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/urls/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/urls/converters.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/urls/exceptions.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/urls/patterns.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/urls/resolvers.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/urls/routers.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/urls/utils.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/README.md +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/cache.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/crypto.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/datastructures.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/dateparse.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/decorators.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/duration.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/encoding.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/functional.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/hashable.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/html.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/http.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/inspect.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/ipv6.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/itercompat.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/module_loading.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/regex_helper.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/safestring.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/text.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/timesince.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/timezone.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/utils/tree.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/validators.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/views/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/views/base.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/views/errors.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/views/exceptions.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/views/forms.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/views/objects.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/views/redirect.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/views/templates.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/plain/wsgi.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/.gitignore +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/app/.gitignore +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/app/settings.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/app/test/__init__.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/app/urls.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/conftest.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/test_cli.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/test_http_hosts.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/test_logs.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/test_runtime.py +0 -0
- {plain-0.78.2 → plain-0.79.0}/tests/test_wsgi.py +0 -0
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# plain changelog
|
|
2
2
|
|
|
3
|
+
## [0.79.0](https://github.com/dropseed/plain/releases/plain@0.79.0) (2025-10-22)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Response objects now have an `exception` attribute that stores the exception that caused 5xx errors ([0a243ba89c](https://github.com/dropseed/plain/commit/0a243ba89c))
|
|
8
|
+
- Middleware classes now use an abstract base class `HttpMiddleware` with a `process_request()` method ([b960eed6c6](https://github.com/dropseed/plain/commit/b960eed6c6))
|
|
9
|
+
- CSRF middleware now raises `PermissionDenied` instead of rendering a custom `CsrfFailureView` ([d4b93e59b3](https://github.com/dropseed/plain/commit/d4b93e59b3))
|
|
10
|
+
- The `HTTP_ERROR_VIEWS` setting has been removed ([7a4e3a31f4](https://github.com/dropseed/plain/commit/7a4e3a31f4))
|
|
11
|
+
- Standalone `plain-changelog` and `plain-upgrade` executables have been removed in favor of the built-in commands ([07c3a4c540](https://github.com/dropseed/plain/commit/07c3a4c540))
|
|
12
|
+
- Standalone `plain-build` executable has been removed ([99301ea797](https://github.com/dropseed/plain/commit/99301ea797))
|
|
13
|
+
- Removed automatic logging of all HTTP 400+ status codes for cleaner logs ([c2769d7281](https://github.com/dropseed/plain/commit/c2769d7281))
|
|
14
|
+
|
|
15
|
+
### Upgrade instructions
|
|
16
|
+
|
|
17
|
+
- If you have custom middleware, inherit from `HttpMiddleware` and rename your `__call__()` method to `process_request()`:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# Before:
|
|
21
|
+
class MyMiddleware:
|
|
22
|
+
def __init__(self, get_response):
|
|
23
|
+
self.get_response = get_response
|
|
24
|
+
|
|
25
|
+
def __call__(self, request):
|
|
26
|
+
response = self.get_response(request)
|
|
27
|
+
return response
|
|
28
|
+
|
|
29
|
+
# After:
|
|
30
|
+
from plain.http import HttpMiddleware
|
|
31
|
+
|
|
32
|
+
class MyMiddleware(HttpMiddleware):
|
|
33
|
+
def process_request(self, request):
|
|
34
|
+
response = self.get_response(request)
|
|
35
|
+
return response
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Remove any custom `HTTP_ERROR_VIEWS` setting from your configuration - error views are now controlled entirely by exception handlers
|
|
39
|
+
- If you were calling `plain-changelog` or `plain-upgrade` as standalone commands, use `plain changelog` or `plain upgrade` instead
|
|
40
|
+
- If you were calling `plain-build` as a standalone command, use `plain build` instead
|
|
41
|
+
|
|
3
42
|
## [0.78.2](https://github.com/dropseed/plain/releases/plain@0.78.2) (2025-10-20)
|
|
4
43
|
|
|
5
44
|
### What's changed
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from .output import style_markdown
|
|
10
|
+
from .runtime import without_runtime_setup
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def parse_version(version_str: str) -> tuple[int, ...]:
|
|
@@ -42,6 +43,7 @@ def compare_versions(v1: str, v2: str) -> int:
|
|
|
42
43
|
return 0
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
@without_runtime_setup
|
|
45
47
|
@click.command("changelog")
|
|
46
48
|
@click.argument("package_label")
|
|
47
49
|
@click.option("--from", "from_version", help="Show entries from this version onwards")
|
|
@@ -6,10 +6,12 @@ from pathlib import Path
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
8
|
from .agent.prompt import prompt_agent
|
|
9
|
+
from .runtime import without_runtime_setup
|
|
9
10
|
|
|
10
11
|
LOCK_FILE = Path("uv.lock")
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
@without_runtime_setup
|
|
13
15
|
@click.command()
|
|
14
16
|
@click.argument("packages", nargs=-1)
|
|
15
17
|
@click.option(
|
|
@@ -144,7 +146,7 @@ def build_prompt(before_after: dict[str, tuple[str | None, str | None]]) -> str:
|
|
|
144
146
|
"## Instructions",
|
|
145
147
|
"",
|
|
146
148
|
"1. **Process each package systematically:**",
|
|
147
|
-
" - For each package, run: `uv run plain
|
|
149
|
+
" - For each package, run: `uv run plain changelog {package} --from {before} --to {after}`",
|
|
148
150
|
" - Read the 'Upgrade instructions' section carefully",
|
|
149
151
|
" - If it says 'No changes required', skip to the next package",
|
|
150
152
|
" - Apply any required code changes as specified",
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import re
|
|
5
4
|
from collections.abc import Callable
|
|
6
5
|
from typing import TYPE_CHECKING
|
|
7
6
|
from urllib.parse import urlparse
|
|
8
7
|
|
|
9
|
-
from plain.
|
|
8
|
+
from plain.exceptions import PermissionDenied
|
|
9
|
+
from plain.http import HttpMiddleware
|
|
10
10
|
from plain.runtime import settings
|
|
11
11
|
|
|
12
|
-
from .views import CsrfFailureView
|
|
13
|
-
|
|
14
12
|
if TYPE_CHECKING:
|
|
15
|
-
from plain.http import Response
|
|
16
|
-
from plain.http.request import Request
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger("plain.security.csrf")
|
|
13
|
+
from plain.http import Request, Response
|
|
19
14
|
|
|
20
15
|
|
|
21
|
-
class CsrfViewMiddleware:
|
|
16
|
+
class CsrfViewMiddleware(HttpMiddleware):
|
|
22
17
|
"""
|
|
23
18
|
Modern CSRF protection middleware using Sec-Fetch-Site headers and origin validation.
|
|
24
19
|
Based on Filippo Valsorda's 2025 research (https://words.filippo.io/csrf/).
|
|
@@ -28,20 +23,20 @@ class CsrfViewMiddleware:
|
|
|
28
23
|
"""
|
|
29
24
|
|
|
30
25
|
def __init__(self, get_response: Callable[[Request], Response]):
|
|
31
|
-
|
|
26
|
+
super().__init__(get_response)
|
|
32
27
|
|
|
33
28
|
# Compile CSRF exempt patterns once for performance
|
|
34
29
|
self.csrf_exempt_patterns: list[re.Pattern[str]] = [
|
|
35
30
|
re.compile(r) for r in settings.CSRF_EXEMPT_PATHS
|
|
36
31
|
]
|
|
37
32
|
|
|
38
|
-
def
|
|
33
|
+
def process_request(self, request: Request) -> Response:
|
|
39
34
|
allowed, reason = self.should_allow_request(request)
|
|
40
35
|
|
|
41
|
-
if allowed:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
if not allowed:
|
|
37
|
+
raise PermissionDenied(reason)
|
|
38
|
+
|
|
39
|
+
return self.get_response(request)
|
|
45
40
|
|
|
46
41
|
def should_allow_request(self, request: Request) -> tuple[bool, str]:
|
|
47
42
|
# 1. Allow safe methods (GET, HEAD, OPTIONS)
|
|
@@ -127,17 +122,3 @@ class CsrfViewMiddleware:
|
|
|
127
122
|
False,
|
|
128
123
|
f"Cross-origin request detected - Origin {origin} does not match Host",
|
|
129
124
|
)
|
|
130
|
-
|
|
131
|
-
def reject(self, request: Request, reason: str) -> Response:
|
|
132
|
-
"""Reject a request with a 403 Forbidden response."""
|
|
133
|
-
|
|
134
|
-
response = CsrfFailureView.as_view()(request, reason=reason)
|
|
135
|
-
log_response(
|
|
136
|
-
"Forbidden (%s): %s",
|
|
137
|
-
reason,
|
|
138
|
-
request.path,
|
|
139
|
-
response=response,
|
|
140
|
-
request=request,
|
|
141
|
-
logger=logger,
|
|
142
|
-
)
|
|
143
|
-
return response
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
1
|
+
from .cookie import parse_cookie
|
|
2
|
+
from .middleware import HttpMiddleware
|
|
3
|
+
from .request import (
|
|
3
4
|
QueryDict,
|
|
4
5
|
RawPostDataException,
|
|
5
6
|
Request,
|
|
6
7
|
RequestHeaders,
|
|
7
8
|
UnreadablePostError,
|
|
8
9
|
)
|
|
9
|
-
from
|
|
10
|
+
from .response import (
|
|
10
11
|
BadHeaderError,
|
|
11
12
|
FileResponse,
|
|
12
13
|
Http404,
|
|
@@ -25,6 +26,7 @@ from plain.http.response import (
|
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
__all__ = [
|
|
29
|
+
"HttpMiddleware",
|
|
28
30
|
"parse_cookie",
|
|
29
31
|
"Request",
|
|
30
32
|
"RequestHeaders",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from plain.http import Request, Response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HttpMiddleware(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Abstract base class for HTTP middleware.
|
|
14
|
+
|
|
15
|
+
Subclasses must implement process_request() to handle the request/response cycle.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
class MyMiddleware(HttpMiddleware):
|
|
19
|
+
def process_request(self, request: Request) -> Response:
|
|
20
|
+
# Pre-processing
|
|
21
|
+
response = self.get_response(request)
|
|
22
|
+
# Post-processing
|
|
23
|
+
return response
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, get_response: Callable[[Request], Response]):
|
|
27
|
+
self.get_response = get_response
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def process_request(self, request: Request) -> Response:
|
|
31
|
+
"""Process the request and return a response. Must be implemented by subclasses."""
|
|
32
|
+
...
|
|
@@ -148,6 +148,8 @@ class ResponseBase:
|
|
|
148
148
|
if not 100 <= self.status_code <= 599:
|
|
149
149
|
raise ValueError("HTTP status code must be an integer from 100 to 599.")
|
|
150
150
|
self._reason_phrase = reason
|
|
151
|
+
# Exception that caused this response, if any (primarily for 500 errors)
|
|
152
|
+
self.exception: Exception | None = None
|
|
151
153
|
|
|
152
154
|
@property
|
|
153
155
|
def reason_phrase(self) -> str:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import types
|
|
5
4
|
from typing import TYPE_CHECKING
|
|
6
5
|
|
|
@@ -8,7 +7,6 @@ from opentelemetry import baggage, trace
|
|
|
8
7
|
from opentelemetry.semconv.attributes import http_attributes, url_attributes
|
|
9
8
|
|
|
10
9
|
from plain.exceptions import ImproperlyConfigured
|
|
11
|
-
from plain.logs.utils import log_response
|
|
12
10
|
from plain.runtime import settings
|
|
13
11
|
from plain.urls import get_resolver
|
|
14
12
|
from plain.utils.module_loading import import_string
|
|
@@ -21,8 +19,6 @@ if TYPE_CHECKING:
|
|
|
21
19
|
from plain.http import Request, Response, ResponseBase
|
|
22
20
|
from plain.urls import ResolverMatch
|
|
23
21
|
|
|
24
|
-
logger = logging.getLogger("plain.request")
|
|
25
|
-
|
|
26
22
|
|
|
27
23
|
# These middleware classes are always used by Plain.
|
|
28
24
|
BUILTIN_BEFORE_MIDDLEWARE = [
|
|
@@ -66,7 +62,7 @@ class BaseHandler:
|
|
|
66
62
|
f"Middleware factory {middleware_path} returned None."
|
|
67
63
|
)
|
|
68
64
|
|
|
69
|
-
handler = convert_exception_to_response(mw_instance)
|
|
65
|
+
handler = convert_exception_to_response(mw_instance.process_request)
|
|
70
66
|
|
|
71
67
|
# We only assign to this when initialization is complete as it is used
|
|
72
68
|
# as a flag for initialization being complete.
|
|
@@ -117,14 +113,9 @@ class BaseHandler:
|
|
|
117
113
|
else trace.StatusCode.ERROR
|
|
118
114
|
)
|
|
119
115
|
|
|
120
|
-
if response.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
response.reason_phrase,
|
|
124
|
-
request.path,
|
|
125
|
-
response=response,
|
|
126
|
-
request=request,
|
|
127
|
-
)
|
|
116
|
+
if response.exception:
|
|
117
|
+
span.record_exception(response.exception)
|
|
118
|
+
|
|
128
119
|
return response
|
|
129
120
|
|
|
130
121
|
def _get_response(self, request: Request) -> ResponseBase:
|
|
@@ -4,7 +4,6 @@ import logging
|
|
|
4
4
|
from functools import wraps
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from plain import signals
|
|
8
7
|
from plain.exceptions import (
|
|
9
8
|
BadRequest,
|
|
10
9
|
PermissionDenied,
|
|
@@ -15,9 +14,7 @@ from plain.exceptions import (
|
|
|
15
14
|
)
|
|
16
15
|
from plain.http import Http404, ResponseServerError
|
|
17
16
|
from plain.http.multipartparser import MultiPartParserError
|
|
18
|
-
from plain.logs.utils import log_response
|
|
19
17
|
from plain.runtime import settings
|
|
20
|
-
from plain.utils.module_loading import import_string
|
|
21
18
|
from plain.views.errors import ErrorView
|
|
22
19
|
|
|
23
20
|
if TYPE_CHECKING:
|
|
@@ -26,6 +23,9 @@ if TYPE_CHECKING:
|
|
|
26
23
|
from plain.http import Request, Response
|
|
27
24
|
|
|
28
25
|
|
|
26
|
+
request_logger = logging.getLogger("plain.request")
|
|
27
|
+
|
|
28
|
+
|
|
29
29
|
def convert_exception_to_response(
|
|
30
30
|
get_response: Callable[[Request], Response],
|
|
31
31
|
) -> Callable[[Request], Response]:
|
|
@@ -63,37 +63,34 @@ def response_for_exception(request: Request, exc: Exception) -> Response:
|
|
|
63
63
|
response = get_exception_response(
|
|
64
64
|
request=request, status_code=403, exception=exc
|
|
65
65
|
)
|
|
66
|
-
|
|
66
|
+
request_logger.warning(
|
|
67
67
|
"Forbidden (Permission denied): %s",
|
|
68
68
|
request.path,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
exception=exc,
|
|
69
|
+
extra={"status_code": response.status_code, "request": request},
|
|
70
|
+
exc_info=exc,
|
|
72
71
|
)
|
|
73
72
|
|
|
74
73
|
elif isinstance(exc, MultiPartParserError):
|
|
75
74
|
response = get_exception_response(
|
|
76
75
|
request=request, status_code=400, exception=None
|
|
77
76
|
)
|
|
78
|
-
|
|
77
|
+
request_logger.warning(
|
|
79
78
|
"Bad request (Unable to parse request body): %s",
|
|
80
79
|
request.path,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
exception=exc,
|
|
80
|
+
extra={"status_code": response.status_code, "request": request},
|
|
81
|
+
exc_info=exc,
|
|
84
82
|
)
|
|
85
83
|
|
|
86
84
|
elif isinstance(exc, BadRequest):
|
|
87
85
|
response = get_exception_response(
|
|
88
86
|
request=request, status_code=400, exception=exc
|
|
89
87
|
)
|
|
90
|
-
|
|
88
|
+
request_logger.warning(
|
|
91
89
|
"%s: %s",
|
|
92
90
|
str(exc),
|
|
93
91
|
request.path,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
exception=exc,
|
|
92
|
+
extra={"status_code": response.status_code, "request": request},
|
|
93
|
+
exc_info=exc,
|
|
97
94
|
)
|
|
98
95
|
elif isinstance(exc, SuspiciousOperation):
|
|
99
96
|
if isinstance(exc, RequestDataTooBig | TooManyFieldsSent | TooManyFilesSent):
|
|
@@ -106,25 +103,23 @@ def response_for_exception(request: Request, exc: Exception) -> Response:
|
|
|
106
103
|
security_logger = logging.getLogger(f"plain.security.{exc.__class__.__name__}")
|
|
107
104
|
security_logger.error(
|
|
108
105
|
str(exc),
|
|
109
|
-
exc_info=exc,
|
|
110
106
|
extra={"status_code": 400, "request": request},
|
|
107
|
+
exc_info=exc,
|
|
111
108
|
)
|
|
112
109
|
response = get_exception_response(
|
|
113
110
|
request=request, status_code=400, exception=None
|
|
114
111
|
)
|
|
115
112
|
|
|
116
113
|
else:
|
|
117
|
-
signals.got_request_exception.send(sender=None, request=request)
|
|
118
114
|
response = get_exception_response(
|
|
119
115
|
request=request, status_code=500, exception=None
|
|
120
116
|
)
|
|
121
|
-
|
|
117
|
+
request_logger.error(
|
|
122
118
|
"%s: %s",
|
|
123
119
|
response.reason_phrase,
|
|
124
120
|
request.path,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
exception=exc,
|
|
121
|
+
extra={"status_code": response.status_code, "request": request},
|
|
122
|
+
exc_info=exc,
|
|
128
123
|
)
|
|
129
124
|
|
|
130
125
|
return response
|
|
@@ -134,29 +129,18 @@ def get_exception_response(
|
|
|
134
129
|
*, request: Request, status_code: int, exception: Exception | None
|
|
135
130
|
) -> Response:
|
|
136
131
|
try:
|
|
137
|
-
view_class =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
view_class = ErrorView.as_view(status_code=status_code, exception=exception)
|
|
133
|
+
response = view_class(request)
|
|
134
|
+
if response.status_code >= 500 and exception is not None:
|
|
135
|
+
# Attach the exception to the response for logging/observability
|
|
136
|
+
response.exception = exception
|
|
137
|
+
return response
|
|
138
|
+
except Exception as e:
|
|
142
139
|
# In development mode, re-raise the exception to get a full stack trace
|
|
143
140
|
if settings.DEBUG:
|
|
144
141
|
raise
|
|
145
142
|
|
|
146
143
|
# If we can't load the view, return a 500 response
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def get_error_view(
|
|
151
|
-
*, status_code: int, exception: Exception | None
|
|
152
|
-
) -> Callable[[Request], Response]:
|
|
153
|
-
views_by_status = settings.HTTP_ERROR_VIEWS
|
|
154
|
-
if status_code in views_by_status:
|
|
155
|
-
view = views_by_status[status_code]
|
|
156
|
-
if isinstance(view, str):
|
|
157
|
-
# Import the view if it's a string
|
|
158
|
-
view = import_string(view)
|
|
159
|
-
return view.as_view()
|
|
160
|
-
|
|
161
|
-
# Create a standard view for any other status code
|
|
162
|
-
return ErrorView.as_view(status_code=status_code, exception=exception)
|
|
144
|
+
response = ResponseServerError()
|
|
145
|
+
response.exception = e
|
|
146
|
+
return response
|
|
@@ -2,19 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
from plain.http import HttpMiddleware
|
|
5
6
|
from plain.runtime import settings
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
8
|
-
from collections.abc import Callable
|
|
9
|
-
|
|
10
9
|
from plain.http import Request, Response
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
class DefaultHeadersMiddleware:
|
|
14
|
-
def
|
|
15
|
-
self.get_response = get_response
|
|
16
|
-
|
|
17
|
-
def __call__(self, request: Request) -> Response:
|
|
12
|
+
class DefaultHeadersMiddleware(HttpMiddleware):
|
|
13
|
+
def process_request(self, request: Request) -> Response:
|
|
18
14
|
response = self.get_response(request)
|
|
19
15
|
|
|
20
16
|
for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
|
|
@@ -4,13 +4,11 @@ import ipaddress
|
|
|
4
4
|
import logging
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from plain.http import Request, ResponseBadRequest
|
|
7
|
+
from plain.http import HttpMiddleware, Request, ResponseBadRequest
|
|
8
8
|
from plain.runtime import settings
|
|
9
9
|
from plain.utils.regex_helper import _lazy_re_compile
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
from collections.abc import Callable
|
|
13
|
-
|
|
14
12
|
from plain.http import Response
|
|
15
13
|
|
|
16
14
|
logger = logging.getLogger(__name__)
|
|
@@ -20,7 +18,7 @@ host_validation_re = _lazy_re_compile(
|
|
|
20
18
|
)
|
|
21
19
|
|
|
22
20
|
|
|
23
|
-
class HostValidationMiddleware:
|
|
21
|
+
class HostValidationMiddleware(HttpMiddleware):
|
|
24
22
|
"""
|
|
25
23
|
Middleware to validate the Host header against ALLOWED_HOSTS.
|
|
26
24
|
|
|
@@ -29,10 +27,7 @@ class HostValidationMiddleware:
|
|
|
29
27
|
host is not allowed.
|
|
30
28
|
"""
|
|
31
29
|
|
|
32
|
-
def
|
|
33
|
-
self.get_response = get_response
|
|
34
|
-
|
|
35
|
-
def __call__(self, request: Request) -> Response:
|
|
30
|
+
def process_request(self, request: Request) -> Response:
|
|
36
31
|
if not is_host_valid(request):
|
|
37
32
|
host = request.host
|
|
38
33
|
msg = f"Invalid HTTP_HOST header: {host!r}."
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from plain.http import ResponseRedirect
|
|
5
|
+
from plain.http import HttpMiddleware, ResponseRedirect
|
|
6
6
|
from plain.runtime import settings
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
@@ -11,14 +11,14 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from plain.http import Request, Response
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class HttpsRedirectMiddleware:
|
|
15
|
-
def __init__(self, get_response: Callable[[Request], Response])
|
|
16
|
-
|
|
14
|
+
class HttpsRedirectMiddleware(HttpMiddleware):
|
|
15
|
+
def __init__(self, get_response: Callable[[Request], Response]):
|
|
16
|
+
super().__init__(get_response)
|
|
17
17
|
|
|
18
18
|
# Settings for HTTPS
|
|
19
19
|
self.https_redirect_enabled = settings.HTTPS_REDIRECT_ENABLED
|
|
20
20
|
|
|
21
|
-
def
|
|
21
|
+
def process_request(self, request: Request) -> Response:
|
|
22
22
|
"""
|
|
23
23
|
Perform a blanket HTTP→HTTPS redirect when enabled.
|
|
24
24
|
"""
|
|
@@ -2,23 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from plain.http import ResponseRedirect
|
|
5
|
+
from plain.http import HttpMiddleware, ResponseRedirect
|
|
6
6
|
from plain.runtime import settings
|
|
7
7
|
from plain.urls import Resolver404, get_resolver
|
|
8
8
|
from plain.utils.http import escape_leading_slashes
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
from collections.abc import Callable
|
|
12
|
-
|
|
13
11
|
from plain.http import Request, Response
|
|
14
12
|
from plain.urls import ResolverMatch
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
class RedirectSlashMiddleware:
|
|
18
|
-
def
|
|
19
|
-
self.get_response = get_response
|
|
20
|
-
|
|
21
|
-
def __call__(self, request: Request) -> Response:
|
|
15
|
+
class RedirectSlashMiddleware(HttpMiddleware):
|
|
16
|
+
def process_request(self, request: Request) -> Response:
|
|
22
17
|
"""
|
|
23
18
|
Rewrite the URL based on settings.APPEND_SLASH
|
|
24
19
|
"""
|
|
@@ -111,9 +111,6 @@ DATA_UPLOAD_MAX_NUMBER_FILES = 100
|
|
|
111
111
|
# (i.e. "/tmp" on *nix systems).
|
|
112
112
|
FILE_UPLOAD_TEMP_DIR = None
|
|
113
113
|
|
|
114
|
-
# User-defined overrides for error views by status code
|
|
115
|
-
HTTP_ERROR_VIEWS: dict[int, type] = {}
|
|
116
|
-
|
|
117
114
|
# MARK: Middleware
|
|
118
115
|
|
|
119
116
|
# List of middleware to use. Order is important; in the request phase, these
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import sys
|
|
5
4
|
from http import HTTPStatus
|
|
6
5
|
from http.cookies import SimpleCookie
|
|
7
6
|
from io import BytesIO, IOBase
|
|
@@ -14,7 +13,7 @@ from plain.internal.handlers.base import BaseHandler
|
|
|
14
13
|
from plain.internal.handlers.wsgi import WSGIRequest
|
|
15
14
|
from plain.json import PlainJSONEncoder
|
|
16
15
|
from plain.runtime import settings
|
|
17
|
-
from plain.signals import
|
|
16
|
+
from plain.signals import request_started
|
|
18
17
|
from plain.urls import get_resolver
|
|
19
18
|
from plain.utils.encoding import force_bytes
|
|
20
19
|
from plain.utils.functional import SimpleLazyObject
|
|
@@ -55,7 +54,6 @@ class ClientResponse:
|
|
|
55
54
|
response: ResponseBase,
|
|
56
55
|
client: Client,
|
|
57
56
|
request: dict[str, Any],
|
|
58
|
-
exc_info: tuple[Any, Any, Any] | None,
|
|
59
57
|
):
|
|
60
58
|
# Store wrapped response in __dict__ directly to avoid __setattr__ recursion
|
|
61
59
|
object.__setattr__(self, "_response", response)
|
|
@@ -66,7 +64,6 @@ class ClientResponse:
|
|
|
66
64
|
self.wsgi_request: WSGIRequest
|
|
67
65
|
self.redirect_chain: list[tuple[str, int]]
|
|
68
66
|
self.resolver_match: SimpleLazyObject | ResolverMatch
|
|
69
|
-
self.exc_info = exc_info
|
|
70
67
|
# Optional: set by plain.auth if available
|
|
71
68
|
# self.user: Model
|
|
72
69
|
|
|
@@ -530,7 +527,6 @@ class Client:
|
|
|
530
527
|
self._request_factory = RequestFactory(headers=headers, **defaults)
|
|
531
528
|
self.handler = ClientHandler()
|
|
532
529
|
self.raise_request_exception = raise_request_exception
|
|
533
|
-
self.exc_info: tuple[Any, Any, Any] | None = None
|
|
534
530
|
self.extra: dict[str, Any] | None = None
|
|
535
531
|
self.headers: dict[str, str] | None = None
|
|
536
532
|
|
|
@@ -553,25 +549,20 @@ class Client:
|
|
|
553
549
|
"""
|
|
554
550
|
environ = self._request_factory._base_environ(**request)
|
|
555
551
|
|
|
556
|
-
#
|
|
557
|
-
|
|
558
|
-
got_request_exception.connect(self.store_exc_info, dispatch_uid=exception_uid)
|
|
559
|
-
try:
|
|
560
|
-
response = self.handler(environ)
|
|
561
|
-
finally:
|
|
562
|
-
# signals.template_rendered.disconnect(dispatch_uid=signal_uid)
|
|
563
|
-
got_request_exception.disconnect(dispatch_uid=exception_uid)
|
|
552
|
+
# Make the request
|
|
553
|
+
response = self.handler(environ)
|
|
564
554
|
|
|
565
555
|
# Wrap the response in ClientResponse for test-specific attributes
|
|
566
556
|
client_response = ClientResponse(
|
|
567
557
|
response=response,
|
|
568
558
|
client=self,
|
|
569
559
|
request=request,
|
|
570
|
-
exc_info=self.exc_info,
|
|
571
560
|
)
|
|
572
561
|
|
|
573
|
-
#
|
|
574
|
-
|
|
562
|
+
# Re-raise the exception if configured to do so
|
|
563
|
+
# Only 5xx errors have response.exception set
|
|
564
|
+
if client_response.exception and self.raise_request_exception:
|
|
565
|
+
raise client_response.exception
|
|
575
566
|
|
|
576
567
|
# If the request had a user, make it available on the response.
|
|
577
568
|
try:
|
|
@@ -901,18 +892,6 @@ class Client:
|
|
|
901
892
|
|
|
902
893
|
return response
|
|
903
894
|
|
|
904
|
-
def store_exc_info(self, **kwargs: Any) -> None:
|
|
905
|
-
"""Store exceptions when they are generated by a view."""
|
|
906
|
-
self.exc_info = sys.exc_info()
|
|
907
|
-
|
|
908
|
-
def check_exception(self) -> None:
|
|
909
|
-
"""Check for signaled exceptions and potentially re-raise."""
|
|
910
|
-
if self.exc_info:
|
|
911
|
-
_, exc_value, _ = self.exc_info
|
|
912
|
-
self.exc_info = None
|
|
913
|
-
if self.raise_request_exception:
|
|
914
|
-
raise exc_value
|
|
915
|
-
|
|
916
895
|
@property
|
|
917
896
|
def session(self) -> Any:
|
|
918
897
|
"""Return the current session variables."""
|