Skip to content

Commit dc0e2d9

Browse files
committed
refactor: migrate mocket.compat.mockhttp to use mocket.http
1 parent 69fc895 commit dc0e2d9

File tree

2 files changed

+110
-239
lines changed

2 files changed

+110
-239
lines changed

mocket/compat/mockhttp.py

Lines changed: 108 additions & 237 deletions
Original file line numberDiff line numberDiff line change
@@ -1,264 +1,135 @@
1-
import re
2-
import time
3-
from functools import cached_property
4-
from http.server import BaseHTTPRequestHandler
5-
from urllib.parse import parse_qs, unquote, urlsplit
1+
from __future__ import annotations
62

7-
from h11 import SERVER, Connection, Data
8-
from h11 import Request as H11Request
3+
from io import BufferedReader
4+
from typing import Any
95

10-
from mocket.compat.entry import MocketEntry
11-
from mocket.core.compat import (
12-
ENCODING,
13-
decode_from_bytes,
14-
do_the_magic,
15-
encode_to_bytes,
6+
from mocket.http import (
7+
MocketHttpEntry,
8+
MocketHttpMethod,
9+
MocketHttpRequest,
10+
MocketHttpResponse,
1611
)
17-
from mocket.core.mocket import Mocket
18-
19-
STATUS = {k: v[0] for k, v in BaseHTTPRequestHandler.responses.items()}
20-
CRLF = "\r\n"
21-
ASCII = "ascii"
22-
23-
24-
class Request:
25-
_parser = None
26-
_event = None
27-
28-
def __init__(self, data):
29-
self._parser = Connection(SERVER)
30-
self.add_data(data)
31-
32-
def add_data(self, data):
33-
self._parser.receive_data(data)
12+
from mocket.mocket import Mocket
13+
14+
15+
class Response(MocketHttpResponse):
16+
def __init__(
17+
self,
18+
body: str | bytes | BufferedReader = b"",
19+
status: int = 200,
20+
headers: dict[str, str] | None = None,
21+
) -> None:
22+
super().__init__(
23+
status_code=status,
24+
headers=headers,
25+
body=body,
26+
)
3427

3528
@property
36-
def event(self):
37-
if not self._event:
38-
self._event = self._parser.next_event()
39-
return self._event
29+
def status(self) -> int:
30+
return self.status_code
4031

41-
@cached_property
42-
def method(self):
43-
return self.event.method.decode(ASCII)
4432

45-
@cached_property
46-
def path(self):
47-
return self.event.target.decode(ASCII)
33+
class Request(MocketHttpRequest):
34+
@property
35+
def body(self) -> str | None: # type: ignore
36+
body = super().body
37+
if body is None:
38+
return None
39+
return body.decode()
4840

49-
@cached_property
50-
def headers(self):
51-
return {k.decode(ASCII): v.decode(ASCII) for k, v in self.event.headers}
5241

53-
@cached_property
54-
def querystring(self):
55-
parts = self.path.split("?", 1)
56-
return (
57-
parse_qs(unquote(parts[1]), keep_blank_values=True)
58-
if len(parts) == 2
59-
else {}
42+
class Entry(MocketHttpEntry):
43+
request_cls = Request
44+
response_cls = Response # type: ignore[assignment]
45+
46+
CONNECT = MocketHttpMethod.CONNECT
47+
DELETE = MocketHttpMethod.DELETE
48+
GET = MocketHttpMethod.GET
49+
HEAD = MocketHttpMethod.HEAD
50+
OPTIONS = MocketHttpMethod.OPTIONS
51+
PATCH = MocketHttpMethod.PATCH
52+
POST = MocketHttpMethod.POST
53+
PUT = MocketHttpMethod.PUT
54+
TRACE = MocketHttpMethod.TRACE
55+
56+
METHODS = list(MocketHttpMethod)
57+
58+
def __init__(
59+
self,
60+
uri: str,
61+
method: MocketHttpMethod,
62+
responses: list[Response | Exception],
63+
match_querystring: bool = True,
64+
add_trailing_slash: bool = True,
65+
) -> None:
66+
super().__init__(
67+
method=method,
68+
uri=uri,
69+
responses=responses,
70+
match_querystring=match_querystring,
71+
add_trailing_slash=add_trailing_slash,
6072
)
6173

62-
@cached_property
63-
def body(self):
64-
while True:
65-
event = self._parser.next_event()
66-
if isinstance(event, H11Request):
67-
self._event = event
68-
elif isinstance(event, Data):
69-
return event.data.decode(ENCODING)
70-
71-
def __str__(self):
72-
return f"{self.method} - {self.path} - {self.headers}"
73-
74-
75-
class Response:
76-
headers = None
77-
is_file_object = False
78-
79-
def __init__(self, body="", status=200, headers=None):
80-
headers = headers or {}
81-
try:
82-
# File Objects
83-
self.body = body.read()
84-
self.is_file_object = True
85-
except AttributeError:
86-
self.body = encode_to_bytes(body)
87-
self.status = status
88-
89-
self.set_base_headers()
90-
91-
if headers is not None:
92-
self.set_extra_headers(headers)
93-
94-
self.data = self.get_protocol_data() + self.body
95-
96-
def get_protocol_data(self, str_format_fun_name="capitalize"):
97-
status_line = f"HTTP/1.1 {self.status} {STATUS[self.status]}"
98-
header_lines = CRLF.join(
99-
(
100-
f"{getattr(k, str_format_fun_name)()}: {v}"
101-
for k, v in self.headers.items()
102-
)
74+
def __repr__(self) -> str:
75+
return (
76+
f"{self.__class__.__name__}("
77+
f"method='{self.method.name}', "
78+
f"schema='{self.schema}', "
79+
f"location={self.address}, "
80+
f"path='{self.path}', "
81+
f"query='{self.query}'"
82+
")"
10383
)
104-
return f"{status_line}\r\n{header_lines}\r\n\r\n".encode(ENCODING)
105-
106-
def set_base_headers(self):
107-
self.headers = {
108-
"Status": str(self.status),
109-
"Date": time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()),
110-
"Server": "Python/Mocket",
111-
"Connection": "close",
112-
"Content-Length": str(len(self.body)),
113-
}
114-
if not self.is_file_object:
115-
self.headers["Content-Type"] = f"text/plain; charset={ENCODING}"
116-
else:
117-
self.headers["Content-Type"] = do_the_magic(self.body)
118-
119-
def set_extra_headers(self, headers):
120-
r"""
121-
>>> r = Response(body="<html />")
122-
>>> len(r.headers.keys())
123-
6
124-
>>> r.set_extra_headers({"foo-bar": "Foobar"})
125-
>>> len(r.headers.keys())
126-
7
127-
>>> encode_to_bytes(r.headers.get("Foo-Bar")) == encode_to_bytes("Foobar")
128-
True
129-
"""
130-
for k, v in headers.items():
131-
self.headers["-".join(token.capitalize() for token in k.split("-"))] = v
132-
133-
134-
class Entry(MocketEntry):
135-
CONNECT = "CONNECT"
136-
DELETE = "DELETE"
137-
GET = "GET"
138-
HEAD = "HEAD"
139-
OPTIONS = "OPTIONS"
140-
PATCH = "PATCH"
141-
POST = "POST"
142-
PUT = "PUT"
143-
TRACE = "TRACE"
14484

145-
METHODS = (CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE)
146-
147-
request_cls = Request
148-
response_cls = Response
149-
150-
def __init__(self, uri, method, responses, match_querystring=True):
151-
uri = urlsplit(uri)
152-
153-
port = uri.port
154-
if not port:
155-
port = 443 if uri.scheme == "https" else 80
156-
157-
super().__init__((uri.hostname, port), responses)
158-
self.schema = uri.scheme
159-
self.path = uri.path
160-
self.query = uri.query
161-
self.method = method.upper()
162-
self._sent_data = b""
163-
self._match_querystring = match_querystring
164-
165-
def __repr__(self):
166-
return f"{self.__class__.__name__}(method={self.method!r}, schema={self.schema!r}, location={self.location!r}, path={self.path!r}, query={self.query!r})"
167-
168-
def collect(self, data):
169-
consume_response = True
170-
171-
decoded_data = decode_from_bytes(data)
172-
if not decoded_data.startswith(Entry.METHODS):
173-
Mocket.remove_last_request()
174-
self._sent_data += data
175-
consume_response = False
176-
else:
177-
self._sent_data = data
178-
179-
super().collect(self._sent_data)
180-
181-
return consume_response
182-
183-
def can_handle(self, data):
184-
r"""
185-
>>> e = Entry('http://www.github.com/?bar=foo&foobar', Entry.GET, (Response(b'<html/>'),))
186-
>>> e.can_handle(b'GET /?bar=foo HTTP/1.1\r\nHost: github.com\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUser-Agent: python-requests/2.7.0 CPython/3.4.3 Linux/3.19.0-16-generic\r\nAccept: */*\r\n\r\n')
187-
False
188-
>>> e = Entry('http://www.github.com/?bar=foo&foobar', Entry.GET, (Response(b'<html/>'),))
189-
>>> e.can_handle(b'GET /?bar=foo&foobar HTTP/1.1\r\nHost: github.com\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUser-Agent: python-requests/2.7.0 CPython/3.4.3 Linux/3.19.0-16-generic\r\nAccept: */*\r\n\r\n')
190-
True
191-
"""
192-
try:
193-
requestline, _ = decode_from_bytes(data).split(CRLF, 1)
194-
method, path, _ = self._parse_requestline(requestline)
195-
except ValueError:
196-
return self is getattr(Mocket, "_last_entry", None)
197-
198-
uri = urlsplit(path)
199-
can_handle = uri.path == self.path and method == self.method
200-
if self._match_querystring:
201-
kw = dict(keep_blank_values=True)
202-
can_handle = can_handle and parse_qs(uri.query, **kw) == parse_qs(
203-
self.query, **kw
204-
)
205-
if can_handle:
206-
Mocket._last_entry = self
207-
return can_handle
208-
209-
@staticmethod
210-
def _parse_requestline(line):
211-
"""
212-
http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5
213-
214-
>>> Entry._parse_requestline('GET / HTTP/1.0') == ('GET', '/', '1.0')
215-
True
216-
>>> Entry._parse_requestline('post /testurl htTP/1.1') == ('POST', '/testurl', '1.1')
217-
True
218-
>>> Entry._parse_requestline('Im not a RequestLine')
219-
Traceback (most recent call last):
220-
...
221-
ValueError: Not a Request-Line
222-
"""
223-
m = re.match(
224-
r"({})\s+(.*)\s+HTTP/(1.[0|1])".format("|".join(Entry.METHODS)), line, re.I
225-
)
226-
if m:
227-
return m.group(1).upper(), m.group(2), m.group(3)
228-
raise ValueError("Not a Request-Line")
85+
@property
86+
def schema(self) -> str:
87+
return self.scheme
22988

23089
@classmethod
231-
def register(cls, method, uri, *responses, **config):
90+
def register(
91+
cls,
92+
method: MocketHttpMethod,
93+
uri: str,
94+
*responses: Response | Exception,
95+
**config: Any,
96+
) -> None:
23297
if "body" in config or "status" in config:
23398
raise AttributeError("Did you mean `Entry.single_register(...)`?")
23499

235-
default_config = dict(match_querystring=True, add_trailing_slash=True)
236-
default_config.update(config)
237-
config = default_config
238-
239-
if config["add_trailing_slash"] and not urlsplit(uri).path:
240-
uri += "/"
100+
if isinstance(config, dict):
101+
match_querystring = config.get("match_querystring", True)
102+
add_trailing_slash = config.get("add_trailing_slash", True)
241103

242-
Mocket.register(
243-
cls(uri, method, responses, match_querystring=config["match_querystring"])
104+
entry = cls(
105+
method=method,
106+
uri=uri,
107+
responses=list(responses),
108+
match_querystring=match_querystring,
109+
add_trailing_slash=add_trailing_slash,
244110
)
111+
Mocket.register(entry)
245112

246113
@classmethod
247114
def single_register(
248115
cls,
249-
method,
250-
uri,
251-
body="",
252-
status=200,
253-
headers=None,
254-
match_querystring=True,
255-
exception=None,
256-
):
257-
response = (
258-
exception
259-
if exception
260-
else cls.response_cls(body=body, status=status, headers=headers)
261-
)
116+
method: MocketHttpMethod,
117+
uri: str,
118+
body: str | bytes | BufferedReader = b"",
119+
status: int = 200,
120+
headers: dict[str, str] | None = None,
121+
match_querystring: bool = True,
122+
exception: Exception | None = None,
123+
) -> None:
124+
response: Response | Exception
125+
if exception is not None:
126+
response = exception
127+
else:
128+
response = Response(
129+
body=body,
130+
status=status,
131+
headers=headers,
132+
)
262133

263134
cls.register(
264135
method,

mocket/core/socket.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,8 @@ def send(
271271
self.sendall(data, *args, **kwargs)
272272
else:
273273
req = Mocket.last_request()
274-
if hasattr(req, "add_data"):
275-
req.add_data(data)
274+
if hasattr(req, "_add_data"):
275+
req._add_data(data)
276276
self._entry = entry
277277
return len(data)
278278

0 commit comments

Comments
 (0)