Skip to content

Commit 8344fe4

Browse files
authored
Merge pull request #39 from paulsuh/feature-responses-native-file-load
Feature - Responses native file load
2 parents b29968c + b191eda commit 8344fe4

15 files changed

+202
-8
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
#### 1.3.0 - 2025-09-02
4+
5+
- Add loading of native Responses files.
6+
37
#### 1.2.1 - 2025-08-11
48

59
- Update tests to ensure compatibility with pytest 8.4.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ YAML format.
3535
- Can specify indirect parameterization
3636
- Intuitive and sane data file structure
3737
- Integration with [Responses][link08]
38-
- **NEW** - Integration with [Respx][link09]
38+
- **NEW** - Can load native Responses files
39+
- Integration with [Respx][link09]
3940

4041
### Compatibility
4142

docs/source/about.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ open source software.
1515

1616
Future Directions
1717
-----------------
18-
- Support for loading native Responses files is planned.
1918
- Support for additional matchers for both Responses and Respx (for matching
2019
headers, query parameters, json body, etc.) is under consideration.
2120
- An automated method for specifying Responses or Respx overrides is under
2221
consideration.
22+
- I'm going to see if there is any traction for rolling the (relatively simple)
23+
code to specify parameterization into ``pytest`` so that it becomes easier
24+
for other people.
2325

2426
Motivation
2527
----------
@@ -45,6 +47,10 @@ many of them, and also which group of values corresponds to which test id. The f
4547
structure uses a dict to keep the test case id’s, fixture names, and data values
4648
together in a way that is easier on the human brain.
4749

50+
Loading the native Responses save files wasn't too hard. The worst part was figuring
51+
out how to specify the file to be loaded, and fixing up the tests so that they covered
52+
the cases.
53+
4854
----
4955

5056
This plug-in was originally named ``pytest-parameterize-from-files``. It was inspired by

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
project = "Pytest Scenario Files"
1414
copyright = "2024, 2025 Paul Suh"
1515
author = "Paul Suh"
16-
release = "1.2"
16+
release = "1.3"
1717

1818
# -- General configuration ---------------------------------------------------
1919
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

docs/source/responses_integration.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ code checks to see whether the loaded value is a dict or a list and
7272
handles it accordingly. Having both suffixes is just to make reading
7373
the data files easier for humans.
7474

75+
If a fixture with a name that ends in ``_response`` or ``_responses``
76+
has a string value, the string will be treated as the name of a file in the
77+
native Responses format. The contents of this file will be loaded into the
78+
``psf_responses`` fixture using the Responses internal data file loading
79+
mechanism in addition to any other responses. The search path for locating
80+
this file will be the same as for other data files.
81+
82+
.. code-block:: yaml
83+
84+
scenario_3:
85+
native_file_responses: responses_replay_data.yaml
86+
87+
Responses data for scenario_3 will be loaded from a file named
88+
``responses_replay_data.yaml``.
89+
7590
The ``psf-responses`` fixture
7691
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7792
Pytest-Scenario-Files provides a ``psf_responses`` fixture that is used

src/pytest_scenario_files/plugin.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from contextlib import AbstractContextManager, nullcontext
66
from json import load
77
from os.path import join
8+
from pathlib import Path
89
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Union, cast
910

1011
import pytest
@@ -301,7 +302,10 @@ def psf_responses(request: pytest.FixtureRequest) -> Generator[RequestsMock, Non
301302
psf_fire_all_responses = _psf_configs.psf_fire_all_responses
302303
with RequestsMock(assert_all_requests_are_fired=psf_fire_all_responses) as rsps:
303304
for one_response in request.param:
304-
rsps.add(**one_response)
305+
if isinstance(one_response, Path):
306+
rsps._add_from_file(one_response)
307+
else:
308+
rsps.add(**one_response)
305309
yield rsps
306310

307311

@@ -403,6 +407,7 @@ def _extract_responses(
403407
:param fixture_key: name of the fixture key that will be added
404408
:type fixture_key: value must be "psf_responses_indirect" or "psf_respx_mock_indirect"
405409
"""
410+
# TODO: once we hit a minimum of Python 3.11, switch fixture_key to be a StrEnum
406411
# for each scenario
407412
# for each fixture
408413
# check if fixture name ends with _responses or _response
@@ -421,18 +426,32 @@ def _extract_responses(
421426
# Need to differentiate between the case where responses are not specified
422427
# at all and where there are only null responses
423428
if len(responses_fixture_names) > 0:
424-
psf_responses_data = []
429+
psf_responses_data: list[dict[str, str] | Path] = []
425430
for one_fixture_name in responses_fixture_names:
426431
current_fixture_data = one_scenario.pop(one_fixture_name)
427432
# TODO: once Python 3.9 is EOL, change this to the cleaner structural
428433
# pattern matching form.
429434
# It's entirely possible that the contents of either the list
430435
# or the dict are not usable, but that will be caught when the
431436
# mocks are constructed.
437+
# breakpoint()
432438
if isinstance(current_fixture_data, list):
433439
psf_responses_data.extend(current_fixture_data)
434440
elif isinstance(current_fixture_data, dict):
435441
psf_responses_data.append(current_fixture_data)
442+
elif isinstance(current_fixture_data, str) and fixture_key == "psf_responses_indirect":
443+
# if it doesn't find the Responses file (or it finds it more than once) raise an exception
444+
file_loc = tuple(Path.cwd().rglob(current_fixture_data))
445+
if len(file_loc) == 0:
446+
raise RuntimeError(
447+
f"Pytest-Scenario-Files: {one_fixture_name}: file '{current_fixture_data}' not found."
448+
)
449+
elif len(file_loc) > 1:
450+
raise RuntimeError(
451+
f"Pytest-Scenario-Files: {one_fixture_name}: file '{current_fixture_data}' multiple copies found."
452+
)
453+
file_abs_path_obj = file_loc[0].resolve(strict=True)
454+
psf_responses_data.append(file_abs_path_obj)
436455
elif current_fixture_data is None:
437456
pass
438457
else:
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Test indirect fixture loading"""
2+
3+
import pytest
4+
5+
# skip these tests if the responses module is not present
6+
responses = pytest.importorskip("responses")
7+
8+
9+
def test_load_native_responses_failure_bad_filename(pytester: pytest.Pytester):
10+
# create the test code file
11+
test_file_path = pytester.copy_example("example_test_native_responses_failure_tester.py")
12+
test_file_path.rename("test_native_responses_failure_tester.py")
13+
14+
# create the data file and native Responses file
15+
pytester.copy_example("data_native_responses_failure_tester_bad_filename.yaml")
16+
17+
result = pytester.runpytest("-v", "--psf-load-responses")
18+
19+
result.assert_outcomes(errors=1)
20+
21+
22+
def test_load_native_responses_failure_duplicate_files(pytester: pytest.Pytester):
23+
# create the test code file
24+
test_file_path = pytester.copy_example("example_test_native_responses_failure_tester.py")
25+
test_file_path.rename("test_native_responses_failure_tester.py")
26+
27+
# create the data file and two copies of the native Responses file
28+
pytester.copy_example("data_native_responses_failure_tester_duplicate_files.yaml")
29+
30+
# first copy goes into subdir
31+
datafile_path = pytester.copy_example("responses_replay_data.yaml")
32+
subdir_path = datafile_path.parent / "subdir"
33+
subdir_path.mkdir()
34+
datafile_path.rename(subdir_path / datafile_path.name)
35+
36+
# second copy at top level
37+
pytester.copy_example("responses_replay_data.yaml")
38+
39+
result = pytester.runpytest("-v", "--psf-load-responses")
40+
41+
result.assert_outcomes(errors=1)

tests/failure cases/test_responses_fixture_data_not_dict_or_list.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ def test_responses_fixture_data_not_dict_or_list(pytester):
1111
# create the data files
1212
pytester.copy_example("data_responses_fixture_data_not_dict_or_list_tester_1.yaml")
1313

14-
result = pytester.runpytest(
15-
"-k", "test_responses_fixture_data_not_dict_or_list_tester", "--psf-load-responses", "-v"
16-
)
14+
result = pytester.runpytest("-k", "test_responses_fixture_data_not_dict_or_list_tester", "--psf-load-respx", "-v")
1715

1816
result.assert_outcomes(errors=1)
1917
result.stdout.fnmatch_lines("E RuntimeError: Pytest-Scenario-Files: example_responses is not a list or dict.")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
test_1:
2+
native_file_responses: bad_filename.yaml
3+
psf_expected_result_indirect:
4+
expected_exception_name: RuntimeError
5+
target_url: https://httpstat.us/202
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#test_1:
2+
# native_file_responses: bad_filename.yaml
3+
# psf_expected_result_indirect:
4+
# expected_exception_name: RuntimeError
5+
# target_url: https://httpstat.us/202
6+
7+
test_2:
8+
native_file_responses: responses_replay_data.yaml
9+
psf_expected_result_indirect: 202 Accepted
10+
target_url: https://httpstat.us/202

0 commit comments

Comments
 (0)