├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── parametrized.py ├── pyproject.toml └── tests ├── __init__.py └── test_all.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | - package-ecosystem: "pip" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.10', '3.11', '3.12', '3.13', '3.14.0-alpha - 3.14'] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - run: pip install pytest-cov 22 | - run: make check 23 | - run: coverage xml 24 | - uses: codecov/codecov-action@v5 25 | with: 26 | token: ${{ secrets.CODECOV_TOKEN }} 27 | 28 | lint: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-python@v5 33 | with: 34 | python-version: 3.x 35 | - run: pip install ruff 36 | - run: make lint 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: write-all 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: 3.x 17 | - run: pip install build 18 | - run: python -m build 19 | - uses: pypa/gh-action-pypi-publish@release/v1 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .coverage 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 5 | 6 | ## Unreleased 7 | 8 | ## [1.7](https://pypi.org/project/pytest-parametrized/1.7/) - 2024-12-21 9 | ### Changed 10 | * Python >=3.10 required 11 | * `zip` uses strict option 12 | 13 | ## [1.6](https://pypi.org/project/pytest-parametrized/1.6/) - 2024-10-21 14 | ### Changed 15 | * Python >=3.9 required 16 | * `pytest.param` supported 17 | 18 | ## [1.5](https://pypi.org/project/pytest-parametrized/1.5/) - 2023-11-03 19 | ### Changed 20 | * Python >=3.8 required 21 | 22 | ## [1.4](https://pypi.org/project/pytest-parametrized/1.4/) - 2022-09-13 23 | ### Changed 24 | * Python >=3.7 required 25 | 26 | ## [1.3](https://pypi.org/project/pytest-parametrized/1.3/) - 2020-10-18 27 | * Python >=3.6 required 28 | 29 | ## [1.2](https://pypi.org/project/pytest-parametrized/1.2/) - 2019-12-01 30 | * Namespace plugin removed 31 | 32 | ## [1.1](https://pypi.org/project/pytest-parametrized/1.1/) - 2019-01-08 33 | * pytest 4 compatibility 34 | 35 | ## [1.0](https://pypi.org/project/pytest-parametrized/1.0/) - 2018-12-08 36 | * `parametrized` keyword options 37 | 38 | ## [0.2](https://pypi.org/project/pytest-parametrized/0.2/) - 2017-12-12 39 | * `fixture` keyword options 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 Aric Coady 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | check: 2 | pytest -s --cov 3 | 4 | lint: 5 | ruff check . 6 | ruff format --check . 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![image](https://img.shields.io/pypi/v/pytest-parametrized.svg)](https://pypi.org/project/pytest-parametrized/) 2 | ![image](https://img.shields.io/pypi/pyversions/pytest-parametrized.svg) 3 | [![image](https://pepy.tech/badge/pytest-parametrized)](https://pepy.tech/project/pytest-parametrized) 4 | ![image](https://img.shields.io/pypi/status/pytest-parametrized.svg) 5 | [![build](https://github.com/coady/pytest-parametrized/actions/workflows/build.yml/badge.svg)](https://github.com/coady/pytest-parametrized/actions/workflows/build.yml) 6 | [![image](https://codecov.io/gh/coady/pytest-parametrized/branch/main/graph/badge.svg)](https://codecov.io/gh/coady/pytest-parametrized/) 7 | [![CodeQL](https://github.com/coady/pytest-parametrized/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/coady/pytest-parametrized/actions/workflows/github-code-scanning/codeql) 8 | [![image](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 9 | 10 | [Pytest](https://pytest.org/) decorator for parametrizing tests with default iterables, providing alternative syntax for [pytest.mark.parametrize](https://docs.pytest.org/en/latest/how-to/parametrize.html). 11 | 12 | # Usage 13 | Decorate tests with iterable default values. Other fixtures can still be used as normal. 14 | 15 | ## functions 16 | ```python 17 | from parametrized import parametrized 18 | 19 | @parametrized 20 | def test(..., name=values): 21 | """test single parametrized arg with each value""" 22 | 23 | @parametrized.zip 24 | def test(name=values, name1=values1, ...): 25 | """test parametrized args with zipped values""" 26 | 27 | @parametrized.product 28 | def test(name=values, name1=values1, ...): 29 | """test parametrized args with cartesian product of values""" 30 | ``` 31 | 32 | Zip before and after example: 33 | ```python 34 | @pytest.mark.parametrize("test_input,expected", [ 35 | ("3+5", 8), 36 | ("2+4", 6), 37 | ("6*9", 42), 38 | ]) 39 | def test_eval(test_input, expected): 40 | assert eval(test_input) == expected 41 | 42 | @parametrized.zip 43 | def test_eval(test_input=["3+5", "2+4", "6*9"], expected=[8, 6, 42]): 44 | assert eval(test_input) == expected 45 | ``` 46 | 47 | Product before and after example: 48 | ```python 49 | @pytest.mark.parametrize("x", [0, 1]) 50 | @pytest.mark.parametrize("y", [2, 3]) 51 | def test_foo(x, y): 52 | pass 53 | 54 | @parametrized.product 55 | def test_foo(x=[0, 1], y=[2, 3]): 56 | pass 57 | ``` 58 | 59 | `pytest.param` is supported for single values or `.product`. 60 | 61 | ## fixtures 62 | [Parametrized fixtures](https://docs.pytest.org/en/latest/how-to/fixtures.html#fixture-parametrize) which simply return their param. 63 | 64 | ```python 65 | fixture_name = parametrized.fixture(*params, **kwargs) 66 | ``` 67 | 68 | Before and after example: 69 | ```python 70 | @pytest.fixture(params=[0, 1], ids=["spam", "ham"]) 71 | def a(request): 72 | return request.param 73 | 74 | a = parametrized.fixture(0, 1, ids=["spam", "ham"]) 75 | ``` 76 | 77 | # Installation 78 | ```console 79 | % pip install pytest-parametrized 80 | ``` 81 | 82 | # Tests 83 | 100% branch coverage. 84 | 85 | ```console 86 | % pytest [--cov] 87 | ``` 88 | -------------------------------------------------------------------------------- /parametrized.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import itertools 3 | from functools import partial 4 | import pytest 5 | 6 | 7 | def parametrized(func, combine=None, **kwargs): 8 | """Decorate a function with combined parameters.""" 9 | argspec = inspect.getfullargspec(func) 10 | params = dict(zip(reversed(argspec.args), reversed(argspec.defaults))) 11 | func.__defaults__ = () # pytest ignores params with defaults 12 | if combine is None and len(params) > 1: 13 | raise ValueError("multiple keywords require combine function, e.g., zip") 14 | if combine not in (None, itertools.product): 15 | params = {','.join(params): combine(*params.values())} 16 | for param in params.items(): 17 | func = pytest.mark.parametrize(*param, **kwargs)(func) 18 | return func 19 | 20 | 21 | def fixture(*params, **kwargs): 22 | return pytest.fixture(params=params, **kwargs)(lambda request: request.param) 23 | 24 | 25 | parametrized.fixture = fixture 26 | parametrized.zip = partial(parametrized, combine=partial(zip, strict=True)) 27 | parametrized.product = partial(parametrized, combine=itertools.product) 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pytest-parametrized" 3 | version = "1.7" 4 | description = "Pytest decorator for parametrizing tests with default iterables." 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | license = {file = "LICENSE.txt"} 8 | authors = [{name = "Aric Coady", email = "aric.coady@gmail.com"}] 9 | keywords = ["pytest", "parametrize", "parameterize", "fixture"] 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Framework :: Pytest", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: Apache Software License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Programming Language :: Python :: 3.13", 21 | "Programming Language :: Python :: 3.14", 22 | "Topic :: Software Development :: Testing", 23 | ] 24 | dependencies = ["pytest"] 25 | 26 | [project.urls] 27 | Homepage = "https://github.com/coady/pytest-parametrized" 28 | Changelog = "https://github.com/coady/pytest-parametrized/blob/main/CHANGELOG.md" 29 | Issues = "https://github.com/coady/pytest-parametrized/issues" 30 | 31 | [tool.ruff] 32 | line-length = 100 33 | 34 | [tool.ruff.format] 35 | quote-style = "preserve" 36 | 37 | [tool.coverage.run] 38 | source = ["parametrized"] 39 | branch = true 40 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coady/pytest-parametrized/11a16040b11af753610d0ab8b6ccc0417818b16e/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_all.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from parametrized import parametrized 3 | 4 | data = parametrized.fixture('one', 'two') 5 | 6 | 7 | def test_options(): 8 | fixture = parametrized.fixture(name='override') 9 | assert fixture._pytestfixturefunction.name == 'override' 10 | assert parametrized(lambda x='': x, scope='module').kwargs == {'scope': 'module'} 11 | 12 | 13 | def test_fixture(data): 14 | assert data in ('one', 'two') 15 | 16 | 17 | @parametrized 18 | def test_single(name='abc'): 19 | assert name in set('abc') 20 | 21 | 22 | @parametrized.zip 23 | def test_zip(name='abc', value=range(3)): 24 | assert (value, name) in enumerate('abc') 25 | 26 | 27 | @parametrized.product 28 | def test_product(name='abc', value=range(3)): 29 | assert name in set('abc') and value in (0, 1, 2) 30 | 31 | 32 | def test_error(): 33 | with pytest.raises(ValueError): 34 | 35 | @parametrized 36 | def _(name=(), value=()): ... 37 | 38 | @parametrized.zip 39 | def strict(name='abc', value=()): ... 40 | 41 | with pytest.raises(ValueError): 42 | list(strict.pytestmark[0].args[1]) 43 | 44 | 45 | @parametrized.product 46 | def test_param(key=[0], value=[0, pytest.param(1, marks=pytest.mark.xfail())]): 47 | assert key == value 48 | --------------------------------------------------------------------------------