mirror of
https://github.com/simonw/datasette.git
synced 2026-10-18 15:26:57 +02:00
Tools for profiling Datasette's request/response cycle in-process with Python 3.15's profiling.sampling, plus the patches proposed by the study (one per optimisation, and ALL-combined.diff). The patches are not applied to the code. A report with measurements follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8mEgRS8swZfYqwqrVMeo5
70 lines
2.7 KiB
Diff
70 lines
2.7 KiB
Diff
diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py
|
|
index 2d4a6cf..0bf7ca1 100644
|
|
--- a/datasette/utils/asgi.py
|
|
+++ b/datasette/utils/asgi.py
|
|
@@ -123,16 +123,30 @@ class Request:
|
|
def scheme(self):
|
|
return self.scope.get("scheme") or "http"
|
|
|
|
+ # Decoded headers are cached against a snapshot of the raw scope headers
|
|
+ # they came from, so they stay correct if request.scope is replaced (as
|
|
+ # DatasetteRouter.route_path does) or scope["headers"] is modified
|
|
+ _raw_headers = None
|
|
+ _decoded_headers = None
|
|
+
|
|
+ def _headers(self):
|
|
+ raw_headers = list(self.scope.get("headers") or [])
|
|
+ if self._decoded_headers is None or raw_headers != self._raw_headers:
|
|
+ self._decoded_headers = _RequestHeaders(
|
|
+ (k.decode("latin-1").lower(), v.decode("latin-1"))
|
|
+ for k, v in raw_headers
|
|
+ )
|
|
+ self._raw_headers = raw_headers
|
|
+ return self._decoded_headers
|
|
+
|
|
@property
|
|
def headers(self):
|
|
- return _RequestHeaders(
|
|
- (k.decode("latin-1").lower(), v.decode("latin-1"))
|
|
- for k, v in self.scope.get("headers") or []
|
|
- )
|
|
+ # A copy, so callers cannot modify the cached headers
|
|
+ return _RequestHeaders(self._headers())
|
|
|
|
@property
|
|
def host(self):
|
|
- return self.headers.get("host") or "localhost"
|
|
+ return self._headers().get("host") or "localhost"
|
|
|
|
@property
|
|
def cookies(self):
|
|
diff --git a/tests/test_internals_request.py b/tests/test_internals_request.py
|
|
index 91ef368..f1ef381 100644
|
|
--- a/tests/test_internals_request.py
|
|
+++ b/tests/test_internals_request.py
|
|
@@ -80,6 +80,25 @@ def test_request_headers_missing(scope):
|
|
headers["Content-Type"]
|
|
|
|
|
|
+def test_request_headers_follow_scope_changes():
|
|
+ scope = {"headers": [(b"host", b"example.com"), (b"x-one", b"1")]}
|
|
+ request = Request(scope, None)
|
|
+ headers = request.headers
|
|
+ assert headers == {"host": "example.com", "x-one": "1"}
|
|
+ # Modifying the returned headers does not affect later lookups
|
|
+ headers["x-one"] = "changed"
|
|
+ assert request.headers["x-one"] == "1"
|
|
+ # Replacing request.scope, as DatasetteRouter.route_path does
|
|
+ request.scope = dict(scope, headers=[(b"host", b"other.com")])
|
|
+ assert request.headers == {"host": "other.com"}
|
|
+ assert request.host == "other.com"
|
|
+ # Modifying scope["headers"] in place
|
|
+ request.scope["headers"].append((b"x-two", b"2"))
|
|
+ assert request.headers == {"host": "other.com", "x-two": "2"}
|
|
+ request.scope["headers"][0] = (b"host", b"third.com")
|
|
+ assert request.host == "third.com"
|
|
+
|
|
+
|
|
@pytest.mark.asyncio
|
|
async def test_request_post_vars():
|
|
scope = {
|