├── .bumpversion.cfg ├── .flake8 ├── .github └── workflows │ └── check.yml ├── .gitignore ├── CHANGES.rst ├── LICENSE ├── README.rst ├── pyproject.toml ├── pytest_xpara ├── __init__.py └── plugin.py ├── requirements └── dev.txt ├── setup.cfg ├── setup.py ├── tests ├── conftest.py ├── test_plugin.py └── test_version.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | files = setup.py pytest_xpara/__init__.py tests/test_version.py 3 | current_version = 0.3.0 4 | commit = True 5 | tag = True 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | # References: 2 | # - https://github.com/tox-dev/tox-gh 3 | # - https://github.com/coverallsapp/github-action 4 | 5 | name: check 6 | on: 7 | push: 8 | branches: 9 | - "master" 10 | - "release/**" 11 | tags: 12 | - "v*" 13 | pull_request: 14 | 15 | concurrency: 16 | group: check-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | test: 21 | name: test with ${{ matrix.py }} on ${{ matrix.os }} 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | py: 27 | - "3.12" 28 | - "3.11" 29 | - "3.10" 30 | - "3.9" 31 | - "3.8" 32 | os: 33 | - ubuntu-latest 34 | - macos-latest 35 | - windows-latest 36 | steps: 37 | - uses: actions/checkout@v3 38 | with: 39 | fetch-depth: 0 40 | - name: Setup python for test ${{ matrix.py }} 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: ${{ matrix.py }} 44 | - name: Install tox 45 | run: python -m pip install tox-gh>=1.2 coveralls 46 | - name: Setup test suite 47 | run: tox -vv --notest 48 | - name: Run test suite 49 | run: tox --skip-pkg-install 50 | - name: Coveralls Parallel 51 | uses: coverallsapp/github-action@v2 52 | with: 53 | flag-name: py-${{ join(matrix.*, '-') }} 54 | parallel: true 55 | if: ${{ matrix.os != 'macos-latest' }} 56 | finish: 57 | needs: test 58 | if: ${{ always() }} 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Coveralls Finished 62 | uses: coverallsapp/github-action@v2 63 | with: 64 | parallel-finished: true 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv/ 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # End of https://www.gitignore.io/api/python 97 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | 0.3.0 (Aug 07, 2024) 5 | -------------------- 6 | 7 | - Give warning message for a lack of markup parser 8 | - Update CI matrix for testing on Python 3.12 9 | 10 | 0.2.x (Unpublished) 11 | ------------------- 12 | 13 | - Fix incompatible behaviors on new release of pytest (Thanks for @Zheaoli) 14 | - Update CI matrix for testing on Python 3.10 (Thanks for @tssujt) 15 | 16 | 0.1.1 (Oct 30, 2017) 17 | -------------------- 18 | 19 | - Fix a crashing scene if there is only one parameter (Thanks for @mozillazg) 20 | - Strip whitespace characters between arguments 21 | 22 | 0.1.0 (Jan 3, 2017) 23 | ------------------- 24 | 25 | - First public release. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Jiangge Zhang 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Build Status| |Coverage Status| |PyPI Version| 2 | 3 | pytest-xpara 4 | ============ 5 | 6 | *pytest-xpara* is an extended parametrizing plugin of pytest. 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | :: 13 | 14 | pip install pytest-xpara 15 | 16 | 17 | Usage 18 | ----- 19 | 20 | :: 21 | 22 | py.test --xpara test_foo.py 23 | 24 | 25 | Example 26 | ------- 27 | 28 | .. code-block:: python 29 | 30 | # test_foo.py 31 | import pytest 32 | 33 | @pytest.mark.xparametrize 34 | def test_bar(lhs, rhs): 35 | assert lhs == -rhs 36 | 37 | .. code-block:: yaml 38 | 39 | # test_foo.yaml 40 | test_bar: 41 | args: lhs,rhs 42 | data: 43 | - lhs: 1 44 | rhs: -1 45 | - lhs: -1 46 | rhs: 1 47 | dataids: 48 | - left_to_right 49 | - right_to_left 50 | 51 | :: 52 | 53 | $ py.test -v --xpara test_foo.py 54 | ========================== test session starts =========================== 55 | platform darwin -- Python 2.7.12, pytest-3.0.5, py-1.4.32, pluggy-0.4.0 56 | cachedir: ../.cache 57 | rootdir: /Users/tonyseek/Sites/pytest-xpara, inifile: setup.cfg 58 | plugins: xpara-0.0.0, cov-2.4.0 59 | collecting ... collected 2 items 60 | 61 | test_foo.py::test_bar[left_to_right] PASSED 62 | test_foo.py::test_bar[right_to_left] PASSED 63 | 64 | ======================== 2 passed in 0.03 seconds ======================== 65 | 66 | 67 | Contributing 68 | ------------ 69 | 70 | If you want to report bugs or request features, please feel free to open issues 71 | or create pull requests on GitHub_. 72 | 73 | 74 | .. _GitHub: https://github.com/tonyseek/pytest-xpara/issues 75 | .. |Build Status| image:: https://img.shields.io/github/actions/workflow/status/tonyseek/pytest-xpara/check.yml?branch=master&style=flat 76 | :target: https://github.com/tonyseek/pytest-xpara/actions/workflows/check.yml 77 | :alt: Build Status 78 | .. |Coverage Status| image:: https://img.shields.io/coverallsCoverage/github/tonyseek/pytest-xpara?style=flat&branch=master 79 | :target: https://coveralls.io/github/tonyseek/pytest-xpara 80 | :alt: Coverage Status 81 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/pytest-xpara?style=flat 82 | :target: https://pypi.org/project/pytest-xpara/ 83 | :alt: PyPI Version 84 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 61.0"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /pytest_xpara/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.0' 2 | -------------------------------------------------------------------------------- /pytest_xpara/plugin.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import py.path 4 | 5 | 6 | def pytest_configure(config): 7 | config.addinivalue_line( 8 | "markers", "xparametrize: mark test to run only on named environment" 9 | ) 10 | 11 | 12 | def pytest_addoption(parser): 13 | group = parser.getgroup('xpara', 'extended parametrizing plugin') 14 | group.addoption('--xpara', action='store_true', default=False, 15 | help='Enable the extended parametrizing support. ' 16 | 'default: False') 17 | 18 | 19 | def pytest_generate_tests(metafunc): 20 | if not metafunc.config.option.xpara: 21 | return 22 | 23 | try: 24 | mark = getattr(metafunc.function, "xparametrize", None) 25 | if not mark: 26 | temp_mark = getattr(metafunc.function, "pytestmark", None) 27 | if temp_mark is not None: 28 | mark = next(iter(x for x in temp_mark if x.name == "xparametrize"), None) 29 | if not mark: 30 | return 31 | else: 32 | return 33 | except AttributeError: 34 | return 35 | else: 36 | xpara_data_name = ( 37 | mark.args[0] if mark.args else metafunc.function.__name__) 38 | 39 | xpara_data = _load_data(metafunc, loaders=[ 40 | _load_data_as_json, 41 | _load_data_as_yaml, 42 | _load_data_as_toml, 43 | ]) 44 | item = xpara_data.get(xpara_data_name) if xpara_data else None 45 | if item: 46 | item_args = [ 47 | arg.strip() for arg in item['args'].split(',') if arg.strip()] 48 | item_data = [ 49 | [data.get(arg) for arg in item_args] for data in item['data']] 50 | metafunc.parametrize(item_args, item_data, ids=item.get('dataids')) 51 | 52 | 53 | def _load_data(metafunc, loaders): 54 | data = getattr(metafunc.module, '__xpara_data__', None) 55 | if data is None: 56 | current_dir = py.path.local(metafunc.module.__file__).dirpath() 57 | file_name = metafunc.module.__name__.rsplit('.', 1)[-1] 58 | for loader in loaders: 59 | data = loader(current_dir, file_name) 60 | if data is not None: 61 | metafunc.module.__xpara_data__ = data 62 | break 63 | return data 64 | 65 | 66 | IMPORT_TIPS = ( 67 | 'The fixture data file ("{0.basename}") exists but cannot be loaded because' 68 | ' of a lack of {1} parser, which could be installed by "pip install ' 69 | '\'pytest-xpara[{2}]\'".') 70 | 71 | 72 | def _load_data_as_json(current_dir, file_name): 73 | try: 74 | import simplejson as json 75 | except ImportError: 76 | import json 77 | 78 | data_file = current_dir.join('%s.json' % file_name) 79 | if not data_file.exists(): 80 | return 81 | return json.loads(data_file.read()) 82 | 83 | 84 | def _load_data_as_yaml(current_dir, file_name): 85 | try: 86 | import yaml 87 | except ImportError: 88 | yaml = None 89 | 90 | for ext_name in ('yaml', 'yml'): 91 | data_file = current_dir.join('%s.%s' % (file_name, ext_name)) 92 | if not data_file.exists(): 93 | continue 94 | if yaml is None: 95 | warnings.warn(IMPORT_TIPS.format(data_file, 'YAML', 'yaml')) 96 | return 97 | return yaml.safe_load(data_file.read()) 98 | 99 | 100 | def _load_data_as_toml(current_dir, file_name): 101 | try: 102 | import toml 103 | except ImportError: 104 | toml = None 105 | 106 | data_file = current_dir.join('%s.toml' % file_name) 107 | if not data_file.exists(): 108 | return 109 | if toml is None: 110 | warnings.warn(IMPORT_TIPS.format(data_file, 'TOML', 'toml')) 111 | return 112 | return toml.loads(data_file.read()) 113 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | tox>=2.5.0 2 | bumpversion>=0.5.3 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [tool:pytest] 5 | testpaths = tests 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.rst') as readme: 4 | next(readme) 5 | long_description = ''.join(readme).strip() 6 | 7 | setup( 8 | name='pytest-xpara', 9 | version='0.3.0', 10 | description='An extended parametrizing plugin of pytest.', 11 | url='https://github.com/tonyseek/pytest-xpara', 12 | long_description=long_description, 13 | long_description_content_type='text/x-rst', 14 | keywords=['pytest', 'parametrize', 'yaml', 'toml'], 15 | author='Jiangge Zhang', 16 | author_email='tonyseek@gmail.com', 17 | license='MIT', 18 | packages=find_packages(), 19 | zip_safe=False, 20 | platforms=['any'], 21 | install_requires=[ 22 | 'pytest', 23 | ], 24 | extras_require={ 25 | 'yaml': ['PyYAML'], 26 | 'toml': ['toml'], 27 | }, 28 | entry_points={ 29 | 'pytest11': [ 30 | 'xpara = pytest_xpara.plugin', 31 | ], 32 | }, 33 | classifiers=[ 34 | 'Development Status :: 3 - Alpha', 35 | 'Environment :: Plugins', 36 | 'Intended Audience :: Developers', 37 | 'License :: OSI Approved :: MIT License', 38 | 'Operating System :: OS Independent', 39 | 'Programming Language :: Python :: 3.5', 40 | 'Programming Language :: Python :: 3.6', 41 | 'Programming Language :: Python :: 3.7', 42 | 'Programming Language :: Python :: 3.8', 43 | 'Programming Language :: Python :: 3.9', 44 | 'Topic :: Software Development :: Testing', 45 | 'Topic :: Software Development :: Libraries :: Python Modules', 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pytest 4 | 5 | pytest_plugins = 'pytester' 6 | 7 | # TODO hmm.. bootstrap in future 8 | parametrize_data = pytest.mark.parametrize('data_ext,data_content', [ 9 | ('.json', ''' 10 | { 11 | "test_foo": { 12 | "args": "foo,bar", 13 | "data": [ 14 | {"foo": 13, "bar": 15}, 15 | {"foo": 15, "bar": 16} 16 | ] 17 | }, 18 | "test_bar": { 19 | "args": "foo,bar", 20 | "data": [ 21 | {"foo": 13, "bar": 14}, 22 | {"foo": 15, "bar": 16} 23 | ] 24 | }, 25 | "test_one_parameter": { 26 | "args": "foo", 27 | "data": [ 28 | {"foo": 15}, 29 | {"foo": "16"} 30 | ] 31 | } 32 | } 33 | '''), 34 | ('.yaml', ''' 35 | test_foo: 36 | args: foo,bar 37 | data: 38 | - foo: 13 39 | bar: 15 40 | - foo: 15 41 | bar: 16 42 | 43 | test_bar: 44 | args: foo,bar 45 | data: 46 | - foo: 13 47 | bar: 14 48 | - foo: 15 49 | bar: 16 50 | 51 | test_one_parameter: 52 | args: foo 53 | data: 54 | - foo: 15 55 | - foo: "16" 56 | '''), 57 | ('.toml', ''' 58 | [test_foo] 59 | args = "foo,bar" 60 | [[test_foo.data]] 61 | foo = 13 62 | bar = 15 63 | [[test_foo.data]] 64 | foo = 15 65 | bar = 16 66 | 67 | [test_bar] 68 | args = "foo,bar" 69 | [[test_bar.data]] 70 | foo = 13 71 | bar = 14 72 | [[test_bar.data]] 73 | foo = 15 74 | bar = 16 75 | 76 | [test_one_parameter] 77 | args = "foo" 78 | [[test_one_parameter.data]] 79 | foo = 15 80 | [[test_one_parameter.data]] 81 | foo = "16" 82 | '''), 83 | ], ids=['json', 'yaml', 'toml']) 84 | 85 | 86 | @parametrize_data 87 | def test_run(testdir, data_ext, data_content): 88 | testdir.makefile(data_ext, test_foobar=data_content) 89 | testdir.makefile('.py', test_foobar=''' 90 | import pytest 91 | 92 | @pytest.mark.xparametrize 93 | def test_foo(foo, bar): 94 | assert foo + 2 == bar 95 | 96 | @pytest.mark.xparametrize 97 | def test_bar(foo, bar): 98 | assert foo + 1 == bar 99 | 100 | @pytest.mark.xparametrize 101 | def test_one_parameter(foo): 102 | assert isinstance(foo, int) 103 | 104 | def test_baz(): 105 | assert 1 + 1 == 2 106 | ''') 107 | testdir.makefile('.py', test_baz=''' 108 | def test_baz(): 109 | assert 1 + 1 == 2 110 | ''') 111 | result = testdir.runpytest_subprocess('--verbose', '--xpara') 112 | result.assert_outcomes(passed=6, skipped=0, failed=2) 113 | result.stdout.re_match_lines_random( 114 | [ 115 | r"^.*?test_foobar.py::test_foo\[13\-15\].*?PASSED.*?$", 116 | r"^.*?test_foobar.py::test_foo\[15\-16\].*?FAILED.*?$", 117 | r"^.*?test_foobar.py::test_bar\[13\-14\].*?PASSED.*?$", 118 | r"^.*?test_foobar.py::test_bar\[15\-16\].*?PASSED.*?$", 119 | r"^.*?test_foobar.py::test_one_parameter\[15\].*?PASSED.*?$", 120 | r"^.*?test_foobar.py::test_one_parameter\[16\].*?FAILED.*?$", 121 | r"^.*?test_foobar.py::test_baz.*?PASSED.*?$" 122 | ] 123 | ) 124 | 125 | 126 | @parametrize_data 127 | def test_run_with_custom_name(testdir, data_ext, data_content): 128 | testdir.makefile(data_ext, test_foobar=data_content) 129 | testdir.makefile('.py', test_foobar=''' 130 | import pytest 131 | 132 | @pytest.mark.xparametrize('test_bar') 133 | def test_boom(foo, bar): 134 | assert foo + 1 == bar 135 | ''') 136 | result = testdir.runpytest_subprocess('--xpara') 137 | result.assert_outcomes(passed=2, skipped=0, failed=0) 138 | 139 | 140 | def test_run_disabled(testdir): 141 | testdir.makefile('.py', test_foobar=''' 142 | import pytest 143 | 144 | @pytest.mark.xparametrize 145 | def test_foo(foo, bar): 146 | assert foo + 2 == bar 147 | ''') 148 | result = testdir.runpytest_subprocess('--verbose') 149 | result.stdout.re_match_lines_random(r"^.*?fixture 'foo' not found.*?$") 150 | 151 | 152 | @pytest.mark.parametrize('ext_name,pkg_fullname,warning_pattern', [ 153 | ('.yaml', 'yaml', r'.*YAML parser.+pytest-xpara\[yaml\].*'), 154 | ('.toml', 'toml', r'.*TOML parser.+pytest-xpara\[toml\].*'), 155 | ]) 156 | def test_run_without_parser(testdir, ext_name, pkg_fullname, warning_pattern): 157 | testdir.makefile(ext_name, test_foobar='') 158 | testdir.makefile('.py', test_foobar=''' 159 | import pytest 160 | 161 | @pytest.mark.xparametrize 162 | def test_boom(foo, bar): 163 | assert foo + 3 == bar 164 | ''') 165 | testdir.makefile('.py', conftest=''' 166 | import sys 167 | import pytest 168 | 169 | class MockMetaPathFinder: 170 | def find_spec(self, fullname, path, target=None): 171 | if fullname == '{0}': 172 | raise ImportError('mock') 173 | 174 | def pytest_configure(config): 175 | sys.meta_path.insert(0, MockMetaPathFinder()) 176 | '''.format(pkg_fullname)) 177 | result = testdir.runpytest_subprocess('--xpara') 178 | result.stdout.re_match_lines_random(warning_pattern) 179 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from pytest_xpara import __version__ as version 4 | 5 | 6 | def test_version(): 7 | assert version == '0.3.0' 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.5 3 | envlist = py38,py39,py310,py311,py312 4 | 5 | [gh] 6 | python = 7 | 3.12 = py312 8 | 3.11 = py311 9 | 3.10 = py310 10 | 3.9 = py39 11 | 3.8 = py38 12 | 13 | [testenv] 14 | deps = 15 | pytest 16 | pytest-cov 17 | flake8 18 | build 19 | twine 20 | extras = 21 | yaml 22 | toml 23 | commands = 24 | python -m build 25 | python -m twine check --strict dist/* 26 | flake8 27 | py.test --cov={envsitepackagesdir}/pytest_xpara --cov-append {posargs} 28 | 29 | [run] 30 | parallel = True 31 | --------------------------------------------------------------------------------