Skip to content

Commit 2076471

Browse files
authored
Support for Python 3.11 (#181)
* Testing Python 3.11 * Switching to `httptools.parser.HttpRequestParser`. * Disabling tests for `pook` when testing Python 3.11 * Removing DeprecationWarning all over the place. * Python 3.11 needs an async decorator. * Adding Redis as a service container. * Removing `async-timeout` dependency. * Refactoring using `event_loop` fixture. * Refactoring using `tempfile` as a context manager. * Mark `aiohttp` tests as failing on Py3.11 - first time ever - and add a similar test with `httpx`. * Bump version.
1 parent bf60db4 commit 2076471

File tree

16 files changed

+425
-371
lines changed

16 files changed

+425
-371
lines changed

.github/workflows/main.yml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,25 @@ concurrency:
1515

1616
jobs:
1717
build:
18-
1918
runs-on: ubuntu-20.04
2019
strategy:
2120
matrix:
22-
python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', 'pypy3.9']
21+
python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', 'pypy3.9']
22+
23+
services:
24+
# https://docs.github.com/en/actions/using-containerized-services/about-service-containers
25+
redis:
26+
# Docker Hub image
27+
image: redis
28+
# Set health checks to wait until redis has started
29+
options: >-
30+
--health-cmd "redis-cli ping"
31+
--health-interval 10s
32+
--health-timeout 5s
33+
--health-retries 5
34+
ports:
35+
# Maps port 6379 on service container to the host
36+
- 6379:6379
2337

2438
steps:
2539
- uses: actions/checkout@v3
@@ -31,9 +45,6 @@ jobs:
3145
cache-dependency-path: |
3246
Pipfile
3347
setup.py
34-
- name: Install Redis
35-
run: |
36-
sudo apt install redis-server
3748
- name: Install dependencies
3849
run: |
3950
make develop

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
install-dev-requirements:
44
pip install -U pip
5-
pip install pipenv
5+
pip install pipenv pre-commit
66

77
install-test-requirements:
88
pipenv install --dev
9-
pipenv run python -c "import pipfile; pf = pipfile.load('Pipfile'); print('\n'.join(package+version for package, version in pf.data['default'].items()))" > requirements.txt
9+
pipenv run python -c "import pipfile; pf = pipfile.load('Pipfile'); print('\n'.join(package+version if version != '*' else package for package, version in pf.data['default'].items()))" > requirements.txt
1010

1111
test-python:
1212
@echo "Running Python tests"
@@ -34,7 +34,7 @@ publish: install-test-requirements
3434
pipenv run anaconda upload dist/mocket-$(shell python -c 'import mocket; print(mocket.__version__)').tar.gz
3535

3636
clean:
37-
rm -rf *.egg-info dist/
37+
rm -rf *.egg-info dist/ requirements.txt Pipfile.lock
3838
find . -type d -name __pycache__ -exec rm -rf {} \;
3939

4040
.PHONY: clean publish safetest test setup develop lint-python test-python install-test-requirements install-dev-requirements

Pipfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ name = "pypi"
77
python-magic = ">=0.4.5"
88
decorator = ">=4.0.0"
99
urllib3 = ">=1.25.3"
10-
http-parser = ">=0.9.0"
10+
httptools = "*"
1111

1212
[dev-packages]
1313
pre-commit = "*"
14-
pytest = ">4.6"
14+
pytest = "*"
1515
pytest-cov = "*"
1616
pytest-asyncio = "*"
17+
asgiref = "*"
1718
requests = "*"
1819
redis = "*"
1920
gevent = "*"
@@ -22,7 +23,6 @@ pook = "*"
2223
flake8 = "<7"
2324
xxhash = "*"
2425
aiohttp = "*"
25-
async-timeout = "*"
2626
httpx = "*"
2727
pipfile = "*"
2828
build = "*"

README.rst

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ Example:
231231
232232
import aiohttp
233233
import asyncio
234-
import async_timeout
235234
from unittest import TestCase
236235
237236
from mocket.plugins.httpretty import httpretty, httprettified
@@ -248,13 +247,14 @@ Example:
248247
)
249248
250249
async def main(l):
251-
async with aiohttp.ClientSession(loop=l) as session:
252-
with async_timeout.timeout(3):
253-
async with session.get(url) as get_response:
254-
assert get_response.status == 200
255-
assert await get_response.text() == '{"origin": "127.0.0.1"}'
250+
async with aiohttp.ClientSession(
251+
loop=l, timeout=aiohttp.ClientTimeout(total=3)
252+
) as session:
253+
async with session.get(url) as get_response:
254+
assert get_response.status == 200
255+
assert await get_response.text() == '{"origin": "127.0.0.1"}'
256256
257-
loop = asyncio.get_event_loop()
257+
loop = asyncio.new_event_loop()
258258
loop.set_debug(True)
259259
loop.run_until_complete(main(loop))
260260
@@ -277,18 +277,18 @@ Example:
277277
Entry.single_register(Entry.POST, url, body=body*2, status=201)
278278
279279
async def main(l):
280-
async with aiohttp.ClientSession(loop=l) as session:
281-
with async_timeout.timeout(3):
282-
async with session.get(url) as get_response:
283-
assert get_response.status == 404
284-
assert await get_response.text() == body
285-
286-
with async_timeout.timeout(3):
287-
async with session.post(url, data=body * 6) as post_response:
288-
assert post_response.status == 201
289-
assert await post_response.text() == body * 2
290-
291-
loop = asyncio.get_event_loop()
280+
async with aiohttp.ClientSession(
281+
loop=l, timeout=aiohttp.ClientTimeout(total=3)
282+
) as session:
283+
async with session.get(url) as get_response:
284+
assert get_response.status == 404
285+
assert await get_response.text() == body
286+
287+
async with session.post(url, data=body * 6) as post_response:
288+
assert post_response.status == 201
289+
assert await post_response.text() == body * 2
290+
291+
loop = asyncio.new_event_loop()
292292
loop.run_until_complete(main(loop))
293293
294294
# or again with a unittest.IsolatedAsyncioTestCase
@@ -302,18 +302,18 @@ Example:
302302
Entry.single_register(Entry.GET, url, body=body, status=404)
303303
Entry.single_register(Entry.POST, url, body=body * 2, status=201)
304304
305-
async with aiohttp.ClientSession() as session:
306-
with async_timeout.timeout(3):
307-
async with session.get(url) as get_response:
308-
assert get_response.status == 404
309-
assert await get_response.text() == body
310-
311-
with async_timeout.timeout(3):
312-
async with session.post(url, data=body * 6) as post_response:
313-
assert post_response.status == 201
314-
assert await post_response.text() == body * 2
315-
assert Mocket.last_request().method == 'POST'
316-
assert Mocket.last_request().body == body * 6
305+
async with aiohttp.ClientSession(
306+
timeout=aiohttp.ClientTimeout(total=3)
307+
) as session:
308+
async with session.get(url) as get_response:
309+
assert get_response.status == 404
310+
assert await get_response.text() == body
311+
312+
async with session.post(url, data=body * 6) as post_response:
313+
assert post_response.status == 201
314+
assert await post_response.text() == body * 2
315+
assert Mocket.last_request().method == 'POST'
316+
assert Mocket.last_request().body == body * 6
317317
318318
319319
Works well with others

mocket/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
__all__ = ("async_mocketize", "mocketize", "Mocket", "MocketEntry", "Mocketizer")
55

6-
__version__ = "3.10.9"
6+
__version__ = "3.11.0"

mocket/mockhttp.py

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
from http.server import BaseHTTPRequestHandler
44
from urllib.parse import parse_qs, unquote, urlsplit
55

6-
from .compat import decode_from_bytes, do_the_magic, encode_to_bytes
7-
from .mocket import Mocket, MocketEntry
6+
from httptools.parser import HttpRequestParser
87

9-
try:
10-
from http_parser.parser import HttpParser
11-
except ImportError:
12-
from http_parser.pyparser import HttpParser
8+
from .compat import ENCODING, decode_from_bytes, do_the_magic, encode_to_bytes
9+
from .mocket import Mocket, MocketEntry
1310

1411
try:
1512
import magic
@@ -21,31 +18,59 @@
2118
CRLF = "\r\n"
2219

2320

21+
class Protocol:
22+
def __init__(self):
23+
self.url = None
24+
self.body = None
25+
self.headers = {}
26+
27+
def on_header(self, name: bytes, value: bytes):
28+
self.headers[name.decode("ascii")] = value.decode("ascii")
29+
30+
def on_body(self, body: bytes):
31+
try:
32+
self.body = body.decode(ENCODING)
33+
except UnicodeDecodeError:
34+
self.body = body
35+
36+
def on_url(self, url: bytes):
37+
self.url = url.decode("ascii")
38+
39+
2440
class Request:
25-
parser = None
26-
_body = None
41+
_protocol = None
42+
_parser = None
2743

2844
def __init__(self, data):
29-
self.parser = HttpParser()
30-
self.parser.execute(data, len(data))
31-
32-
self.method = self.parser.get_method()
33-
self.path = self.parser.get_path()
34-
self.headers = self.parser.get_headers()
35-
self.querystring = parse_qs(
36-
unquote(self.parser.get_query_string()), keep_blank_values=True
37-
)
38-
if self.querystring:
39-
self.path += "?{}".format(self.parser.get_query_string())
45+
self._protocol = Protocol()
46+
self._parser = HttpRequestParser(self._protocol)
47+
self.add_data(data)
4048

4149
def add_data(self, data):
42-
self.parser.execute(data, len(data))
50+
self._parser.feed_data(data)
51+
52+
@property
53+
def method(self):
54+
return self._parser.get_method().decode("ascii")
55+
56+
@property
57+
def path(self):
58+
return self._protocol.url
59+
60+
@property
61+
def headers(self):
62+
return self._protocol.headers
63+
64+
@property
65+
def querystring(self):
66+
parts = self._protocol.url.split("?", 1)
67+
if len(parts) == 2:
68+
return parse_qs(unquote(parts[1]), keep_blank_values=True)
69+
return {}
4370

4471
@property
4572
def body(self):
46-
if self._body is None:
47-
self._body = decode_from_bytes(self.parser.recv_body())
48-
return self._body
73+
return self._protocol.body
4974

5075
def __str__(self):
5176
return "{} - {} - {}".format(self.method, self.path, self.headers)

mocket/plugins/httpretty/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from mocket import Mocket, mocketize
22
from mocket.async_mocket import async_mocketize
3-
from mocket.compat import byte_type, text_type
3+
from mocket.compat import ENCODING, byte_type, text_type
44
from mocket.mockhttp import Entry as MocketHttpEntry
55
from mocket.mockhttp import Request as MocketHttpRequest
66
from mocket.mockhttp import Response as MocketHttpResponse
@@ -13,9 +13,11 @@ def httprettifier_headers(headers):
1313
class Request(MocketHttpRequest):
1414
@property
1515
def body(self):
16-
if self._body is None:
17-
self._body = self.parser.recv_body()
18-
return self._body
16+
return super().body.encode(ENCODING)
17+
18+
@property
19+
def headers(self):
20+
return httprettifier_headers(super().headers)
1921

2022

2123
class Response(MocketHttpResponse):
@@ -116,6 +118,7 @@ def __getattr__(self, name):
116118

117119
__all__ = (
118120
"HTTPretty",
121+
"httpretty",
119122
"activate",
120123
"async_httprettified",
121124
"httprettified",

0 commit comments

Comments
 (0)