Repository: m-burst/flake8-pytest-style Branch: master Commit: dd7acd396fa2 Files: 98 Total size: 147.6 KB Directory structure: gitextract_db9ui74u/ ├── .bumpversion.cfg ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.md │ │ └── rule-request.md │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── THIRD-PARTY-LICENSES ├── docs/ │ └── rules/ │ ├── PT001.md │ ├── PT002.md │ ├── PT003.md │ ├── PT004.md │ ├── PT005.md │ ├── PT006.md │ ├── PT007.md │ ├── PT008.md │ ├── PT009.md │ ├── PT010.md │ ├── PT011.md │ ├── PT012.md │ ├── PT013.md │ ├── PT014.md │ ├── PT015.md │ ├── PT016.md │ ├── PT017.md │ ├── PT018.md │ ├── PT019.md │ ├── PT020.md │ ├── PT021.md │ ├── PT022.md │ ├── PT023.md │ ├── PT024.md │ ├── PT025.md │ ├── PT026.md │ ├── PT027.md │ ├── PT028.md │ ├── PT029.md │ ├── PT030.md │ └── PT031.md ├── flake8_pytest_style/ │ ├── __init__.py │ ├── config.py │ ├── errors.py │ ├── plugin.py │ ├── py.typed │ ├── utils.py │ └── visitors/ │ ├── __init__.py │ ├── assertion.py │ ├── fail.py │ ├── fixtures.py │ ├── imports.py │ ├── marks.py │ ├── parametrize.py │ ├── patch.py │ ├── raises.py │ ├── t_functions.py │ ├── try_except.py │ └── warns.py ├── pyproject.toml ├── scripts/ │ └── pypi_readme.py ├── setup.cfg └── tests/ ├── __init__.py ├── conftest.py ├── test_PT001_incorrect_fixture_parentheses_style.py ├── test_PT002_fixture_positional_args.py ├── test_PT003_extraneous_scope_function.py ├── test_PT004_missing_fixture_name_underscore.py ├── test_PT005_incorrect_fixture_name_underscore.py ├── test_PT006_parametrize_names_wrong_type.py ├── test_PT007_parametrize_values_wrong_type.py ├── test_PT008_patch_with_lambda.py ├── test_PT009_unittest_assertion.py ├── test_PT010_raises_without_exception.py ├── test_PT011_raises_too_broad.py ├── test_PT012_raises_with_multiple_statements.py ├── test_PT013_incorrect_pytest_import.py ├── test_PT014_duplicate_parametrize_test_cases.py ├── test_PT015_assert_always_false.py ├── test_PT016_fail_without_message.py ├── test_PT017_assert_in_except.py ├── test_PT018_composite_assertion.py ├── test_PT019_fixture_param_without_value.py ├── test_PT020_deprecated_yield_fixture.py ├── test_PT021_fixture_finalizer_callback.py ├── test_PT022_useless_yield_fixture.py ├── test_PT023_incorrect_mark_parentheses_style.py ├── test_PT024_unncessary_asyncio_mark_on_fixture.py ├── test_PT025_erroneous_usefixtures_on_fixture.py ├── test_PT026_usefixtures_without_parameters.py ├── test_PT027_unittest_raises_assertion.py ├── test_PT028_test_function_argument_with_default.py ├── test_PT029_warns_without_warning.py ├── test_PT030_warns_too_broad.py ├── test_PT031_warns_with_multiple_statements.py ├── test_config.py └── test_plugin.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bumpversion.cfg ================================================ [bumpversion] current_version = 2.2.0 commit = True tag = True [bumpversion:file:pyproject.toml] search = version = "{current_version}" replace = version = "{new_version}" [bumpversion:file:README.md] search = **Unreleased** replace = **Unreleased** ... **{new_version} - {now:%Y-%m-%d}** [bumpversion:file:flake8_pytest_style/plugin.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" ================================================ FILE: .github/ISSUE_TEMPLATE/bug.md ================================================ --- name: Bug about: Report a bug in the plugin title: When I have {{ SOMETHING EXPECTED }} in test, {{ AN UNEXPECTED THING }} happens labels: bug assignees: '' --- # Bug report ## What's wrong ## How it should work ## System information * Operating system: * Python version: * flake8 version: * flake8-pytest-style version: ================================================ FILE: .github/ISSUE_TEMPLATE/rule-request.md ================================================ --- name: Rule request about: Suggest a new rule for the plugin title: I want a rule that will {{ DO SOMETHING AWESOME }} labels: enhancement, rule request assignees: '' --- # Rule request ## Description ## Rationale ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily time: "02:00" open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [master] tags: ['*'] pull_request: jobs: ci: runs-on: ubuntu-latest strategy: matrix: python-version: - "3.10" - 3.11 - 3.12 - 3.13 - 3.14 steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - uses: snok/install-poetry@v1 with: version: 2.2.1 - name: Show environment info run: | python --version pip --version poetry --version poetry config --list - name: Setup dependencies cache uses: actions/cache@v4 with: path: ~/.cache/pypoetry/virtualenvs key: ${{ runner.os }}-${{ matrix.python-version }}-poetry-${{ hashFiles('poetry.lock') }} - name: Install dependencies run: | poetry install pip install codecov - name: Run lint run: make lint - name: Run tests run: make test - name: Upload coverage data run: codecov ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: workflow_dispatch: jobs: release: runs-on: ubuntu-latest strategy: matrix: python-version: - 3.14 steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - uses: snok/install-poetry@v1 with: version: 2.2.1 - name: Setup dependencies cache uses: actions/cache@v4 with: path: ~/.cache/pypoetry/virtualenvs key: ${{ runner.os }}-${{ matrix.python-version }}-poetry-${{ hashFiles('poetry.lock') }} - name: Install dependencies run: poetry install - name: Build distribution run: | poetry run ./scripts/pypi_readme.py poetry build - name: Publish to PyPI run: poetry publish --no-interaction env: POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ *.egg-info/ .installed.cfg *.egg MANIFEST README-pypi.md # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # IDE .idea/ .vscode/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Mikhail Burshteyn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: init test test-cov lint format CODE = flake8_pytest_style TEST = poetry run pytest --verbosity=2 --showlocals --strict-markers --cov=$(CODE) init: poetry install echo '#!/bin/sh\nmake lint test\n' > .git/hooks/pre-commit chmod +x .git/hooks/pre-commit test: $(TEST) -k "$(k)" test-cov: $(TEST) --cov-report=html lint: poetry run flake8 --jobs 4 --statistics --show-source $(CODE) tests scripts poetry run pylint --jobs 4 --rcfile=setup.cfg $(CODE) poetry run mypy $(CODE) tests scripts poetry run black --check $(CODE) tests scripts poetry run pytest --dead-fixtures --dup-fixtures format: poetry run isort $(CODE) tests scripts poetry run black $(CODE) tests scripts bump_major: poetry run bumpversion major bump_minor: poetry run bumpversion minor bump_patch: poetry run bumpversion patch ================================================ FILE: README.md ================================================ # flake8-pytest-style [![pypi](https://badge.fury.io/py/flake8-pytest-style.svg)](https://pypi.org/project/flake8-pytest-style) [![Python: 3.10+](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://pypi.org/project/flake8-pytest-style) [![Downloads](https://img.shields.io/pypi/dm/flake8-pytest-style.svg)](https://pypistats.org/packages/flake8-pytest-style) [![Build Status](https://github.com/m-burst/flake8-pytest-style/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/m-burst/flake8-pytest-style/actions/workflows/ci.yml) [![Code coverage](https://codecov.io/gh/m-burst/flake8-pytest-style/branch/master/graph/badge.svg)](https://codecov.io/gh/m-burst/flake8-pytest-style) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://en.wikipedia.org/wiki/MIT_License) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) ## Description A `flake8` plugin checking common style issues or inconsistencies with `pytest`-based tests. Currently the following errors are reported: | Code | Description | | ------- | ----------- | | [PT001] | use @pytest.fixture over @pytest.fixture()
(configurable by `pytest-fixture-no-parentheses`) | | [PT002] | configuration for fixture '{name}' specified via positional args, use kwargs | | [PT003] | scope='function' is implied in @pytest.fixture() | | [PT004] | fixture '{name}' does not return anything, add leading underscore | | [PT005] | fixture '{name}' returns a value, remove leading underscore | | [PT006] | wrong name(s) type in @pytest.mark.parametrize, expected {expected_type}
(configurable by `pytest-parametrize-names-type`) | | [PT007] | wrong values type in @pytest.mark.parametrize, expected {expected_type}
(configurable by `pytest-parametrize-values-type` and `pytest-parametrize-values-row-type`) | | [PT008] | use return_value= instead of patching with lambda | | [PT009] | use a regular assert instead of unittest-style '{assertion}' | | [PT010] | set the expected exception in pytest.raises() | | [PT011] | pytest.raises({exception}) is too broad, set the match parameter or use a more specific exception
(configurable by `pytest-raises-require-match-for`) | | [PT012] | pytest.raises() block should contain a single simple statement | | [PT013] | found incorrect import of pytest, use simple 'import pytest' instead | | [PT014] | found duplicate test cases {indexes} in @pytest.mark.parametrize | | [PT015] | assertion always fails, replace with pytest.fail() | | [PT016] | no message passed to pytest.fail() | | [PT017] | found assertion on exception {name} in except block, use pytest.raises() instead | | [PT018] | assertion should be broken down into multiple parts | | [PT019] | fixture {name} without value is injected as parameter, use @pytest.mark.usefixtures instead | | [PT020] | @pytest.yield_fixture is deprecated, use @pytest.fixture | | [PT021] | use yield instead of request.addfinalizer | | [PT022] | no teardown in fixture {name}, use return instead of yield | | [PT023] | use @pytest.mark.foo over @pytest.mark.foo()
(configurable by `pytest-mark-no-parentheses`) | | [PT024] | pytest.mark.asyncio is unnecessary for fixtures | | [PT025] | pytest.mark.usefixtures has no effect on fixtures | | [PT026] | useless pytest.mark.usefixtures without parameters | | [PT027] | use pytest.raises() instead of unittest-style '{assertion}' | | [PT028] | test function {name} has default value for argument {arg}, remove it | | [PT029] | set the expected warning in pytest.warns() | | [PT030] | pytest.warns({warning}) is too broad, set the match parameter or use a more specific warning
(configurable by `pytest-warns-require-match-for`) | | [PT031] | pytest.warns() block should contain a single simple statement | ## Installation pip install flake8-pytest-style ## Configuration The plugin has the following configuration options: * `pytest-fixture-no-parentheses` — see [PT001] * `pytest-parametrize-names-type` — see [PT006] * `pytest-parametrize-values-type` — see [PT007] * `pytest-parametrize-values-row-type` — see [PT007] * `pytest-raises-require-match-for` — see [PT011] * `pytest-mark-no-parentheses` — see [PT023] * `pytest-warns-require-match-for` — see [PT030] ## For developers ### Install deps and setup pre-commit hook make init ### Run linters, autoformat, tests etc. make format lint test ### Bump new version make bump_major make bump_minor make bump_patch ## License MIT ## Change Log **Unreleased** ... **2.2.0 - 2025-10-20** * require at least Python 3.10 * support Python 3.14 **2.1.0 - 2025-01-10** * support `reason=` kwarg in `pytest.fail` for [PT016] * add [PT028] (checks for default values in test functions) * add [PT029] (checks for `pytest.warns` without expected warning) * add [PT030] (checks for too broad `pytest.warns` clauses) * add [PT031] (checks for multiple statements in `pytest.warns` blocks) * require at least Python 3.9 * support Python 3.13 **2.0.0 - 2024-04-01** * **BREAKING:** invert default values for `pytest-fixture-no-parentheses` and `pytest-mark-no-parentheses` to conform with `pytest` official style * If you get a lot of [PT001] or [PT023] violations after upgrading, consider setting explicit values for these configuration options * require at least Python 3.8.1 * support Python 3.12 **1.7.2 - 2023-02-15** * fix false positive for [PT009] on `pytest.fail` **1.7.1 - 2023-02-15** * update list of unittest-style assert methods for [PT009]/[PT027] **1.7.0 - 2023-02-09** * require at least Python 3.7.2 * support Python 3.11 * add [PT027] (checks for unittest-style `assertRaises`) **1.6.0 - 2021-12-23** * require at least Python 3.6.2 * expose `py.typed` file **1.5.1 - 2021-11-05** * better wording for [PT011] * support Python 3.10 **1.5.0 - 2021-06-18** * add [PT025] (checks for erroneous `pytest.mark.usefixtures` on fixtures) * add [PT026] (checks for `pytest.mark.usefixtures` without parameters) **1.4.4 - 2021-06-17** * fix [PT023] not checking marks in classes * fix [PT004] incorrectly firing on fixtures with `yield from` **1.4.2 - 2021-05-24** * update `flake8-plugin-utils` version to improve stability **1.4.1 - 2021-04-01** * fix argparse-related warnings **1.4.0 - 2021-03-14** * add [PT023] (checks for parentheses consistency in `pytest.mark` usage) * add [PT024] (checks for unnecessary `pytest.mark.asyncio` on fixtures) * fix [PT004], [PT005] firing on abstract fixtures * fix [PT012] firing on `with` statements containing a single `pass` **1.3.0 - 2020-08-30** * add [PT022] (checks for `yield` fixtures without teardown) **1.2.3 - 2020-08-06** * update `flake8-plugin-utils` dependency to fix encoding problems on Windows **1.2.2 - 2020-07-23** * fix [PT004]/[PT005] inspecting returns of nested functions **1.2.1 - 2020-06-15** * fix [PT021] for factory fixtures (#46) **1.2.0 - 2020-06-12** * support scoped `mocker` fixtures from `pytest-mock` for [PT008] * check for positional-only lambda arguments in [PT008] * add [PT020] (checks for `pytest.yield_fixture`) * add [PT021] (checks for `request.addfinalizer`) * add documentation pages for all rules **1.1.1 - 2020-04-17** * fix [PT011] not reporting `match=''` as a violation **1.1.0 - 2020-04-14** * add [PT015] (checks for `assert False`) * add [PT016] (checks for `pytest.fail()` without message) * add [PT017] (checks for assertions on exceptions in `except` blocks) * add [PT018] (checks for composite assertions) * add [PT019] (checks for fixtures without value injected as parameters) **1.0.0 - 2020-03-26** * add [PT014] (checks for duplicate test cases in `@pytest.mark.parametrize`) **0.6.0 - 2020-03-21** * add configuration option `pytest-parametrize-names-type` for [PT006] * add configuration options `pytest-parametrize-values-type` and `pytest-parametrize-values-row-type` for [PT007] **0.5.0 - 2020-03-09** * add configuration option `pytest-fixture-no-parentheses` for [PT001] * add [PT013] (checks for `from`-imports from `pytest`) **0.4.0 - 2020-03-09** * add [PT012] (checks for multiple statements in `with pytest.raises()`) **0.3.1 - 2020-03-09** * fix default value of `pytest-raises-require-match-for` config option **0.3.0 - 2020-03-09** * add [PT010] and [PT011] (checks for `pytest.raises` parameters) **0.2.0 - 2020-03-01** * add [PT009] (ported from [flake8-pytest](https://github.com/vikingco/flake8-pytest)) **0.1.3 - 2019-05-24** * add `yield` fixtures support * fix changelog entry for 0.1.2 **0.1.2 - 2019-05-23** * fix parametrize checkers not working in decorators **0.1.1 - 2019-05-23** * update PyPI description **0.1.0 - 2019-05-23** * initial [PT001]: /docs/rules/PT001.md [PT002]: /docs/rules/PT002.md [PT003]: /docs/rules/PT003.md [PT004]: /docs/rules/PT004.md [PT005]: /docs/rules/PT005.md [PT006]: /docs/rules/PT006.md [PT007]: /docs/rules/PT007.md [PT008]: /docs/rules/PT008.md [PT009]: /docs/rules/PT009.md [PT010]: /docs/rules/PT010.md [PT011]: /docs/rules/PT011.md [PT012]: /docs/rules/PT012.md [PT013]: /docs/rules/PT013.md [PT014]: /docs/rules/PT014.md [PT015]: /docs/rules/PT015.md [PT016]: /docs/rules/PT016.md [PT017]: /docs/rules/PT017.md [PT018]: /docs/rules/PT018.md [PT019]: /docs/rules/PT019.md [PT020]: /docs/rules/PT020.md [PT021]: /docs/rules/PT021.md [PT022]: /docs/rules/PT022.md [PT023]: /docs/rules/PT023.md [PT024]: /docs/rules/PT024.md [PT025]: /docs/rules/PT025.md [PT026]: /docs/rules/PT026.md [PT027]: /docs/rules/PT027.md [PT028]: /docs/rules/PT028.md [PT029]: /docs/rules/PT029.md [PT030]: /docs/rules/PT030.md [PT031]: /docs/rules/PT031.md ================================================ FILE: THIRD-PARTY-LICENSES ================================================ ******************************************************************************* https://github.com/m-burst/cookiecutter-pypackage-poetry ******************************************************************************* MIT License Copyright (c) 2019 Afonasev Evgeniy Copyright (c) 2019 Mikhail Burshteyn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: docs/rules/PT001.md ================================================ # PT001 `use @pytest.fixture over @pytest.fixture()` ## Configuration * `pytest-fixture-no-parentheses` Boolean flag specifying whether `@pytest.fixture()` without parameters should have parentheses. If the option is set to true (the default), `@pytest.fixture` is valid and `@pytest.fixture()` is an error. If set to false, `@pytest.fixture()` is valid and `@pytest.fixture` is an error. ## Examples Bad code (assuming `pytest-fixture-no-parentheses` set to true): ```python import pytest @pytest.fixture() def my_fixture(): ... ``` Good code: ```python import pytest @pytest.fixture def my_fixture(): ... ``` ## Rationale * to enforce consistency between all fixtures in a codebase ================================================ FILE: docs/rules/PT002.md ================================================ # PT002 `configuration for fixture '{name}' specified via positional args, use kwargs` ## Examples Bad code: ```python import pytest @pytest.fixture('module') def my_fixture(): ... ``` Good code: ```python import pytest @pytest.fixture(scope='module') def my_fixture(): ... ``` ## Rationale * to make parameters meaning more obvious * to enforce consistency between all fixtures in a codebase ================================================ FILE: docs/rules/PT003.md ================================================ # PT003 `scope='function' is implied in @pytest.fixture()` Fixtures with function scope should not specify scope explicitly because function scope is implied by default. ## Examples Bad code: ```python import pytest @pytest.fixture(scope='function') def my_fixture(): ... ``` Good code: ```python import pytest @pytest.fixture() def my_fixture(): ... ``` ## Rationale * to enforce consistency between all fixtures in a codebase ================================================ FILE: docs/rules/PT004.md ================================================ # PT004 `fixture '{name}' does not return anything, add leading underscore` This rule does not fire on abstract fixtures (those decorated with `@abc.abstractmethod`), so that only the actual fixture implementations will be checked. This rule also does not fire on fixtures containing `yield from`, because there is no reasonable way to determine whether the generator yields a value. ## Examples Bad code: ```python import pytest @pytest.fixture() def patch_something(mocker): mocker.patch('module.object') @pytest.fixture() def use_context(): with create_context(): yield ``` Good code: ```python import pytest @pytest.fixture() def _patch_something(mocker): mocker.patch('module.object') @pytest.fixture() def _use_context(): with create_context(): yield ``` ## Rationale * to enforce a naming convention for fixtures: fixtures that don't return a value start with a leading underscore (e.g. `_patch_something`), fixtures that return a value don't start with a leading underscore (e.g. `some_object`) See also [PT005](PT005.md). ================================================ FILE: docs/rules/PT005.md ================================================ # PT005 `fixture '{name}' returns a value, remove leading underscore` This rule does not fire on abstract fixtures (those decorated with `@abc.abstractmethod`), so that only the actual fixture implementations will be checked. ## Examples Bad code: ```python import pytest @pytest.fixture() def _some_object(): return SomeClass() @pytest.fixture() def _some_object_with_cleanup(): obj = SomeClass() yield obj obj.cleanup() ``` Good code: ```python import pytest @pytest.fixture() def some_object(): return SomeClass() @pytest.fixture() def some_object_with_cleanup(): obj = SomeClass() yield obj obj.cleanup() ``` ## Rationale * to enforce a naming convention for fixtures: fixtures that don't return a value start with a leading underscore (e.g. `_patch_something`), fixtures that return a value don't start with a leading underscore (e.g. `some_object`) See also [PT004](PT004.md). ================================================ FILE: docs/rules/PT006.md ================================================ # PT006 `wrong name(s) type in @pytest.mark.parametrize, expected {expected_type}` For a single name the expected type is always a plain string. For multiple names the expected type is controlled by the configuration variable `pytest-parametrize-names-type`. ## Configuration * `pytest-parametrize-names-type` Expected type for multiple argument names in `@pytest.mark.parametrize`. The following values are supported: * `csv` — a comma-separated list, e.g. `@pytest.mark.parametrize('name1,name2', ...)` * `tuple` (default) — e.g. `@pytest.mark.parametrize(('name1', 'name2'), ...)` * `list` — e.g. `@pytest.mark.parametrize(['name1', 'name2'], ...)` ## Examples Bad code (assuming `pytest-parametrize-names-type` is set to `tuple`): ```python import pytest # single parameter, always expecting string @pytest.mark.parametrize(('param', ), [1, 2, 3]) def test_foo(param): ... # multiple parameters, expecting tuple due to settings @pytest.mark.parametrize(['param1', 'param2'], [(1, 2), (3, 4)]) def test_bar(param1, param2): ... # multiple parameters, expecting tuple due to settings @pytest.mark.parametrize('param1,param2', [(1, 2), (3, 4)]) def test_baz(param1, param2): ... ``` Good code: ```python import pytest @pytest.mark.parametrize('param', [1, 2, 3]) def test_foo(param): ... @pytest.mark.parametrize(('param1', 'param2'), [(1, 2), (3, 4)]) def test_bar(param1, param2): ... ``` ## Rationale * to enforce consistency between all tests in a codebase ================================================ FILE: docs/rules/PT007.md ================================================ # PT007 `wrong values type in @pytest.mark.parametrize, expected {expected_type}` The expected type of the list of rows is controlled by the configuration variable `pytest-parametrize-values-type`. The expected type of each row in case of multiple arguments is controlled by the configuration variable `pytest-parametrize-values-row-type`. ## Configuration * `pytest-parametrize-values-type` Expected type for the list of values rows in `@pytest.mark.parametrize`. The following values are supported: * `tuple` — e.g. `@pytest.mark.parametrize('name', (1, 2, 3))` * `list` (default) — e.g. `@pytest.mark.parametrize('name', [1, 2, 3])` * `pytest-parametrize-values-row-type` Expected type for each row of values in `@pytest.mark.parametrize` in case of multiple parameters. The following values are supported: * `tuple` (default) — e.g. `@pytest.mark.parametrize(('name1', 'name2'), [(1, 2), (3, 4)])` * `list` — e.g. `@pytest.mark.parametrize(('name1', 'name2'), [[1, 2], [3, 4]])` ## Examples Bad code (assuming `pytest-parametrize-values-type` set to `list` and `pytest-parametrize-values-row-type` set to `tuple`): ```python import pytest # expected list, got tuple @pytest.mark.parametrize('param', (1, 2)) def test_foo(param): ... # expected top-level list, got tuple @pytest.mark.parametrize( ('param1', 'param2'), ( (1, 2), (3, 4), ), ) def test_bar(param1, param2): ... # expected individual rows to be tuples, got lists @pytest.mark.parametrize( ('param1', 'param2'), [ [1, 2], [3, 4], ], ) def test_baz(param1, param2): ... ``` ```python import pytest @pytest.mark.parametrize('param', [1, 2]) def test_foo(param): ... @pytest.mark.parametrize( ('param1', 'param2'), [ (1, 2), (3, 4), ], ) def test_bar(param1, param2): ... ``` ## Rationale * to enforce consistency between all tests in a codebase ================================================ FILE: docs/rules/PT008.md ================================================ # PT008 `use return_value= instead of patching with lambda` This checks calls to `unittest.mock.patch` from standard library, as well as `mocker`, `session_mocker` etc. fixtures from [`pytest-mock`](https://pypi.org/project/pytest-mock/) library. e.g. `mocker.patch('target', return_value=7)` is OK, and `mocker.patch('target', lambda *args: 7)` is an error ## Examples Bad code: ```python def test_foo(mocker): mocker.patch('module.target', lambda x, y: 7) ``` Good code: ```python def test_foo(mocker): mocker.patch('module.target', return_value=7) # if lambda parameters are used, it's not a violation mocker.patch('module.other_target', lambda x, y: x) ``` ## Rationale * this style of patching allows to use all mock functionality, such as checking the list of calls to the patched function: ```python def test_foo(mocker): mock_target = mocker.patch('module.target', return_value=7) do_something() mock_target.assert_called_once() ``` ================================================ FILE: docs/rules/PT009.md ================================================ # PT009 `use a regular assert instead of unittest-style '{assertion}'` ## Examples Bad code: ```python import unittest class TestFoo(unittest.TestCase): def test_foo(self): self.assertEqual(a, b) ``` Good code: ```python import unittest class TestFoo(unittest.TestCase): def test_foo(self): assert a == b ``` ## Rationale * to enforce the assertion style recommended by pytest * to make use of pytest's assertion rewriting ================================================ FILE: docs/rules/PT010.md ================================================ # PT010 `set the expected exception in pytest.raises()` ## Examples Bad code: ```python import pytest def test_foo(): with pytest.raises(): do_something() ``` Good code: ```python import pytest def test_foo(): with pytest.raises(SomeException): do_something() ``` ## Rationale * not passing the exception class will fail at runtime ================================================ FILE: docs/rules/PT011.md ================================================ # PT011 `pytest.raises({exception}) is too broad, set the match parameter or use a more specific exception` ## Configuration * `pytest-raises-require-match-for` Comma-separated list of exception names that require a `match=` parameter in a `pytest.raises()` call. By default the list contains the following exceptions: * `BaseException`, `Exception` * `ValueError` * `OSError`, `IOError`, `EnvironmentError`, `socket.error` ## Examples Bad code: ```python import pytest def test_foo(): with pytest.raises(ValueError): ... # empty string is also an error with pytest.raises(ValueError, match=''): ... ``` Good code: ```python import pytest def test_foo(): with pytest.raises(ValueError, match='expected message'): ... ``` ## Rationale * to help ensure that the `pytest.raises` clause is not too broad ================================================ FILE: docs/rules/PT012.md ================================================ # PT012 `pytest.raises() block should contain a single simple statement` This forbids multiple statements and control flow structures within `pytest.raises()` blocks. ## Examples Bad code: ```python import pytest def test_foo(): with pytest.raises(MyException): # if get_object() raises MyException, the test is incorrect obj = get_object() obj.do_something() with pytest.raises(MyException): if some_condition: do_something() ``` Good code: ```python import pytest def test_foo(): obj = get_object() with pytest.raises(MyException): obj.do_something() ``` An empty `with`-statement (containing only a `pass` inside) is allowed in order to allow testing of exceptions raised by context manager enter/exit methods. ```python import pytest def test_my_context_manager(): context_manager = get_context_manager() with pytest.raises(MyException): with context_manager: pass ``` ## Rationale * to help ensure that only the expected exception from code under test is caught in a `pytest.raises` block (e.g. not an exception from initialization/finalization code) ================================================ FILE: docs/rules/PT013.md ================================================ # PT013 `found incorrect import of pytest, use simple 'import pytest' instead` ## Examples Bad code: ```python import pytest as pt from pytest import fixture ``` Good code: ```python import pytest ``` ## Rationale * to enforce consistency * to help the linter check usage of members imported from `pytest` ================================================ FILE: docs/rules/PT014.md ================================================ # PT014 `found duplicate test cases {indexes} in @pytest.mark.parametrize` ## Examples Bad code: ```python import pytest @pytest.mark.parametrize( ('param1', 'param2'), [ (1, 2), (1, 2), ] ) def test_foo(param1, param2): ... ``` ## Rationale * to help avoid duplicate test cases and accidental mistakes ================================================ FILE: docs/rules/PT015.md ================================================ # PT015 `assertion always fails, replace with pytest.fail()` ## Examples Bad code: ```python def test_foo(): if some_condition: assert False, 'some_condition was True' ``` Good code: ```python import pytest def test_foo(): if some_condition: pytest.fail('some_condition was True') ``` ## Rationale * to enforce consistency across all tests in a codebase * to improve readability of test code and error messages ================================================ FILE: docs/rules/PT016.md ================================================ # PT016 `no message passed to pytest.fail()` The function `pytest.fail` must be called either with a positional argument, a keyword argument `reason=` (since pytest version 7.0.0), or a keyword argument `msg=` (for compatibility with older versions of pytest). Passing a keyword argument under a wrong name will also be reported as a violation. ## Examples Bad code: ```python import pytest def test_foo(): pytest.fail() def test_bar(): pytest.fail('') def test_baz(): pytest.fail(message='...') # wrong kwarg name ``` Good code: ```python import pytest def test_foo(): pytest.fail('...') def test_bar(): pytest.fail(reason='...') def test_baz(): pytest.fail(msg='...') ``` ## Rationale * to make it easier to understand and debug test failures ================================================ FILE: docs/rules/PT017.md ================================================ # PT017 `found assertion on exception {name} in except block, use pytest.raises() instead` ## Examples Bad code: ```python def test_foo(): try: 1 / 0 except ZeroDivisionError as e: assert e.args ``` Good code: ```python import pytest def test_foo(): with pytest.raises(ZeroDivisionError) as e: 1 / 0 assert e.value.args ``` ## Rationale * to avoid the situations when the test incorrectly passes because exception was not raised ================================================ FILE: docs/rules/PT018.md ================================================ # PT018 `assertion should be broken down into multiple parts` This violation is reported when the plugin encounter an assertion on multiple conditions. ## Examples Bad code: ```python def test_foo(): assert something and something_else def test_bar(): assert not (something or something_else) ``` Good code: ```python def test_foo(): assert something assert something_else def test_bar(): assert not something assert not something_else ``` ## Rationale * to make it easier to understand and debug test failures ================================================ FILE: docs/rules/PT019.md ================================================ # PT019 `fixture {name} without value is injected as parameter, use @pytest.mark.usefixtures instead` ## Examples Bad code: ```python def test_foo(_patch_something): ... ``` Good code: ```python import pytest @pytest.mark.usefixtures('_patch_something') def test_foo(): ... ``` ## Rationale * to avoid unused parameters in test functions ================================================ FILE: docs/rules/PT020.md ================================================ # PT020 `@pytest.yield_fixture is deprecated, use @pytest.fixture` ## Examples Bad code: ```python import pytest @pytest.yield_fixture() def my_fixture(): obj = SomeClass() yield obj obj.cleanup() ``` Good code: ```python import pytest @pytest.fixture() def my_fixture(): obj = SomeClass() yield obj obj.cleanup() ``` ## Rationale * to avoid using the deprecated function ================================================ FILE: docs/rules/PT021.md ================================================ # PT021 `use yield instead of request.addfinalizer` `pytest` offers two ways to perform cleanup in fixture code. One is sequential (`yield` statement), and the other is callback-based (`request.addfinalizer`). `request.addfinalizer` is allowed when implementing [Factories as fixtures] pattern, see examples below for more details. ## Examples Bad code: ```python import pytest @pytest.fixture() def my_fixture(request): resource = acquire_resource() request.addfinalizer(resource.release) return resource ``` Good code: ```python import pytest @pytest.fixture() def my_fixture(): resource = acquire_resource() yield resource resource.release() # "Factories as fixtures" pattern @pytest.fixture() def my_factory(request): def create_resource(arg): resource = acquire_resource(arg) request.addfinalizer(resource.release) return resource return create_resource ``` ## Rationale * to make fixture code more linear and straightforward [Factories as fixtures]: https://docs.pytest.org/en/stable/fixture.html#factories-as-fixtures ================================================ FILE: docs/rules/PT022.md ================================================ # PT022 `no teardown in fixture {name}, use return instead of yield` `yield` in fixtures is only useful and semantically correct when the fixture contains some teardown code. ## Examples Bad code: ```python import pytest @pytest.fixture() def my_fixture(): resource = acquire_resource() yield resource ``` Good code: ```python import pytest @pytest.fixture() def my_fixture_with_teardown(): resource = acquire_resource() yield resource resource.release() @pytest.fixture() def my_fixture_without_teardown(): resource = acquire_resource() return resource ``` ## Rationale * to make sure that all `yield` usages are semanticaly correct ================================================ FILE: docs/rules/PT023.md ================================================ # PT023 `use @pytest.mark.foo over @pytest.mark.foo()` ## Configuration * `pytest-mark-no-parentheses` Boolean flag specifying whether `@pytest.mark.foo()` without parameters should have parentheses. If the option is set to true (the default), `@pytest.mark.foo` is valid and `@pytest.mark.foo()` is an error. If set to false, `@pytest.mark.foo()` is valid and `@pytest.mark.foo` is an error. ## Examples Bad code (assuming `pytest-mark-no-parentheses` set to true): ```python import pytest @pytest.mark.foo() def test_something(): ... ``` Good code: ```python import pytest @pytest.mark.foo def test_something(): ... ``` ## Rationale * to enforce consistency between all tests in a codebase ================================================ FILE: docs/rules/PT024.md ================================================ # PT024 `pytest.mark.asyncio is unnecessary for fixtures` ## Examples Bad code: ```python import pytest @pytest.mark.asyncio() @pytest.fixture() async def my_fixture(): return 0 ``` Good code: ```python import pytest @pytest.fixture() async def my_fixture(): return 0 ``` ## Rationale * the mark is useless on fixtures and therefore unnecessary ================================================ FILE: docs/rules/PT025.md ================================================ # PT025 `pytest.mark.usefixtures has no effect on fixtures` ## Examples Bad code: ```python import pytest @pytest.fixture() def a(): pass @pytest.mark.usefixtures('a') @pytest.fixture() def b(): pass ``` Good code: ```python import pytest @pytest.fixture() def a(): pass @pytest.fixture() def b(a): pass ``` ## Rationale * according to the pytest [docs](https://docs.pytest.org/en/6.2.x/reference.html#pytest-mark-usefixtures) on `pytest.mark.usefixtures`: > Also note that this mark has no effect when applied to fixtures. * pytest does not raise any error or warning when fixtures are decorated with `pytest.mark.usefixtures`, which can lead to incorrect results and broken tests ================================================ FILE: docs/rules/PT026.md ================================================ # PT023 `useless pytest.mark.usefixtures without parameters` ## Examples Bad code: ```python import pytest @pytest.mark.usefixtures() def test_something(): ... @pytest.mark.usefixtures def test_something_else(): ... ``` Good code: ```python import pytest @pytest.mark.usefixtures('a') def test_something(): ... ``` ## Rationale * such mark has no effect and is unnecessary ================================================ FILE: docs/rules/PT027.md ================================================ # PT027 `use pytest.raises() instead of unittest-style '{assertion}'` ## Examples Bad code: ```python import unittest class TestFoo(unittest.TestCase): def test_foo(self): with self.assertRaises(ValueError): raise ValueError('foo') ``` Good code: ```python import pytest import unittest class TestFoo(unittest.TestCase): def test_foo(self): with pytest.raises(ValueError): raise ValueError('foo') ``` ## Rationale * to enforce the assertion style recommended by pytest ================================================ FILE: docs/rules/PT028.md ================================================ # PT028 `test function {name} has default value for argument {arg}, remove it` ## Examples Bad code: ```python def test_foo(bar=42): pass ``` Good code: ```python def test_foo(bar): pass ``` ## Rationale * even if the corresponding fixture is defined, current behavior of pytest is to use the default value instead of injecting the fixture * see original pytest issue: https://github.com/pytest-dev/pytest#12693 ================================================ FILE: docs/rules/PT029.md ================================================ # PT029 `set the expected warning in pytest.warns()` ## Examples Bad code: ```python import pytest def test_foo(): with pytest.warns(): do_something() ``` Good code: ```python import pytest def test_foo(): with pytest.warns(SomeWarning): do_something() ``` ## Rationale * not passing the warning class will fail at runtime ================================================ FILE: docs/rules/PT030.md ================================================ # PT030 `pytest.warns({warning}) is too broad, set the match parameter or use a more specific warning` ## Configuration * `pytest-warns-require-match-for` Comma-separated list of warning names that require a `match=` parameter in a `pytest.warning()` call. By default the list contains the following warnings: * `Warning` * `UserWarning` * `DeprecationWarning` ## Examples Bad code: ```python import pytest def test_foo(): with pytest.warns(UserWarning): ... # empty string is also an error with pytest.warns(UserWarning, match=''): ... ``` Good code: ```python import pytest def test_foo(): with pytest.warns(UserWarning, match='expected message'): ... ``` ## Rationale * to help ensure that the `pytest.warns` clause is not too broad ================================================ FILE: docs/rules/PT031.md ================================================ # PT031 `pytest.warns() block should contain a single simple statement` This forbids multiple statements and control flow structures within `pytest.warns()` blocks. ## Examples Bad code: ```python import pytest def test_foo(): with pytest.warns(MyWarning): # if get_object() raises MyWarning, the test is incorrect obj = get_object() obj.do_something() with pytest.warns(MyWarning): if some_condition: do_something() ``` Good code: ```python import pytest def test_foo(): obj = get_object() with pytest.warns(MyWarning): obj.do_something() ``` An empty `with`-statement (containing only a `pass` inside) is allowed in order to allow testing of warnings raised by context manager enter/exit methods. ```python import pytest def test_my_context_manager(): context_manager = get_context_manager() with pytest.warns(MyWarning): with context_manager: pass ``` ## Rationale * to help ensure that only the expected warning from code under test is caught in a `pytest.warns` block (e.g. not an warning from initialization/finalization code) ================================================ FILE: flake8_pytest_style/__init__.py ================================================ ================================================ FILE: flake8_pytest_style/config.py ================================================ from enum import Enum from typing import Any, List, NamedTuple, Type def enum_choices(enum: Type[Enum]) -> List[Any]: return [member.value for member in enum] class ParametrizeNamesType(Enum): CSV = "csv" TUPLE = "tuple" LIST = "list" class ParametrizeValuesType(Enum): TUPLE = "tuple" LIST = "list" class ParametrizeValuesRowType(Enum): TUPLE = "tuple" LIST = "list" class Config(NamedTuple): fixture_parentheses: bool raises_require_match_for: List[str] warns_require_match_for: List[str] parametrize_names_type: ParametrizeNamesType parametrize_values_type: ParametrizeValuesType parametrize_values_row_type: ParametrizeValuesRowType mark_parentheses: bool DEFAULT_CONFIG = Config( fixture_parentheses=False, raises_require_match_for=[ "BaseException", "Exception", "ValueError", "IOError", "OSError", "EnvironmentError", "socket.error", ], warns_require_match_for=["Warning", "UserWarning", "DeprecationWarning"], parametrize_names_type=ParametrizeNamesType.TUPLE, parametrize_values_type=ParametrizeValuesType.LIST, parametrize_values_row_type=ParametrizeValuesRowType.TUPLE, mark_parentheses=False, ) ================================================ FILE: flake8_pytest_style/errors.py ================================================ from flake8_plugin_utils import Error class IncorrectFixtureParenthesesStyle(Error): code = "PT001" message = "use @pytest.fixture{expected_parens} over @pytest.fixture{actual_parens}" class FixturePositionalArgs(Error): code = "PT002" message = ( "configuration for fixture '{name}' specified via positional args, use kwargs" ) class ExtraneousScopeFunction(Error): code = "PT003" message = "scope='function' is implied in @pytest.fixture()" class MissingFixtureNameUnderscore(Error): code = "PT004" message = "fixture '{name}' does not return anything, add leading underscore" class IncorrectFixtureNameUnderscore(Error): code = "PT005" message = "fixture '{name}' returns a value, remove leading underscore" class ParametrizeNamesWrongType(Error): code = "PT006" message = "wrong name(s) type in @pytest.mark.parametrize, expected {expected_type}" class ParametrizeValuesWrongType(Error): code = "PT007" message = "wrong values type in @pytest.mark.parametrize, expected {expected_type}" class PatchWithLambda(Error): code = "PT008" message = "use return_value= instead of patching with lambda" class UnittestAssertion(Error): code = "PT009" message = "use a regular assert instead of unittest-style '{assertion}'" class RaisesWithoutException(Error): code = "PT010" message = "set the expected exception in pytest.raises()" class RaisesTooBroad(Error): code = "PT011" message = ( "pytest.raises({exception}) is too broad," " set the match parameter or use a more specific exception" ) class RaisesWithMultipleStatements(Error): code = "PT012" message = "pytest.raises() block should contain a single simple statement" class IncorrectPytestImport(Error): code = "PT013" message = "found incorrect import of pytest, use simple 'import pytest' instead" class DuplicateParametrizeTestCases(Error): code = "PT014" message = "found duplicate test cases {indexes} in @pytest.mark.parametrize" class AssertAlwaysFalse(Error): code = "PT015" message = "assertion always fails, replace with pytest.fail()" class FailWithoutMessage(Error): code = "PT016" message = "no message passed to pytest.fail()" class AssertInExcept(Error): code = "PT017" message = ( "found assertion on exception {name} in except block," " use pytest.raises() instead" ) class CompositeAssertion(Error): code = "PT018" message = "assertion should be broken down into multiple parts" class FixtureParamWithoutValue(Error): code = "PT019" message = ( "fixture {name} without value is injected as parameter," " use @pytest.mark.usefixtures instead" ) class DeprecatedYieldFixture(Error): code = "PT020" message = "@pytest.yield_fixture is deprecated, use @pytest.fixture" class FixtureFinalizerCallback(Error): code = "PT021" message = "use yield instead of request.addfinalizer" class UselessYieldFixture(Error): code = "PT022" message = "no teardown in fixture {name}, use return instead of yield" class IncorrectMarkParenthesesStyle(Error): code = "PT023" message = ( "use @pytest.mark.{mark_name}{expected_parens}" " over @pytest.mark.{mark_name}{actual_parens}" ) class UnnecessaryAsyncioMarkOnFixture(Error): code = "PT024" message = "pytest.mark.asyncio is unnecessary for fixtures" class ErroneousUseFixturesOnFixture(Error): code = "PT025" message = "pytest.mark.usefixtures has no effect on fixtures" class UseFixturesWithoutParameters(Error): code = "PT026" message = "useless pytest.mark.usefixtures without parameters" class UnittestRaisesAssertion(Error): code = "PT027" message = "use pytest.raises() instead of unittest-style '{assertion}'" # This should be named `TestFunctionArgumentWithDefault`, # but this way it is easier to avoid confusion with the `Test` prefix in pytest. class TFunctionArgumentWithDefault(Error): code = "PT028" message = "test function {name} has default value for argument {arg}, remove it" class WarnsWithoutException(Error): code = "PT029" message = "set the expected warning in pytest.warns()" class WarnsTooBroad(Error): code = "PT030" message = ( "pytest.warns({warning}) is too broad," " set the match parameter or use a more specific warning" ) class WarnsWithMultipleStatements(Error): code = "PT031" message = "pytest.warns() block should contain a single simple statement" ================================================ FILE: flake8_pytest_style/plugin.py ================================================ import argparse from typing import List from flake8.options.manager import OptionManager from flake8_plugin_utils import Plugin from .config import ( DEFAULT_CONFIG, Config, ParametrizeNamesType, ParametrizeValuesRowType, ParametrizeValuesType, enum_choices, ) from .visitors import ( AssertionVisitor, FailVisitor, FixturesVisitor, ImportsVisitor, MarksVisitor, ParametrizeVisitor, PatchVisitor, RaisesVisitor, TFunctionsVisitor, TryExceptVisitor, UnittestAssertionVisitor, WarnsVisitor, ) __version__ = "2.2.0" class PytestStylePlugin(Plugin[Config]): name = "flake8-pytest-style" version = __version__ visitors = [ AssertionVisitor, FailVisitor, FixturesVisitor, ImportsVisitor, MarksVisitor, PatchVisitor, ParametrizeVisitor, RaisesVisitor, TFunctionsVisitor, TryExceptVisitor, UnittestAssertionVisitor, WarnsVisitor, ] @classmethod def add_options(cls, option_manager: OptionManager) -> None: option_manager.add_option( "--pytest-fixture-no-parentheses", action="store_true", parse_from_config=True, default=not DEFAULT_CONFIG.fixture_parentheses, help="Omit parentheses for @pytest.fixture decorators" " without parameters. (Default: %(default)s)", ) option_manager.add_option( "--pytest-raises-require-match-for", comma_separated_list=True, parse_from_config=True, default=DEFAULT_CONFIG.raises_require_match_for, help="List of exceptions for which flake8-pytest-style requires" " a match= argument in pytest.raises(). (Default: %(default)s)", ) option_manager.add_option( "--pytest-parametrize-names-type", choices=enum_choices(ParametrizeNamesType), parse_from_config=True, default=DEFAULT_CONFIG.parametrize_names_type.value, help="Preferred type for multiple parameter names in" " @pytest.mark.parametrize. (Default: %(default)s)", ) option_manager.add_option( "--pytest-parametrize-values-type", choices=enum_choices(ParametrizeValuesType), parse_from_config=True, default=DEFAULT_CONFIG.parametrize_values_type.value, help="Preferred type for values in @pytest.mark.parametrize." " (Default: %(default)s)", ) option_manager.add_option( "--pytest-parametrize-values-row-type", choices=enum_choices(ParametrizeValuesRowType), parse_from_config=True, default=DEFAULT_CONFIG.parametrize_values_row_type.value, help="Preferred type for each row in @pytest.mark.parametrize" " in case of multiple parameters. (Default: %(default)s)", ) option_manager.add_option( "--pytest-mark-no-parentheses", action="store_true", parse_from_config=True, default=not DEFAULT_CONFIG.mark_parentheses, help="Omit parentheses for @pytest.mark.foo decorators" " without parameters. (Default: %(default)s)", ) option_manager.add_option( "--pytest-warns-require-match-for", comma_separated_list=True, parse_from_config=True, default=DEFAULT_CONFIG.warns_require_match_for, help="List of warnings for which flake8-pytest-style requires" " a match= argument in pytest.warns(). (Default: %(default)s)", ) @classmethod def parse_options_to_config( # pylint: disable=unused-argument cls, option_manager: OptionManager, options: argparse.Namespace, args: List[str] ) -> Config: return Config( fixture_parentheses=not options.pytest_fixture_no_parentheses, raises_require_match_for=options.pytest_raises_require_match_for, parametrize_names_type=ParametrizeNamesType( options.pytest_parametrize_names_type ), parametrize_values_type=ParametrizeValuesType( options.pytest_parametrize_values_type ), parametrize_values_row_type=ParametrizeValuesRowType( options.pytest_parametrize_values_row_type ), mark_parentheses=not options.pytest_mark_no_parentheses, warns_require_match_for=options.pytest_warns_require_match_for, ) ================================================ FILE: flake8_pytest_style/py.typed ================================================ ================================================ FILE: flake8_pytest_style/utils.py ================================================ import ast from collections import deque from typing import Dict, Iterator, List, NamedTuple, Optional, Tuple, Union AnyFunctionDef = Union[ast.AsyncFunctionDef, ast.FunctionDef] AnyDecoratorTarget = Union[ast.ClassDef, AnyFunctionDef] def get_qualname(node: ast.AST) -> Optional[str]: """ If node represents a chain of attribute accesses, return is qualified name. """ parts = [] while True: if isinstance(node, ast.Name): parts.append(node.id) break if isinstance(node, ast.Attribute): parts.append(node.attr) node = node.value else: return None return ".".join(reversed(parts)) class SimpleCallArgs(NamedTuple): args: Tuple[ast.AST, ...] kwargs: Dict[str, ast.AST] def get_argument( self, name: str, position: Optional[int] = None ) -> Optional[ast.AST]: """Get argument by name in kwargs or position in args.""" kwarg = self.kwargs.get(name) if kwarg is not None: return kwarg if position is not None and len(self.args) > position: return self.args[position] return None def get_simple_call_args(node: ast.Call) -> SimpleCallArgs: """ Get call arguments which are specified explicitly (positional and keyword). """ # list of leading non-starred args args = [] for arg in node.args: if isinstance(arg, ast.Starred): break args.append(arg) # dict of keyword args keywords: Dict[str, ast.AST] = {} for keyword in node.keywords: if keyword.arg is not None: keywords[keyword.arg] = keyword.value return SimpleCallArgs(tuple(args), keywords) def is_parametrize_call(node: ast.Call) -> bool: """Checks if given call is to `pytest.mark.parametrize`.""" return get_qualname(node.func) == "pytest.mark.parametrize" def is_raises_call(node: ast.Call) -> bool: """Checks if given call is to `pytest.raises`.""" return get_qualname(node.func) == "pytest.raises" def is_warns_call(node: ast.Call) -> bool: """Checks if given call is to `pytest.warns`.""" return get_qualname(node.func) == "pytest.warns" def is_fail_call(node: ast.Call) -> bool: """Checks if given call is to `pytest.fail`.""" return get_qualname(node.func) == "pytest.fail" def is_raises_with(node: ast.With) -> bool: """Checks that a given `with` statement has a `pytest.raises` context.""" for item in node.items: if isinstance(item.context_expr, ast.Call) and is_raises_call( item.context_expr ): return True return False def is_warns_with(node: ast.With) -> bool: """Checks that a given `with` statement has a `pytest.warns` context.""" for item in node.items: if isinstance(item.context_expr, ast.Call) and is_warns_call(item.context_expr): return True return False class ParametrizeArgs(NamedTuple): names: ast.AST values: Optional[ast.AST] ids: Optional[ast.AST] def extract_parametrize_call_args(node: ast.Call) -> Optional[ParametrizeArgs]: """Extracts argnames, argvalues and ids from a parametrize call.""" args = get_simple_call_args(node) names_arg = args.get_argument("argnames", 0) if names_arg is None: return None values_arg = args.get_argument("argvalues", 1) ids_arg = args.get_argument("ids") return ParametrizeArgs(names_arg, values_arg, ids_arg) def _is_pytest_fixture(node: ast.AST) -> bool: """Checks if node is a `pytest.fixture` attribute access.""" return get_qualname(node) == "pytest.fixture" def is_pytest_yield_fixture(node: ast.AST) -> bool: """Checks if node is a `pytest.yield_fixture` attribute access.""" return get_qualname(node) == "pytest.yield_fixture" def _is_any_pytest_fixture(node: ast.AST) -> bool: """Checks if node is a `pytest.fixture` or `pytest.yield_fixture`.""" return _is_pytest_fixture(node) or is_pytest_yield_fixture(node) def get_fixture_decorator(node: AnyFunctionDef) -> Union[ast.Call, ast.Attribute, None]: """ Returns a @pytest.fixture decorator applied to given function definition, if any. Return value is either: * ast.Call, if decorator is written as @pytest.fixture() * ast.Attribute, if decorator is written as @pytest.fixture * None, if decorator not found """ for decorator in node.decorator_list: if ( isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute) and _is_any_pytest_fixture(decorator.func) ): return decorator if isinstance(decorator, ast.Attribute) and _is_any_pytest_fixture(decorator): return decorator return None def _is_mark(node: ast.AST) -> bool: """Checks if node is a `pytest.mark.foo` attribute access.""" qualname = get_qualname(node) if qualname is None: return False return qualname.startswith("pytest.mark.") def get_mark_decorators( node: AnyDecoratorTarget, ) -> List[Union[ast.Call, ast.Attribute]]: """ Returns all @pytest.mark.foo decorators applied to given function definition. Return value is a list of: * ast.Call, if decorator is written as @pytest.mark.foo() * ast.Attribute, if decorator is written as @pytest.mark.foo """ result: List[Union[ast.Call, ast.Attribute]] = [] for decorator in node.decorator_list: if ( isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute) and _is_mark(decorator.func) ): result.append(decorator) if isinstance(decorator, ast.Attribute) and _is_mark(decorator): result.append(decorator) return result def get_mark_name(node: ast.AST) -> str: """ Returns the name of the mark in a `pytest.mark.foo` attribute access or in a `pytest.mark.foo()` call. If the given node is not suitable as described above, a ValueError is raised. """ mark_prefix = "pytest.mark." if isinstance(node, ast.Call): node = node.func qualname = get_qualname(node) if qualname is None or not qualname.startswith(mark_prefix): raise ValueError("Given node is not a pytest mark") return qualname[len(mark_prefix) :] # noqa: E203 def is_none(node: ast.AST) -> bool: """ Checks if the node is a constant representing the value `None`. Drop-in replacement for `flake8_plugin_utils.utils.is_none` without using removed `ast.NameConstant` class. """ return isinstance(node, ast.Constant) and node.value is None def is_empty_string(node: ast.AST) -> bool: """ Checks if the node is a constant empty string. """ # empty string literal if isinstance(node, ast.Constant) and node.value == "": return True # empty f-string if isinstance(node, ast.JoinedStr) and not node.values: return True return False def _is_empty_iterable( # pylint:disable=too-many-return-statements node: ast.AST, ) -> bool: """ Checks if the node is a constant empty iterable. """ if is_empty_string(node): return True # empty list or tuple literal if isinstance(node, (ast.List, ast.Tuple)) and not node.elts: return True # empty dict literal if isinstance(node, ast.Dict) and not node.keys: return True if isinstance(node, ast.Call) and get_qualname(node.func) in ( "list", "set", "tuple", "dict", "frozenset", ): if not node.args and not node.keywords: # no args return True if ( len(node.args) == 1 and not node.keywords and _is_empty_iterable(node.args[0]) ): # single arg, empty iterable return True return False def is_falsy_constant(node: ast.AST) -> bool: """ Checks if the node is a constant with a falsy value. """ # constant node: None, False, zero, empty string (not f-string) if isinstance(node, ast.Constant) and not node.value: return True return _is_empty_iterable(node) def is_test_function(node: AnyFunctionDef) -> bool: """Checks if the given function is a test function.""" return node.name.startswith("test_") def get_all_argument_names(node: ast.arguments) -> List[str]: """Returns a list of all argument names from the given node.""" pos_only_args = getattr(node, "posonlyargs", []) result = [arg.arg for arg in pos_only_args + node.args] if node.vararg: result.append(node.vararg.arg) result.extend([arg.arg for arg in node.kwonlyargs]) if node.kwarg: result.append(node.kwarg.arg) return result def walk_without_nested_functions(root: ast.AST) -> Iterator[ast.AST]: """Similar to `ast.walk`, but does not descend into nested functions.""" todo = deque([root]) while todo: node = todo.popleft() if node is root or not isinstance( node, (ast.FunctionDef, ast.AsyncFunctionDef) ): todo.extend(ast.iter_child_nodes(node)) yield node def is_abstract_method(node: AnyFunctionDef) -> bool: """ Returns true if the node is decorated with an abstract method decorator, otherwise false. """ for decorator in node.decorator_list: qualname = get_qualname(decorator) if qualname in ("abstractmethod", "abc.abstractmethod"): return True return False def is_nontrivial_with_statement(node: ast.AST) -> bool: """ Returns true if the given node is a `with` (or `async with`) statement containing any code inside (anything which is not a single `pass` statement). """ if not isinstance(node, (ast.With, ast.AsyncWith)): return False body = node.body return len(node.body) != 1 or not isinstance(body[0], ast.Pass) ================================================ FILE: flake8_pytest_style/visitors/__init__.py ================================================ from .assertion import AssertionVisitor, UnittestAssertionVisitor from .fail import FailVisitor from .fixtures import FixturesVisitor from .imports import ImportsVisitor from .marks import MarksVisitor from .parametrize import ParametrizeVisitor from .patch import PatchVisitor from .raises import RaisesVisitor from .t_functions import TFunctionsVisitor from .try_except import TryExceptVisitor from .warns import WarnsVisitor __all__ = ( "AssertionVisitor", "FailVisitor", "FixturesVisitor", "ImportsVisitor", "MarksVisitor", "ParametrizeVisitor", "PatchVisitor", "RaisesVisitor", "TFunctionsVisitor", "TryExceptVisitor", "UnittestAssertionVisitor", "WarnsVisitor", ) ================================================ FILE: flake8_pytest_style/visitors/assertion.py ================================================ import ast from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import ( CompositeAssertion, UnittestAssertion, UnittestRaisesAssertion, ) _UNITTEST_ASSERT_NAMES = ( # taken from dir(unittest.TestCase) under Python 3.11 "assertAlmostEqual", "assertAlmostEquals", "assertCountEqual", "assertDictContainsSubset", "assertDictEqual", "assertEqual", "assertEquals", "assertFalse", "assertGreater", "assertGreaterEqual", "assertIn", "assertIs", "assertIsInstance", "assertIsNone", "assertIsNot", "assertIsNotNone", "assertLess", "assertLessEqual", "assertListEqual", "assertLogs", "assertMultiLineEqual", "assertNoLogs", "assertNotAlmostEqual", "assertNotAlmostEquals", "assertNotEqual", "assertNotEquals", "assertNotIn", "assertNotIsInstance", "assertNotRegex", "assertNotRegexpMatches", "assertRegex", "assertRegexpMatches", "assertSequenceEqual", "assertSetEqual", "assertTrue", "assertTupleEqual", "assertWarns", "assertWarnsRegex", "assert_", "failIf", "failIfAlmostEqual", "failIfEqual", "failUnless", "failUnlessAlmostEqual", "failUnlessEqual", # below is from Django's SimpleTestCase, leaked here when porting flake8-pytest # TODO(m_burst) disallow Django's SimpleTestCase/TransactionTestCase assertions # where feasible (#220) "assertNotContains", ) _UNITTEST_ASSERT_RAISES_NAMES = ( # taken from dir(unittest.TestCase) under Python 3.11 "assertRaises", "assertRaisesRegex", "assertRaisesRegexp", "failUnlessRaises", # below is from Django's SimpleTestCase, leaked here when porting flake8-pytest "assertRaisesMessage", ) class UnittestAssertionVisitor(Visitor[Config]): def visit_Call(self, node: ast.Call) -> None: if isinstance(node.func, ast.Attribute): if node.func.attr in _UNITTEST_ASSERT_NAMES: self.error_from_node(UnittestAssertion, node, assertion=node.func.attr) elif node.func.attr in _UNITTEST_ASSERT_RAISES_NAMES: self.error_from_node( UnittestRaisesAssertion, node, assertion=node.func.attr ) class AssertionVisitor(Visitor[Config]): def _is_composite_condition(self, expr: ast.AST) -> bool: # e.g. `a and b` if isinstance(expr, ast.BoolOp) and isinstance(expr.op, ast.And): return True # e.g. `not (a or b)` if ( isinstance(expr, ast.UnaryOp) and isinstance(expr.op, ast.Not) and isinstance(expr.operand, ast.BoolOp) and isinstance(expr.operand.op, ast.Or) ): return True return False def visit_Assert(self, node: ast.Assert) -> None: if self._is_composite_condition(node.test): self.error_from_node(CompositeAssertion, node) ================================================ FILE: flake8_pytest_style/visitors/fail.py ================================================ import ast from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import AssertAlwaysFalse, FailWithoutMessage from flake8_pytest_style.utils import ( get_simple_call_args, is_empty_string, is_fail_call, is_falsy_constant, ) class FailVisitor(Visitor[Config]): def _check_fail_call(self, node: ast.Call) -> None: """Checks for PT016.""" args = get_simple_call_args(node) # Since pytest 7.0 the argument is named 'reason', and 'msg' is deprecated but # supported (at the time of writing this code). We check 'reason', then first # positional argument, and then 'msg'. The edge cases like # `pytest.fail('foo', msg='bar')` and `pytest.fail(reason='foo', msg='bar')` # are deliberately ignored. message_argument = args.get_argument("reason", 0) or args.get_argument("msg") if not message_argument or is_empty_string(message_argument): self.error_from_node(FailWithoutMessage, node) def visit_Assert(self, node: ast.Assert) -> None: """Checks for PT015.""" if is_falsy_constant(node.test): self.error_from_node(AssertAlwaysFalse, node) def visit_Call(self, node: ast.Call) -> None: if is_fail_call(node): self._check_fail_call(node) ================================================ FILE: flake8_pytest_style/visitors/fixtures.py ================================================ import ast from typing import Set, Type, Union from flake8_plugin_utils import Error, Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import ( DeprecatedYieldFixture, ErroneousUseFixturesOnFixture, ExtraneousScopeFunction, FixtureFinalizerCallback, FixturePositionalArgs, IncorrectFixtureNameUnderscore, IncorrectFixtureParenthesesStyle, MissingFixtureNameUnderscore, UnnecessaryAsyncioMarkOnFixture, UselessYieldFixture, ) from flake8_pytest_style.utils import ( AnyFunctionDef, get_all_argument_names, get_fixture_decorator, get_mark_decorators, get_mark_name, get_qualname, is_abstract_method, is_pytest_yield_fixture, walk_without_nested_functions, ) class FixturesVisitor(Visitor[Config]): def _check_fixture_decorator_name( self, fixture_decorator: Union[ast.Call, ast.Attribute] ) -> None: """Checks for PT020.""" if isinstance(fixture_decorator, ast.Call): is_yield = is_pytest_yield_fixture(fixture_decorator.func) else: is_yield = is_pytest_yield_fixture(fixture_decorator) if is_yield: self.error_from_node(DeprecatedYieldFixture, fixture_decorator) def _check_fixture_decorator( self, fixture_decorator: Union[ast.Call, ast.Attribute], fixture_func: AnyFunctionDef, ) -> None: """Checks for PT001, PT002, PT003.""" if not isinstance(fixture_decorator, ast.Call): if self.config.fixture_parentheses: self.error_from_node( IncorrectFixtureParenthesesStyle, fixture_decorator, expected_parens="()", actual_parens="", ) return if ( not self.config.fixture_parentheses and not fixture_decorator.args and not fixture_decorator.keywords ): self.error_from_node( IncorrectFixtureParenthesesStyle, fixture_decorator, expected_parens="", actual_parens="()", ) if fixture_decorator.args: self.error_from_node( FixturePositionalArgs, fixture_decorator, name=fixture_func.name ) for keyword in fixture_decorator.keywords: if ( keyword.arg == "scope" and isinstance(keyword.value, ast.Constant) and keyword.value.value == "function" ): self.error_from_node( ExtraneousScopeFunction, fixture_decorator, name=fixture_func.name ) def _check_fixture_returns(self, node: AnyFunctionDef) -> None: """Checks for PT004, PT005, PT022.""" # skip these checks for abstract fixtures if is_abstract_method(node): return has_return_with_value = False has_yield_from = False yield_statements = [] for child in walk_without_nested_functions(node): if isinstance(child, ast.Yield): yield_statements.append(child) if isinstance(child, (ast.Return, ast.Yield)) and child.value is not None: has_return_with_value = True if isinstance(child, ast.YieldFrom): has_yield_from = True if has_return_with_value and node.name.startswith("_"): self.error_from_node(IncorrectFixtureNameUnderscore, node, name=node.name) elif ( not has_return_with_value and not has_yield_from and not node.name.startswith("_") ): # we shouldn't fire PT004 if we found a `yield from` because # there is no adequate way to determine whether a value is actually yielded self.error_from_node(MissingFixtureNameUnderscore, node, name=node.name) last_statement_is_yield = isinstance(node.body[-1], ast.Expr) and isinstance( node.body[-1].value, ast.Yield ) if last_statement_is_yield and len(yield_statements) == 1: self.error_from_node(UselessYieldFixture, node, name=node.name) def _check_fixture_addfinalizer(self, node: AnyFunctionDef) -> None: """Checks for PT021.""" if "request" not in get_all_argument_names(node.args): return for child in walk_without_nested_functions(node): # pragma: no branch if ( isinstance(child, ast.Call) and get_qualname(child.func) == "request.addfinalizer" ): self.error_from_node(FixtureFinalizerCallback, child) return def _check_fixture_marks(self, node: AnyFunctionDef) -> None: """Checks for PT024, PT025.""" reported_errors: Set[Type[Error]] = set() marks = get_mark_decorators(node) for mark in marks: mark_name = get_mark_name(mark) if ( mark_name == "asyncio" and UnnecessaryAsyncioMarkOnFixture not in reported_errors ): self.error_from_node(UnnecessaryAsyncioMarkOnFixture, mark) reported_errors.add(UnnecessaryAsyncioMarkOnFixture) if ( mark_name == "usefixtures" and ErroneousUseFixturesOnFixture not in reported_errors ): self.error_from_node(ErroneousUseFixturesOnFixture, mark) reported_errors.add(ErroneousUseFixturesOnFixture) def visit_FunctionDef(self, node: AnyFunctionDef) -> None: fixture_decorator = get_fixture_decorator(node) if fixture_decorator: self._check_fixture_decorator_name(fixture_decorator) self._check_fixture_decorator(fixture_decorator, node) self._check_fixture_returns(node) self._check_fixture_addfinalizer(node) self._check_fixture_marks(node) self.generic_visit(node) visit_AsyncFunctionDef = visit_FunctionDef # noqa: N815 ================================================ FILE: flake8_pytest_style/visitors/imports.py ================================================ import ast from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import IncorrectPytestImport def _is_pytest_or_subpackage(imported_name: str) -> bool: return imported_name == "pytest" or imported_name.startswith("pytest.") class ImportsVisitor(Visitor[Config]): def visit_Import(self, node: ast.Import) -> None: for name in node.names: if ( _is_pytest_or_subpackage(name.name) and name.asname and name.asname != name.name ): self.error_from_node(IncorrectPytestImport, node) def visit_ImportFrom(self, node: ast.ImportFrom) -> None: if node.level != 0 or node.module is None: # relative import return if _is_pytest_or_subpackage(node.module): self.error_from_node(IncorrectPytestImport, node) ================================================ FILE: flake8_pytest_style/visitors/marks.py ================================================ import ast from typing import Union from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import ( IncorrectMarkParenthesesStyle, UseFixturesWithoutParameters, ) from flake8_pytest_style.utils import ( AnyDecoratorTarget, get_mark_decorators, get_mark_name, ) class MarksVisitor(Visitor[Config]): def _check_mark_parentheses( self, mark_decorator: Union[ast.Call, ast.Attribute] ) -> None: """Checks for PT023.""" if not isinstance(mark_decorator, ast.Call): if self.config.mark_parentheses: self.error_from_node( IncorrectMarkParenthesesStyle, mark_decorator, mark_name=get_mark_name(mark_decorator), expected_parens="()", actual_parens="", ) return if ( not self.config.mark_parentheses and not mark_decorator.args and not mark_decorator.keywords ): self.error_from_node( IncorrectMarkParenthesesStyle, mark_decorator, mark_name=get_mark_name(mark_decorator.func), expected_parens="", actual_parens="()", ) def _check_useless_usefixtures( self, mark_decorator: Union[ast.Call, ast.Attribute] ) -> None: """Checks for PT026.""" if get_mark_name(mark_decorator) != "usefixtures": return has_parameters = isinstance(mark_decorator, ast.Call) and bool( mark_decorator.args or mark_decorator.keywords ) if not has_parameters: self.error_from_node(UseFixturesWithoutParameters, mark_decorator) def visit_FunctionDef(self, node: AnyDecoratorTarget) -> None: mark_decorators = get_mark_decorators(node) for mark_decorator in mark_decorators: self._check_mark_parentheses(mark_decorator) self._check_useless_usefixtures(mark_decorator) visit_AsyncFunctionDef = visit_FunctionDef # noqa: N815 def visit_ClassDef(self, node: ast.ClassDef) -> None: self.visit_FunctionDef(node) # recurse into classes self.generic_visit(node) ================================================ FILE: flake8_pytest_style/visitors/parametrize.py ================================================ import ast import itertools from typing import Optional from flake8_plugin_utils import Visitor, check_equivalent_nodes from ..config import ( Config, ParametrizeNamesType, ParametrizeValuesRowType, ParametrizeValuesType, ) from ..errors import ( DuplicateParametrizeTestCases, ParametrizeNamesWrongType, ParametrizeValuesWrongType, ) from ..utils import extract_parametrize_call_args, is_parametrize_call class ParametrizeVisitor(Visitor[Config]): def _check_parametrize_names( self, node: ast.Call, names: ast.AST ) -> Optional[bool]: """ Handles names in parametrize, checks for PT006. Returns a flag indicating whether parametrize has multiple names, or None if we can't tell. """ multiple_names: Optional[bool] = None found_type: Optional[ParametrizeNamesType] = None if isinstance(names, ast.Constant) and isinstance(names.value, str): if "," in names.value: found_type = ParametrizeNamesType.CSV multiple_names = True else: multiple_names = False elif isinstance(names, (ast.List, ast.Tuple)): multiple_names = len(names.elts) > 1 if not multiple_names: self.error_from_node( ParametrizeNamesWrongType, node, expected_type="string" ) elif isinstance(names, ast.Tuple): found_type = ParametrizeNamesType.TUPLE else: found_type = ParametrizeNamesType.LIST if multiple_names and found_type != self.config.parametrize_names_type: self.error_from_node( ParametrizeNamesWrongType, node, expected_type=self.config.parametrize_names_type.value, ) return multiple_names def _get_expected_values_type_str(self, multiple_names: Optional[bool]) -> str: if multiple_names: return ( f"{self.config.parametrize_values_type.value}" f" of {self.config.parametrize_values_row_type.value}s" ) return self.config.parametrize_values_type.value def _check_parametrize_values( self, node: ast.Call, values: Optional[ast.AST], multiple_names: Optional[bool] ) -> None: """Checks for PT007.""" expected_type_str = self._get_expected_values_type_str(multiple_names) if isinstance(values, ast.List): top_level_type = ParametrizeValuesType.LIST elif isinstance(values, ast.Tuple): top_level_type = ParametrizeValuesType.TUPLE else: return if top_level_type != self.config.parametrize_values_type: self.error_from_node( ParametrizeValuesWrongType, node, expected_type=expected_type_str ) return if multiple_names: for element in values.elts: found_row_type: Optional[ParametrizeValuesRowType] = None if isinstance(element, ast.List): found_row_type = ParametrizeValuesRowType.LIST elif isinstance(element, ast.Tuple): found_row_type = ParametrizeValuesRowType.TUPLE if ( found_row_type and found_row_type != self.config.parametrize_values_row_type ): self.error_from_node( ParametrizeValuesWrongType, node, expected_type=expected_type_str, ) break def _check_parametrize_duplicates( self, node: ast.AST, values: Optional[ast.AST] ) -> None: """Checks for PT014.""" if not isinstance(values, (ast.List, ast.Tuple, ast.Set)): return for (i, element1), (j, element2) in itertools.combinations( enumerate(values.elts, start=1), 2 ): if check_equivalent_nodes(element1, element2): self.error_from_node( DuplicateParametrizeTestCases, node, indexes=(i, j) ) def _check_parametrize_call(self, node: ast.Call) -> None: """Checks for all violations regarding `pytest.mark.parametrize` calls.""" args = extract_parametrize_call_args(node) if not args: return multiple_names = self._check_parametrize_names(node, args.names) self._check_parametrize_values(node, args.values, multiple_names) self._check_parametrize_duplicates(node, args.values) def visit_Call(self, node: ast.Call) -> None: if is_parametrize_call(node): self._check_parametrize_call(node) ================================================ FILE: flake8_pytest_style/visitors/patch.py ================================================ import ast from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import PatchWithLambda from flake8_pytest_style.utils import ( get_all_argument_names, get_qualname, get_simple_call_args, ) _PATCH_NAMESPACES = ( "mocker", "class_mocker", "module_mocker", "package_mocker", "session_mocker", "mock", "unittest.mock", ) _PATCH_NAMES = ("patch",) + tuple( f"{namespace}.patch" for namespace in _PATCH_NAMESPACES ) _PATCH_OBJECT_NAMES = tuple(f"{name}.object" for name in _PATCH_NAMES) class PatchVisitor(Visitor[Config]): def _check_patch_call(self, node: ast.Call, new_arg_number: int) -> None: """ Checks for PT008. :param node: patch call node :param new_arg_number: number of `new` positional argument of patch func """ args = get_simple_call_args(node) if args.get_argument("return_value") is not None: return new_arg = args.get_argument("new", new_arg_number) if not isinstance(new_arg, ast.Lambda): return lambda_argnames = set(get_all_argument_names(new_arg.args)) for child_node in ast.walk(new_arg.body): if isinstance(child_node, ast.Name) and child_node.id in lambda_argnames: break else: self.error_from_node(PatchWithLambda, node) def visit_Call(self, node: ast.Call) -> None: if get_qualname(node.func) in _PATCH_NAMES: # attributes are (target, new, ...) self._check_patch_call(node, 1) if get_qualname(node.func) in _PATCH_OBJECT_NAMES: # attributes are (target, attribute, new, ...) self._check_patch_call(node, 2) ================================================ FILE: flake8_pytest_style/visitors/raises.py ================================================ import ast from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import ( RaisesTooBroad, RaisesWithMultipleStatements, RaisesWithoutException, ) from flake8_pytest_style.utils import ( get_qualname, get_simple_call_args, is_empty_string, is_none, is_nontrivial_with_statement, is_raises_call, is_raises_with, ) class RaisesVisitor(Visitor[Config]): def _check_raises_call(self, node: ast.Call) -> None: """ Checks for violations regarding `pytest.raises` call args (PT010 and PT011). """ args = get_simple_call_args(node) exception = args.get_argument("expected_exception", position=0) if not exception: self.error_from_node(RaisesWithoutException, node) return exception_name = get_qualname(exception) if exception_name not in self.config.raises_require_match_for: return match = args.get_argument("match") if match is None or is_none(match) or is_empty_string(match): self.error_from_node(RaisesTooBroad, node, exception=exception_name) def _check_raises_with(self, node: ast.With) -> None: """Checks for PT012.""" body = node.body is_complex_body = False if len(body) != 1: is_complex_body = True elif isinstance( body[0], ( ast.If, ast.For, ast.AsyncFor, ast.While, ast.Try, ), ): is_complex_body = True elif is_nontrivial_with_statement(body[0]): is_complex_body = True if is_complex_body: self.error_from_node(RaisesWithMultipleStatements, node) def visit_Call(self, node: ast.Call) -> None: if is_raises_call(node): self._check_raises_call(node) def visit_With(self, node: ast.With) -> None: if is_raises_with(node): self._check_raises_with(node) self.generic_visit(node) ================================================ FILE: flake8_pytest_style/visitors/t_functions.py ================================================ from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import ( FixtureParamWithoutValue, TFunctionArgumentWithDefault, ) from flake8_pytest_style.utils import AnyFunctionDef, is_test_function # This should be named `TestFunctionsVisitor` (and the module `test_functions`), # but this way it is easier to avoid confusion with the `Test` prefix in pytest. class TFunctionsVisitor(Visitor[Config]): def _check_test_function_args(self, node: AnyFunctionDef) -> None: """Checks for PT019, P028.""" # intentionally not looking at posonlyargs because pytest passes everything # as kwargs, so declaring fixture args as positional-only will fail anyway for arg in node.args.args + node.args.kwonlyargs: if arg.arg.startswith("_"): # The error is raised at the position of `node` (function call), # not `arg`, to preserve backwards compatibility. self.error_from_node(FixtureParamWithoutValue, node, name=arg.arg) if node.args.defaults: pos_args = node.args.posonlyargs + node.args.args pos_args_with_defaults = pos_args[-len(node.args.defaults) :] # noqa: E203 for arg in pos_args_with_defaults: self.error_from_node( TFunctionArgumentWithDefault, arg, name=node.name, arg=arg.arg ) for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults): if default is not None: self.error_from_node( TFunctionArgumentWithDefault, arg, name=node.name, arg=arg.arg ) def visit_FunctionDef(self, node: AnyFunctionDef) -> None: if is_test_function(node): self._check_test_function_args(node) self.generic_visit(node) visit_AsyncFunctionDef = visit_FunctionDef # noqa: N815 ================================================ FILE: flake8_pytest_style/visitors/try_except.py ================================================ import ast from typing import List, Optional from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import AssertInExcept class TryExceptVisitor(Visitor[Config]): def __init__(self, config: Optional[Config] = None) -> None: super().__init__(config=config) self._exception_names: List[str] = [] self._current_assert: Optional[ast.Assert] = None def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None: if node.name: self._exception_names.append(node.name) try: self.generic_visit(node) finally: if node.name: self._exception_names.pop() def visit_Assert(self, node: ast.Assert) -> None: self._current_assert = node try: self.visit(node.test) finally: self._current_assert = None if node.msg: self.visit(node.msg) def visit_Name(self, node: ast.Name) -> None: if self._current_assert: if node.id in self._exception_names: self.error_from_node(AssertInExcept, self._current_assert, name=node.id) ================================================ FILE: flake8_pytest_style/visitors/warns.py ================================================ import ast from flake8_plugin_utils import Visitor from flake8_pytest_style.config import Config from flake8_pytest_style.errors import ( WarnsTooBroad, WarnsWithMultipleStatements, WarnsWithoutException, ) from flake8_pytest_style.utils import ( get_qualname, get_simple_call_args, is_empty_string, is_none, is_nontrivial_with_statement, is_warns_call, is_warns_with, ) class WarnsVisitor(Visitor[Config]): def _check_warns_call(self, node: ast.Call) -> None: """ Checks for violations regarding `pytest.warns` call args (PT029 and PT030). """ args = get_simple_call_args(node) warning = args.get_argument("expected_warning", position=0) if not warning: self.error_from_node(WarnsWithoutException, node) return warning_name = get_qualname(warning) if warning_name not in self.config.warns_require_match_for: return match = args.get_argument("match") if match is None or is_none(match) or is_empty_string(match): self.error_from_node(WarnsTooBroad, node, warning=warning_name) def _check_warns_with(self, node: ast.With) -> None: """Checks for PT031.""" body = node.body is_complex_body = False if len(body) != 1: is_complex_body = True elif isinstance( body[0], ( ast.If, ast.For, ast.AsyncFor, ast.While, ast.Try, ), ): is_complex_body = True elif is_nontrivial_with_statement(body[0]): is_complex_body = True if is_complex_body: self.error_from_node(WarnsWithMultipleStatements, node) def visit_Call(self, node: ast.Call) -> None: if is_warns_call(node): self._check_warns_call(node) def visit_With(self, node: ast.With) -> None: if is_warns_with(node): self._check_warns_with(node) self.generic_visit(node) ================================================ FILE: pyproject.toml ================================================ [project] name = "flake8-pytest-style" version = "2.2.0" description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests." authors = [ { name = "Mikhail Burshteyn", email = "mdburshteyn@gmail.com" }, ] license = "MIT" readme = 'README.md' keywords = ["flake8", "pytest"] dynamic = [ "classifiers", "dependencies", ] requires-python = ">=3.10" [project.urls] repository = "https://github.com/m-burst/flake8-pytest-style" homepage = "https://pypi.org/project/flake8-pytest-style" [tool.poetry] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: Plugins", "Framework :: Flake8", "Framework :: Pytest", "Intended Audience :: Developers", "Operating System :: OS Independent", "Topic :: Software Development :: Quality Assurance", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Testing :: Unit", ] [project.entry-points."flake8.extension"] PT = 'flake8_pytest_style.plugin:PytestStylePlugin' [tool.poetry.dependencies] python = "^3.10" flake8-plugin-utils = "^1.3.2" [tool.poetry.group.dev.dependencies] black = "^26.3.1" bump2version = "^1.0.1" mypy = ">=1.20.2" pylint = "^4.0.5" pytest = "^9.0.3" pytest-cov = "^7.1.0" pytest-deadfixtures = "^3.1" flake8 = "^7.3.0" pytest-mock = "^3.15.1" tomlkit = ">=0.12.1,<0.15.0" # The below group is taken from flake8-awesome with some exclusions # that are irrelevant or unsupported. # TODO remove Python version restriction after Bandit supports 3.14 # https://github.com/PyCQA/bandit/issues/1314 flake8-bandit = {version = "*", python="<3.14"} flake8-breakpoint = "*" flake8-bugbear = "*" flake8-builtins = "*" flake8-comprehensions = "*" flake8-eradicate = "*" flake8-if-expr = "*" flake8-isort = "*" flake8-print = "*" flake8-requirements = "*" pep8-naming = "*" # broken on 3.14+ # flake8-annotations-complexity = "*" # broken on 3.14+ # flake8-expression-complexity = "*" # kinda broken on 3.12+ (needs pkg_resources), not needed here # flake8-logging-format = "*" # functionality implemented in this plugin # flake8-pytest = "*" # actually this plugin # flake8-pytest-style = "*" # broken on 3.14+, and probably not needed with mypy # flake8-return = "*" # end of flake8-awesome [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: scripts/pypi_readme.py ================================================ #!/usr/bin/env python3 import pathlib import re from typing import Match, cast import tomlkit from tomlkit.container import Container def process_readme(readme: str, project_metadata: Container) -> str: urls = cast(Container, project_metadata["urls"]) repository = cast(str, urls["repository"]) version = cast(str, project_metadata["version"]) base_url = f"{repository}/blob/v{version}/" def replace_url(match: Match[str]) -> str: path_without_leading_slash = match.group(0).lstrip("/") return f"{base_url}{path_without_leading_slash}" return re.sub(r"/?\S+\.md", replace_url, readme) def make_output_filename(input_filename: str) -> str: path = pathlib.Path(input_filename) return str(path.parent / f"{path.stem}-pypi{path.suffix}") def main() -> None: with open("pyproject.toml") as pyproject_file: pyproject_data = tomlkit.loads(pyproject_file.read()) project_metadata = cast(Container, pyproject_data["project"]) readme_filename = cast(str, project_metadata["readme"]) with open(readme_filename) as input_file: input_readme = input_file.read() output_readme = process_readme(input_readme, project_metadata) output_filename = make_output_filename(readme_filename) with open(output_filename, "w") as output_file: output_file.write(output_readme) project_metadata["readme"] = output_filename with open("pyproject.toml", "w") as pyproject_file: pyproject_file.write(tomlkit.dumps(pyproject_data)) if __name__ == "__main__": main() ================================================ FILE: setup.cfg ================================================ [flake8] enable-extensions = G exclude = .git, .venv extend-ignore = ; 'id' is a python builtin, consider renaming the class attribute A003, ; line break before binary operator W503, max-complexity = 10 max-line-length = 88 per-file-ignores = flake8_pytest_style/visitors/*.py:N802 tests/*.py:S101 show-source = True [mypy] check_untyped_defs = True disallow_any_generics = True disallow_incomplete_defs = True disallow_untyped_defs = True ignore_missing_imports = True no_implicit_optional = True [mypy-tests.*] disallow_incomplete_defs = False disallow_untyped_defs = False [isort] balanced_wrapping = True default_section = THIRDPARTY include_trailing_comma = True known_first_party = flake8_pytest_style, tests line_length = 88 multi_line_output = 3 [pylint] good-names = i,j,k,e,x,_,pk,id max-args = 5 max-attributes = 10 max-bool-expr = 5 max-branches = 10 max-locals = 8 max-module-lines = 500 max-nested-blocks = 3 max-public-methods = 10 max-returns = 5 max-statements = 25 output-format = colorized disable = C0103, ; Constant name "api" doesn't conform to UPPER_CASE naming style (invalid-name) C0111, ; Missing module docstring (missing-docstring) E0213, ; Method should have "self" as first argument (no-self-argument) - N805 for flake8 R0801, ; Similar lines in 2 files (duplicate-code) R0901, ; Too many ancestors (m/n) (too-many-ancestors) R0903, ; Too few public methods (m/n) (too-few-public-methods) ignored-classes = contextlib.closing, [coverage:run] omit = tests/*,**/__main__.py branch = True [coverage:report] show_missing = True skip_covered = True fail_under = 90 ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/conftest.py ================================================ ================================================ FILE: tests/test_PT001_incorrect_fixture_parentheses_style.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import IncorrectFixtureParenthesesStyle from flake8_pytest_style.visitors import FixturesVisitor # make the configs independent of the actual default _CONFIG_WITHOUT_PARENS = DEFAULT_CONFIG._replace(fixture_parentheses=False) _CONFIG_WITH_PARENS = DEFAULT_CONFIG._replace(fixture_parentheses=True) def test_ok_no_parameters(): code = """ import pytest @pytest.fixture() def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=_CONFIG_WITH_PARENS) def test_ok_with_parameters(): code = """ import pytest @pytest.fixture(scope='module') def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=_CONFIG_WITH_PARENS) def test_ok_without_parens(): code = """ import pytest @pytest.fixture def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=_CONFIG_WITHOUT_PARENS) def test_error_without_parens(): code = """ import pytest @pytest.fixture def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, IncorrectFixtureParenthesesStyle, config=_CONFIG_WITH_PARENS, expected_parens="()", actual_parens="", ) def test_error_with_parens(): code = """ import pytest @pytest.fixture() def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, IncorrectFixtureParenthesesStyle, config=_CONFIG_WITHOUT_PARENS, expected_parens="", actual_parens="()", ) ================================================ FILE: tests/test_PT002_fixture_positional_args.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import FixturePositionalArgs from flake8_pytest_style.visitors import FixturesVisitor def test_ok_no_args(): code = """ import pytest @pytest.fixture def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_only_kwargs(): code = """ import pytest @pytest.fixture(scope='module') def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_error_only_args(): code = """ import pytest @pytest.fixture('module') def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, FixturePositionalArgs, name="my_fixture", config=DEFAULT_CONFIG, ) def test_error_mixed(): code = """ import pytest @pytest.fixture('module', autouse=True) def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, FixturePositionalArgs, name="my_fixture", config=DEFAULT_CONFIG, ) ================================================ FILE: tests/test_PT003_extraneous_scope_function.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import ExtraneousScopeFunction from flake8_pytest_style.visitors import FixturesVisitor def test_ok_no_scope(): code = """ import pytest @pytest.fixture def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_other_scope(): code = """ import pytest @pytest.fixture(scope='module') def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_error(): code = """ import pytest @pytest.fixture(scope='function') def my_fixture(): return 0 """ assert_error(FixturesVisitor, code, ExtraneousScopeFunction, config=DEFAULT_CONFIG) ================================================ FILE: tests/test_PT004_missing_fixture_name_underscore.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import MissingFixtureNameUnderscore from flake8_pytest_style.visitors import FixturesVisitor def test_ok_simple(): code = """ import pytest @pytest.fixture def _patch_something(mocker): mocker.patch('some.thing') """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_with_return(): code = """ import pytest @pytest.fixture def _patch_something(mocker): if something: return mocker.patch('some.thing') """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_with_yield(): code = """ import pytest @pytest.fixture def _activate_context(): with context: yield """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_abstract_with_import_abc(): code = """ import abc import pytest class BaseTest: @pytest.fixture @abc.abstractmethod def my_fixture(): raise NotImplementedError """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_abstract_with_from_import(): code = """ from abc import abstractmethod import pytest class BaseTest: @pytest.fixture @abstractmethod def my_fixture(): raise NotImplementedError """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_ignoring_yield_from(): code = """ import pytest @pytest.fixture def my_fixture(): yield from some_generator() """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_error_simple(): code = """ import pytest @pytest.fixture def patch_something(mocker): mocker.patch('some.thing') """ assert_error( FixturesVisitor, code, MissingFixtureNameUnderscore, name="patch_something", config=DEFAULT_CONFIG, ) def test_error_with_yield(): code = """ import pytest @pytest.fixture def activate_context(): with context: yield """ assert_error( FixturesVisitor, code, MissingFixtureNameUnderscore, name="activate_context", config=DEFAULT_CONFIG, ) ================================================ FILE: tests/test_PT005_incorrect_fixture_name_underscore.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import IncorrectFixtureNameUnderscore from flake8_pytest_style.visitors import FixturesVisitor def test_ok_with_return(): code = """ import pytest @pytest.fixture def my_fixture(mocker): return 0 """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_with_yield(): code = """ import pytest @pytest.fixture def activate_context(): with get_context() as context: yield context """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_nested_function(): code = """ @pytest.fixture def _any_fixture(mocker): def nested_function(): return 1 mocker.patch('...', nested_function) """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_abstract_with_import_abc(): code = """ import abc import pytest class BaseTest: @pytest.fixture @abc.abstractmethod def _my_fixture(): return NotImplemented """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_abstract_with_from_import(): code = """ from abc import abstractmethod import pytest class BaseTest: @pytest.fixture @abstractmethod def _my_fixture(): return NotImplemented """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_error_with_return(): code = """ import pytest @pytest.fixture def _my_fixture(mocker): return 0 """ assert_error( FixturesVisitor, code, IncorrectFixtureNameUnderscore, name="_my_fixture", config=DEFAULT_CONFIG, ) def test_error_with_yield(): code = """ import pytest @pytest.fixture def _activate_context(): with get_context() as context: yield context """ assert_error( FixturesVisitor, code, IncorrectFixtureNameUnderscore, name="_activate_context", config=DEFAULT_CONFIG, ) def test_error_with_conditional_yield_from(): code = """ import pytest @pytest.fixture def _activate_context(): if some_condition: with get_context() as context: yield context else: yield from other_context() """ # since we have yield with value in one branch, # we assume that the fixture yields a value assert_error( FixturesVisitor, code, IncorrectFixtureNameUnderscore, name="_activate_context", config=DEFAULT_CONFIG, ) ================================================ FILE: tests/test_PT006_parametrize_names_wrong_type.py ================================================ import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG, ParametrizeNamesType from flake8_pytest_style.errors import ParametrizeNamesWrongType from flake8_pytest_style.visitors import ParametrizeVisitor NAMES_CSV = "name1,name2" NAMES_LIST = ["name1", "name2"] NAMES_TUPLE = ("name1", "name2") def test_ok_single(): code = """ import pytest pytest.mark.parametrize( 'name', ['a', 'b', 'c'], ) """ assert_not_error(ParametrizeVisitor, code, config=DEFAULT_CONFIG) @pytest.mark.parametrize( ("cfg_type", "names"), [ (ParametrizeNamesType.CSV, NAMES_CSV), (ParametrizeNamesType.LIST, NAMES_LIST), (ParametrizeNamesType.TUPLE, NAMES_TUPLE), ], ) def test_ok_multiple(cfg_type, names): code = f""" import pytest pytest.mark.parametrize( {names!r}, [ ('a', 'b'), ('c', 'd'), ], ) """ config = DEFAULT_CONFIG._replace(parametrize_names_type=cfg_type) assert_not_error(ParametrizeVisitor, code, config=config) def test_error_single_tuple(): code = """ import pytest pytest.mark.parametrize( ('name',), ['a', 'b', 'c'], ) """ assert_error( ParametrizeVisitor, code, ParametrizeNamesWrongType, expected_type="string", config=DEFAULT_CONFIG, ) @pytest.mark.parametrize( ("cfg_type", "names"), [ (ParametrizeNamesType.CSV, NAMES_LIST), (ParametrizeNamesType.CSV, NAMES_TUPLE), (ParametrizeNamesType.LIST, NAMES_CSV), (ParametrizeNamesType.LIST, NAMES_TUPLE), (ParametrizeNamesType.TUPLE, NAMES_CSV), (ParametrizeNamesType.TUPLE, NAMES_LIST), ], ) def test_error_multiple(cfg_type, names): code = f""" import pytest pytest.mark.parametrize( {names!r}, [ ('a', 'b'), ('c', 'd'), ], ) """ config = DEFAULT_CONFIG._replace(parametrize_names_type=cfg_type) assert_error( ParametrizeVisitor, code, ParametrizeNamesWrongType, expected_type=cfg_type.value, config=config, ) ================================================ FILE: tests/test_PT007_parametrize_values_wrong_type.py ================================================ import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import ( DEFAULT_CONFIG, ParametrizeValuesRowType, ParametrizeValuesType, ) from flake8_pytest_style.errors import ParametrizeValuesWrongType from flake8_pytest_style.visitors import ParametrizeVisitor VALUES_LIST = ["a", "b", "c"] VALUES_TUPLE = ("a", "b", "c") VALUES_LIST_OF_LISTS = [["a", "b"], ["c", "d"]] VALUES_LIST_OF_TUPLES = [("a", "b"), ("c", "d")] VALUES_TUPLE_OF_LISTS = (["a", "b"], ["c", "d"]) VALUES_TUPLE_OF_TUPLES = (("a", "b"), ("c", "d")) VALUES_LIST_OF_MIXED = [["a", "b"], ("c", "d")] VALUES_TUPLE_OF_MIXED = (["a", "b"], ("c", "d")) def _get_expected_type_str(values_cfg_type, rows_cfg_type): return f"{values_cfg_type.value} of {rows_cfg_type.value}s" @pytest.mark.parametrize( ("values_cfg_type", "values"), [ (ParametrizeValuesType.LIST, VALUES_LIST), (ParametrizeValuesType.TUPLE, VALUES_TUPLE), ], ) def test_ok_single(values_cfg_type, values): code = f""" import pytest pytest.mark.parametrize( 'name', {values!r}, ) """ config = DEFAULT_CONFIG._replace(parametrize_values_type=values_cfg_type) assert_not_error(ParametrizeVisitor, code, config=config) @pytest.mark.parametrize( ("values_cfg_type", "rows_cfg_type", "values"), [ ( ParametrizeValuesType.LIST, ParametrizeValuesRowType.LIST, VALUES_LIST_OF_LISTS, ), ( ParametrizeValuesType.TUPLE, ParametrizeValuesRowType.LIST, VALUES_TUPLE_OF_LISTS, ), ( ParametrizeValuesType.LIST, ParametrizeValuesRowType.TUPLE, VALUES_LIST_OF_TUPLES, ), ( ParametrizeValuesType.TUPLE, ParametrizeValuesRowType.TUPLE, VALUES_TUPLE_OF_TUPLES, ), ], ) def test_ok_multiple(values_cfg_type, rows_cfg_type, values): code = f""" import pytest pytest.mark.parametrize( ('name1', 'name2'), {values!r}, ) """ config = DEFAULT_CONFIG._replace( parametrize_values_type=values_cfg_type, parametrize_values_row_type=rows_cfg_type, ) assert_not_error(ParametrizeVisitor, code, config=config) @pytest.mark.parametrize( ("values_cfg_type", "values"), [ (ParametrizeValuesType.LIST, VALUES_TUPLE), (ParametrizeValuesType.TUPLE, VALUES_LIST), ], ) def test_error_single(values_cfg_type, values): code = f""" import pytest pytest.mark.parametrize( 'name', {values!r}, ) """ config = DEFAULT_CONFIG._replace(parametrize_values_type=values_cfg_type) assert_error( ParametrizeVisitor, code, ParametrizeValuesWrongType, expected_type=values_cfg_type.value, config=config, ) def test_error_single_tuple_as_decorator(): code = """ import pytest @pytest.mark.parametrize( 'name', ('a', 'b', 'c'), ) def test_smth(name): pass """ assert_error( ParametrizeVisitor, code, ParametrizeValuesWrongType, expected_type="list", config=DEFAULT_CONFIG, ) @pytest.mark.parametrize( ("values_cfg_type", "rows_cfg_type", "values"), [ ( ParametrizeValuesType.LIST, ParametrizeValuesRowType.LIST, VALUES_LIST_OF_TUPLES, ), ( ParametrizeValuesType.LIST, ParametrizeValuesRowType.LIST, VALUES_TUPLE_OF_LISTS, ), ( ParametrizeValuesType.LIST, ParametrizeValuesRowType.LIST, VALUES_TUPLE_OF_TUPLES, ), ( ParametrizeValuesType.TUPLE, ParametrizeValuesRowType.LIST, VALUES_LIST_OF_LISTS, ), ( ParametrizeValuesType.TUPLE, ParametrizeValuesRowType.LIST, VALUES_LIST_OF_TUPLES, ), ( ParametrizeValuesType.TUPLE, ParametrizeValuesRowType.LIST, VALUES_TUPLE_OF_TUPLES, ), ( ParametrizeValuesType.LIST, ParametrizeValuesRowType.TUPLE, VALUES_LIST_OF_LISTS, ), ( ParametrizeValuesType.LIST, ParametrizeValuesRowType.TUPLE, VALUES_TUPLE_OF_LISTS, ), ( ParametrizeValuesType.LIST, ParametrizeValuesRowType.TUPLE, VALUES_TUPLE_OF_TUPLES, ), ( ParametrizeValuesType.TUPLE, ParametrizeValuesRowType.TUPLE, VALUES_LIST_OF_LISTS, ), ( ParametrizeValuesType.TUPLE, ParametrizeValuesRowType.TUPLE, VALUES_LIST_OF_TUPLES, ), ( ParametrizeValuesType.TUPLE, ParametrizeValuesRowType.TUPLE, VALUES_TUPLE_OF_LISTS, ), ], ) def test_error_multiple(values_cfg_type, rows_cfg_type, values): code = f""" import pytest pytest.mark.parametrize( ('name1', 'name2'), {values!r}, ) """ config = DEFAULT_CONFIG._replace( parametrize_values_type=values_cfg_type, parametrize_values_row_type=rows_cfg_type, ) assert_error( ParametrizeVisitor, code, ParametrizeValuesWrongType, expected_type=_get_expected_type_str(values_cfg_type, rows_cfg_type), config=config, ) @pytest.mark.parametrize("values_cfg_type", list(ParametrizeValuesType)) @pytest.mark.parametrize("rows_cfg_type", list(ParametrizeValuesRowType)) @pytest.mark.parametrize("values", [VALUES_LIST_OF_MIXED, VALUES_TUPLE_OF_MIXED]) def test_error_multiple_mixed(values_cfg_type, rows_cfg_type, values): code = f""" import pytest pytest.mark.parametrize( ('name1', 'name2'), {values!r}, ) """ config = DEFAULT_CONFIG._replace( parametrize_values_type=values_cfg_type, parametrize_values_row_type=rows_cfg_type, ) assert_error( ParametrizeVisitor, code, ParametrizeValuesWrongType, expected_type=_get_expected_type_str(values_cfg_type, rows_cfg_type), config=config, ) ================================================ FILE: tests/test_PT008_patch_with_lambda.py ================================================ import sys import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.errors import PatchWithLambda from flake8_pytest_style.visitors import PatchVisitor HAS_POSITIONAL_ONLY_ARGS = sys.version_info >= (3, 8) parametrize_code_template = pytest.mark.parametrize( "code_template", [ "mocker.patch('module.name', {})", "module_mocker.patch('module.name', {})", "mocker.patch.object(obj, 'attr', {})", "module_mocker.patch.object(obj, 'attr', {})", ], ids=( "mocker.patch", "module_mocker.patch", "mocker.patch.object", "module_mocker.patch.object", ), ) @parametrize_code_template @pytest.mark.parametrize( "patch_with", [ "not_lambda", "return_value=None", "lambda x, y: x", "lambda *args: args", "lambda **kwargs: kwargs", pytest.param( "lambda x, /, y: x", marks=[ pytest.mark.skipif( not HAS_POSITIONAL_ONLY_ARGS, reason=f"unsupported in {sys.version}" ) ], ), ], ) def test_ok(code_template, patch_with): code = code_template.format(patch_with) assert_not_error(PatchVisitor, code) @parametrize_code_template @pytest.mark.parametrize( "patch_with", ["lambda: None", "lambda x, y: None", "lambda *args, **kwargs: None"] ) def test_error(code_template, patch_with): code = code_template.format(patch_with) assert_error(PatchVisitor, code, PatchWithLambda) ================================================ FILE: tests/test_PT009_unittest_assertion.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.errors import UnittestAssertion from flake8_pytest_style.visitors import UnittestAssertionVisitor def test_ok_no_parameters(): code = """ import pytest def test_xxx(): assert 1 == 1 """ assert_not_error(UnittestAssertionVisitor, code) def test_error(): code = """ import pytest def test_xxx(): self.assertEqual(1, 1) """ assert_error( UnittestAssertionVisitor, code, UnittestAssertion, assertion="assertEqual" ) ================================================ FILE: tests/test_PT010_raises_without_exception.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import RaisesWithoutException from flake8_pytest_style.visitors import RaisesVisitor def test_ok(): code = """ import pytest def test_something(): with pytest.raises(UnicodeError): pass """ assert_not_error(RaisesVisitor, code, config=DEFAULT_CONFIG) def test_error(): code = """ import pytest def test_something(): with pytest.raises(): pass """ assert_error(RaisesVisitor, code, RaisesWithoutException, config=DEFAULT_CONFIG) ================================================ FILE: tests/test_PT011_raises_too_broad.py ================================================ import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import RaisesTooBroad from flake8_pytest_style.visitors import RaisesVisitor def test_ok(): code = """ import pytest def test_something(): with pytest.raises(ValueError, match="Can't divide by 0"): raise ValueError("Can't divide by 0") """ assert_not_error(RaisesVisitor, code, config=DEFAULT_CONFIG) def test_ok_different_error_from_config(): code = """ import pytest def test_something(): with pytest.raises(ZeroDivisionError): raise ZeroDivisionError("Can't divide by 0") """ assert_not_error(RaisesVisitor, code, config=DEFAULT_CONFIG) @pytest.mark.parametrize("exception", ["ValueError", "socket.error"]) def test_error_no_argument_given(exception): code = f""" import pytest def test_something(): with pytest.raises({exception}): raise ValueError("Can't divide 1 by 0") """ assert_error( RaisesVisitor, code, RaisesTooBroad, config=DEFAULT_CONFIG, exception=exception, ) @pytest.mark.parametrize("match", ["None", '""', 'f""']) def test_error_match_is_empty(match): code = f""" import pytest def test_something(): with pytest.raises(ValueError, match={match}): raise ValueError("Can't divide 1 by 0") """ assert_error( RaisesVisitor, code, RaisesTooBroad, config=DEFAULT_CONFIG, exception="ValueError", ) ================================================ FILE: tests/test_PT012_raises_with_multiple_statements.py ================================================ import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import RaisesWithMultipleStatements from flake8_pytest_style.visitors import RaisesVisitor def test_ok(): code = """ def test_something(): with pytest.raises(AttributeError): [].size """ assert_not_error(RaisesVisitor, code, config=DEFAULT_CONFIG) @pytest.mark.parametrize("maybe_async", ["", "async "]) def test_ok_trivial_with(maybe_async): code = f""" async def test_something(): with pytest.raises(AttributeError): {maybe_async}with context_manager_under_test(): pass """ assert_not_error(RaisesVisitor, code, config=DEFAULT_CONFIG) def test_error_multiple_statements(): code = """ def test_something(): with pytest.raises(AttributeError): len([]) [].size """ assert_error( RaisesVisitor, code, RaisesWithMultipleStatements, config=DEFAULT_CONFIG ) @pytest.mark.parametrize( "statement", ["if", "for i in", "async for i in", "while", "with", "async with"] ) def test_error_complex_statement(statement): code = f""" async def test_something(): with pytest.raises(AttributeError): {statement} True: [].size """ assert_error( RaisesVisitor, code, RaisesWithMultipleStatements, config=DEFAULT_CONFIG ) def test_error_try(): code = """ def test_something(): with pytest.raises(AttributeError): try: [].size except: raise """ assert_error( RaisesVisitor, code, RaisesWithMultipleStatements, config=DEFAULT_CONFIG ) ================================================ FILE: tests/test_PT013_incorrect_pytest_import.py ================================================ import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.errors import IncorrectPytestImport from flake8_pytest_style.visitors.imports import ImportsVisitor @pytest.mark.parametrize( "code", [ "import pytest", "import pytest as pytest", "from notpytest import fixture", "from . import fixture", "from .pytest import fixture", ], ) def test_ok(code): assert_not_error(ImportsVisitor, code) @pytest.mark.parametrize( "code", [ "import pytest as other_name", "from pytest import fixture", "from pytest import fixture as other_name", ], ) def test_error(code): assert_error(ImportsVisitor, code, IncorrectPytestImport) ================================================ FILE: tests/test_PT014_duplicate_parametrize_test_cases.py ================================================ from flake8_plugin_utils import assert_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import DuplicateParametrizeTestCases from flake8_pytest_style.visitors import ParametrizeVisitor def test_error(): code = """ pytest.mark.parametrize( 'name', [ {1: 2, 3: {*other_set, 4, 5}, **otherdict}, {x: y}, {3: {5, *other_set, 4}, 1: 2, **otherdict}, ] ) """ assert_error( ParametrizeVisitor, code, DuplicateParametrizeTestCases, indexes=(1, 3), config=DEFAULT_CONFIG, ) ================================================ FILE: tests/test_PT015_assert_always_false.py ================================================ import pytest from flake8_plugin_utils.utils import assert_error, assert_not_error from flake8_pytest_style.errors import AssertAlwaysFalse from flake8_pytest_style.visitors import FailVisitor def test_ok(): code = """ def test_xxx(): assert [0] """ assert_not_error(FailVisitor, code) @pytest.mark.parametrize( "falsy_constant", [ "None", "False", "0", "0.0", '""', 'f""', "[]", "()", "{}", "list()", "set()", "tuple()", "dict()", "frozenset()", "list([])", "set(set())", 'tuple("")', ], ) def test_error(falsy_constant): code = f""" def test_xxx(): assert {falsy_constant} """ assert_error(FailVisitor, code, AssertAlwaysFalse) ================================================ FILE: tests/test_PT016_fail_without_message.py ================================================ import pytest from flake8_plugin_utils.utils import assert_error, assert_not_error from flake8_pytest_style.errors import FailWithoutMessage from flake8_pytest_style.visitors import FailVisitor def test_ok_arg(): code = """ def test_xxx(): pytest.fail('this is a failure') """ assert_not_error(FailVisitor, code) def test_ok_kwarg(): code = """ def test_xxx(): pytest.fail(reason='this is a failure') """ assert_not_error(FailVisitor, code) def test_ok_kwarg_legacy(): code = """ def test_xxx(): pytest.fail(msg='this is a failure') """ assert_not_error(FailVisitor, code) @pytest.mark.parametrize( "args", ["", '""', 'f""', 'reason=""', 'reason=f""', 'msg=""', 'msg=f""'] ) def test_error(args): code = f""" def test_xxx(): pytest.fail({args}) """ assert_error(FailVisitor, code, FailWithoutMessage) ================================================ FILE: tests/test_PT017_assert_in_except.py ================================================ from flake8_plugin_utils.utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import AssertInExcept from flake8_pytest_style.visitors import TryExceptVisitor def test_ok(): code = """ def test_xxx(): try: something() except Exception as e: something_else() with pytest.raises(ZeroDivisionError) as e: 1 / 0 assert e.value.message """ assert_not_error(TryExceptVisitor, code, config=DEFAULT_CONFIG) def test_error(): code = """ def test_xxx(): try: something() except Exception as e: assert e.message, 'blah blah' """ assert_error( TryExceptVisitor, code, AssertInExcept, name="e", config=DEFAULT_CONFIG ) ================================================ FILE: tests/test_PT018_composite_assertion.py ================================================ import pytest from flake8_plugin_utils.utils import assert_error, assert_not_error from flake8_pytest_style.errors import CompositeAssertion from flake8_pytest_style.visitors.assertion import AssertionVisitor def test_ok(): code = """ def test_xxx(): assert something assert something or something_else assert something or something_else and something_third assert not (something and something_else) """ assert_not_error(AssertionVisitor, code) @pytest.mark.parametrize( "condition", [ "something and something_else", "something and something_else and something_third", "something and not something_else", "something and (something_else or something_third)", "not (something or something_else)", "not (something or something_else or something_third)", "not (something or something_else and something_third)", ], ) def test_error(condition): code = f""" def test_xxx(): assert {condition} """ assert_error(AssertionVisitor, code, CompositeAssertion) ================================================ FILE: tests/test_PT019_fixture_param_without_value.py ================================================ from flake8_plugin_utils.utils import assert_error, assert_not_error from flake8_pytest_style.errors import FixtureParamWithoutValue from flake8_pytest_style.visitors import TFunctionsVisitor def test_ok_good_param_name(): code = """ def test_xxx(fixture): pass """ assert_not_error(TFunctionsVisitor, code) def test_ok_non_test_function(): code = """ def xxx(_param): pass """ assert_not_error(TFunctionsVisitor, code) def test_error_arg(): code = """ def test_xxx(_fixture): pass """ assert_error(TFunctionsVisitor, code, FixtureParamWithoutValue, name="_fixture") def test_error_kwonly(): code = """ def test_xxx(*, _fixture): pass """ assert_error(TFunctionsVisitor, code, FixtureParamWithoutValue, name="_fixture") ================================================ FILE: tests/test_PT020_deprecated_yield_fixture.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import DeprecatedYieldFixture from flake8_pytest_style.visitors import FixturesVisitor # make the configs independent of the actual default _CONFIG_WITHOUT_PARENS = DEFAULT_CONFIG._replace(fixture_parentheses=False) _CONFIG_WITH_PARENS = DEFAULT_CONFIG._replace(fixture_parentheses=True) def test_ok_no_parameters(): code = """ import pytest @pytest.fixture() def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=_CONFIG_WITH_PARENS) def test_ok_without_parens(): code = """ import pytest @pytest.fixture def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=_CONFIG_WITHOUT_PARENS) def test_error_without_parens(): code = """ import pytest @pytest.yield_fixture def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, DeprecatedYieldFixture, config=_CONFIG_WITHOUT_PARENS ) def test_error_with_parens(): code = """ import pytest @pytest.yield_fixture() def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, DeprecatedYieldFixture, config=_CONFIG_WITH_PARENS ) ================================================ FILE: tests/test_PT021_fixture_finalizer_callback.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import FixtureFinalizerCallback from flake8_pytest_style.visitors import FixturesVisitor def test_ok_return(): code = """ import pytest @pytest.fixture def my_fixture(): return 0 """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_yield(): code = """ import pytest @pytest.fixture def my_fixture(): resource = acquire_resource() yield resource resource.release() """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_other_request(): code = """ import pytest @pytest.fixture def my_fixture(): request = get_request() request.addfinalizer(finalizer) return request """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_other_function(): # factory fixtures are a tricky use-case where addfinalizer cannot be replaced # inspired by tls_http_server fixture in cherrypy/cheroot code = """ import functools import pytest def create_resource(arg, request): resource = Resource(arg) request.addfinalizer(resource.release) return resource @pytest.fixture def resource_factory(request): return functools.partial(create_resource, request=request) """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_nested_function(): # https://github.com/m-burst/flake8-pytest-style/issues/46 code = """ import pytest @pytest.fixture def resource_factory(request): def create_resource(arg) -> Resource: resource = Resource(arg) request.addfinalizer(resource.release) return resource return create_resource """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_error_return(): code = """ import pytest @pytest.fixture def my_fixture(request): resource = acquire_resource() request.addfinalizer(resource.release) return resource """ assert_error(FixturesVisitor, code, FixtureFinalizerCallback, config=DEFAULT_CONFIG) def test_error_yield(): code = """ import pytest @pytest.fixture def my_fixture(request): resource = acquire_resource() request.addfinalizer(resource.release) yield resource resource # prevent PT022 """ assert_error(FixturesVisitor, code, FixtureFinalizerCallback, config=DEFAULT_CONFIG) ================================================ FILE: tests/test_PT022_useless_yield_fixture.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import UselessYieldFixture from flake8_pytest_style.visitors import FixturesVisitor # Skipping basic OK tests because we have A LOT of valid fixtures in other # test files def test_ok_complex_logic(): code = """ import pytest @pytest.fixture def my_fixture(): if some_condition: resource = acquire_resource() yield resource resource.release() return yield None """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_error(): code = """ import pytest @pytest.fixture def my_fixture(): resource = acquire_resource() yield resource """ assert_error( FixturesVisitor, code, UselessYieldFixture, name="my_fixture", config=DEFAULT_CONFIG, ) ================================================ FILE: tests/test_PT023_incorrect_mark_parentheses_style.py ================================================ import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import IncorrectMarkParenthesesStyle from flake8_pytest_style.visitors import MarksVisitor _CONFIG_WITHOUT_PARENS = DEFAULT_CONFIG._replace(mark_parentheses=False) _CONFIG_WITH_PARENS = DEFAULT_CONFIG._replace(mark_parentheses=True) SAMPLES_WITHOUT_PARENTHESES = [ pytest.param( """ import pytest @pytest.mark.foo def test_something(): pass """, id="function", ), pytest.param( """ import pytest @pytest.mark.foo class TestClass: def test_something(): pass """, id="class", ), pytest.param( """ import pytest class TestClass: @pytest.mark.foo def test_something(): pass """, id="method", ), pytest.param( """ import pytest class TestClass: @pytest.mark.foo class TestNestedClass: def test_something(): pass """, id="nested_class", ), pytest.param( """ import pytest class TestClass: class TestNestedClass: @pytest.mark.foo def test_something(): pass """, id="nested_class_method", ), ] SAMPLES_WITH_PARENTHESES = [ pytest.param( """ import pytest @pytest.mark.foo() def test_something(): pass """, id="function", ), pytest.param( """ import pytest @pytest.mark.foo() class TestClass: def test_something(): pass """, id="class", ), pytest.param( """ import pytest class TestClass: @pytest.mark.foo() def test_something(): pass """, id="method", ), pytest.param( """ import pytest class TestClass: @pytest.mark.foo() class TestNestedClass: def test_something(): pass """, id="nested_class", ), pytest.param( """ import pytest class TestClass: class TestNestedClass: @pytest.mark.foo() def test_something(): pass """, id="nested_class_method", ), ] @pytest.mark.parametrize("mark_parentheses", [True, False]) def test_ok_with_parameters_regardless_of_config(mark_parentheses: bool): code = """ import pytest @pytest.mark.foo(scope='module') def test_something(): pass """ config = DEFAULT_CONFIG._replace(mark_parentheses=mark_parentheses) assert_not_error(MarksVisitor, code, config=config) @pytest.mark.parametrize("code", SAMPLES_WITHOUT_PARENTHESES) def test_ok_without_parens(code: str): assert_not_error(MarksVisitor, code, config=_CONFIG_WITHOUT_PARENS) @pytest.mark.parametrize("code", SAMPLES_WITH_PARENTHESES) def test_ok_with_parens(code: str): assert_not_error(MarksVisitor, code, config=_CONFIG_WITH_PARENS) @pytest.mark.parametrize("code", SAMPLES_WITHOUT_PARENTHESES) def test_error_without_parens(code: str): assert_error( MarksVisitor, code, IncorrectMarkParenthesesStyle, config=_CONFIG_WITH_PARENS, mark_name="foo", expected_parens="()", actual_parens="", ) @pytest.mark.parametrize("code", SAMPLES_WITH_PARENTHESES) def test_error_with_parens(code: str): assert_error( MarksVisitor, code, IncorrectMarkParenthesesStyle, config=_CONFIG_WITHOUT_PARENS, mark_name="foo", expected_parens="", actual_parens="()", ) ================================================ FILE: tests/test_PT024_unncessary_asyncio_mark_on_fixture.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import UnnecessaryAsyncioMarkOnFixture from flake8_pytest_style.visitors import FixturesVisitor def test_ok_not_fixture(): code = """ import pytest @pytest.mark.asyncio() async def test_something(): pass """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_ok_not_fixture_no_parens(): code = """ import pytest @pytest.mark.asyncio async def test_something(): pass """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_error_before(): code = """ import pytest @pytest.mark.asyncio() @pytest.fixture async def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, UnnecessaryAsyncioMarkOnFixture, config=DEFAULT_CONFIG ) def test_error_before_no_parens(): code = """ import pytest @pytest.mark.asyncio @pytest.fixture async def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, UnnecessaryAsyncioMarkOnFixture, config=DEFAULT_CONFIG ) def test_error_after(): code = """ import pytest @pytest.fixture @pytest.mark.asyncio() async def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, UnnecessaryAsyncioMarkOnFixture, config=DEFAULT_CONFIG ) def test_error_after_no_parens(): code = """ import pytest @pytest.fixture @pytest.mark.asyncio async def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, UnnecessaryAsyncioMarkOnFixture, config=DEFAULT_CONFIG ) ================================================ FILE: tests/test_PT025_erroneous_usefixtures_on_fixture.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import ErroneousUseFixturesOnFixture from flake8_pytest_style.visitors import FixturesVisitor def test_ok_not_fixture(): code = """ import pytest @pytest.mark.usefixtures('a') def test_something(): pass """ assert_not_error(FixturesVisitor, code, config=DEFAULT_CONFIG) def test_error_before(): code = """ import pytest @pytest.mark.usefixtures('a') @pytest.fixture def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, ErroneousUseFixturesOnFixture, config=DEFAULT_CONFIG, ) def test_error_after(): code = """ import pytest @pytest.fixture @pytest.mark.usefixtures('a') def my_fixture(): return 0 """ assert_error( FixturesVisitor, code, ErroneousUseFixturesOnFixture, config=DEFAULT_CONFIG, ) ================================================ FILE: tests/test_PT026_usefixtures_without_parameters.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import UseFixturesWithoutParameters from flake8_pytest_style.visitors import MarksVisitor def test_ok(): code = """ import pytest @pytest.mark.usefixtures('a') def test_something(): pass """ assert_not_error(MarksVisitor, code, config=DEFAULT_CONFIG) def test_ok_another_mark_with_parens(): code = """ import pytest @pytest.mark.foo() def test_something(): pass """ config = DEFAULT_CONFIG._replace(mark_parentheses=True) assert_not_error(MarksVisitor, code, config=config) def test_ok_another_mark_no_parens(): code = """ import pytest @pytest.mark.foo def test_something(): pass """ config = DEFAULT_CONFIG._replace(mark_parentheses=False) assert_not_error(MarksVisitor, code, config=config) def test_error_with_parens(): code = """ import pytest @pytest.mark.usefixtures() def test_something(): pass """ config = DEFAULT_CONFIG._replace(mark_parentheses=True) assert_error(MarksVisitor, code, UseFixturesWithoutParameters, config=config) def test_error_no_parens(): code = """ import pytest @pytest.mark.usefixtures def test_something(): pass """ config = DEFAULT_CONFIG._replace(mark_parentheses=False) assert_error(MarksVisitor, code, UseFixturesWithoutParameters, config=config) ================================================ FILE: tests/test_PT027_unittest_raises_assertion.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.errors import UnittestRaisesAssertion from flake8_pytest_style.visitors import UnittestAssertionVisitor def test_ok_no_parameters(): code = """ import pytest def test_xxx(): with pytest.raises(ValueError): raise ValueError() """ assert_not_error(UnittestAssertionVisitor, code) def test_error(): code = """ import pytest def test_xxx(): with self.assertRaises(ValueError): raise ValueError() """ assert_error( UnittestAssertionVisitor, code, UnittestRaisesAssertion, assertion="assertRaises", ) ================================================ FILE: tests/test_PT028_test_function_argument_with_default.py ================================================ import ast import textwrap from flake8_plugin_utils.utils import assert_error, assert_not_error from flake8_pytest_style.errors import TFunctionArgumentWithDefault from flake8_pytest_style.visitors import TFunctionsVisitor def test_ok(): code = """ def test_xxx(fixture1, /, fixture2, *, fixture3): pass """ assert_not_error(TFunctionsVisitor, code) def test_ok_non_test_function(): code = """ def xxx(posonly1, posonly2=2, /, arg=3, *, kwonly=4): pass """ assert_not_error(TFunctionsVisitor, code) def test_error_posonly(): code = """ def test_xxx(posonly1, posonly2=2, /): pass """ assert_error( TFunctionsVisitor, code, TFunctionArgumentWithDefault, name="test_xxx", arg="posonly2", ) def test_error_arg(): code = """ def test_xxx(arg1, arg2=2): pass """ assert_error( TFunctionsVisitor, code, TFunctionArgumentWithDefault, name="test_xxx", arg="arg2", ) def test_error_kwonly(): code = """ def test_xxx(*, kwonly1, kwonly2=2): pass """ assert_error( TFunctionsVisitor, code, TFunctionArgumentWithDefault, name="test_xxx", arg="kwonly2", ) def test_error_multiple(): code = """ def test_xxx( posonly=1, /, arg=2, *, kwonly=3, ): pass """ # flake8-plugin-utils does not allow multiple errors in a single test visitor = TFunctionsVisitor() visitor.visit(ast.parse(textwrap.dedent(code))) assert len(visitor.errors) == 3 posonly_error, arg_error, kwonly_error = visitor.errors assert isinstance(posonly_error, TFunctionArgumentWithDefault) assert posonly_error.message == TFunctionArgumentWithDefault.formatted_message( name="test_xxx", arg="posonly" ) assert posonly_error.lineno == 3 assert isinstance(arg_error, TFunctionArgumentWithDefault) assert arg_error.message == TFunctionArgumentWithDefault.formatted_message( name="test_xxx", arg="arg" ) assert arg_error.lineno == 5 assert isinstance(kwonly_error, TFunctionArgumentWithDefault) assert kwonly_error.message == TFunctionArgumentWithDefault.formatted_message( name="test_xxx", arg="kwonly" ) assert kwonly_error.lineno == 7 ================================================ FILE: tests/test_PT029_warns_without_warning.py ================================================ from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import WarnsWithoutException from flake8_pytest_style.visitors import WarnsVisitor def test_ok(): code = """ import pytest def test_something(): with pytest.warns(UnicodeError): pass """ assert_not_error(WarnsVisitor, code, config=DEFAULT_CONFIG) def test_error(): code = """ import pytest def test_something(): with pytest.warns(): pass """ assert_error(WarnsVisitor, code, WarnsWithoutException, config=DEFAULT_CONFIG) ================================================ FILE: tests/test_PT030_warns_too_broad.py ================================================ import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import WarnsTooBroad from flake8_pytest_style.visitors import WarnsVisitor def test_ok(): code = """ import pytest import warnings def test_something(): with pytest.warns(UserWarning, match="foo"): warnings.warn("foo", UserWarning) """ assert_not_error(WarnsVisitor, code, config=DEFAULT_CONFIG) def test_ok_different_error_from_config(): code = """ import pytest import warnings def test_something(): with pytest.warns(BytesWarning): warnings.warn("foo", UserWarning) """ assert_not_error(WarnsVisitor, code, config=DEFAULT_CONFIG) @pytest.mark.parametrize("warning", ["Warning", "UserWarning"]) def test_error_no_argument_given(warning): code = f""" import pytest import warnings def test_something(): with pytest.warns({warning}): warnings.warn("foo", UserWarning) """ assert_error( WarnsVisitor, code, WarnsTooBroad, config=DEFAULT_CONFIG, warning=warning, ) @pytest.mark.parametrize("match", ["None", '""', 'f""']) def test_error_match_is_empty(match): code = f""" import pytest import warnings def test_something(): with pytest.warns(UserWarning, match={match}): warnings.warn("foo", UserWarning) """ assert_error( WarnsVisitor, code, WarnsTooBroad, config=DEFAULT_CONFIG, warning="UserWarning", ) ================================================ FILE: tests/test_PT031_warns_with_multiple_statements.py ================================================ import pytest from flake8_plugin_utils import assert_error, assert_not_error from flake8_pytest_style.config import DEFAULT_CONFIG from flake8_pytest_style.errors import WarnsWithMultipleStatements from flake8_pytest_style.visitors import WarnsVisitor def test_ok(): code = """ def test_something(): with pytest.warns(BytesWarning): [].size """ assert_not_error(WarnsVisitor, code, config=DEFAULT_CONFIG) @pytest.mark.parametrize("maybe_async", ["", "async "]) def test_ok_trivial_with(maybe_async): code = f""" async def test_something(): with pytest.warns(BytesWarning): {maybe_async}with context_manager_under_test(): pass """ assert_not_error(WarnsVisitor, code, config=DEFAULT_CONFIG) def test_error_multiple_statements(): code = """ def test_something(): with pytest.warns(BytesWarning): len([]) [].size """ assert_error(WarnsVisitor, code, WarnsWithMultipleStatements, config=DEFAULT_CONFIG) @pytest.mark.parametrize( "statement", ["if", "for i in", "async for i in", "while", "with", "async with"] ) def test_error_complex_statement(statement): code = f""" async def test_something(): with pytest.warns(BytesWarning): {statement} True: [].size """ assert_error(WarnsVisitor, code, WarnsWithMultipleStatements, config=DEFAULT_CONFIG) def test_error_try(): code = """ def test_something(): with pytest.warns(BytesWarning): try: [].size except: raise """ assert_error(WarnsVisitor, code, WarnsWithMultipleStatements, config=DEFAULT_CONFIG) ================================================ FILE: tests/test_config.py ================================================ from typing import List import flake8 import pytest from flake8.options.manager import OptionManager from flake8_pytest_style.config import ( DEFAULT_CONFIG, Config, ParametrizeNamesType, ParametrizeValuesRowType, ParametrizeValuesType, ) from flake8_pytest_style.plugin import PytestStylePlugin @pytest.fixture def option_manager() -> OptionManager: manager = OptionManager( version=flake8.__version__, plugin_versions="", # Not necessary in tests parents=[], formatter_names=[], ) PytestStylePlugin.add_options(option_manager=manager) return manager def parse_options(manager: OptionManager, args: List[str]) -> Config: namespace = manager.parse_args(args) return PytestStylePlugin.parse_options_to_config( manager, namespace, # Not sure if this is semantically correct, but this is what flake8 passes # to plugin's parse_options namespace.filenames, ) def test_parse_default(option_manager): assert parse_options(option_manager, []) == DEFAULT_CONFIG def test_parse_raises_require_match_for(option_manager): config = parse_options( option_manager, ["--pytest-raises-require-match-for", "ValueError,TypeError"] ) assert config.raises_require_match_for == ["ValueError", "TypeError"] def test_parse_fixture_parentheses(option_manager): config = parse_options(option_manager, ["--pytest-fixture-no-parentheses"]) assert config.fixture_parentheses is False @pytest.mark.parametrize("value", list(ParametrizeNamesType)) def test_parse_parametrize_names_type(option_manager, value): config = parse_options( option_manager, ["--pytest-parametrize-names-type", value.value] ) assert config.parametrize_names_type is value @pytest.mark.parametrize("value", list(ParametrizeValuesType)) def test_parse_parametrize_values_type(option_manager, value): config = parse_options( option_manager, ["--pytest-parametrize-values-type", value.value] ) assert config.parametrize_values_type is value @pytest.mark.parametrize("value", list(ParametrizeValuesRowType)) def test_parse_parametrize_values_row_type(option_manager, value): config = parse_options( option_manager, ["--pytest-parametrize-values-row-type", value.value] ) assert config.parametrize_values_row_type is value @pytest.mark.parametrize( "args", [ ["--pytest-parametrize-names-type", "str"], ["--pytest-parametrize-values-type", "str"], ["--pytest-parametrize-values-row-type", "str"], ], ) def test_parse_invalid_enum_values(option_manager, args): with pytest.raises(SystemExit): # as raised by optparse parse_options(option_manager, args) ================================================ FILE: tests/test_plugin.py ================================================ import importlib import inspect import pkgutil from collections import Counter from types import ModuleType from typing import List, Set, Type, TypeVar from flake8_pytest_style.plugin import PytestStylePlugin T = TypeVar("T") def _collect_subclasses(modules: List[ModuleType], base_class: Type[T]) -> Set[Type[T]]: result = set() for module in modules: for _, member in inspect.getmembers(module): if ( inspect.isclass(member) and issubclass(member, base_class) and member is not base_class ): result.add(member) return result def test_plugin_has_all_visitors(): from flake8_plugin_utils import Visitor from flake8_pytest_style import visitors as visitors_package visitor_module_infos = pkgutil.iter_modules( visitors_package.__path__ # type: ignore ) visitor_modules = [ importlib.import_module( f"{visitors_package.__name__}.{visitor_module_info.name}" ) for visitor_module_info in visitor_module_infos ] visitor_classes = _collect_subclasses(visitor_modules, Visitor) assert set(PytestStylePlugin.visitors) == visitor_classes def test_all_error_codes_are_different(): from flake8_plugin_utils import Error from flake8_pytest_style import errors error_classes = _collect_subclasses([errors], Error) count_by_code = Counter(error_class.code for error_class in error_classes) duplicate_error_codes = [code for code, count in count_by_code.items() if count > 1] assert not duplicate_error_codes