Skip to content

Commit bed765f

Browse files
committed
Test client creates Request directly without WSGI environ
Rewrite RequestFactory and ClientHandler to build and accept Request objects directly instead of going through WSGI environ dicts. Move LimitedStream to plain.http for shared use by both the server and test client.
1 parent ed086b3 commit bed765f

File tree

9 files changed

+271
-303
lines changed

9 files changed

+271
-303
lines changed

plain-htmx/tests/test_views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@ def get(self):
99

1010

1111
def test_is_htmx_request():
12-
request = RequestFactory().get("/", HTTP_HX_REQUEST="true")
12+
request = RequestFactory().get("/", headers={"HX-Request": "true"})
1313
view = V()
1414
view.setup(request)
1515
assert view.is_htmx_request()
1616

1717

1818
def test_plain_hx_fragment():
19-
request = RequestFactory().get("/", HTTP_PLAIN_HX_FRAGMENT="main")
19+
request = RequestFactory().get("/", headers={"Plain-HX-Fragment": "main"})
2020
view = V()
2121
view.setup(request)
2222
assert view.get_htmx_fragment_name() == "main"
2323

2424

2525
def test_plain_hx_action():
26-
request = RequestFactory().get("/", HTTP_PLAIN_HX_ACTION="create")
26+
request = RequestFactory().get("/", headers={"Plain-HX-Action": "create"})
2727
view = V()
2828
view.setup(request)
2929
assert view.get_htmx_action_name() == "create"

plain/plain/cli/request.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def request(
9292
raise SystemExit(1)
9393

9494
# Create test client
95-
client = Client(SERVER_NAME="localhost")
95+
client = Client(headers={"Host": "localhost"})
9696

9797
# If user_id provided, force login
9898
if user_id:
@@ -170,7 +170,7 @@ def request(
170170
click.echo(f" Status: {response.status_code}")
171171

172172
# Request ID
173-
click.echo(f" Request ID: {response.wsgi_request.unique_id}")
173+
click.echo(f" Request ID: {response.request.unique_id}")
174174

175175
# User
176176
if getattr(response, "user", None):

plain/plain/http/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313
from .middleware import HttpMiddleware
1414
from .request import (
15+
LimitedStream,
1516
QueryDict,
1617
RawPostDataException,
1718
Request,
@@ -42,6 +43,7 @@
4243
"QueryDict",
4344
"RawPostDataException",
4445
"UnreadablePostError",
46+
"LimitedStream",
4547
# Response
4648
"Response",
4749
"ResponseBase",

plain/plain/http/request.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import uuid
77
from collections.abc import Iterator
88
from functools import cached_property
9-
from io import BytesIO
9+
from io import BytesIO, IOBase
1010
from itertools import chain
1111
from typing import TYPE_CHECKING, Any, TypeVar, overload
1212
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
@@ -36,6 +36,47 @@
3636
_T = TypeVar("_T")
3737

3838

39+
class LimitedStream(IOBase):
40+
"""
41+
Wrap another stream to disallow reading it past a number of bytes.
42+
43+
Based on the implementation from werkzeug.wsgi.LimitedStream
44+
See https://github.com/pallets/werkzeug/blob/dbf78f67/src/werkzeug/wsgi.py#L828
45+
"""
46+
47+
def __init__(self, stream: Any, limit: int) -> None:
48+
self._read = stream.read
49+
self._readline = stream.readline
50+
self._pos = 0
51+
self.limit = limit
52+
53+
def read(self, size: int = -1, /) -> bytes:
54+
_pos = self._pos
55+
limit = self.limit
56+
if _pos >= limit:
57+
return b""
58+
if size == -1 or size is None:
59+
size = limit - _pos
60+
else:
61+
size = min(size, limit - _pos)
62+
data = self._read(size)
63+
self._pos += len(data)
64+
return data
65+
66+
def readline(self, size: int | None = -1, /) -> bytes:
67+
_pos = self._pos
68+
limit = self.limit
69+
if _pos >= limit:
70+
return b""
71+
if size is None or size == -1:
72+
size = limit - _pos
73+
else:
74+
size = min(size, limit - _pos)
75+
line = self._readline(size)
76+
self._pos += len(line)
77+
return line
78+
79+
3980
class UnreadablePostError(OSError):
4081
pass
4182

plain/plain/internal/handlers/wsgi.py

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import codecs
44
from functools import cached_property
5-
from io import IOBase
65
from typing import TYPE_CHECKING
76
from urllib.parse import quote
87

98
from plain import signals
10-
from plain.http import FileResponse, QueryDict, Request, parse_cookie
9+
from plain.http import FileResponse, LimitedStream, QueryDict, Request, parse_cookie
1110
from plain.internal.handlers import base
1211
from plain.utils.http import parse_header_parameters
1312
from plain.utils.regex_helper import _lazy_re_compile
@@ -41,47 +40,6 @@ def _extract_headers_from_environ(environ: dict[str, Any]) -> dict[str, str]:
4140
return headers
4241

4342

44-
class LimitedStream(IOBase):
45-
"""
46-
Wrap another stream to disallow reading it past a number of bytes.
47-
48-
Based on the implementation from werkzeug.wsgi.LimitedStream
49-
See https://github.com/pallets/werkzeug/blob/dbf78f67/src/werkzeug/wsgi.py#L828
50-
"""
51-
52-
def __init__(self, stream: Any, limit: int) -> None:
53-
self._read = stream.read
54-
self._readline = stream.readline
55-
self._pos = 0
56-
self.limit = limit
57-
58-
def read(self, size: int = -1, /) -> bytes:
59-
_pos = self._pos
60-
limit = self.limit
61-
if _pos >= limit:
62-
return b""
63-
if size == -1 or size is None:
64-
size = limit - _pos
65-
else:
66-
size = min(size, limit - _pos)
67-
data = self._read(size)
68-
self._pos += len(data)
69-
return data
70-
71-
def readline(self, size: int | None = -1, /) -> bytes:
72-
_pos = self._pos
73-
limit = self.limit
74-
if _pos >= limit:
75-
return b""
76-
if size is None or size == -1:
77-
size = limit - _pos
78-
else:
79-
size = min(size, limit - _pos)
80-
line = self._readline(size)
81-
self._pos += len(line)
82-
return line
83-
84-
8543
class WSGIRequest(Request):
8644
non_picklable_attrs = Request.non_picklable_attrs | frozenset(["environ"])
8745

plain/plain/server/http/wsgi.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@
1818
from urllib.parse import quote, unquote_to_bytes
1919

2020
import plain.runtime
21-
from plain.http import FileResponse
21+
from plain.http import FileResponse, LimitedStream
2222
from plain.http import Request as HttpRequest
23-
from plain.internal.handlers.wsgi import LimitedStream
2423
from plain.utils.http import parse_header_parameters
2524

2625
from .. import util

plain/plain/test/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ The [`ClientResponse`](./client.py#ClientResponse) wrapper provides access to:
119119
- `content` - Response body as bytes
120120
- `headers` - Response headers
121121
- `cookies` - Cookies set by the response
122-
- `wsgi_request` - The original request object
122+
- `request` - The original request object
123123
- `resolver_match` - URL resolver match information
124124
- `redirect_chain` - List of redirects when using `follow=True`
125125

0 commit comments

Comments
 (0)