pytest-scenario-files 0.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ """Package that will generate Pytest unit test scenarios based on data files."""
2
+ from .plugin import pytest_generate_tests
@@ -0,0 +1,241 @@
1
+ import os
2
+ from json import load
3
+ from os.path import join
4
+ from typing import Any, Union
5
+
6
+ from yaml import safe_load
7
+
8
+
9
+ class BadTestCaseDataException(Exception):
10
+ """An exception class used to mark bad test case data."""
11
+
12
+ pass
13
+
14
+
15
+ def _load_test_data_from_file(filepath: str) -> dict[str, Any]:
16
+ """Load test data from a file and return it as a dictionary.
17
+
18
+ :param filepath: The path of the file to load.
19
+ :type filepath: str
20
+ :return: The loaded test data as a dictionary.
21
+ :rtype: dict[str, Any]
22
+ :raises BadTestCaseDataException: If the loaded test case data are of the incorrect format.
23
+ """
24
+ with open(filepath) as fp:
25
+ if filepath.endswith(".json"):
26
+ test_data = load(fp)
27
+ elif filepath.endswith((".yaml", ".yml")):
28
+ test_data = safe_load(fp)
29
+ else:
30
+ raise BadTestCaseDataException(f"{filepath} does not have an extension of .json, .yaml, or .yml.")
31
+
32
+ if not isinstance(test_data, dict):
33
+ raise BadTestCaseDataException(f"{filepath} did not produce a dictionary when loaded.")
34
+
35
+ for case_name, case_data in test_data.items():
36
+ if not isinstance(case_data, dict):
37
+ raise BadTestCaseDataException(f"From {filepath}: data for test case {case_name} is not a dict. ")
38
+
39
+ _load_referenced_data(test_data)
40
+
41
+ return test_data
42
+
43
+
44
+ def _load_referenced_data(base_data_dict: dict[str, dict[str, Any]]) -> None:
45
+ """Load data for fixtures that refer to fixtures in other files.
46
+
47
+ :param base_data_dict: A dictionary containing test cases and their fixtures.
48
+ :return: None
49
+ """
50
+ for one_test_case in base_data_dict.values():
51
+ for one_fixture_name, one_fixture_value in one_test_case.items():
52
+ if isinstance(one_fixture_value, str):
53
+ if one_fixture_value.startswith("__"):
54
+ data_file_path = one_fixture_value.removeprefix("__")
55
+ ref_data_file_name, ref_test_case, ref_fixture = data_file_path.split(":")
56
+ referenced_data_file_contents = _locate_and_load_data_files(ref_data_file_name, os.getcwd())
57
+ one_test_case[one_fixture_name] = referenced_data_file_contents[ref_test_case][ref_fixture]
58
+
59
+
60
+ def _locate_and_load_test_data(test_name: str, dir_name: str) -> dict[str, dict[str, Any]]:
61
+ """Locates and loads test data for the given test name.
62
+
63
+ :param test_name: The name of the test.
64
+ :return: A dictionary containing the loaded test data.
65
+ """
66
+ return _locate_and_load_data_files("data_" + test_name, start_dir_path=dir_name)
67
+
68
+
69
+ def _locate_and_load_data_files(filename_base: str, start_dir_path: str) -> dict[str, dict[str, Any]]:
70
+ """Locates and loads data for the given file name.
71
+
72
+ :param filename_base: The root name of the files to be loaded.
73
+ :type filename_base: str
74
+ :param start_dir_path: path where to start the search for the data files
75
+ :type start_dir_path: str
76
+ :return: A dictionary containing the loaded data.
77
+ :rtype: dict
78
+ """
79
+ result: dict[str, dict[str, Any]] = {}
80
+ for root, dirs, files in os.walk(start_dir_path):
81
+ test_data_filenames = [one_filename for one_filename in files if one_filename.startswith(filename_base)]
82
+
83
+ for one_data_file in test_data_filenames:
84
+ test_data = _load_test_data_from_file(join(root, one_data_file))
85
+ _merge_new_test_data(result, test_data, one_data_file)
86
+
87
+ return result
88
+
89
+
90
+ def _merge_new_test_data(
91
+ result: dict[str, dict[str, Any]], new_test_data: dict[str, dict[str, Any]], new_data_file: str
92
+ ) -> None:
93
+ """Merge test case data from a new file into the existing set of test cases.
94
+
95
+ :param result: A dictionary representing the result of merging the test data.
96
+ :param new_test_data: A dictionary containing new test data to be merged.
97
+ :param new_data_file: A string representing the file from which the new test data is loaded.
98
+ :return: None
99
+ :raises BadTestCaseDataException: raised if there is a conflict between data files for a given
100
+ test case id and fixture
101
+ """
102
+ for new_test_case in new_test_data.keys():
103
+ if result.get(new_test_case) is None:
104
+ # No existing test case with that id, just add it
105
+ result[new_test_case] = new_test_data[new_test_case]
106
+ else:
107
+ # There is an existing test case with that id, try to merge
108
+ existing_test_case = result[new_test_case]
109
+ for new_fixture in new_test_data[new_test_case].keys():
110
+ if existing_test_case.get(new_fixture) is None:
111
+ # No existing fixture, just merge
112
+ existing_test_case[new_fixture] = new_test_data[new_test_case][new_fixture]
113
+ else:
114
+ # Fixture already defined, raise an exception
115
+ raise BadTestCaseDataException(
116
+ f"In file {new_data_file} for test case {new_test_case}, fixture {new_fixture} already loaded."
117
+ )
118
+
119
+
120
+ def _extract_fixture_names(fixture_dict: dict[str, dict[str, Any]]) -> list[str]:
121
+ """Check fixture names for consistency between test cases
122
+
123
+ If all of the fixture names are consistent, return a sorted list of fixture names
124
+
125
+ :param fixture_dict: Dict of dicts containing test case data.
126
+ :return: A list of fixture names sorted alphabetically.
127
+ :raises BadTestCaseData: If the fixture keys are mismatched between the test cases.
128
+ """
129
+ # check that all of the test cases have the same sets of keys
130
+ # get all of the keys from all of the test cases
131
+ all_fixture_keys = {
132
+ one_fixture_name for test_case in fixture_dict.values() for one_fixture_name in test_case.keys()
133
+ }
134
+
135
+ # find test cases where there are keys in one set but not the other
136
+ bad_test_cases = {
137
+ test_case_id: problem_keys
138
+ for test_case_id, test_case_data in fixture_dict.items()
139
+ if len(problem_keys := all_fixture_keys ^ set(test_case_data.keys())) > 0
140
+ }
141
+
142
+ # raise an exception if there are bad test cases
143
+ if len(bad_test_cases) > 0:
144
+ raise BadTestCaseDataException(f"Mismatched fixture keys {bad_test_cases}")
145
+
146
+ # return the list of fixture names sorted alphabetically
147
+ return sorted(all_fixture_keys)
148
+
149
+
150
+ def _extract_indirect_fixtures(
151
+ fixture_data_dict: dict[str, dict[str, Any]], all_fixture_names: list[str]
152
+ ) -> tuple[list[str], Union[list[str], bool]]: # noqa UP007 Allow old style Union for Python 3.9
153
+ """Extracts indirect fixtures
154
+
155
+ :param fixture_data_dict: A dictionary of fixture data after loading referenced fixtures.
156
+ :param all_fixture_names: A list of all fixture names.
157
+ :return: A tuple containing the list of all fixture names with any _indirect suffixes removed and either
158
+ a list of indirect fixture names or False if there are no indirect fixtures (so that this
159
+ value can be fed directly into metafunc.parameterize()).
160
+ """
161
+ indirect_fixtures = []
162
+ all_fixtures_trimmed = []
163
+ for fixture_name in all_fixture_names:
164
+ if fixture_name.endswith("_indirect"):
165
+ fixture_name = fixture_name.removesuffix("_indirect")
166
+ indirect_fixtures.append(fixture_name)
167
+ all_fixtures_trimmed.append(fixture_name)
168
+
169
+ if len(indirect_fixtures) > 0:
170
+ for one_test_case_data in fixture_data_dict.values():
171
+ for one_fixture_name in indirect_fixtures:
172
+ fixture_data = one_test_case_data.pop(one_fixture_name + "_indirect")
173
+ one_test_case_data[one_fixture_name] = fixture_data
174
+
175
+ return all_fixtures_trimmed, indirect_fixtures
176
+ else:
177
+ return all_fixtures_trimmed, False
178
+
179
+
180
+ def _extract_fixture_data(fixture_raw_data_dict: dict[str, dict[str, Any]]) -> tuple[list[str], list[list[Any]]]:
181
+ """Extracts fixture data into a format ready for the parameterize call.
182
+
183
+ metafunc.parameterize() expects a list of fixture names, a list of lists of data, and an optional list
184
+ of case id's. _extract_fixture_names() gets the names, this function sets up the other two lists.
185
+
186
+ :param fixture_raw_data_dict: A dictionary containing fixture data, where the keys are test case IDs and the
187
+ values are dictionaries mapping fixture names to fixture data.
188
+ :type fixture_raw_data_dict: dict[str, dict[str, Any]]
189
+ :return: A tuple containing the list of test case IDs and a list of lists representing the fixture data.
190
+ :rtype: tuple[list[str], list[list[Any]]]
191
+ """
192
+ # this sets up a dict of dicts by case id, with the keys of each sub-dict being the
193
+ # fixture names in alphabetical order
194
+ fixture_cases_dict = {
195
+ test_case_id: {key: test_case_data[key] for key in sorted(fixture_raw_data_dict[test_case_id])}
196
+ for test_case_id, test_case_data in fixture_raw_data_dict.items()
197
+ }
198
+ # derive a list of lists from the dict of dicts. It's important to do this
199
+ # so that the lists are in the same order as the case id's
200
+ fixture_data_list = [
201
+ [test_case_values for test_case_values in test_case.values()] for test_case in fixture_cases_dict.values()
202
+ ]
203
+ return list(fixture_cases_dict.keys()), fixture_data_list
204
+
205
+
206
+ def pytest_generate_tests(metafunc):
207
+ """Hook called by Pytest for each test.
208
+
209
+ This is where the heavy lifting is done. This walks the directory tree
210
+ looking for files that match the name of the test. Any data are loaded
211
+ and used to parameterize the test for scenarios.
212
+
213
+ :param metafunc: Pytest fixture used to create the parameterization
214
+ """
215
+ # load up files in same or lower dirs that start with the same
216
+ # name as the test function prefixed by data_
217
+ # E.g.,
218
+ # parameterize against list of names if match
219
+ test_name = metafunc.definition.name.removeprefix("test_")
220
+ test_file_dir = metafunc.definition.path.parent
221
+
222
+ fixture_raw_data_dict = _locate_and_load_test_data(test_name, test_file_dir)
223
+
224
+ if len(fixture_raw_data_dict) > 0:
225
+ # do processing only if the search found cases
226
+
227
+ # get the list of fixture names sorted alphabetically
228
+ # will raise an exception if the fixture names are inconsistent
229
+ fixture_names = _extract_fixture_names(fixture_raw_data_dict)
230
+
231
+ # pull out indirect fixtures and remove suffix from fixture names
232
+ # could be False if there are no indirect fixtures
233
+ fixture_names, indirect_fixture_names = _extract_indirect_fixtures(fixture_raw_data_dict, fixture_names)
234
+
235
+ # reformat the case ids and fixture data into list and list of lists respectively
236
+ case_ids, fixture_data_list = _extract_fixture_data(fixture_raw_data_dict)
237
+
238
+ # do the parameterization
239
+ metafunc.parametrize(
240
+ fixture_names, fixture_data_list, ids=case_ids, indirect=indirect_fixture_names, scope="function"
241
+ )
@@ -0,0 +1,373 @@
1
+ Metadata-Version: 2.3
2
+ Name: pytest-scenario-files
3
+ Version: 0.12
4
+ Summary: A pytest plugin that generates unit test scenarios from data files.
5
+ Project-URL: Repository, https://github.com/paulsuh/pytest-scenario-files
6
+ Project-URL: Issues, https://github.com/paulsuh/pytest-scenario-files/issues
7
+ Project-URL: Changelog, https://github.com/paulsuh/pytest-scenario-files/blob/main/CHANGES.md
8
+ Author-email: Paul Suh <paul@mspex.net>
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: Pytest
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: pytest>=7.2.0
27
+ Requires-Dist: pyyaml
28
+ Provides-Extra: precommit
29
+ Requires-Dist: ruff; extra == 'precommit'
30
+ Provides-Extra: unittest
31
+ Requires-Dist: pytest; extra == 'unittest'
32
+ Requires-Dist: pytest-cov; extra == 'unittest'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # pytest-scenario-files
36
+
37
+ ***Making Pytest Scenarios Easy and Scalable***
38
+
39
+ ![PyPI pyversions][link01] ![Pytest][link02]
40
+
41
+ ![Hatch project][link03] ![Ruff][link04] ![Pre-Commit][link05]
42
+
43
+ [`pytest-scenario-files`][link06] is a [`pytest`][link07] plugin that
44
+ runs unit test scenarios using data loaded from files.
45
+
46
+ ______________________________________________________________________
47
+
48
+ ## Introduction
49
+
50
+ `pytest` has a feature called parameterization that allows you to run
51
+ the same test function repeatedly using different inputs to test
52
+ multiple scenarios. However, managing the test data for parameterization
53
+ can be a problem. Sometimes the input data is very large or there are
54
+ many test cases, so that it is impractical to put all of the data into
55
+ the source code of the test.
56
+
57
+ This plug-in loads the data for the test scenarios from separate data
58
+ files that are automatically matched up against the test functions. Each
59
+ function can have one or more data files associated with it, and each
60
+ file can contain multiple scenarios. The data files can be in JSON or
61
+ YAML format.
62
+
63
+ An additional issue with the basic pytest parameterization API is how
64
+ the user must provide the parameters. First all of the test case fixture
65
+ names in a list, followed by a list of lists with the values the
66
+ fixtures will take on, and then an optional list of test case id's.
67
+ Since the labels, values, and test case id's are in separate lists it
68
+ can be difficult to keep track of which fixture corresponds to which
69
+ value if you have many of them, and also which group of values
70
+ corresponds to which test id. The file structure uses a dict to keep the
71
+ test case id's, fixture names, and data values together in a way that is
72
+ easier on the human brain.
73
+
74
+ ### Features
75
+
76
+ - Loads data for scenarios from files into fixtures
77
+ - Multiple scenario data sets may be in one file
78
+ - There may be multiple data files for each test
79
+ - Fixtures may refer to fixtures in other files
80
+ - Can specify indirect parameterization
81
+ - Intuitive and sane data file structure
82
+
83
+ ### Compatibility
84
+
85
+ This package is a plug-in for `pytest` and works with Python 3.9 and up.
86
+
87
+ - Tested with `pytest` version 7.4.x, should work with any version 6.2.5
88
+ or higher
89
+ - Tested with CPython 3.9-3.12 and PyPy 3.9-3.10
90
+
91
+ While this code currently has a classifier of "Development Status :: 4 -
92
+ Beta", it is solid and well-tested. I will likely promote it to
93
+ "Development Status :: 5 - Production/Stable" after a little more
94
+ real-world usage.
95
+
96
+ ______________________________________________________________________
97
+
98
+ ## Installation
99
+
100
+ You can install `pytest-scenario-files` from PyPI by using `pip` :
101
+
102
+ ```
103
+ $ pip install pytest-scenario-files
104
+ ```
105
+
106
+ ## Usage
107
+
108
+ To use this plugin you need to make only two changes to your project:
109
+
110
+ 1. Install the plug-in
111
+ 2. Create the data file(s) with the proper names and formats
112
+
113
+ You can then access the data from the files via the fixtures defined in
114
+ those files. The most common usage is to manage test case inputs and
115
+ expected results. This allows the developer to change and add test cases
116
+ without making changes to the test code.
117
+
118
+ Just as with pytest's basic parameterization, the test function must
119
+ have all of the fixtures in its parameter list. Otherwise, an exception
120
+ will be raised.
121
+
122
+ The unit tests for this package are good examples of possible ways to
123
+ use this package. Look in the files in the `tests/` directory and the
124
+ corresponding files in the `tests/pytester_example_files` directory.
125
+
126
+ This package is also designed to be transparent for non-parametric
127
+ usage. If there are no data files associated with a particular test the
128
+ fixtures will not be parameterized and everything will work as though
129
+ the plug-in was not present.
130
+
131
+ ### Data File Matching
132
+
133
+ A data file will be loaded if it matches all of the following criteria:
134
+
135
+ 1. The filename starts with `data_`, followed by the name of the test
136
+ function with the prefix `test_` removed. The remainder of the
137
+ filename may be any value, and is usually used to identify the tests
138
+ contained in the file.
139
+ 2. The filename must end in `.json`, `.yaml`, or `.yml`.
140
+ 3. The file is contained in a folder at or below the file that contains
141
+ the test.
142
+
143
+ For example, for a test function
144
+
145
+ ```
146
+ test_foo(...)
147
+ ```
148
+
149
+ the files
150
+
151
+ ```
152
+ data_foo_part_1.json
153
+ data_foo_part_2.yaml
154
+ subfolder/data_foo.yaml
155
+ ```
156
+
157
+ would all be loaded.
158
+
159
+ *Caution*: Beware of situations where the name of one test is an
160
+ extended version of another. E.g., if you have two tests named
161
+ `test_foo()` and `test_foo_bar()`, a data file with the name
162
+ `data_foo_bar.yaml` will be loaded for *both* tests. To prevent this,
163
+ split the two test functions into two separate files in two different
164
+ directories or change the name of one of the test functions. See
165
+ `test_load_file_extended_name.py` and `test_load_separate_subdirs.py` in
166
+ the unit test files for this package for concrete examples of what might
167
+ happen and how to avoid it.
168
+
169
+ ### Data File Structure
170
+
171
+ Each data file may contain one or more sets of test data, in either yaml
172
+ or json format. The top level is a dict whose keys are the test id's.
173
+ Each test id is a dict whose keys are fixture names and whose values are
174
+ the test data. The test data may be anything, including container types
175
+ such as lists or dicts. An example input file `data_foo_bar.yaml` might
176
+ contain:
177
+
178
+ ```yaml
179
+ test1:
180
+ input_data_1: 17
181
+ input_data_2: 3
182
+ expected_result: 51
183
+
184
+ test2:
185
+ input_data_1:
186
+ - abc
187
+ input_data_2: 3
188
+ expected_result:
189
+ - abc
190
+ - abc
191
+ - abc
192
+ ```
193
+
194
+ This would parameterize into two test cases labeled `test1` and `test2`,
195
+ each with three fixtures, `input_data_1`, `input_data_2`, and
196
+ `expected_result`.
197
+
198
+ ### Test Case Merging and Conflicts
199
+
200
+ If the same test case id is present in two different files, the fixtures
201
+ from the two files will be merged as long as a fixture with the same
202
+ name is not defined more than once for any particular test case id. For
203
+ example, for a test function named `test_foo()` with two data files:
204
+
205
+ File `data_foo_1.yaml`;
206
+
207
+ ```yaml
208
+ test_case_one:
209
+ fixture_one: 17
210
+ ```
211
+
212
+ File `data_foo_2.yaml`;
213
+
214
+ ```yaml
215
+ test_case_one:
216
+ fixture_two: 170
217
+ ```
218
+
219
+ The function will be passed two fixtures `fixture_one=17` and
220
+ `fixture_two=170` for a test case with `id=test_case_one`.
221
+
222
+ *However*, if the fixture names are the same there will be a conflict
223
+ and the code that merges the test cases will raise an exception.
224
+
225
+ ### Loading Fixture Values by Reference
226
+
227
+ An additional powerful feature is the ability to load the value for a
228
+ fixture from another data file. You can have fixture data loaded from
229
+ another file by setting the fixture value to a specially formatted
230
+ string. It must be prefixed with two underscores and be of the format:
231
+
232
+ ```
233
+ __<Filename>:<test case id>:<fixture name>
234
+ ```
235
+
236
+ For instance, a data file `data_other_check_3.yaml` might reference the
237
+ data file `data_foo_2.yaml` from the previous section:
238
+
239
+ ```yaml
240
+ check_functionality:
241
+ input_data_1: 42
242
+ other_data: __data_foo_2.yaml:test_case_one:fixture_two
243
+ ```
244
+
245
+ This would result in two fixture values being sent into the test
246
+ function, `input_data_1 = 42` and `other_data = 170`, for a test case
247
+ with `id = check_functionality`. (Note that there is nothing preventing
248
+ an infinite self-referential loop although that is something that should
249
+ be avoided).
250
+
251
+ ### Indirect Parameterization
252
+
253
+ Pytest has a feature called [indirect parameterization][link08], where
254
+ the parameter value is passed to a fixture function, and the return
255
+ value of the fixture function is then passed downstream. You can specify
256
+ that a fixture should be marked for indirect parameterization by
257
+ appending the suffix `_indirect` to the fixture name in the data file.
258
+ If the data file contains:
259
+
260
+ ```yaml
261
+ test_case_1:
262
+ variable_A: 51
263
+ variable_B_indirect: 3
264
+
265
+ test_case_2:
266
+ variable_A: 85
267
+ variable_B_indirect: 5
268
+ ```
269
+
270
+ the corresponding test code would be:
271
+
272
+ ```python
273
+ @pytest.fixture
274
+ def variable_B(request):
275
+ return request.param * 17
276
+
277
+
278
+ def test_func(variable_A, variable_B):
279
+ assert variable_A == variable_B
280
+ ```
281
+
282
+ The values for fixture `variable_A` would be passed directly to
283
+ `test_func()`, but the values for `variable_B_indirect` would be passed
284
+ to the `variable_B()` function and the return value would be passed in
285
+ as the `variable_B` parameter to `test_func()`.
286
+
287
+ ______________________________________________________________________
288
+
289
+ ## Reporting Issues
290
+
291
+ If you encounter any problems, please [file an issue][link09] including
292
+ a detailed description and (if possible) an example of the problem.
293
+
294
+ ## Contributing
295
+
296
+ Since this project is a pytest plug-in, it really does require
297
+ test-driven development. If you want to contribute a bug fix or new
298
+ feature, please first create a test case that demonstrates what your new
299
+ code is supposed to do. Note that you need to set things up using the
300
+ `pytester` fixture, rather than testing directly.
301
+
302
+ This project uses [hatch][link10] for its environments and build system,
303
+ as well as [pre-commit][link11], [ruff][link12], and [mdformat][link13]
304
+ for formatting and linting. Before you send in a pull request, please:
305
+
306
+ - Set up `pre-commit` and use it to run `ruff` and `mdformat` with the
307
+ settings included in the `pyproject.toml` and
308
+ `.pre-commit-config.yaml` files
309
+ - Run tests using the command `hatch run test:test`, which will run all
310
+ of the tests against CPython 3.9-3.12 and PyPy 3.9-3.10
311
+ - Check test coverage with `hatch run cov`
312
+
313
+ ## License
314
+
315
+ Distributed under the terms of the `MIT` license,
316
+ `pytest-scenario-files` is free and open source software.
317
+
318
+ ## Colophon
319
+
320
+ This plug-in was originally named `pytest-parameterize-from-files`. It
321
+ was inspired by the pytest plug-ins `pytest-datadir`,
322
+ `pytest-datafixtures`, and `pytest-xpara`. I also later found the
323
+ non-plug-in package `parameterize-from-file`. To avoid confusion and
324
+ provide a more descriptive title, I renamed this project to
325
+ `pytest-scenario-files`.
326
+
327
+ - I wanted to load data from files without having to write any
328
+ additional code. However, `pytest-datadir` and `pytest-datafixtures`
329
+ required code in the test or fixtures specifically to read in the
330
+ file.
331
+ - I liked the way that `pytest-xpara` loaded data into a fixture, but
332
+ didn't like that it would only work with one file and that I had to
333
+ specify the file on the command line.
334
+ - After I wrote much of this project I found the package
335
+ `parameterize-from-files` which has a similar name. It's a powerful
336
+ and capable tool, but it's not to my taste as I think it's trying too
337
+ hard.
338
+ - It requires a decorator per test function, with potentially complex
339
+ syntax inside the decorator's arguments.
340
+ - It lets the user place code snippets into the data files which will
341
+ be a maintenance problem down the road. It's cleaner to take
342
+ advantage of Pytest's indirect parameterization feature instead.
343
+ - Having to import the package in every test file and decorate each
344
+ function increases the complexity of the test code.
345
+
346
+ In general I wanted the data file handling to be scalable:
347
+
348
+ - If you have 50 unit tests you don't have to specify all 50 files to
349
+ load in code or on the command line.
350
+ - You can reference data from other files to keep duplication low.
351
+
352
+ This `pytest` plugin was developed using a skeleton generated by
353
+ [`cookiecutter`][link14] along with the
354
+ [`cookiecutter-pytest-plugin`][link15] template, then extensively
355
+ modified to bring it up to modern standards.
356
+
357
+ ______________________________________________________________________
358
+
359
+ [link01]: https://img.shields.io/pypi/pyversions/pytest-parameterize-from-files.svg
360
+ [link02]: https://img.shields.io/badge/Pytest-Plug--in-orange?logo=pytest
361
+ [link03]: https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg
362
+ [link04]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
363
+ [link05]: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit
364
+ [link06]: https://github.com/paulsuh/pytest-scenario-files
365
+ [link07]: https://docs.pytest.org/en/stable/index.html
366
+ [link08]: https://docs.pytest.org/en/stable/example/parametrize.html#indirect-parametrization
367
+ [link09]: https://github.com/paulsuh/pytest-scenario-files/issues
368
+ [link10]: https://github.com/pypa/hatch
369
+ [link11]: https://pre-commit.com
370
+ [link12]: https://github.com/astral-sh/ruff
371
+ [link13]: https://github.com/executablebooks/mdformat
372
+ [link14]: https://pypi.org/project/cookiecutter/
373
+ [link15]: https://github.com/pytest-dev/cookiecutter-pytest-plugin
@@ -0,0 +1,7 @@
1
+ pytest_scenario_files/__init__.py,sha256=nKzlx7rTJiODKeKGH9n0L6mCUWFKhaeF2o8u8xLT1bk,123
2
+ pytest_scenario_files/plugin.py,sha256=T66Qnb4U0Ai1YSS3tja2lDG8Q_1yAreMYPKUl25IvJA,10897
3
+ pytest_scenario_files-0.12.dist-info/METADATA,sha256=w6u4Jp9Usk6uKtUErYsn7CINvX1V-CpC7NApExax6O4,13932
4
+ pytest_scenario_files-0.12.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
5
+ pytest_scenario_files-0.12.dist-info/entry_points.txt,sha256=_r47EouP6CBBxnGx5Nw6XrPQsGxB_94EYEDbHYKEhmU,57
6
+ pytest_scenario_files-0.12.dist-info/licenses/LICENSE,sha256=oqcbU6kEl8arA4ea0dSyk6W1hKZ4XkjNHBugCIgfig0,1076
7
+ pytest_scenario_files-0.12.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.24.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ pytest_scenario_files = pytest_scenario_files
@@ -0,0 +1,22 @@
1
+
2
+ The MIT License (MIT)
3
+
4
+ Copyright (c) 2024 Paul Suh
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.