Skip to content

Commit acec7df

Browse files
committed
Centralize header normalization in RequestHeaders and simplify Request attributes
Move header normalization (underscore-to-hyphen, title-case) into RequestHeaders.__init__ so callers don't need to pre-normalize. Change headers and query_string from properties to plain attributes. Rename _scheme to server_scheme to match server_name/server_port convention.
1 parent f7d2491 commit acec7df

File tree

4 files changed

+22
-42
lines changed

4 files changed

+22
-42
lines changed

plain-observer/plain/observer/core.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4-
from collections.abc import MutableMapping
4+
from collections.abc import Mapping, MutableMapping
55
from enum import Enum
66
from typing import TYPE_CHECKING, Any, cast
77

@@ -70,7 +70,9 @@ class Observer:
7070
SUMMARY_COOKIE_DURATION = 60 * 60 * 24 * 7 # 1 week in seconds
7171
PERSIST_COOKIE_DURATION = 60 * 60 * 24 # 1 day in seconds
7272

73-
def __init__(self, *, cookies: dict[str, str], headers: dict[str, str]) -> None:
73+
def __init__(
74+
self, *, cookies: Mapping[str, str], headers: Mapping[str, str]
75+
) -> None:
7476
self.cookies = cookies
7577
self.headers = headers
7678

plain/plain/http/request.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def __init__(
6060
path: str,
6161
headers: dict[str, str] | None = None,
6262
query_string: str = "",
63-
scheme: str = "http",
63+
server_scheme: str = "http",
6464
server_name: str = "",
6565
server_port: str = "",
6666
remote_addr: str = "",
@@ -75,16 +75,15 @@ def __init__(
7575
self.server_name = server_name
7676
self.server_port = server_port
7777
self.remote_addr = remote_addr
78-
self._query_string = query_string
79-
self._scheme = scheme
80-
self._headers = headers or {}
78+
self.query_string = query_string
79+
self.server_scheme = server_scheme
80+
self.headers = RequestHeaders(headers or {})
8181

8282
# Parse content type, params, and encoding from headers
83-
content_type_header = self._headers.get("Content-Type", "")
8483
self.content_type: str | None
8584
self.content_params: dict[str, str] | None
8685
self.content_type, self.content_params = parse_header_parameters(
87-
content_type_header
86+
self.headers.get("Content-Type", "")
8887
)
8988
self.encoding: str | None = None
9089
if "charset" in (self.content_params or {}):
@@ -115,13 +114,9 @@ def __deepcopy__(self, memo: dict[int, Any]) -> Request:
115114
memo[id(self)] = obj
116115
return obj
117116

118-
@cached_property
119-
def headers(self) -> RequestHeaders:
120-
return RequestHeaders(self._headers)
121-
122117
@cached_property
123118
def query_params(self) -> QueryDict:
124-
return QueryDict(self._query_string, encoding=self.encoding)
119+
return QueryDict(self.query_string, encoding=self.encoding)
125120

126121
@cached_property
127122
def cookies(self) -> dict[str, str]:
@@ -219,11 +214,6 @@ def client_ip(self) -> str:
219214
return xff.split(",")[0].strip()
220215
return self.remote_addr
221216

222-
@property
223-
def query_string(self) -> str:
224-
"""Return the raw query string from the request URL."""
225-
return self._query_string
226-
227217
@property
228218
def content_length(self) -> int:
229219
"""Return the Content-Length header value, or 0 if not provided."""
@@ -306,10 +296,6 @@ def build_absolute_uri(self, location: str | None = None) -> str:
306296

307297
return iri_to_uri(location) or ""
308298

309-
def _get_scheme(self) -> str:
310-
"""Return the URL scheme for the request."""
311-
return self._scheme
312-
313299
@property
314300
def scheme(self) -> str:
315301
if settings.HTTPS_PROXY_HEADER:
@@ -325,7 +311,7 @@ def scheme(self) -> str:
325311
if header_value is not None:
326312
header_value, *_ = header_value.split(",", 1)
327313
return "https" if header_value.strip() == secure_value else "http"
328-
return self._get_scheme()
314+
return self.server_scheme
329315

330316
def is_https(self) -> bool:
331317
return self.scheme == "https"
@@ -505,11 +491,11 @@ def get_signed_cookie(
505491

506492

507493
class RequestHeaders(CaseInsensitiveMapping):
508-
"""Case-insensitive mapping of HTTP request headers.
509-
510-
Headers are expected to be pre-normalized to Title-Case by the
511-
server or test client before being passed to Request.__init__.
512-
"""
494+
def __init__(self, headers: dict[str, str]):
495+
normalized = {
496+
name.replace("_", "-").title(): value for name, value in headers.items()
497+
}
498+
super().__init__(normalized)
513499

514500
def __getitem__(self, key: str) -> str:
515501
"""Allow header lookup using underscores in place of hyphens."""

plain/plain/server/http/response.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,11 @@ def create_request(
7474
elif hdr_name == "SCRIPT_NAME":
7575
script_name = hdr_value
7676

77-
# Convert to standard header name (Title-Case)
78-
name = hdr_name.replace("_", "-").title()
7977
# Handle duplicate headers by joining with comma
80-
if name in headers:
81-
headers[name] = f"{headers[name]},{hdr_value}"
78+
if hdr_name in headers:
79+
headers[hdr_name] = f"{headers[hdr_name]},{hdr_value}"
8280
else:
83-
headers[name] = hdr_value
81+
headers[hdr_name] = hdr_value
8482

8583
# Remote address
8684
if isinstance(client, str):
@@ -112,7 +110,7 @@ def create_request(
112110
path=path,
113111
headers=headers,
114112
query_string=req.query or "",
115-
scheme=req.scheme,
113+
server_scheme=req.scheme,
116114
server_name=server_name,
117115
server_port=server_port,
118116
remote_addr=remote_addr,

plain/plain/test/client.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -269,18 +269,12 @@ def _build_request(
269269
all_headers["Content-Type"] = content_type
270270
all_headers["Content-Length"] = str(len(data))
271271

272-
# Normalize header names to Title-Case so that direct dict lookups
273-
# (e.g. _headers.get("Cookie")) work regardless of caller casing.
274-
normalized_headers = {
275-
k.replace("_", "-").title(): v for k, v in all_headers.items()
276-
}
277-
278272
request = Request(
279273
method=method,
280274
path=path,
281-
headers=normalized_headers,
275+
headers=all_headers,
282276
query_string=query_string,
283-
scheme="https" if secure else "http",
277+
server_scheme="https" if secure else "http",
284278
server_name=server_name,
285279
server_port=server_port or ("443" if secure else "80"),
286280
remote_addr="127.0.0.1",

0 commit comments

Comments
 (0)