yaml-test-params 0.1.0__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.
- yaml_test_params/__init__.py +29 -0
- yaml_test_params/args_loader.py +29 -0
- yaml_test_params/models.py +209 -0
- yaml_test_params/parametrize_args.py +52 -0
- yaml_test_params/py.typed +1 -0
- yaml_test_params-0.1.0.dist-info/METADATA +454 -0
- yaml_test_params-0.1.0.dist-info/RECORD +9 -0
- yaml_test_params-0.1.0.dist-info/WHEEL +4 -0
- yaml_test_params-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .args_loader import load_parametrize_args
|
|
2
|
+
from .models import (
|
|
3
|
+
BaseTestCase,
|
|
4
|
+
BaseTestConfig,
|
|
5
|
+
BaseTestConfigCollection,
|
|
6
|
+
ListConfig,
|
|
7
|
+
ParametrizeInteger,
|
|
8
|
+
ParametrizeIntegerConfigModels,
|
|
9
|
+
ParametrizeString,
|
|
10
|
+
ParametrizeStringConfigModels,
|
|
11
|
+
RangeConfig,
|
|
12
|
+
ValueConfig,
|
|
13
|
+
)
|
|
14
|
+
from .parametrize_args import ParametrizeArgs
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BaseTestCase",
|
|
18
|
+
"BaseTestConfig",
|
|
19
|
+
"BaseTestConfigCollection",
|
|
20
|
+
"ListConfig",
|
|
21
|
+
"ParametrizeArgs",
|
|
22
|
+
"ParametrizeInteger",
|
|
23
|
+
"ParametrizeIntegerConfigModels",
|
|
24
|
+
"ParametrizeString",
|
|
25
|
+
"ParametrizeStringConfigModels",
|
|
26
|
+
"RangeConfig",
|
|
27
|
+
"ValueConfig",
|
|
28
|
+
"load_parametrize_args",
|
|
29
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Type, TypeVar, Union
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from .models import BaseTestConfigCollection
|
|
7
|
+
from .parametrize_args import ParametrizeArgs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
TestConfigCollection = TypeVar(
|
|
11
|
+
"TestConfigCollection",
|
|
12
|
+
bound=BaseTestConfigCollection,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_parametrize_args(
|
|
17
|
+
path_to_configs: Union[pathlib.Path, str],
|
|
18
|
+
config_collection_model: Type[TestConfigCollection],
|
|
19
|
+
collection_name: str,
|
|
20
|
+
) -> ParametrizeArgs:
|
|
21
|
+
with open(path_to_configs, "r", encoding="utf-8") as f:
|
|
22
|
+
yaml_config = yaml.safe_load(f) # type: ignore[attr-defined]
|
|
23
|
+
test_configs = config_collection_model.model_validate(yaml_config)
|
|
24
|
+
|
|
25
|
+
for cfg in test_configs.collection:
|
|
26
|
+
if cfg.name == collection_name:
|
|
27
|
+
return cfg.generate_parametrize_args()
|
|
28
|
+
|
|
29
|
+
raise ValueError(f"No test configuration found for collection name: {collection_name}")
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from itertools import product
|
|
3
|
+
from typing import Union, List, get_args, get_origin
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from .parametrize_args import ParametrizeArgs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValueConfig(BaseModel):
|
|
11
|
+
"""Configuration for parameters with a simple value"""
|
|
12
|
+
|
|
13
|
+
value: Union[int, str] = Field(..., alias="value")
|
|
14
|
+
|
|
15
|
+
model_config = {"populate_by_name": True}
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str:
|
|
18
|
+
return str(self.value)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ListConfig(BaseModel):
|
|
22
|
+
"""Configuration for parameters with a list of values"""
|
|
23
|
+
|
|
24
|
+
values: list[Union[int, str]] = Field(..., alias="values")
|
|
25
|
+
|
|
26
|
+
model_config = {"populate_by_name": True}
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return str(self.values)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RangeConfig(BaseModel):
|
|
33
|
+
"""Configuration for parameters with a range (from/to/step)"""
|
|
34
|
+
|
|
35
|
+
from_: int = Field(..., alias="from")
|
|
36
|
+
to: int
|
|
37
|
+
step: int = Field(..., gt=0)
|
|
38
|
+
|
|
39
|
+
model_config = {"populate_by_name": True}
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return f"From {self.from_} To {self.to} Step {self.step}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
ParametrizeIntegerConfigModels = Union[RangeConfig, ListConfig, ValueConfig]
|
|
46
|
+
ParametrizeStringConfigModels = Union[ListConfig, ValueConfig]
|
|
47
|
+
ParametrizeInteger = Union[int, ParametrizeIntegerConfigModels]
|
|
48
|
+
ParametrizeString = Union[str, ParametrizeStringConfigModels]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BaseTestCase(BaseModel, ABC):
|
|
52
|
+
test_name: str
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def arg_id(self) -> str:
|
|
57
|
+
return self.test_name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BaseTestConfig(BaseModel):
|
|
61
|
+
name: str
|
|
62
|
+
test_cases: list[BaseTestCase]
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _generate_range_values(range_config: RangeConfig) -> list[int]:
|
|
66
|
+
"""
|
|
67
|
+
Generate a list of values from a RangeConfig.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
range_config: RangeConfig object with from_, to, and step
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of integer values in the range
|
|
74
|
+
"""
|
|
75
|
+
return list(range(range_config.from_, range_config.to + 1, range_config.step))
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _format_range_values(values: tuple) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Format range values into a string for test naming.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
values: Tuple of values
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Formatted string representation
|
|
87
|
+
"""
|
|
88
|
+
return "_".join(str(v) for v in values)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def test_case_type(self) -> BaseTestCase:
|
|
92
|
+
raw_type = self.__annotations__["test_cases"]
|
|
93
|
+
origin = get_origin(raw_type)
|
|
94
|
+
|
|
95
|
+
if origin is None or origin not in (list, List):
|
|
96
|
+
raise TypeError(f"Field 'test_cases' has to be List[…] , not {origin!r}")
|
|
97
|
+
|
|
98
|
+
(inner_type,) = get_args(raw_type)
|
|
99
|
+
return inner_type
|
|
100
|
+
|
|
101
|
+
def _expand_range_configs(self, test_case: BaseTestCase) -> list[BaseTestCase]:
|
|
102
|
+
"""
|
|
103
|
+
Expand a test case with RangeConfig and ListConfig values into multiple test cases.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
test_case: Single BaseTestCase with potential RangeConfig or ListConfig values
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of expanded BaseTestCase objects
|
|
110
|
+
"""
|
|
111
|
+
# Check if any of the parameters use ValueConfig, ListConfig or RangeConfig
|
|
112
|
+
params_with_configs = []
|
|
113
|
+
|
|
114
|
+
fields = getattr(type(test_case), "model_fields")
|
|
115
|
+
|
|
116
|
+
for key in fields.keys():
|
|
117
|
+
value = getattr(test_case, key)
|
|
118
|
+
if isinstance(value, (ValueConfig, ListConfig, RangeConfig)):
|
|
119
|
+
params_with_configs.append((key, value))
|
|
120
|
+
|
|
121
|
+
# If no value configs, return the original test case
|
|
122
|
+
if not params_with_configs:
|
|
123
|
+
return [test_case]
|
|
124
|
+
|
|
125
|
+
# Get all possible values for each parameter
|
|
126
|
+
param_values = []
|
|
127
|
+
param_names = []
|
|
128
|
+
|
|
129
|
+
for param_name, config in params_with_configs:
|
|
130
|
+
values: list[int] | list[int | str]
|
|
131
|
+
if isinstance(config, ValueConfig):
|
|
132
|
+
values = [config.value]
|
|
133
|
+
elif isinstance(config, ListConfig):
|
|
134
|
+
values = config.values
|
|
135
|
+
elif isinstance(config, RangeConfig):
|
|
136
|
+
values = self._generate_range_values(config)
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError("Unsupported configuration type.")
|
|
139
|
+
param_values.append(values)
|
|
140
|
+
param_names.append(param_name)
|
|
141
|
+
|
|
142
|
+
# Generate all combinations
|
|
143
|
+
combinations = list(product(*param_values))
|
|
144
|
+
|
|
145
|
+
# Create new test cases for each combination
|
|
146
|
+
expanded_cases = []
|
|
147
|
+
for combo in combinations:
|
|
148
|
+
# Create a copy of the original test case
|
|
149
|
+
new_test_case = test_case.model_copy(deep=True)
|
|
150
|
+
|
|
151
|
+
# Update the parameters with specific values
|
|
152
|
+
for i, param_name in enumerate(param_names):
|
|
153
|
+
setattr(new_test_case, param_name, ValueConfig(value=combo[i]))
|
|
154
|
+
|
|
155
|
+
# Update test name to reflect the specific values
|
|
156
|
+
new_test_case.test_name = f"{test_case.test_name}__{self._format_range_values(combo)}"
|
|
157
|
+
|
|
158
|
+
expanded_cases.append(new_test_case)
|
|
159
|
+
|
|
160
|
+
return expanded_cases
|
|
161
|
+
|
|
162
|
+
def generate_parametrize_args(self) -> ParametrizeArgs:
|
|
163
|
+
"""
|
|
164
|
+
Generate parametrize args (test_cases) based on the BaseTestCase model,
|
|
165
|
+
handling value configs appropriately.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
MetafuncParametrizeArgs object
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
parametrize_args = ParametrizeArgs()
|
|
172
|
+
parametrize_args.init_arg_names(self.test_case_type)
|
|
173
|
+
|
|
174
|
+
for test_case in self.test_cases:
|
|
175
|
+
# Handle RangeConfig values by expanding them into individual test cases
|
|
176
|
+
expanded_test_cases = self._expand_range_configs(test_case)
|
|
177
|
+
|
|
178
|
+
for expanded_case in expanded_test_cases:
|
|
179
|
+
# Create a tuple of parameters for this test case
|
|
180
|
+
arg_values = []
|
|
181
|
+
|
|
182
|
+
for key in parametrize_args.keys:
|
|
183
|
+
attr = getattr(expanded_case, key)
|
|
184
|
+
if isinstance(attr, ValueConfig):
|
|
185
|
+
arg_values.append(attr.value)
|
|
186
|
+
else:
|
|
187
|
+
arg_values.append(attr)
|
|
188
|
+
|
|
189
|
+
parametrize_args.add_params(arg_id=expanded_case.arg_id, arg_values=tuple(arg_values))
|
|
190
|
+
|
|
191
|
+
return parametrize_args
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class BaseTestConfigCollection(BaseModel):
|
|
195
|
+
collection: list[BaseTestConfig]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
__all__ = [
|
|
199
|
+
"ValueConfig",
|
|
200
|
+
"ListConfig",
|
|
201
|
+
"RangeConfig",
|
|
202
|
+
"ParametrizeIntegerConfigModels",
|
|
203
|
+
"ParametrizeStringConfigModels",
|
|
204
|
+
"ParametrizeInteger",
|
|
205
|
+
"ParametrizeString",
|
|
206
|
+
"BaseTestCase",
|
|
207
|
+
"BaseTestConfig",
|
|
208
|
+
"BaseTestConfigCollection",
|
|
209
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from dataclasses import asdict, dataclass, field
|
|
2
|
+
from typing import Type, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ERROR_INIT_ARGS_MSG = "Argument names are not initialized"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _filter_private(items: list) -> dict:
|
|
11
|
+
return {k: v for k, v in items if not k.startswith("_")}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ParametrizeArgs:
|
|
16
|
+
argnames: Union[str, None] = None
|
|
17
|
+
argvalues: list[tuple] = field(default_factory=list)
|
|
18
|
+
ids: list[str] = field(default_factory=list)
|
|
19
|
+
_model_cls: Union[Type[BaseModel], BaseModel] = None
|
|
20
|
+
_arg_keys: Union[tuple, None] = None
|
|
21
|
+
|
|
22
|
+
def init_arg_names(self, model_cls: BaseModel) -> None:
|
|
23
|
+
if self.argnames is None:
|
|
24
|
+
model_cls = model_cls if isinstance(model_cls, type) else type(model_cls)
|
|
25
|
+
if hasattr(model_cls, "model_fields"):
|
|
26
|
+
self._model_cls = model_cls
|
|
27
|
+
fields = getattr(model_cls, "model_fields")
|
|
28
|
+
self._arg_keys = tuple(fields.keys())
|
|
29
|
+
self.argnames = ", ".join(fields.keys())
|
|
30
|
+
else:
|
|
31
|
+
raise ValueError("No pydantic fields found for model")
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError("Argname already set")
|
|
34
|
+
|
|
35
|
+
def add_params(self, arg_id: str, arg_values: tuple) -> None:
|
|
36
|
+
self.ids.append(arg_id)
|
|
37
|
+
self.argvalues.append(arg_values)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def keys(self) -> tuple:
|
|
41
|
+
if self._arg_keys is None:
|
|
42
|
+
raise ValueError(ERROR_INIT_ARGS_MSG)
|
|
43
|
+
return self._arg_keys
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def keys_set(self) -> set:
|
|
47
|
+
if self._arg_keys is None:
|
|
48
|
+
raise ValueError(ERROR_INIT_ARGS_MSG)
|
|
49
|
+
return set(self._arg_keys)
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict:
|
|
52
|
+
return asdict(self, dict_factory=_filter_private)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yaml-test-params
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate pytest and unittest parameters from YAML configuration files.
|
|
5
|
+
Project-URL: Homepage, https://github.com/fomenko-ai/yaml-test-params
|
|
6
|
+
Project-URL: Repository, https://github.com/fomenko-ai/yaml-test-params
|
|
7
|
+
Project-URL: Issues, https://github.com/fomenko-ai/yaml-test-params/issues
|
|
8
|
+
Author-email: Aleksei Fomenko <fomenko_ai@proton.me>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Keywords: parametrize,pydantic,pytest,testing,unittest,yaml
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.4.2; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# yaml-test-params
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/yaml-test-params/)
|
|
33
|
+
[](LICENSE.txt)
|
|
34
|
+
|
|
35
|
+
A Python library for dynamic test parameter generation from YAML configuration files. This library enables flexible, data-driven test scenarios by combining Pydantic models with pytest's parametrize functionality or Python's built-in unittest library.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Configuration-driven tests**: Define test parameters in YAML files instead of hardcoding them
|
|
40
|
+
- **Pydantic validation**: Type-safe configuration models with automatic validation
|
|
41
|
+
- **Automatic test expansion**: Range configurations are automatically expanded into individual test cases
|
|
42
|
+
- **Seamless pytest integration**: Works with pytest's native parametrize mechanism
|
|
43
|
+
- **unittest support**: Generated parameter sets can be iterated in standard `unittest.TestCase` tests
|
|
44
|
+
- **Flexible parameter types**: Support for simple values, lists, and ranges
|
|
45
|
+
- **Custom test case data**: `test_cases` can include any YAML data types accepted by your Pydantic models
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv add yaml-test-params
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Dependencies:**
|
|
54
|
+
|
|
55
|
+
- Python >= 3.9
|
|
56
|
+
- pydantic >= 2.0
|
|
57
|
+
- pyyaml >= 6.0.2
|
|
58
|
+
|
|
59
|
+
For local development and examples, install the development extra:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv sync --extra dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## How It Works
|
|
66
|
+
|
|
67
|
+
The project supports dynamic parameter generation for tests from configuration files, enabling flexible test scenarios.
|
|
68
|
+
|
|
69
|
+
### Workflow
|
|
70
|
+
|
|
71
|
+
1. **Base Pydantic models** define the structure of test cases and the YAML configuration file structure.
|
|
72
|
+
2. **YAML configuration** defines test parameters and scenarios.
|
|
73
|
+
3. **Test runner integration** uses the generated arguments either through a `pytest_generate_tests` hook or directly inside `unittest.TestCase`.
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
### Step 1: Define Your Models
|
|
78
|
+
|
|
79
|
+
Create Pydantic models that represent your test case structure:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from yaml_test_params.models import (
|
|
83
|
+
BaseTestCase,
|
|
84
|
+
BaseTestConfig,
|
|
85
|
+
BaseTestConfigCollection,
|
|
86
|
+
ParametrizeInteger,
|
|
87
|
+
ParametrizeString,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ExampleTestCase(BaseTestCase):
|
|
92
|
+
test_name: str
|
|
93
|
+
integer: ParametrizeInteger
|
|
94
|
+
string: ParametrizeString
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def arg_id(self) -> str:
|
|
98
|
+
return self.test_name
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ExampleTestConfig(BaseTestConfig):
|
|
102
|
+
test_cases: list[ExampleTestCase]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ExampleTestConfigCollection(BaseTestConfigCollection):
|
|
106
|
+
collection: list[ExampleTestConfig]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Step 2: Create YAML Configuration
|
|
110
|
+
|
|
111
|
+
Define your test parameters in a YAML file:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
collection:
|
|
115
|
+
- name: examples
|
|
116
|
+
test_cases:
|
|
117
|
+
- test_name: int_1,2,3__str_a
|
|
118
|
+
integer:
|
|
119
|
+
values: [1, 2, 3]
|
|
120
|
+
string: a
|
|
121
|
+
|
|
122
|
+
- test_name: int_42__str_a,b,c
|
|
123
|
+
integer: 42
|
|
124
|
+
string:
|
|
125
|
+
values: [a, b, c]
|
|
126
|
+
|
|
127
|
+
- test_name: int_1_10_1__str_a
|
|
128
|
+
integer:
|
|
129
|
+
from: 1
|
|
130
|
+
to: 10
|
|
131
|
+
step: 1
|
|
132
|
+
string: a
|
|
133
|
+
|
|
134
|
+
- test_name: int_1_10_2__str_a,b,c
|
|
135
|
+
integer:
|
|
136
|
+
from: 1
|
|
137
|
+
to: 10
|
|
138
|
+
step: 2
|
|
139
|
+
string:
|
|
140
|
+
values: [a, b, c]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Step 3: Use Generated Parameters in Tests
|
|
144
|
+
|
|
145
|
+
You can use the generated parameters with either pytest or unittest.
|
|
146
|
+
|
|
147
|
+
#### Option A: Configure pytest Hook
|
|
148
|
+
|
|
149
|
+
Add the hook to your `conftest.py`:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import pytest
|
|
153
|
+
|
|
154
|
+
from yaml_test_params.args_loader import load_parametrize_args
|
|
155
|
+
from ..models import ExampleTestConfigCollection
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
|
|
159
|
+
test_cls = getattr(metafunc, "cls", None)
|
|
160
|
+
if test_cls is None:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
full_cls_name = f"{test_cls.__module__}.{test_cls.__qualname__}"
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
full_cls_name == "examples.pytest_tests.test_examples.TestParametrizeExamples"
|
|
167
|
+
and metafunc.function.__name__ == "test_values"
|
|
168
|
+
):
|
|
169
|
+
parametrize_args = load_parametrize_args(
|
|
170
|
+
path_to_configs="examples/collection.yaml",
|
|
171
|
+
config_collection_model=ExampleTestConfigCollection,
|
|
172
|
+
collection_name="examples",
|
|
173
|
+
)
|
|
174
|
+
needed_args = parametrize_args.keys_set
|
|
175
|
+
if needed_args.issubset(set(metafunc.fixturenames)):
|
|
176
|
+
metafunc.parametrize(**parametrize_args.to_dict())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Then write tests that accept the parameters defined in your configuration:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
class TestParametrizeExamples:
|
|
183
|
+
"""Test class demonstrating pytest parametrize with integer and string variables."""
|
|
184
|
+
|
|
185
|
+
def test_values(self, test_name: str, integer: int, string: str):
|
|
186
|
+
"""Test that integer and string parameters are correctly passed."""
|
|
187
|
+
print(f"\n==============\n")
|
|
188
|
+
print(f"Test name: {test_name}")
|
|
189
|
+
print(f"integer: {integer}\nstring: {string}")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Run the pytest example:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
uv run pytest -s examples/pytest_tests
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Option B: Use unittest
|
|
199
|
+
|
|
200
|
+
Load the generated arguments once and iterate over them in a `unittest.TestCase`:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import unittest
|
|
204
|
+
|
|
205
|
+
from yaml_test_params.args_loader import load_parametrize_args
|
|
206
|
+
from ..models import ExampleTestConfigCollection
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
parametrize_args = load_parametrize_args(
|
|
210
|
+
path_to_configs="examples/collection.yaml",
|
|
211
|
+
config_collection_model=ExampleTestConfigCollection,
|
|
212
|
+
collection_name="examples",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestParametrizeExamples(unittest.TestCase):
|
|
217
|
+
"""Test class demonstrating unittest with generated YAML parameters."""
|
|
218
|
+
|
|
219
|
+
cases = parametrize_args.argvalues
|
|
220
|
+
|
|
221
|
+
def test_values(self):
|
|
222
|
+
"""Test that integer and string parameters are correctly passed."""
|
|
223
|
+
for test_name, integer, string in self.cases:
|
|
224
|
+
with self.subTest(test_name=test_name, integer=integer, string=string):
|
|
225
|
+
print(f"\n==============\n")
|
|
226
|
+
print(f"Test name: {test_name}")
|
|
227
|
+
print(f"integer: {integer}\nstring: {string}")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Run the unittest example:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
uv run python -m unittest examples.unittest_tests.test_examples
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
See the full examples in [`examples/pytest_tests`](examples/pytest_tests/) and [`examples/unittest_tests`](examples/unittest_tests/).
|
|
237
|
+
|
|
238
|
+
## Configuration Types
|
|
239
|
+
|
|
240
|
+
The library supports three types of parameter configurations:
|
|
241
|
+
|
|
242
|
+
`test_cases` may also contain any additional fields and data types that can be
|
|
243
|
+
represented in YAML and validated by your Pydantic models, such as booleans,
|
|
244
|
+
lists, dictionaries, nested models, dates, or enums. These fields are passed to
|
|
245
|
+
generated test arguments according to the model definition.
|
|
246
|
+
|
|
247
|
+
The built-in parametrization config models currently expand only `int` and
|
|
248
|
+
`str` values through `ParametrizeInteger` and `ParametrizeString`.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
class ExampleTestCase(BaseTestCase):
|
|
252
|
+
integer: ParametrizeInteger
|
|
253
|
+
string: ParametrizeString
|
|
254
|
+
list_of_types: list[int, str, float]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```yaml
|
|
258
|
+
test_cases:
|
|
259
|
+
- test_name: int_1_10_2__str_a,b,c__42_abc_0.07
|
|
260
|
+
integer:
|
|
261
|
+
from: 1
|
|
262
|
+
to: 10
|
|
263
|
+
step: 2
|
|
264
|
+
string:
|
|
265
|
+
values: [a, b, c]
|
|
266
|
+
list_of_types: [42, abc, 0.07]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Simple Value
|
|
270
|
+
|
|
271
|
+
A single value for a parameter:
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
integer: 42
|
|
275
|
+
string: "hello"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### List of Values
|
|
279
|
+
|
|
280
|
+
Multiple discrete values:
|
|
281
|
+
|
|
282
|
+
```yaml
|
|
283
|
+
integer:
|
|
284
|
+
values: [1, 2, 3]
|
|
285
|
+
string:
|
|
286
|
+
values: [a, b, c]
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Range
|
|
290
|
+
|
|
291
|
+
A range of values with start, end, and step:
|
|
292
|
+
|
|
293
|
+
```yaml
|
|
294
|
+
integer:
|
|
295
|
+
from: 1
|
|
296
|
+
to: 10
|
|
297
|
+
step: 2
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
This generates values: `[1, 3, 5, 7, 9]`.
|
|
301
|
+
|
|
302
|
+
`step` must be a positive integer.
|
|
303
|
+
|
|
304
|
+
## Available Models
|
|
305
|
+
|
|
306
|
+
### ValueConfig
|
|
307
|
+
|
|
308
|
+
Configuration for parameters with a simple value:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
class ValueConfig(BaseModel):
|
|
312
|
+
value: int | str
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### ListConfig
|
|
316
|
+
|
|
317
|
+
Configuration for parameters with a list of values:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
class ListConfig(BaseModel):
|
|
321
|
+
values: list[int | str]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### RangeConfig
|
|
325
|
+
|
|
326
|
+
Configuration for parameters with a range:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
class RangeConfig(BaseModel):
|
|
330
|
+
from_: int
|
|
331
|
+
to: int
|
|
332
|
+
step: int
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Type Aliases
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
ParametrizeIntegerConfigModels = RangeConfig | ListConfig | ValueConfig
|
|
339
|
+
ParametrizeStringConfigModels = ListConfig | ValueConfig
|
|
340
|
+
ParametrizeInteger = int | ParametrizeIntegerConfigModels
|
|
341
|
+
ParametrizeString = str | ParametrizeStringConfigModels
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Base Classes
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
class BaseTestCase(BaseModel, ABC):
|
|
348
|
+
test_name: str
|
|
349
|
+
|
|
350
|
+
class BaseTestConfig(BaseModel):
|
|
351
|
+
name: str
|
|
352
|
+
test_cases: list[BaseTestCase]
|
|
353
|
+
|
|
354
|
+
class BaseTestConfigCollection(BaseModel):
|
|
355
|
+
collection: list[BaseTestConfig]
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## API Reference
|
|
359
|
+
|
|
360
|
+
### `load_parametrize_args()`
|
|
361
|
+
|
|
362
|
+
Loads and parses a YAML configuration file and returns parametrize arguments.
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
def load_parametrize_args(
|
|
366
|
+
path_to_configs: Union[pathlib.Path, str],
|
|
367
|
+
config_collection_model: Type[TestConfigCollection],
|
|
368
|
+
collection_name: str,
|
|
369
|
+
) -> ParametrizeArgs:
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Parameters:**
|
|
373
|
+
|
|
374
|
+
| Parameter | Type | Description |
|
|
375
|
+
|-----------|------|-------------|
|
|
376
|
+
| `path_to_configs` | `pathlib.Path \| str` | Path to the YAML configuration file |
|
|
377
|
+
| `config_collection_model` | `Type[TestConfigCollection]` | Pydantic model class for parsing the configuration |
|
|
378
|
+
| `collection_name` | `str` | Name of the test collection to use from the configuration |
|
|
379
|
+
|
|
380
|
+
**Returns:** `ParametrizeArgs` object containing parametrize arguments
|
|
381
|
+
|
|
382
|
+
**Raises:** `ValueError` if no configuration is found for the given collection name
|
|
383
|
+
|
|
384
|
+
### `ParametrizeArgs`
|
|
385
|
+
|
|
386
|
+
Dataclass holding generated test parameters for pytest and unittest integrations:
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
@dataclass
|
|
390
|
+
class ParametrizeArgs:
|
|
391
|
+
argnames: str | None = None
|
|
392
|
+
argvalues: list[tuple] = field(default_factory=list)
|
|
393
|
+
ids: list[str] = field(default_factory=list)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Methods:**
|
|
397
|
+
|
|
398
|
+
| Method | Description |
|
|
399
|
+
|--------|-------------|
|
|
400
|
+
| `init_arg_names(model_cls)` | Initialize argument names from a Pydantic model |
|
|
401
|
+
| `add_params(arg_id, arg_values)` | Add a parameterized test case |
|
|
402
|
+
| `to_dict()` | Convert to dictionary for `metafunc.parametrize()` |
|
|
403
|
+
| `keys` | Property returning the tuple of argument keys |
|
|
404
|
+
| `keys_set` | Property returning the set of argument keys |
|
|
405
|
+
|
|
406
|
+
## Exported Symbols
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
__all__ = [
|
|
410
|
+
"BaseTestCase",
|
|
411
|
+
"BaseTestConfig",
|
|
412
|
+
"BaseTestConfigCollection",
|
|
413
|
+
"ListConfig",
|
|
414
|
+
"ParametrizeArgs",
|
|
415
|
+
"ParametrizeInteger",
|
|
416
|
+
"ParametrizeIntegerConfigModels",
|
|
417
|
+
"ParametrizeString",
|
|
418
|
+
"ParametrizeStringConfigModels",
|
|
419
|
+
"RangeConfig",
|
|
420
|
+
"ValueConfig",
|
|
421
|
+
"load_parametrize_args",
|
|
422
|
+
]
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Project Structure
|
|
426
|
+
|
|
427
|
+
```
|
|
428
|
+
yaml-test-params/
|
|
429
|
+
├── examples/
|
|
430
|
+
│ ├── collection.yaml
|
|
431
|
+
│ ├── models.py
|
|
432
|
+
│ ├── pytest_tests/
|
|
433
|
+
│ │ ├── conftest.py
|
|
434
|
+
│ │ └── test_examples.py
|
|
435
|
+
│ └── unittest_tests/
|
|
436
|
+
│ └── test_examples.py
|
|
437
|
+
├── tests/
|
|
438
|
+
│ ├── test_args_loader.py
|
|
439
|
+
│ ├── test_models.py
|
|
440
|
+
│ └── test_parametrize_args.py
|
|
441
|
+
├── yaml_test_params/
|
|
442
|
+
│ ├── __init__.py
|
|
443
|
+
│ ├── args_loader.py # YAML configuration loader
|
|
444
|
+
│ ├── models.py # Pydantic model definitions
|
|
445
|
+
│ └── parametrize_args.py # Parametrize arguments dataclass
|
|
446
|
+
├── CONTRIBUTING.md
|
|
447
|
+
├── LICENSE.txt
|
|
448
|
+
├── pyproject.toml
|
|
449
|
+
└── README.md
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## License
|
|
453
|
+
|
|
454
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
yaml_test_params/__init__.py,sha256=JDcCzCarO5AuGL2jXYda9WuCndJSq7zNZtsg899SLHQ,676
|
|
2
|
+
yaml_test_params/args_loader.py,sha256=G14mFL9OWMxMPMcjsgt2laEMAGatSF84-VLchkrwWZA,871
|
|
3
|
+
yaml_test_params/models.py,sha256=aoDPyzZ7FaoGZMppUs4b8JqD6YJXuzut5-MgeRmUqd4,6476
|
|
4
|
+
yaml_test_params/parametrize_args.py,sha256=HtbghZsa8tXkrZsp9oA87hnLGoBc1wdmTYd1Neyp64c,1714
|
|
5
|
+
yaml_test_params/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
6
|
+
yaml_test_params-0.1.0.dist-info/METADATA,sha256=K6aLCk32dDFq3EZzm195QJ6sooNq5ZKjsMUIOLKuBYs,12248
|
|
7
|
+
yaml_test_params-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
yaml_test_params-0.1.0.dist-info/licenses/LICENSE.txt,sha256=F-35ycqPcYW243OjxMHCggr_AHQgZLX2zvtZgobVEvo,1072
|
|
9
|
+
yaml_test_params-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aleksei Fomenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|