Skip to content

Commit a47edc9

Browse files
authored
refactor(tests): use pytest.importorskip for some optional dependencies (#2509)
* Continue from the PR #2490 This will update the remaining changes with the importorskip * apply ruff and pep8 fix for the pipeline * Revert test_ws.py changes for pytest.importorskip As the pytest.importskip on the top for msgpack resulted in causing the entire test file to be skip if the msgpack is not installed, which was following the coverage to reduced * Use pytest.fixtures instead defining globally at module level * bypass the pep8 fail in the CI * use fixtures instead of defining globally at the module level * Refactor the pytest.importorskip: * Addressing PR review comments and reverting back where required: * replace aiofiles_lib to aiofiles * reverting the changes for test_hello_asgi * removing extra line space * Revert test_ws.py
1 parent 0928a32 commit a47edc9

10 files changed

Lines changed: 95 additions & 125 deletions

tests/asgi/test_asgi_servers.py

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,20 @@
1111

1212
import pytest
1313

14-
try:
15-
import httpx
16-
except ImportError:
17-
httpx = None # type: ignore
14+
from falcon import testing
15+
16+
from . import _asgi_test_app
17+
18+
19+
@pytest.fixture(scope='session')
20+
def httpx():
21+
return pytest.importorskip('httpx')
22+
23+
24+
@pytest.fixture(scope='session')
25+
def requests():
26+
return pytest.importorskip('requests')
1827

19-
try:
20-
import requests
21-
import requests.exceptions
22-
except ImportError:
23-
requests = None # type: ignore
2428

2529
try:
2630
import websockets
@@ -30,10 +34,6 @@
3034
websockets = None # type: ignore
3135

3236

33-
from falcon import testing
34-
35-
from . import _asgi_test_app
36-
3737
_MODULE_DIR = os.path.abspath(os.path.dirname(__file__))
3838

3939
_PYPY = platform.python_implementation() == 'PyPy'
@@ -49,27 +49,24 @@
4949
_REQUEST_TIMEOUT = 10
5050

5151

52-
@pytest.mark.skipif(
53-
requests is None, reason='requests module is required for this test'
54-
)
5552
class TestASGIServer:
56-
def test_get(self, server_base_url):
53+
def test_get(self, server_base_url, requests):
5754
resp = requests.get(server_base_url, timeout=_REQUEST_TIMEOUT)
5855
assert resp.status_code == 200
5956
assert resp.text == '127.0.0.1'
6057

61-
def test_put(self, server_base_url):
58+
def test_put(self, server_base_url, requests):
6259
body = '{}'
6360
resp = requests.put(server_base_url, data=body, timeout=_REQUEST_TIMEOUT)
6461
assert resp.status_code == 200
6562
assert resp.text == '{}'
6663

67-
def test_head_405(self, server_base_url):
64+
def test_head_405(self, server_base_url, requests):
6865
body = '{}'
6966
resp = requests.head(server_base_url, data=body, timeout=_REQUEST_TIMEOUT)
7067
assert resp.status_code == 405
7168

72-
def test_post_multipart_form(self, server_base_url):
69+
def test_post_multipart_form(self, server_base_url, requests):
7370
size = random.randint(16 * _SIZE_1_MB, 32 * _SIZE_1_MB)
7471
data = os.urandom(size)
7572
digest = hashlib.sha1(data).hexdigest()
@@ -93,7 +90,7 @@ def test_post_multipart_form(self, server_base_url):
9390
},
9491
}
9592

96-
def test_post_multiple(self, server_base_url):
93+
def test_post_multiple(self, server_base_url, requests):
9794
body = testing.rand_string(_SIZE_1_KB // 2, _SIZE_1_KB)
9895
resp = requests.post(server_base_url, data=body, timeout=_REQUEST_TIMEOUT)
9996
assert resp.status_code == 200
@@ -105,7 +102,7 @@ def test_post_multiple(self, server_base_url):
105102
resp = requests.post(server_base_url, data=body, timeout=_REQUEST_TIMEOUT)
106103
assert resp.headers['X-Counter'] == '2002'
107104

108-
def test_post_invalid_content_length(self, server_base_url):
105+
def test_post_invalid_content_length(self, server_base_url, requests):
109106
headers = {'Content-Length': 'invalid'}
110107

111108
try:
@@ -124,15 +121,15 @@ def test_post_invalid_content_length(self, server_base_url):
124121
# get a heads-up if the request is no longer blocked.
125122
pass
126123

127-
def test_post_read_bounded_stream(self, server_base_url):
124+
def test_post_read_bounded_stream(self, server_base_url, requests):
128125
body = testing.rand_string(_SIZE_1_KB // 2, _SIZE_1_KB)
129126
resp = requests.post(
130127
server_base_url + 'bucket', data=body, timeout=_REQUEST_TIMEOUT
131128
)
132129
assert resp.status_code == 200
133130
assert resp.text == body
134131

135-
def test_post_read_bounded_stream_large(self, server_base_url):
132+
def test_post_read_bounded_stream_large(self, server_base_url, requests):
136133
"""Test that we can correctly read large bodies chunked server-side.
137134
138135
ASGI servers typically employ some type of flow control to stream
@@ -152,11 +149,11 @@ def test_post_read_bounded_stream_large(self, server_base_url):
152149
assert resp.json().get('drops') > size_mb
153150
assert resp.json().get('sha1') == hashlib.sha1(body).hexdigest()
154151

155-
def test_post_read_bounded_stream_no_body(self, server_base_url):
152+
def test_post_read_bounded_stream_no_body(self, server_base_url, requests):
156153
resp = requests.post(server_base_url + 'bucket', timeout=_REQUEST_TIMEOUT)
157154
assert not resp.text
158155

159-
def test_sse(self, server_base_url):
156+
def test_sse(self, server_base_url, requests):
160157
resp = requests.get(server_base_url + 'events', timeout=_REQUEST_TIMEOUT)
161158
assert resp.status_code == 200
162159

@@ -167,7 +164,7 @@ def test_sse(self, server_base_url):
167164

168165
assert not events[-1]
169166

170-
def test_sse_client_disconnects_early(self, server_base_url):
167+
def test_sse_client_disconnects_early(self, server_base_url, requests):
171168
"""Test that when the client connection is lost, the server task does not hang.
172169
173170
In the case of SSE, Falcon should detect when the client connection is
@@ -182,8 +179,7 @@ def test_sse_client_disconnects_early(self, server_base_url):
182179
timeout=(_asgi_test_app.SSE_TEST_MAX_DELAY_SEC / 2),
183180
)
184181

185-
@pytest.mark.skipif(httpx is None, reason='httpx is required for this test')
186-
async def test_stream_chunked_request(self, server_base_url):
182+
async def test_stream_chunked_request(self, server_base_url, httpx):
187183
"""Regression test for https://github.com/falconry/falcon/issues/2024"""
188184

189185
async def emitter():
@@ -200,9 +196,6 @@ async def emitter():
200196
assert resp.json().get('drops') >= 1
201197

202198

203-
@pytest.mark.skipif(
204-
requests is None, reason='requests module is required for this test'
205-
)
206199
@pytest.mark.skipif(
207200
websockets is None, reason='websockets is required for this test class'
208201
)
@@ -217,6 +210,7 @@ async def test_hello(
217210
max_receive_queue,
218211
server_base_url,
219212
server_url_events_ws,
213+
requests,
220214
):
221215
resp = requests.patch(
222216
server_base_url + 'wsoptions', json={'max_receive_queue': max_receive_queue}
@@ -617,7 +611,7 @@ def _can_run(factory):
617611

618612

619613
@pytest.fixture(params=[_uvicorn_factory, _daphne_factory, _hypercorn_factory])
620-
def server_base_url(request):
614+
def server_base_url(request, requests):
621615
process_factory = request.param
622616
_can_run(process_factory)
623617

tests/asgi/test_response_media_asgi.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
from falcon import testing
99
import falcon.asgi
1010

11-
try:
12-
import msgpack
13-
except ImportError:
14-
msgpack = None
11+
12+
@pytest.fixture(scope='session')
13+
def msgpack():
14+
return pytest.importorskip('msgpack')
1515

1616

1717
def create_client(resource, handlers=None):
@@ -93,8 +93,7 @@ def test_non_ascii_json_serialization(document):
9393
('application/x-msgpack'),
9494
],
9595
)
96-
@pytest.mark.skipif(msgpack is None, reason='msgpack is required for this test')
97-
def test_msgpack(media_type):
96+
def test_msgpack(media_type, msgpack):
9897
class TestResource:
9998
async def on_get(self, req, resp):
10099
resp.content_type = media_type

tests/test_examples.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import pytest
22

3-
try:
4-
import httpx
5-
except ImportError:
6-
httpx = None # type: ignore
3+
import falcon.testing as testing
74

8-
try:
9-
import requests
10-
except ImportError:
11-
requests = None # type: ignore
125

13-
import falcon.testing as testing
6+
@pytest.fixture(scope='session')
7+
def httpx():
8+
return pytest.importorskip('httpx')
9+
10+
11+
@pytest.fixture(scope='session')
12+
def requests():
13+
return pytest.importorskip('requests')
1414

1515

1616
def test_quote(util):
@@ -38,13 +38,7 @@ def test_things(asgi, util):
3838
)
3939

4040

41-
@pytest.mark.skipif(
42-
httpx is None, reason='things_advanced_asgi.py requires httpx [not found]'
43-
)
44-
@pytest.mark.skipif(
45-
requests is None, reason='things_advanced.py requires requests [not found]'
46-
)
47-
def test_things_advanced(asgi, util):
41+
def test_things_advanced(asgi, util, httpx, requests):
4842
suffix = '_asgi' if asgi else ''
4943
advanced = util.load_module(f'examples/things_advanced{suffix}.py')
5044

tests/test_httperror.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
import falcon.testing as testing
1515
from falcon.util.deprecation import DeprecatedWarning
1616

17-
try:
18-
import yaml
19-
except ImportError:
20-
yaml = None # type: ignore[assignment]
17+
18+
@pytest.fixture(scope='session')
19+
def yaml():
20+
return pytest.importorskip('yaml')
2121

2222

2323
@pytest.fixture
@@ -382,8 +382,7 @@ def test_client_does_not_accept_json_or_xml(self, client):
382382
assert response.headers['Vary'] == 'Accept'
383383
assert not response.content
384384

385-
@pytest.mark.skipif(yaml is None, reason='PyYAML is required for this test')
386-
def test_custom_error_serializer(self, client):
385+
def test_custom_error_serializer(self, client, yaml):
387386
headers = {
388387
'X-Error-Title': 'Storage service down',
389388
'X-Error-Description': (

tests/test_media_multipart.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from falcon.media.multipart import MultipartParseOptions
1212
from falcon.util import BufferedReader
1313

14-
try:
15-
import msgpack
16-
except ImportError:
17-
msgpack = None
14+
15+
@pytest.fixture(scope='session')
16+
def msgpack():
17+
return pytest.importorskip('msgpack')
1818

1919

2020
EXAMPLE1 = (
@@ -414,7 +414,7 @@ async def on_post_mirror(self, req, resp):
414414

415415

416416
@pytest.fixture
417-
def custom_client(asgi, util):
417+
def custom_client(asgi, util, msgpack):
418418
def _factory(options):
419419
multipart_handler = media.MultipartFormHandler()
420420
for key, value in options.items():
@@ -596,9 +596,8 @@ def test_too_many_body_parts(custom_client, max_body_part_count):
596596
assert len(resp.json) == EXAMPLE2_PART_COUNT
597597

598598

599-
@pytest.mark.skipif(not msgpack, reason='msgpack not installed')
600599
@pytest.mark.parametrize('close_delimiter', ['--', '--\r\n'])
601-
def test_random_form(client, close_delimiter):
600+
def test_random_form(client, close_delimiter, msgpack):
602601
part_data = [os.urandom(random.randint(0, 2**18)) for _ in range(64)]
603602
form_data = (
604603
b''.join(

tests/test_request_media.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
from falcon import util
1010
import falcon.asgi
1111

12-
try:
13-
import msgpack
14-
except ImportError:
15-
msgpack = None
12+
13+
@pytest.fixture(scope='session')
14+
def msgpack():
15+
return pytest.importorskip('msgpack')
1616

1717

1818
def create_client(asgi, handlers=None, resource=None):
@@ -104,8 +104,7 @@ def test_json(client, media_type):
104104
('application/x-msgpack'),
105105
],
106106
)
107-
@pytest.mark.skipif(msgpack is None, reason='msgpack is required for this test')
108-
def test_msgpack(asgi, media_type):
107+
def test_msgpack(asgi, media_type, msgpack):
109108
client = create_client(
110109
asgi,
111110
{
@@ -157,8 +156,7 @@ def test_unknown_media_type(asgi, media_type):
157156

158157

159158
@pytest.mark.parametrize('media_type', ['application/json', 'application/msgpack'])
160-
@pytest.mark.skipif(msgpack is None, reason='msgpack is required for this test')
161-
def test_empty_body(asgi, media_type):
159+
def test_empty_body(asgi, media_type, msgpack):
162160
client = _create_client_invalid_media(
163161
asgi,
164162
errors.HTTPBadRequest,
@@ -198,8 +196,7 @@ def test_invalid_json(asgi):
198196
assert str(client.resource.captured_error.value.__cause__) == str(e)
199197

200198

201-
@pytest.mark.skipif(msgpack is None, reason='msgpack is required for this test')
202-
def test_invalid_msgpack(asgi):
199+
def test_invalid_msgpack(asgi, msgpack):
203200
handlers = {'application/msgpack': media.MessagePackHandler()}
204201
client = _create_client_invalid_media(
205202
asgi, errors.HTTPBadRequest, handlers=handlers

tests/test_response_media.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from falcon import media
88
from falcon import testing
99

10-
try:
11-
import msgpack
12-
except ImportError:
13-
msgpack = None
10+
11+
@pytest.fixture(scope='session')
12+
def msgpack():
13+
return pytest.importorskip('msgpack')
1414

1515

1616
@pytest.fixture
@@ -99,8 +99,7 @@ def test_non_ascii_json_serialization(document):
9999
('application/x-msgpack'),
100100
],
101101
)
102-
@pytest.mark.skipif(msgpack is None, reason='msgpack is required for this test')
103-
def test_msgpack(media_type):
102+
def test_msgpack(media_type, msgpack):
104103
client = create_client(
105104
{
106105
'application/msgpack': media.MessagePackHandler(),

tests/test_utils.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
from falcon.util import uri
2727
from falcon.util.time import TimezoneGMT
2828

29-
try:
30-
import msgpack
31-
except ImportError:
32-
msgpack = None
29+
30+
@pytest.fixture(scope='session')
31+
def msgpack():
32+
return pytest.importorskip('msgpack')
3333

3434

3535
@pytest.fixture
@@ -1238,8 +1238,9 @@ def on_post(self, req, resp):
12381238
MEDIA_URLENCODED,
12391239
],
12401240
)
1241-
@pytest.mark.skipif(msgpack is None, reason='msgpack is required for this test')
1242-
def test_simulate_content_type_extra_handler(self, asgi, util, content_type):
1241+
def test_simulate_content_type_extra_handler(
1242+
self, asgi, util, content_type, msgpack
1243+
):
12431244
class TestResourceAsync(testing.SimpleTestResourceAsync):
12441245
def __init__(self):
12451246
super().__init__()

0 commit comments

Comments
 (0)