Skip to content

Commit 6401bf2

Browse files
authored
Add conversion methods to Request (#5)
1 parent 726c01f commit 6401bf2

File tree

7 files changed

+273
-23
lines changed

7 files changed

+273
-23
lines changed

.flake8

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
[flake8]
22
extend-select = TC, TC1
33
ignore =
4+
# D205: 1 blank line required between summary line and description
5+
# D400: First line should end with a period
6+
# We need longer summary lines, specially since we use Sphinx syntax.
7+
D205, D400
48
max-line-length = 88
59
per-file-ignores =
610
# F401: Imported but unused
711
form2request/__init__.py:F401
812
# D100-D104: Missing docstring
913
docs/conf.py:D100
1014
tests/__init__.py:D104
15+
tests/test_conversion.py:D100,D103
1116
tests/test_main.py:D100,D103

.github/workflows/test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ jobs:
1313
include:
1414
- python-version: '3.8'
1515
toxenv: min
16+
- python-version: '3.8'
17+
toxenv: min-extra
1618
- python-version: '3.8'
1719
- python-version: '3.9'
1820
- python-version: '3.10'
1921
- python-version: '3.11'
2022
- python-version: '3.12'
23+
- python-version: '3.12'
24+
toxenv: extra
2125

2226
steps:
2327
- uses: actions/checkout@v4

docs/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
intersphinx_mapping = {
1414
"lxml": ("https://lxml.de/apidoc/", None),
1515
"parsel": ("https://parsel.readthedocs.io/en/stable", None),
16+
"poet": ("https://web-poet.readthedocs.io/en/latest/", None),
1617
"python": ("https://docs.python.org/3", None),
18+
"requests": ("https://requests.readthedocs.io/en/latest/", None),
1719
"scrapy": ("https://docs.scrapy.org/en/latest", None),
1820
}
1921

docs/usage.rst

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ You can use :func:`~form2request.form2request` to generate form submission
1515
request data:
1616

1717
>>> from form2request import form2request
18-
>>> req = form2request(form)
19-
>>> req
18+
>>> request_data = form2request(form)
19+
>>> request_data
2020
Request(url='https://example.com?foo=bar', method='GET', headers=[], body=b'')
2121

2222
:func:`~form2request.form2request` does not make requests, but you can use its
23-
output to build requests with any HTTP client software, e.g. with the requests_
24-
library:
25-
26-
.. _requests: https://requests.readthedocs.io/en/latest/
23+
output to build requests with any HTTP client software. It also provides
24+
:ref:`conversion methods for common use cases <request>`, e.g. for the
25+
:doc:`requests <requests:index>` library:
2726

2827
.. _requests-example:
2928

3029
>>> import requests
31-
>>> requests.request(req.method, req.url, headers=req.headers, data=req.body) # doctest: +SKIP
30+
>>> request = request_data.to_requests()
31+
>>> requests.send(request) # doctest: +SKIP
3232
<Response [200]>
3333

3434
:func:`~form2request.form2request` supports :ref:`user-defined form data
@@ -205,18 +205,28 @@ Using request data
205205
The output of :func:`~form2request.form2request`,
206206
:class:`~form2request.Request`, is a simple request data container:
207207

208-
>>> req = form2request(form)
209-
>>> req
208+
>>> request_data = form2request(form)
209+
>>> request_data
210210
Request(url='https://example.com?foo=bar', method='GET', headers=[], body=b'')
211211

212212
While :func:`~form2request.form2request` does not make requests, you can use
213213
its output request data to build an actual request with any HTTP client
214-
software, like the requests_ library (see an example :ref:`above
215-
<requests-example>`) or the :doc:`Scrapy <scrapy:index>` web scraping
216-
framework:
214+
software.
215+
216+
:class:`~form2request.Request` also provides conversion methods for common use
217+
cases:
218+
219+
- :class:`~form2request.Request.to_scrapy`, for :doc:`Scrapy 1.1.0+
220+
<scrapy:index>`:
221+
222+
>>> request_data.to_scrapy(callback=self.parse) # doctest: +SKIP
223+
<GET https://example.com?foo=bar>
224+
225+
- :class:`~form2request.Request.to_requests`, for :doc:`requests 1.0.0+
226+
<requests:index>` (see an example :ref:`above <requests-example>`).
217227

218-
.. _Scrapy: https://docs.scrapy.org/en/latest/
228+
- :class:`~form2request.Request.to_poet`, for :doc:`web-poet 0.2.0+
229+
<poet:index>`:
219230

220-
>>> from scrapy import Request
221-
>>> Request(req.url, method=req.method, headers=req.headers, body=req.body)
222-
<GET https://example.com?foo=bar>
231+
>>> request_data.to_poet()
232+
HttpRequest(url=RequestUrl('https://example.com?foo=bar'), method='GET', headers=<HttpRequestHeaders()>, body=b'')

form2request/_base.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,57 @@ class Request:
178178
headers: list[tuple[str, str]]
179179
body: bytes
180180

181+
def to_poet(self, **kwargs):
182+
"""Convert the request to :class:`web_poet.HttpRequest
183+
<web_poet.page_inputs.http.HttpRequest>`.
184+
185+
All *kwargs* are passed to :class:`web_poet.HttpRequest
186+
<web_poet.page_inputs.http.HttpRequest>` as is.
187+
"""
188+
import web_poet
189+
190+
return web_poet.HttpRequest(
191+
url=self.url,
192+
method=self.method,
193+
headers=self.headers,
194+
body=self.body,
195+
**kwargs,
196+
)
197+
198+
def to_requests(self, **kwargs):
199+
"""Convert the request to :class:`requests.PreparedRequest`.
200+
201+
All *kwargs* are passed to :class:`requests.Request` as is.
202+
"""
203+
import requests
204+
205+
request = requests.Request(
206+
self.method,
207+
self.url,
208+
headers=dict(self.headers),
209+
data=self.body,
210+
**kwargs,
211+
)
212+
return request.prepare()
213+
214+
def to_scrapy(self, callback, **kwargs):
215+
"""Convert the request to :class:`scrapy.Request
216+
<scrapy.http.Request>`.
217+
218+
All *kwargs* are passed to :class:`scrapy.Request
219+
<scrapy.http.Request>` as is.
220+
"""
221+
import scrapy # type: ignore[import-untyped]
222+
223+
return scrapy.Request(
224+
self.url,
225+
callback=callback,
226+
method=self.method,
227+
headers=self.headers,
228+
body=self.body,
229+
**kwargs,
230+
)
231+
181232

182233
def form2request(
183234
form: FormElement | Selector | SelectorList,

tests/test_conversion.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import pytest
2+
3+
from form2request import Request
4+
5+
web_poet = pytest.importorskip("web_poet")
6+
scrapy = pytest.importorskip("scrapy")
7+
requests = pytest.importorskip("requests")
8+
9+
10+
def fake_scrapy_callback(self, response):
11+
pass
12+
13+
14+
@pytest.mark.parametrize(
15+
("request_data", "method", "kwargs", "expected"),
16+
(
17+
# GET
18+
*(
19+
(
20+
Request(
21+
url="https://example.com?foo=bar",
22+
method="GET",
23+
headers=[],
24+
body=b"",
25+
),
26+
method,
27+
kwargs,
28+
expected,
29+
)
30+
for method, kwargs, expected in (
31+
(
32+
"poet",
33+
{},
34+
web_poet.HttpRequest(
35+
url=web_poet.RequestUrl("https://example.com?foo=bar"),
36+
method="GET",
37+
headers=web_poet.HttpRequestHeaders(),
38+
body=web_poet.HttpRequestBody(b""),
39+
),
40+
),
41+
(
42+
"requests",
43+
{},
44+
requests.Request("GET", "https://example.com?foo=bar").prepare(),
45+
),
46+
(
47+
"scrapy",
48+
{"callback": fake_scrapy_callback},
49+
scrapy.Request(
50+
"https://example.com?foo=bar", callback=fake_scrapy_callback
51+
),
52+
),
53+
)
54+
),
55+
# POST
56+
*(
57+
(
58+
Request(
59+
url="https://example.com",
60+
method="POST",
61+
headers=[("Content-Type", "application/x-www-form-urlencoded")],
62+
body=b"foo=bar",
63+
),
64+
method,
65+
kwargs,
66+
expected,
67+
)
68+
for method, kwargs, expected in (
69+
(
70+
"poet",
71+
{},
72+
web_poet.HttpRequest(
73+
url=web_poet.RequestUrl("https://example.com"),
74+
method="POST",
75+
headers=web_poet.HttpRequestHeaders(
76+
{"Content-Type": "application/x-www-form-urlencoded"}
77+
),
78+
body=web_poet.HttpRequestBody(b"foo=bar"),
79+
),
80+
),
81+
(
82+
"requests",
83+
{},
84+
requests.Request(
85+
"POST",
86+
"https://example.com",
87+
headers={"Content-Type": "application/x-www-form-urlencoded"},
88+
data=b"foo=bar",
89+
).prepare(),
90+
),
91+
(
92+
"scrapy",
93+
{"callback": fake_scrapy_callback},
94+
scrapy.Request(
95+
"https://example.com",
96+
method="POST",
97+
headers={"Content-Type": "application/x-www-form-urlencoded"},
98+
body=b"foo=bar",
99+
callback=fake_scrapy_callback,
100+
),
101+
),
102+
)
103+
),
104+
# kwargs
105+
(
106+
Request(
107+
url="https://example.com",
108+
method="POST",
109+
headers=[("Content-Type", "application/x-www-form-urlencoded")],
110+
body=b"foo=bar",
111+
),
112+
"requests",
113+
{"params": {"foo": "bar"}},
114+
requests.Request(
115+
"POST",
116+
"https://example.com?foo=bar",
117+
headers={"Content-Type": "application/x-www-form-urlencoded"},
118+
data=b"foo=bar",
119+
).prepare(),
120+
),
121+
(
122+
Request(
123+
url="https://example.com",
124+
method="POST",
125+
headers=[("Content-Type", "application/x-www-form-urlencoded")],
126+
body=b"foo=bar",
127+
),
128+
"scrapy",
129+
{"callback": fake_scrapy_callback, "meta": {"foo": "bar"}},
130+
scrapy.Request(
131+
"https://example.com",
132+
method="POST",
133+
headers={"Content-Type": "application/x-www-form-urlencoded"},
134+
body=b"foo=bar",
135+
callback=fake_scrapy_callback,
136+
meta={"foo": "bar"},
137+
),
138+
),
139+
),
140+
)
141+
def test_conversion(request_data, method, kwargs, expected):
142+
actual = getattr(request_data, f"to_{method}")(**kwargs)
143+
if method == "poet":
144+
for field in ("method", "headers", "body"):
145+
assert getattr(actual, field) == getattr(expected, field)
146+
# RequestUrl(…) != RequestUrl(…)
147+
assert str(actual.url) == str(expected.url)
148+
elif method == "requests":
149+
# Request(…).prepare() != Request(…).prepare()
150+
for field in ("url", "method", "headers", "body"):
151+
assert getattr(actual, field) == getattr(expected, field)
152+
else:
153+
assert method == "scrapy"
154+
# Request(…) != Request(…)
155+
for field in ("url", "method", "headers", "body", "callback", "meta"):
156+
assert getattr(actual, field) == getattr(expected, field)

tox.ini

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = pre-commit,mypy,docs,doctest,twinecheck,min,py38,py39,py310,py311,py312
2+
envlist = pre-commit,mypy,docs,doctest,twinecheck,min,min-extra,py38,py39,py310,py311,py312,extra
33

44
[testenv]
55
deps =
@@ -12,14 +12,35 @@ commands =
1212
--cov=form2request \
1313
{posargs:tests}
1414

15-
[testenv:min]
16-
basepython = python3.8
15+
[min]
1716
deps =
1817
{[testenv]deps}
1918
lxml==4.4.1
2019
parsel==1.8.1
20+
21+
[testenv:min]
22+
basepython = python3.8
23+
deps =
24+
{[min]deps}
2125
w3lib==1.19.0
2226

27+
[testenv:extra]
28+
deps =
29+
{[testenv]deps}
30+
requests
31+
scrapy
32+
web-poet
33+
34+
[testenv:min-extra]
35+
basepython = {[testenv:min]basepython}
36+
deps =
37+
{[min]deps}
38+
# web-poet >= 0.2.0 requires w3lib >= 1.22.0
39+
w3lib==1.22.0
40+
requests==1.0.0
41+
scrapy==1.1.0
42+
web-poet==0.2.0
43+
2344
[testenv:pre-commit]
2445
deps =
2546
pre-commit
@@ -29,8 +50,11 @@ commands = pre-commit run --all-files --show-diff-on-failure
2950
basepython = python3.12
3051
deps =
3152
mypy==1.10.0
32-
pytest
3353
lxml-stubs
54+
pytest
55+
scrapy
56+
types-requests
57+
web-poet
3458

3559
commands = mypy form2request tests
3660

@@ -46,10 +70,8 @@ commands =
4670

4771
[testenv:doctest]
4872
deps =
49-
{[testenv]deps}
73+
{[testenv:extra]deps}
5074
parsel
51-
requests
52-
scrapy
5375
commands =
5476
pytest \
5577
--doctest-glob="*.rst" --doctest-modules \

0 commit comments

Comments
 (0)