├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── pylint_pytest ├── __init__.py ├── checkers │ ├── __init__.py │ ├── class_attr_loader.py │ └── fixture.py └── utils.py ├── sandbox └── .placeholder ├── setup.cfg ├── setup.py ├── tests ├── base_tester.py ├── input │ ├── cannot-enumerate-pytest-fixtures │ │ ├── import_corrupted_module.py │ │ └── no_such_package.py │ ├── conftest.py │ ├── deprecated-positional-argument-for-pytest-fixture │ │ ├── with_args_scope.py │ │ ├── with_kwargs_scope.py │ │ └── without_scope.py │ ├── deprecated-pytest-yield-fixture │ │ ├── func.py │ │ └── smoke.py │ ├── no-member │ │ ├── assign_attr_of_attr.py │ │ ├── fixture.py │ │ ├── from_unpack.py │ │ ├── inheritance.py │ │ ├── not_using_cls.py │ │ └── yield_fixture.py │ ├── redefined-outer-name │ │ ├── args_and_kwargs.py │ │ ├── caller_not_a_test_func.py │ │ ├── caller_yield_fixture.py │ │ └── smoke.py │ ├── regression │ │ └── import_twice.py │ ├── unused-argument │ │ ├── args_and_kwargs.py │ │ ├── caller_not_a_test_func.py │ │ ├── caller_yield_fixture.py │ │ └── smoke.py │ ├── unused-import │ │ ├── _fixture_for_conftest.py │ │ ├── _same_name_module.py │ │ ├── caller_yield_fixture.py │ │ ├── conftest.py │ │ ├── same_name_arg.py │ │ ├── same_name_decorator.py │ │ └── smoke.py │ └── useless-pytest-mark-decorator │ │ ├── mark_usefixture_using_for_class.py │ │ ├── mark_usefixture_using_for_fixture_attribute.py │ │ ├── mark_usefixture_using_for_fixture_function.py │ │ ├── mark_usefixture_using_for_test.py │ │ ├── not_pytest_marker.py │ │ └── other_marks_using_for_fixture.py ├── test_cannot_enumerate_fixtures.py ├── test_no_member.py ├── test_pytest_fixture_positional_arguments.py ├── test_pytest_mark_for_fixtures.py ├── test_pytest_yield_fixture.py ├── test_redefined_outer_name.py ├── test_regression.py ├── test_unused_argument.py └── test_unused_import.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Package versions 15 | - pylint 16 | - pytest 17 | - pylint-pytest 18 | 19 | (add any relevant pylint/pytest plugin here) 20 | 21 | Folder structure 22 | ``` 23 | ``` 24 | 25 | File content 26 | ```python 27 | ``` 28 | 29 | pylint output with the plugin 30 | ```bash 31 | ``` 32 | 33 | (Optional) pytest output from fixture collection 34 | ```bash 35 | $ pytest --fixtures --collect-only 36 | 37 | ``` 38 | 39 | **Expected behavior** 40 | A clear and concise description of what you expected to happen. 41 | 42 | **Additional context** 43 | Add any other context about the problem here. 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Feature request type** 11 | - [ ] Suppress false positives from existing pylint warnings 12 | - [ ] Add new pytest-specific warning 13 | - [ ] Other 14 | 15 | **Is your feature request related to a problem? Please describe.** 16 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 17 | 18 | **Describe the solution you'd like** 19 | A clear and concise description of what you want to happen. 20 | 21 | **Describe alternatives you've considered** 22 | A clear and concise description of any alternative solutions or features you've considered. 23 | 24 | **Sample code to demonstrate the current imperfect behavior** 25 | ```python 26 | ``` 27 | 28 | **Additional context** 29 | Add any other context or screenshots about the feature request here. 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | .tox/ 4 | build/ 5 | sandbox/**/*.py 6 | Pipfile* 7 | dist/ 8 | Makefile 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - python: 3.6 6 | env: TOX_ENV=py36 7 | - python: 3.7 8 | env: TOX_ENV=py37 9 | - python: 3.8 10 | env: TOX_ENV=py38 11 | - python: 3.9 12 | env: TOX_ENV=py39 13 | 14 | install: 15 | - pip install tox 16 | 17 | script: 18 | - tox -e $TOX_ENV 19 | 20 | before_cache: 21 | - rm -rf $HOME/.cache/pip/log 22 | 23 | cache: 24 | directories: 25 | - $HOME/.cache/pip 26 | 27 | deploy: 28 | provider: pypi 29 | user: __token__ 30 | on: 31 | tags: true 32 | condition: "$TOX_ENV = py38" 33 | distributions: bdist_wheel 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [1.1.2] - 2021-04-19 6 | ### Fixed 7 | - Fix #18 plugin crash when test case is marked with a non-pytest.mark decorator 8 | 9 | ## [1.1.1] - 2021-04-12 10 | ### Fixed 11 | - Fix pytest fixture collection error on non-test modules 12 | 13 | ## [1.1.0] - 2021-04-11 14 | ### Added 15 | - W6402 `useless-pytest-mark-decorator`: add warning for [using pytest.mark on fixtures](https://docs.pytest.org/en/stable/reference.html#marks) (thanks to @DKorytkin) 16 | - W6403 `deprecated-positional-argument-for-pytest-fixture`: add warning for [positional arguments to pytest.fixture()](https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only) (thanks to @DKorytkin) 17 | - F6401 `cannot-enumerate-pytest-fixtures`: add fatal error when the plugin cannot enumerate and collect pytest fixtures for analysis (#27) 18 | 19 | ## [1.0.3] - 2021-03-13 20 | ### Fixed 21 | - Fix #13 regression caused by mangling `sys.modules` 22 | 23 | ## [1.0.2] - 2021-03-10 24 | ### Fixed 25 | - Fix pytest **Module already imported so cannot be rewritten** warning when the package being linted was used by pytest/conftest already (#10) 26 | - Fix missing Python version constraint (#11) 27 | 28 | ## [1.0.1] - 2021-03-03 29 | ### Added 30 | - Suppressing FP `unused-import` when fixtures defined elsewhere are imported into `conftest.py` but not directly used (#2) 31 | 32 | ## [1.0.0] - 2021-03-02 33 | ### Added 34 | - Python 3.9 support 35 | 36 | ### Removed 37 | - Python 2.7 & 3.5 support 38 | 39 | ### Fixed 40 | - Fix not able to work with `pytest-xdist` plugin when `--dist loadfile` is set in configuration file (#5) 41 | 42 | ## [0.3.0] - 2020-08-10 43 | ### Added 44 | - W6401 `deprecated-pytest-yield-fixture`: add warning for [yield_fixture functions](https://docs.pytest.org/en/latest/yieldfixture.html) 45 | 46 | ### Fixed 47 | - Fix incorrect path separator for Windows (#1) 48 | 49 | ## [0.2.0] - 2020-05-25 50 | ### Added 51 | - Suppressing FP `no-member` from [using workaround of accessing cls in setup fixture](https://github.com/pytest-dev/pytest/issues/3778#issuecomment-411899446) 52 | 53 | ### Changed 54 | - Refactor plugin to group patches and augmentations 55 | 56 | ## [0.1.2] - 2020-05-22 57 | ### Fixed 58 | - Fix fixtures defined with `@pytest.yield_fixture` decorator still showing FP 59 | - Fix crashes when using fixture + if + inline import 60 | - Fix crashes when relatively importing fixtures (`from ..conftest import fixture`) 61 | 62 | ## [0.1.1] - 2020-05-19 63 | ### Fixed 64 | - Fix crashes when `*args` or `**kwargs` is used in FuncDef 65 | 66 | ## [0.1] - 2020-05-18 67 | ### Added 68 | - Suppressing FP `unused-import` with tests 69 | - Suppressing FP `unused-argument` with tests 70 | - Suppressing FP `redefined-outer-scope` with tests 71 | - Add CI/CD configuration with Travis CI 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Reverb Chu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pylint-pytest 2 | 3 | [![PyPI version fury.io](https://badge.fury.io/py/pylint-pytest.svg)](https://pypi.python.org/pypi/pylint-pytest/) 4 | [![Travis CI](https://travis-ci.org/reverbc/pylint-pytest.svg?branch=master)](https://travis-ci.org/reverbc/pylint-pytest) 5 | [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/reverbc/pylint-pytest?branch=master&svg=true)](https://ci.appveyor.com/project/reverbc/pylint-pytest) 6 | 7 | A Pylint plugin to suppress pytest-related false positives. 8 | 9 | ## Installation 10 | 11 | Requirements: 12 | 13 | - `pylint` 14 | - `pytest>=4.6` 15 | 16 | To install: 17 | 18 | ```bash 19 | $ pip install pylint-pytest 20 | ``` 21 | 22 | ## Usage 23 | 24 | Enable via command line option `--load-plugins` 25 | 26 | ```bash 27 | $ pylint --load-plugins pylint_pytest 28 | ``` 29 | 30 | Or in `pylintrc`: 31 | 32 | ```ini 33 | [MASTER] 34 | load-plugins=pylint_pytest 35 | ``` 36 | 37 | ## Suppressed Pylint Warnings 38 | 39 | ### `unused-argument` 40 | 41 | FP when a fixture is used in an applicable function but not referenced in the function body, e.g. 42 | 43 | ```python 44 | def test_something(conftest_fixture): # <- Unused argument 'conftest_fixture' 45 | assert True 46 | ``` 47 | 48 | ### `unused-import` 49 | 50 | FP when an imported fixture is used in an applicable function, e.g. 51 | 52 | ```python 53 | from fixture_collections import imported_fixture # <- Unused imported_fixture imported from fixture_collections 54 | 55 | def test_something(imported_fixture): 56 | ... 57 | ``` 58 | 59 | ### `redefined-outer-name` 60 | 61 | FP when an imported/declared fixture is used in an applicable function, e.g. 62 | 63 | ```python 64 | from fixture_collections import imported_fixture 65 | 66 | def test_something(imported_fixture): # <- Redefining name 'imported_fixture' from outer scope (line 1) 67 | ... 68 | ``` 69 | 70 | ### `no-member` 71 | 72 | FP when class attributes are defined in setup fixtures 73 | 74 | ```python 75 | import pytest 76 | 77 | class TestClass(object): 78 | @staticmethod 79 | @pytest.fixture(scope='class', autouse=True) 80 | def setup_class(request): 81 | cls = request.cls 82 | cls.defined_in_setup_class = True 83 | 84 | def test_foo(self): 85 | assert self.defined_in_setup_class # <- Instance of 'TestClass' has no 'defined_in_setup_class' member 86 | ``` 87 | 88 | ## Raise new warning(s) 89 | 90 | ### W6401 `deprecated-pytest-yield-fixture` 91 | 92 | Raise when using deprecated `@pytest.yield_fixture` decorator ([ref](https://docs.pytest.org/en/latest/yieldfixture.html)) 93 | 94 | ```python 95 | import pytest 96 | 97 | @pytest.yield_fixture # <- Using a deprecated @pytest.yield_fixture decorator 98 | def yield_fixture(): 99 | yield 100 | ``` 101 | 102 | ### W6402 `useless-pytest-mark-decorator` 103 | 104 | Raise when using every `@pytest.mark.*` for the fixture ([ref](https://docs.pytest.org/en/stable/reference.html#marks)) 105 | 106 | ```python 107 | import pytest 108 | 109 | @pytest.fixture 110 | def awesome_fixture(): 111 | ... 112 | 113 | @pytest.fixture 114 | @pytest.mark.usefixtures("awesome_fixture") # <- Using useless `@pytest.mark.*` decorator for fixtures 115 | def another_awesome_fixture(): 116 | ... 117 | ``` 118 | 119 | ### W6403 `deprecated-positional-argument-for-pytest-fixture` 120 | 121 | Raise when using deprecated positional arguments for fixture decorator ([ref](https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only)) 122 | 123 | ```python 124 | import pytest 125 | 126 | @pytest.fixture("module") # <- Using a deprecated positional arguments for fixture 127 | def awesome_fixture(): 128 | ... 129 | ``` 130 | 131 | ### F6401 `cannot-enumerate-pytest-fixtures` 132 | 133 | Raise when the plugin cannot enumerate and collect pytest fixtures for analysis 134 | 135 | NOTE: this warning is only added to test modules (`test_*.py` / `*_test.py`) 136 | 137 | ```python 138 | import no_such_package # <- pylint-pytest plugin cannot enumerate and collect pytest fixtures 139 | ``` 140 | 141 | ## Changelog 142 | 143 | See [CHANGELOG](CHANGELOG.md). 144 | 145 | ## License 146 | 147 | `pylint-pytest` is available under [MIT license](LICENSE). 148 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # What Python version is installed where: 2 | # http://www.appveyor.com/docs/installed-software#python 3 | 4 | image: Visual Studio 2019 5 | 6 | environment: 7 | matrix: 8 | - PYTHON: "C:\\Python36" 9 | TOX_ENV: "py36" 10 | 11 | - PYTHON: "C:\\Python37" 12 | TOX_ENV: "py37" 13 | 14 | - PYTHON: "C:\\Python38" 15 | TOX_ENV: "py38" 16 | 17 | - PYTHON: "C:\\Python39" 18 | TOX_ENV: "py39" 19 | 20 | init: 21 | - "%PYTHON%/python -V" 22 | - "%PYTHON%/python -c \"import struct;print( 8 * struct.calcsize(\'P\'))\"" 23 | 24 | install: 25 | - "%PYTHON%/Scripts/easy_install -U pip" 26 | - "%PYTHON%/Scripts/pip install tox" 27 | - "%PYTHON%/Scripts/pip install wheel" 28 | - "%PYTHON%/Scripts/pip install psutil" 29 | 30 | build: false # Not a C# project, build stuff at the test step instead. 31 | 32 | test_script: 33 | - "%PYTHON%/Scripts/tox -e %TOX_ENV%" 34 | 35 | #on_success: 36 | # - TODO: upload the content of dist/*.whl to a public wheelhouse 37 | -------------------------------------------------------------------------------- /pylint_pytest/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | import importlib 4 | import glob 5 | 6 | from .checkers import BasePytestChecker 7 | 8 | 9 | # pylint: disable=protected-access 10 | def register(linter): 11 | '''auto discover pylint checker classes''' 12 | dirname = os.path.dirname(__file__) 13 | for module in glob.glob(os.path.join(dirname, 'checkers', '*.py')): 14 | # trim file extension 15 | module = os.path.splitext(module)[0] 16 | 17 | # use relative path only 18 | module = module.replace(dirname, '', 1) 19 | 20 | # translate file path into module import path 21 | module = module.replace(os.sep, '.') 22 | 23 | checker = importlib.import_module(module, package=os.path.basename(dirname)) 24 | for attr_name in dir(checker): 25 | attr_val = getattr(checker, attr_name) 26 | if attr_val != BasePytestChecker and \ 27 | inspect.isclass(attr_val) and \ 28 | issubclass(attr_val, BasePytestChecker): 29 | linter.register_checker(attr_val(linter)) 30 | -------------------------------------------------------------------------------- /pylint_pytest/checkers/__init__.py: -------------------------------------------------------------------------------- 1 | from pylint.checkers import BaseChecker 2 | 3 | 4 | class BasePytestChecker(BaseChecker): 5 | name = 'pylint-pytest' 6 | -------------------------------------------------------------------------------- /pylint_pytest/checkers/class_attr_loader.py: -------------------------------------------------------------------------------- 1 | import astroid 2 | from pylint.interfaces import IAstroidChecker 3 | from ..utils import _can_use_fixture, _is_class_autouse_fixture 4 | from . import BasePytestChecker 5 | 6 | 7 | class ClassAttrLoader(BasePytestChecker): 8 | __implements__ = IAstroidChecker 9 | msgs = {'E6400': ('', 'pytest-class-attr-loader', '')} 10 | 11 | in_setup = False 12 | request_cls = set() 13 | class_node = None 14 | 15 | def visit_functiondef(self, node): 16 | '''determine if a method is a class setup method''' 17 | self.in_setup = False 18 | self.request_cls = set() 19 | self.class_node = None 20 | 21 | if _can_use_fixture(node) and _is_class_autouse_fixture(node): 22 | self.in_setup = True 23 | self.class_node = node.parent 24 | 25 | def visit_assign(self, node): 26 | '''store the aliases for `cls`''' 27 | if self.in_setup and isinstance(node.value, astroid.Attribute) and \ 28 | node.value.attrname == 'cls' and \ 29 | node.value.expr.name == 'request': 30 | # storing the aliases for cls from request.cls 31 | self.request_cls = set(map(lambda t: t.name, node.targets)) 32 | 33 | def visit_assignattr(self, node): 34 | if self.in_setup and isinstance(node.expr, astroid.Name) and \ 35 | node.expr.name in self.request_cls and \ 36 | node.attrname not in self.class_node.locals: 37 | try: 38 | # find Assign node which contains the source "value" 39 | assign_node = node 40 | while not isinstance(assign_node, astroid.Assign): 41 | assign_node = assign_node.parent 42 | 43 | # hack class locals 44 | self.class_node.locals[node.attrname] = [assign_node.value] 45 | except: # pylint: disable=bare-except 46 | # cannot find valid assign expr, skipping the entire attribute 47 | pass 48 | -------------------------------------------------------------------------------- /pylint_pytest/checkers/fixture.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | import fnmatch 5 | 6 | import astroid 7 | import pylint 8 | from pylint.checkers.variables import VariablesChecker 9 | from pylint.interfaces import IAstroidChecker 10 | import pytest 11 | from ..utils import ( 12 | _can_use_fixture, 13 | _is_pytest_mark, 14 | _is_pytest_mark_usefixtures, 15 | _is_pytest_fixture, 16 | _is_same_module, 17 | ) 18 | from . import BasePytestChecker 19 | 20 | # TODO: support pytest python_files configuration 21 | FILE_NAME_PATTERNS = ('test_*.py', '*_test.py') 22 | 23 | 24 | class FixtureCollector: 25 | fixtures = {} 26 | errors = set() 27 | 28 | def pytest_sessionfinish(self, session): 29 | # pylint: disable=protected-access 30 | self.fixtures = session._fixturemanager._arg2fixturedefs 31 | 32 | def pytest_collectreport(self, report): 33 | if report.failed: 34 | self.errors.add(report) 35 | 36 | 37 | class FixtureChecker(BasePytestChecker): 38 | __implements__ = IAstroidChecker 39 | msgs = { 40 | 'W6401': ( 41 | 'Using a deprecated @pytest.yield_fixture decorator', 42 | 'deprecated-pytest-yield-fixture', 43 | 'Used when using a deprecated pytest decorator that has been deprecated in pytest-3.0' 44 | ), 45 | 'W6402': ( 46 | 'Using useless `@pytest.mark.*` decorator for fixtures', 47 | 'useless-pytest-mark-decorator', 48 | ( 49 | '@pytest.mark.* decorators can\'t by applied to fixtures. ' 50 | 'Take a look at: https://docs.pytest.org/en/stable/reference.html#marks' 51 | ), 52 | ), 53 | 'W6403': ( 54 | 'Using a deprecated positional arguments for fixture', 55 | 'deprecated-positional-argument-for-pytest-fixture', 56 | ( 57 | 'Pass scope as a kwarg, not positional arg, which is deprecated in future pytest. ' 58 | 'Take a look at: https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only' 59 | ), 60 | ), 61 | 'F6401': ( 62 | ( 63 | 'pylint-pytest plugin cannot enumerate and collect pytest fixtures. ' 64 | 'Please run `pytest --fixtures --collect-only path/to/current/module.py` and resolve any potential syntax error or package dependency issues' 65 | ), 66 | 'cannot-enumerate-pytest-fixtures', 67 | 'Used when pylint-pytest has been unable to enumerate and collect pytest fixtures.', 68 | ), 69 | } 70 | 71 | _pytest_fixtures = {} 72 | _invoked_with_func_args = set() 73 | _invoked_with_usefixtures = set() 74 | _original_add_message = callable 75 | 76 | def open(self): 77 | # patch VariablesChecker.add_message 78 | FixtureChecker._original_add_message = VariablesChecker.add_message 79 | VariablesChecker.add_message = FixtureChecker.patch_add_message 80 | 81 | def close(self): 82 | '''restore & reset class attr for testing''' 83 | # restore add_message 84 | VariablesChecker.add_message = FixtureChecker._original_add_message 85 | FixtureChecker._original_add_message = callable 86 | 87 | # reset fixture info storage 88 | FixtureChecker._pytest_fixtures = {} 89 | FixtureChecker._invoked_with_func_args = set() 90 | FixtureChecker._invoked_with_usefixtures = set() 91 | 92 | def visit_module(self, node): 93 | ''' 94 | - only run once per module 95 | - invoke pytest session to collect available fixtures 96 | - create containers for the module to store args and fixtures 97 | ''' 98 | # storing all fixtures discovered by pytest session 99 | FixtureChecker._pytest_fixtures = {} # Dict[List[_pytest.fixtures.FixtureDef]] 100 | 101 | # storing all used function arguments 102 | FixtureChecker._invoked_with_func_args = set() # Set[str] 103 | 104 | # storing all invoked fixtures through @pytest.mark.usefixture(...) 105 | FixtureChecker._invoked_with_usefixtures = set() # Set[str] 106 | 107 | is_test_module = False 108 | for pattern in FILE_NAME_PATTERNS: 109 | if fnmatch.fnmatch(Path(node.file).name, pattern): 110 | is_test_module = True 111 | break 112 | 113 | try: 114 | with open(os.devnull, 'w') as devnull: 115 | # suppress any future output from pytest 116 | stdout, stderr = sys.stdout, sys.stderr 117 | sys.stderr = sys.stdout = devnull 118 | 119 | # run pytest session with customized plugin to collect fixtures 120 | fixture_collector = FixtureCollector() 121 | 122 | # save and restore sys.path to prevent pytest.main from altering it 123 | sys_path = sys.path.copy() 124 | 125 | ret = pytest.main( 126 | [ 127 | node.file, '--fixtures', '--collect-only', 128 | '--pythonwarnings=ignore:Module already imported:pytest.PytestWarning', 129 | ], 130 | plugins=[fixture_collector], 131 | ) 132 | 133 | # restore sys.path 134 | sys.path = sys_path 135 | 136 | FixtureChecker._pytest_fixtures = fixture_collector.fixtures 137 | 138 | if (ret != pytest.ExitCode.OK or fixture_collector.errors) and is_test_module: 139 | self.add_message('cannot-enumerate-pytest-fixtures', node=node) 140 | finally: 141 | # restore output devices 142 | sys.stdout, sys.stderr = stdout, stderr 143 | 144 | def visit_decorators(self, node): 145 | """ 146 | Walk through all decorators on functions. 147 | Tries to find cases: 148 | When uses `@pytest.fixture` with `scope` as positional argument (deprecated) 149 | https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only 150 | >>> @pytest.fixture("module") 151 | >>> def awesome_fixture(): ... 152 | Instead 153 | >>> @pytest.fixture(scope="module") 154 | >>> def awesome_fixture(): ... 155 | When uses `@pytest.mark.usefixtures` for fixture (useless because didn't work) 156 | https://docs.pytest.org/en/stable/reference.html#marks 157 | >>> @pytest.mark.usefixtures("another_fixture") 158 | >>> @pytest.fixture 159 | >>> def awesome_fixture(): ... 160 | Parameters 161 | ---------- 162 | node : astroid.scoped_nodes.Decorators 163 | """ 164 | uses_fixture_deco, uses_mark_deco = False, False 165 | for decorator in node.nodes: 166 | try: 167 | if _is_pytest_fixture(decorator) and isinstance(decorator, astroid.Call) and decorator.args: 168 | self.add_message( 169 | 'deprecated-positional-argument-for-pytest-fixture', node=decorator 170 | ) 171 | uses_fixture_deco |= _is_pytest_fixture(decorator) 172 | uses_mark_deco |= _is_pytest_mark(decorator) 173 | except AttributeError: 174 | # ignore any parse exceptions 175 | pass 176 | if uses_mark_deco and uses_fixture_deco: 177 | self.add_message("useless-pytest-mark-decorator", node=node) 178 | 179 | def visit_functiondef(self, node): 180 | ''' 181 | - save invoked fixtures for later use 182 | - save used function arguments for later use 183 | ''' 184 | if _can_use_fixture(node): 185 | if node.decorators: 186 | # check all decorators 187 | for decorator in node.decorators.nodes: 188 | if _is_pytest_mark_usefixtures(decorator): 189 | # save all visited fixtures 190 | for arg in decorator.args: 191 | self._invoked_with_usefixtures.add(arg.value) 192 | if int(pytest.__version__.split('.')[0]) >= 3 and \ 193 | _is_pytest_fixture(decorator, fixture=False): 194 | # raise deprecated warning for @pytest.yield_fixture 195 | self.add_message('deprecated-pytest-yield-fixture', node=node) 196 | for arg in node.args.args: 197 | self._invoked_with_func_args.add(arg.name) 198 | 199 | # pylint: disable=protected-access,bad-staticmethod-argument 200 | @staticmethod 201 | def patch_add_message(self, msgid, line=None, node=None, args=None, 202 | confidence=None, col_offset=None): 203 | ''' 204 | - intercept and discard unwanted warning messages 205 | ''' 206 | # check W0611 unused-import 207 | if msgid == 'unused-import': 208 | # actual attribute name is not passed as arg so...dirty hack 209 | # message is usually in the form of '%s imported from %s (as %)' 210 | message_tokens = args.split() 211 | fixture_name = message_tokens[0] 212 | 213 | # ignoring 'import %s' message 214 | if message_tokens[0] == 'import' and len(message_tokens) == 2: 215 | pass 216 | 217 | # fixture is defined in other modules and being imported to 218 | # conftest for pytest magic 219 | elif isinstance(node.parent, astroid.Module) \ 220 | and node.parent.name.split('.')[-1] == 'conftest' \ 221 | and fixture_name in FixtureChecker._pytest_fixtures: 222 | return 223 | 224 | # imported fixture is referenced in test/fixture func 225 | elif fixture_name in FixtureChecker._invoked_with_func_args \ 226 | and fixture_name in FixtureChecker._pytest_fixtures: 227 | if _is_same_module(fixtures=FixtureChecker._pytest_fixtures, 228 | import_node=node, 229 | fixture_name=fixture_name): 230 | return 231 | 232 | # fixture is referenced in @pytest.mark.usefixtures 233 | elif fixture_name in FixtureChecker._invoked_with_usefixtures \ 234 | and fixture_name in FixtureChecker._pytest_fixtures: 235 | if _is_same_module(fixtures=FixtureChecker._pytest_fixtures, 236 | import_node=node, 237 | fixture_name=fixture_name): 238 | return 239 | 240 | # check W0613 unused-argument 241 | if msgid == 'unused-argument' and \ 242 | _can_use_fixture(node.parent.parent) and \ 243 | isinstance(node.parent, astroid.Arguments) and \ 244 | node.name in FixtureChecker._pytest_fixtures: 245 | return 246 | 247 | # check W0621 redefined-outer-name 248 | if msgid == 'redefined-outer-name' and \ 249 | _can_use_fixture(node.parent.parent) and \ 250 | isinstance(node.parent, astroid.Arguments) and \ 251 | node.name in FixtureChecker._pytest_fixtures: 252 | return 253 | 254 | if int(pylint.__version__.split('.')[0]) >= 2: 255 | FixtureChecker._original_add_message( 256 | self, msgid, line, node, args, confidence, col_offset) 257 | else: 258 | # python2 + pylint1.9 backward compatibility 259 | FixtureChecker._original_add_message( 260 | self, msgid, line, node, args, confidence) 261 | -------------------------------------------------------------------------------- /pylint_pytest/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import astroid 3 | 4 | 5 | def _is_pytest_mark_usefixtures(decorator): 6 | # expecting @pytest.mark.usefixture(...) 7 | try: 8 | if isinstance(decorator, astroid.Call) and \ 9 | decorator.func.attrname == 'usefixtures' and \ 10 | decorator.func.expr.attrname == 'mark' and \ 11 | decorator.func.expr.expr.name == 'pytest': 12 | return True 13 | except AttributeError: 14 | pass 15 | return False 16 | 17 | 18 | def _is_pytest_mark(decorator): 19 | try: 20 | deco = decorator # as attribute `@pytest.mark.trylast` 21 | if isinstance(decorator, astroid.Call): 22 | deco = decorator.func # as function `@pytest.mark.skipif(...)` 23 | if deco.expr.attrname == 'mark' and deco.expr.expr.name == 'pytest': 24 | return True 25 | except AttributeError: 26 | pass 27 | return False 28 | 29 | 30 | def _is_pytest_fixture(decorator, fixture=True, yield_fixture=True): 31 | attr = None 32 | to_check = set() 33 | 34 | if fixture: 35 | to_check.add('fixture') 36 | 37 | if yield_fixture: 38 | to_check.add('yield_fixture') 39 | 40 | try: 41 | if isinstance(decorator, astroid.Attribute): 42 | # expecting @pytest.fixture 43 | attr = decorator 44 | 45 | if isinstance(decorator, astroid.Call): 46 | # expecting @pytest.fixture(scope=...) 47 | attr = decorator.func 48 | 49 | if attr and attr.attrname in to_check \ 50 | and attr.expr.name == 'pytest': 51 | return True 52 | except AttributeError: 53 | pass 54 | 55 | return False 56 | 57 | 58 | def _is_class_autouse_fixture(function): 59 | try: 60 | for decorator in function.decorators.nodes: 61 | if isinstance(decorator, astroid.Call): 62 | func = decorator.func 63 | 64 | if func and func.attrname in ('fixture', 'yield_fixture') \ 65 | and func.expr.name == 'pytest': 66 | 67 | is_class = is_autouse = False 68 | 69 | for kwarg in decorator.keywords or []: 70 | if kwarg.arg == 'scope' and kwarg.value.value == 'class': 71 | is_class = True 72 | if kwarg.arg == 'autouse' and kwarg.value.value is True: 73 | is_autouse = True 74 | 75 | if is_class and is_autouse: 76 | return True 77 | except AttributeError: 78 | pass 79 | 80 | return False 81 | 82 | 83 | def _can_use_fixture(function): 84 | if isinstance(function, astroid.FunctionDef): 85 | 86 | # test_*, *_test 87 | if function.name.startswith('test_') or function.name.endswith('_test'): 88 | return True 89 | 90 | if function.decorators: 91 | for decorator in function.decorators.nodes: 92 | # usefixture 93 | if _is_pytest_mark_usefixtures(decorator): 94 | return True 95 | 96 | # fixture 97 | if _is_pytest_fixture(decorator): 98 | return True 99 | 100 | return False 101 | 102 | 103 | def _is_same_module(fixtures, import_node, fixture_name): 104 | '''Comparing pytest fixture node with astroid.ImportFrom''' 105 | try: 106 | for fixture in fixtures[fixture_name]: 107 | for import_from in import_node.root().globals[fixture_name]: 108 | if inspect.getmodule(fixture.func).__file__ == \ 109 | import_from.parent.import_module(import_from.modname, 110 | False, 111 | import_from.level).file: 112 | return True 113 | except: # pylint: disable=bare-except 114 | pass 115 | return False 116 | -------------------------------------------------------------------------------- /sandbox/.placeholder: -------------------------------------------------------------------------------- 1 | DON'T REMOVE ME 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest 3 | 4 | [tool:pytest] 5 | addopts = --verbose 6 | python_files = tests/test_*.py 7 | 8 | [bdist_wheel] 9 | universal = 1 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from os import path 5 | from setuptools import setup, find_packages 6 | 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | with open(path.join(here, 'README.md')) as fin: 10 | long_description = fin.read() 11 | 12 | 13 | setup( 14 | name='pylint-pytest', 15 | version='1.1.2', 16 | author='Reverb Chu', 17 | author_email='pylint-pytest@reverbc.tw', 18 | maintainer='Reverb Chu', 19 | maintainer_email='pylint-pytest@reverbc.tw', 20 | license='MIT', 21 | url='https://github.com/reverbc/pylint-pytest', 22 | description='A Pylint plugin to suppress pytest-related false positives.', 23 | long_description=long_description, 24 | long_description_content_type='text/markdown', 25 | packages=find_packages(exclude=['tests', 'sandbox']), 26 | install_requires=[ 27 | 'pylint', 28 | 'pytest>=4.6', 29 | ], 30 | python_requires='>=3.6', 31 | classifiers=[ 32 | 'Development Status :: 5 - Production/Stable', 33 | 'Intended Audience :: Developers', 34 | 'Topic :: Software Development :: Testing', 35 | 'Topic :: Software Development :: Quality Assurance', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 3', 38 | 'Programming Language :: Python :: 3.6', 39 | 'Programming Language :: Python :: 3.7', 40 | 'Programming Language :: Python :: 3.8', 41 | 'Programming Language :: Python :: 3.9', 42 | 'Programming Language :: Python :: Implementation :: CPython', 43 | 'Operating System :: OS Independent', 44 | 'License :: OSI Approved :: MIT License', 45 | ], 46 | tests_require=['pytest', 'pylint'], 47 | keywords=['pylint', 'pytest', 'plugin'], 48 | ) 49 | -------------------------------------------------------------------------------- /tests/base_tester.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from pprint import pprint 4 | 5 | import astroid 6 | from pylint.testutils import UnittestLinter 7 | try: 8 | from pylint.utils import ASTWalker 9 | except ImportError: 10 | # for pylint 1.9 11 | from pylint.utils import PyLintASTWalker as ASTWalker 12 | from pylint.checkers import BaseChecker 13 | 14 | import pylint_pytest.checkers.fixture 15 | 16 | # XXX: allow all file name 17 | pylint_pytest.checkers.fixture.FILE_NAME_PATTERNS = ('*', ) 18 | 19 | 20 | class BasePytestTester(object): 21 | CHECKER_CLASS = BaseChecker 22 | IMPACTED_CHECKER_CLASSES = [] 23 | MSG_ID = None 24 | MESSAGES = None 25 | CONFIG = {} 26 | 27 | enable_plugin = True 28 | 29 | def run_linter(self, enable_plugin, file_path=None): 30 | self.enable_plugin = enable_plugin 31 | 32 | # pylint: disable=protected-access 33 | if file_path is None: 34 | module = sys._getframe(1).f_code.co_name.replace('test_', '', 1) 35 | file_path = os.path.join( 36 | os.getcwd(), 'tests', 'input', self.MSG_ID, module + '.py') 37 | 38 | with open(file_path) as fin: 39 | content = fin.read() 40 | module = astroid.parse(content, module_name=module) 41 | module.file = fin.name 42 | 43 | self.walk(module) # run all checkers 44 | self.MESSAGES = self.linter.release_messages() 45 | 46 | def verify_messages(self, msg_count, msg_id=None): 47 | msg_id = msg_id or self.MSG_ID 48 | 49 | matched_count = 0 50 | for message in self.MESSAGES: 51 | # only care about ID and count, not the content 52 | if message.msg_id == msg_id: 53 | matched_count += 1 54 | 55 | pprint(self.MESSAGES) 56 | assert matched_count == msg_count, f'expecting {msg_count}, actual {matched_count}' 57 | 58 | def setup_method(self): 59 | self.linter = UnittestLinter() 60 | self.checker = self.CHECKER_CLASS(self.linter) 61 | self.impacted_checkers = [] 62 | 63 | for key, value in self.CONFIG.items(): 64 | setattr(self.checker.config, key, value) 65 | self.checker.open() 66 | 67 | for checker_class in self.IMPACTED_CHECKER_CLASSES: 68 | checker = checker_class(self.linter) 69 | for key, value in self.CONFIG.items(): 70 | setattr(checker.config, key, value) 71 | checker.open() 72 | self.impacted_checkers.append(checker) 73 | 74 | def teardown_method(self): 75 | self.checker.close() 76 | for checker in self.impacted_checkers: 77 | checker.close() 78 | 79 | def walk(self, node): 80 | """recursive walk on the given node""" 81 | walker = ASTWalker(self.linter) 82 | if self.enable_plugin: 83 | walker.add_checker(self.checker) 84 | for checker in self.impacted_checkers: 85 | walker.add_checker(checker) 86 | walker.walk(node) 87 | -------------------------------------------------------------------------------- /tests/input/cannot-enumerate-pytest-fixtures/import_corrupted_module.py: -------------------------------------------------------------------------------- 1 | from no_such_package import fixture 2 | 3 | 4 | def test_something(fixture): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/input/cannot-enumerate-pytest-fixtures/no_such_package.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import this_is_invalid # makes pytest fail 3 | 4 | 5 | @pytest.fixture 6 | def fixture(): 7 | pass 8 | 9 | def test_something(fixture): 10 | pass 11 | -------------------------------------------------------------------------------- /tests/input/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def conftest_fixture_attr(): 6 | return True 7 | 8 | 9 | @pytest.fixture(scope='function') 10 | def conftest_fixture_func(): 11 | return True 12 | -------------------------------------------------------------------------------- /tests/input/deprecated-positional-argument-for-pytest-fixture/with_args_scope.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture('function') 5 | def some_fixture(): 6 | return 'ok' 7 | -------------------------------------------------------------------------------- /tests/input/deprecated-positional-argument-for-pytest-fixture/with_kwargs_scope.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='function') 5 | def some_fixture(): 6 | return 'ok' 7 | -------------------------------------------------------------------------------- /tests/input/deprecated-positional-argument-for-pytest-fixture/without_scope.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def some_fixture(): 6 | return 'ok' 7 | -------------------------------------------------------------------------------- /tests/input/deprecated-pytest-yield-fixture/func.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.yield_fixture() 5 | def yield_fixture(): 6 | yield 7 | 8 | 9 | @pytest.yield_fixture(scope='session') 10 | def yield_fixture_session(): 11 | yield 12 | -------------------------------------------------------------------------------- /tests/input/deprecated-pytest-yield-fixture/smoke.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.yield_fixture 5 | def yield_fixture(): 6 | yield 7 | -------------------------------------------------------------------------------- /tests/input/no-member/assign_attr_of_attr.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class TestClass(object): 5 | @staticmethod 6 | @pytest.fixture(scope='class', autouse=True) 7 | def setup_class(request): 8 | cls = request.cls 9 | cls.defined_in_setup_class = object() 10 | cls.defined_in_setup_class.attr_of_attr = None 11 | 12 | def test_foo(self): 13 | assert self.defined_in_setup_class 14 | -------------------------------------------------------------------------------- /tests/input/no-member/fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class TestClass(object): 5 | @staticmethod 6 | @pytest.fixture(scope='class', autouse=True) 7 | def setup_class(request): 8 | cls = request.cls 9 | cls.defined_in_setup_class = 123 10 | 11 | def test_foo(self): 12 | assert self.defined_in_setup_class 13 | -------------------------------------------------------------------------------- /tests/input/no-member/from_unpack.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def meh(): 5 | return True, False 6 | 7 | 8 | class TestClass(object): 9 | @staticmethod 10 | @pytest.fixture(scope='class', autouse=True) 11 | def setup_class(request): 12 | cls = request.cls 13 | cls.defined_in_setup_class, _ = meh() 14 | 15 | def test_foo(self): 16 | assert self.defined_in_setup_class 17 | -------------------------------------------------------------------------------- /tests/input/no-member/inheritance.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class TestClass(object): 5 | @staticmethod 6 | @pytest.fixture(scope='class', autouse=True) 7 | def setup_class(request): 8 | cls = request.cls 9 | cls.defined_in_setup_class = 123 10 | 11 | 12 | class TestChildClass(TestClass): 13 | def test_foo(self): 14 | assert self.defined_in_setup_class 15 | -------------------------------------------------------------------------------- /tests/input/no-member/not_using_cls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class TestClass(object): 5 | @staticmethod 6 | @pytest.fixture(scope='class', autouse=True) 7 | def setup_class(request): 8 | clls = request.cls 9 | clls.defined_in_setup_class = 123 10 | 11 | def test_foo(self): 12 | assert self.defined_in_setup_class 13 | -------------------------------------------------------------------------------- /tests/input/no-member/yield_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class TestClass(object): 5 | @staticmethod 6 | @pytest.yield_fixture(scope='class', autouse=True) 7 | def setup_class(request): 8 | cls = request.cls 9 | cls.defined_in_setup_class = 123 10 | 11 | def test_foo(self): 12 | assert self.defined_in_setup_class 13 | -------------------------------------------------------------------------------- /tests/input/redefined-outer-name/args_and_kwargs.py: -------------------------------------------------------------------------------- 1 | def args(): 2 | pass 3 | 4 | 5 | def kwargs(): 6 | pass 7 | 8 | 9 | def not_a_test_function(*args, **kwargs): 10 | # invalid test case... 11 | assert True 12 | -------------------------------------------------------------------------------- /tests/input/redefined-outer-name/caller_not_a_test_func.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def this_is_a_fixture(): 6 | return True 7 | 8 | 9 | def not_a_test_function(this_is_a_fixture): 10 | # invalid test case... 11 | assert True 12 | -------------------------------------------------------------------------------- /tests/input/redefined-outer-name/caller_yield_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from conftest import conftest_fixture_attr 3 | 4 | 5 | @pytest.yield_fixture 6 | def caller_yield_fixture(conftest_fixture_attr): 7 | assert conftest_fixture_attr 8 | -------------------------------------------------------------------------------- /tests/input/redefined-outer-name/smoke.py: -------------------------------------------------------------------------------- 1 | from conftest import conftest_fixture_attr 2 | 3 | 4 | def test_conftest_fixture_attr(conftest_fixture_attr): 5 | assert True 6 | -------------------------------------------------------------------------------- /tests/input/regression/import_twice.py: -------------------------------------------------------------------------------- 1 | from conftest import conftest_fixture_func 2 | 3 | 4 | def test_using_conftest_fixture_attr(): 5 | if True: 6 | from conftest import conftest_fixture_func 7 | -------------------------------------------------------------------------------- /tests/input/unused-argument/args_and_kwargs.py: -------------------------------------------------------------------------------- 1 | def not_a_test_function(*args, **kwargs): 2 | # invalid test case... 3 | assert True 4 | -------------------------------------------------------------------------------- /tests/input/unused-argument/caller_not_a_test_func.py: -------------------------------------------------------------------------------- 1 | def not_a_test_function(conftest_fixture_func): 2 | # invalid test case... 3 | assert True 4 | -------------------------------------------------------------------------------- /tests/input/unused-argument/caller_yield_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from conftest import conftest_fixture_attr 3 | 4 | 5 | @pytest.yield_fixture 6 | def caller_yield_fixture(conftest_fixture_attr): 7 | assert True 8 | -------------------------------------------------------------------------------- /tests/input/unused-argument/smoke.py: -------------------------------------------------------------------------------- 1 | def test_conftest_fixture_func(conftest_fixture_func): 2 | assert True 3 | 4 | 5 | def test_conftest_fixture_attr(conftest_fixture_attr): 6 | assert True 7 | -------------------------------------------------------------------------------- /tests/input/unused-import/_fixture_for_conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def conftest_fixture_attr(): 6 | return True 7 | -------------------------------------------------------------------------------- /tests/input/unused-import/_same_name_module.py: -------------------------------------------------------------------------------- 1 | def conftest_fixture_attr(): 2 | # not really a fixture... 3 | return True 4 | -------------------------------------------------------------------------------- /tests/input/unused-import/caller_yield_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from conftest import conftest_fixture_attr 3 | 4 | 5 | @pytest.yield_fixture 6 | def caller_yield_fixture(conftest_fixture_attr): 7 | assert conftest_fixture_attr 8 | -------------------------------------------------------------------------------- /tests/input/unused-import/conftest.py: -------------------------------------------------------------------------------- 1 | from _fixture_for_conftest import conftest_fixture_attr 2 | -------------------------------------------------------------------------------- /tests/input/unused-import/same_name_arg.py: -------------------------------------------------------------------------------- 1 | # an actual unused import, just happened to have the same name as fixture 2 | from _same_name_module import conftest_fixture_attr 3 | 4 | 5 | def test_conftest_fixture_attr(conftest_fixture_attr): 6 | assert True 7 | -------------------------------------------------------------------------------- /tests/input/unused-import/same_name_decorator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | # an actual unused import, just happened to have the same name as fixture 3 | from _same_name_module import conftest_fixture_attr 4 | 5 | 6 | @pytest.mark.usefixtures('conftest_fixture_attr') 7 | def test_conftest_fixture_attr(): 8 | assert True 9 | -------------------------------------------------------------------------------- /tests/input/unused-import/smoke.py: -------------------------------------------------------------------------------- 1 | from conftest import conftest_fixture_attr 2 | 3 | 4 | def test_conftest_fixture_attr(conftest_fixture_attr): 5 | assert True 6 | -------------------------------------------------------------------------------- /tests/input/useless-pytest-mark-decorator/mark_usefixture_using_for_class.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def first(): 6 | return "OK" 7 | 8 | 9 | @pytest.mark.usefixtures("first") 10 | class TestFirst: 11 | @staticmethod 12 | def do(): 13 | return True 14 | 15 | def test_first(self): 16 | assert self.do() is True 17 | -------------------------------------------------------------------------------- /tests/input/useless-pytest-mark-decorator/mark_usefixture_using_for_fixture_attribute.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def first(): 6 | return "OK" 7 | 8 | 9 | @pytest.fixture 10 | @pytest.mark.usefixtures("first") 11 | def second(): 12 | return "NOK" 13 | 14 | 15 | @pytest.mark.usefixtures("first") 16 | @pytest.fixture 17 | def third(): 18 | return "NOK" 19 | -------------------------------------------------------------------------------- /tests/input/useless-pytest-mark-decorator/mark_usefixture_using_for_fixture_function.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope="session") 5 | def first(): 6 | return "OK" 7 | 8 | 9 | @pytest.fixture(scope="function") 10 | @pytest.mark.usefixtures("first") 11 | def second(): 12 | return "NOK" 13 | 14 | 15 | @pytest.mark.usefixtures("first") 16 | @pytest.fixture(scope="function") 17 | def third(): 18 | return "NOK" 19 | -------------------------------------------------------------------------------- /tests/input/useless-pytest-mark-decorator/mark_usefixture_using_for_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def first(): 6 | return "OK" 7 | 8 | 9 | @pytest.mark.usefixtures("first") 10 | def test_first(): 11 | pass 12 | -------------------------------------------------------------------------------- /tests/input/useless-pytest-mark-decorator/not_pytest_marker.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from types import SimpleNamespace 3 | 4 | 5 | def noop(func): 6 | @functools.wraps(func) 7 | def wrapper_noop(*args, **kwargs): 8 | return func(*args, **kwargs) 9 | return wrapper_noop 10 | 11 | 12 | PYTEST = SimpleNamespace( 13 | MARK=SimpleNamespace( 14 | noop=noop 15 | ) 16 | ) 17 | 18 | 19 | @noop 20 | def test_non_pytest_marker(): 21 | pass 22 | 23 | 24 | @PYTEST.MARK.noop 25 | def test_non_pytest_marker_attr(): 26 | pass 27 | -------------------------------------------------------------------------------- /tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | 5 | @pytest.mark.trylast 6 | @pytest.fixture 7 | def fixture(): 8 | return "Not ok" 9 | 10 | 11 | @pytest.mark.parametrize("id", range(2)) 12 | @pytest.fixture 13 | def fixture_with_params(id): 14 | return "{} not OK".format(id) 15 | 16 | 17 | @pytest.mark.custom_mark 18 | @pytest.fixture 19 | def fixture_with_custom_mark(): 20 | return "NOT OK" 21 | 22 | 23 | @pytest.mark.skipif(os.getenv("xXx")) 24 | @pytest.fixture 25 | def fixture_with_conditional_mark(): 26 | return "NOK" 27 | -------------------------------------------------------------------------------- /tests/test_cannot_enumerate_fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pylint.checkers.variables import VariablesChecker 3 | from base_tester import BasePytestTester 4 | from pylint_pytest.checkers.fixture import FixtureChecker 5 | 6 | 7 | class TestCannotEnumerateFixtures(BasePytestTester): 8 | CHECKER_CLASS = FixtureChecker 9 | MSG_ID = 'cannot-enumerate-pytest-fixtures' 10 | 11 | @pytest.mark.parametrize('enable_plugin', [True, False]) 12 | def test_no_such_package(self, enable_plugin): 13 | self.run_linter(enable_plugin) 14 | self.verify_messages(1 if enable_plugin else 0) 15 | 16 | @pytest.mark.parametrize('enable_plugin', [True, False]) 17 | def test_import_corrupted_module(self, enable_plugin): 18 | self.run_linter(enable_plugin) 19 | self.verify_messages(1 if enable_plugin else 0) 20 | -------------------------------------------------------------------------------- /tests/test_no_member.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pylint.checkers.typecheck import TypeChecker 3 | from pylint_pytest.checkers.class_attr_loader import ClassAttrLoader 4 | from base_tester import BasePytestTester 5 | 6 | 7 | class TestNoMember(BasePytestTester): 8 | CHECKER_CLASS = ClassAttrLoader 9 | IMPACTED_CHECKER_CLASSES = [TypeChecker] 10 | MSG_ID = 'no-member' 11 | 12 | @pytest.mark.parametrize('enable_plugin', [True, False]) 13 | def test_fixture(self, enable_plugin): 14 | self.run_linter(enable_plugin) 15 | self.verify_messages(0 if enable_plugin else 1) 16 | 17 | @pytest.mark.parametrize('enable_plugin', [True, False]) 18 | def test_yield_fixture(self, enable_plugin): 19 | self.run_linter(enable_plugin) 20 | self.verify_messages(0 if enable_plugin else 1) 21 | 22 | @pytest.mark.parametrize('enable_plugin', [True, False]) 23 | def test_not_using_cls(self, enable_plugin): 24 | self.run_linter(enable_plugin) 25 | self.verify_messages(0 if enable_plugin else 1) 26 | 27 | @pytest.mark.parametrize('enable_plugin', [True, False]) 28 | def test_inheritance(self, enable_plugin): 29 | self.run_linter(enable_plugin) 30 | self.verify_messages(0 if enable_plugin else 1) 31 | 32 | @pytest.mark.parametrize('enable_plugin', [True, False]) 33 | def test_from_unpack(self, enable_plugin): 34 | self.run_linter(enable_plugin) 35 | self.verify_messages(0 if enable_plugin else 1) 36 | 37 | @pytest.mark.parametrize('enable_plugin', [True, False]) 38 | def test_assign_attr_of_attr(self, enable_plugin): 39 | self.run_linter(enable_plugin) 40 | self.verify_messages(0 if enable_plugin else 1) 41 | -------------------------------------------------------------------------------- /tests/test_pytest_fixture_positional_arguments.py: -------------------------------------------------------------------------------- 1 | from base_tester import BasePytestTester 2 | from pylint_pytest.checkers.fixture import FixtureChecker 3 | 4 | 5 | class TestDeprecatedPytestFixtureScopeAsPositionalParam(BasePytestTester): 6 | CHECKER_CLASS = FixtureChecker 7 | MSG_ID = 'deprecated-positional-argument-for-pytest-fixture' 8 | 9 | def test_with_args_scope(self): 10 | self.run_linter(enable_plugin=True) 11 | self.verify_messages(1) 12 | 13 | def test_with_kwargs_scope(self): 14 | self.run_linter(enable_plugin=True) 15 | self.verify_messages(0) 16 | 17 | def test_without_scope(self): 18 | self.run_linter(enable_plugin=True) 19 | self.verify_messages(0) 20 | -------------------------------------------------------------------------------- /tests/test_pytest_mark_for_fixtures.py: -------------------------------------------------------------------------------- 1 | from base_tester import BasePytestTester 2 | from pylint_pytest.checkers.fixture import FixtureChecker 3 | 4 | 5 | class TestPytestMarkUsefixtures(BasePytestTester): 6 | CHECKER_CLASS = FixtureChecker 7 | MSG_ID = 'useless-pytest-mark-decorator' 8 | 9 | def test_mark_usefixture_using_for_test(self): 10 | self.run_linter(enable_plugin=True) 11 | self.verify_messages(0) 12 | 13 | def test_mark_usefixture_using_for_class(self): 14 | self.run_linter(enable_plugin=True) 15 | self.verify_messages(0) 16 | 17 | def test_mark_usefixture_using_for_fixture_attribute(self): 18 | self.run_linter(enable_plugin=True) 19 | self.verify_messages(2) 20 | 21 | def test_mark_usefixture_using_for_fixture_function(self): 22 | self.run_linter(enable_plugin=True) 23 | self.verify_messages(2) 24 | 25 | def test_other_marks_using_for_fixture(self): 26 | self.run_linter(enable_plugin=True) 27 | self.verify_messages(4) 28 | 29 | def test_not_pytest_marker(self): 30 | self.run_linter(enable_plugin=True) 31 | self.verify_messages(0) 32 | -------------------------------------------------------------------------------- /tests/test_pytest_yield_fixture.py: -------------------------------------------------------------------------------- 1 | from base_tester import BasePytestTester 2 | from pylint_pytest.checkers.fixture import FixtureChecker 3 | 4 | 5 | class TestDeprecatedPytestYieldFixture(BasePytestTester): 6 | CHECKER_CLASS = FixtureChecker 7 | IMPACTED_CHECKER_CLASSES = [] 8 | MSG_ID = 'deprecated-pytest-yield-fixture' 9 | 10 | def test_smoke(self): 11 | self.run_linter(enable_plugin=True) 12 | self.verify_messages(1) 13 | 14 | def test_func(self): 15 | self.run_linter(enable_plugin=True) 16 | self.verify_messages(2) 17 | -------------------------------------------------------------------------------- /tests/test_redefined_outer_name.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pylint.checkers.variables import VariablesChecker 3 | from base_tester import BasePytestTester 4 | from pylint_pytest.checkers.fixture import FixtureChecker 5 | 6 | 7 | class TestRedefinedOuterName(BasePytestTester): 8 | CHECKER_CLASS = FixtureChecker 9 | IMPACTED_CHECKER_CLASSES = [VariablesChecker] 10 | MSG_ID = 'redefined-outer-name' 11 | 12 | @pytest.mark.parametrize('enable_plugin', [True, False]) 13 | def test_smoke(self, enable_plugin): 14 | self.run_linter(enable_plugin) 15 | self.verify_messages(0 if enable_plugin else 1) 16 | 17 | @pytest.mark.parametrize('enable_plugin', [True, False]) 18 | def test_caller_yield_fixture(self, enable_plugin): 19 | self.run_linter(enable_plugin) 20 | self.verify_messages(0 if enable_plugin else 1) 21 | 22 | @pytest.mark.parametrize('enable_plugin', [True, False]) 23 | def test_caller_not_a_test_func(self, enable_plugin): 24 | self.run_linter(enable_plugin) 25 | self.verify_messages(1) 26 | 27 | @pytest.mark.parametrize('enable_plugin', [True, False]) 28 | def test_args_and_kwargs(self, enable_plugin): 29 | self.run_linter(enable_plugin) 30 | self.verify_messages(2) 31 | -------------------------------------------------------------------------------- /tests/test_regression.py: -------------------------------------------------------------------------------- 1 | import pylint 2 | import pytest 3 | from pylint.checkers.variables import VariablesChecker 4 | from base_tester import BasePytestTester 5 | from pylint_pytest.checkers.fixture import FixtureChecker 6 | 7 | 8 | class TestRegression(BasePytestTester): 9 | '''Covering some behaviors that shouldn't get impacted by the plugin''' 10 | CHECKER_CLASS = FixtureChecker 11 | IMPACTED_CHECKER_CLASSES = [VariablesChecker] 12 | MSG_ID = 'regression' 13 | 14 | @pytest.mark.parametrize('enable_plugin', [True, False]) 15 | def test_import_twice(self, enable_plugin): 16 | '''catch a coding error when using fixture + if + inline import''' 17 | self.run_linter(enable_plugin) 18 | 19 | if int(pylint.__version__.split('.')[0]) < 2: 20 | # for some reason pylint 1.9.5 does not raise unused-import for inline import 21 | self.verify_messages(1, msg_id='unused-import') 22 | else: 23 | self.verify_messages(2, msg_id='unused-import') 24 | self.verify_messages(1, msg_id='redefined-outer-name') 25 | -------------------------------------------------------------------------------- /tests/test_unused_argument.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pylint.checkers.variables import VariablesChecker 3 | from base_tester import BasePytestTester 4 | from pylint_pytest.checkers.fixture import FixtureChecker 5 | 6 | 7 | class TestUnusedArgument(BasePytestTester): 8 | CHECKER_CLASS = FixtureChecker 9 | IMPACTED_CHECKER_CLASSES = [VariablesChecker] 10 | MSG_ID = 'unused-argument' 11 | 12 | @pytest.mark.parametrize('enable_plugin', [True, False]) 13 | def test_smoke(self, enable_plugin): 14 | self.run_linter(enable_plugin) 15 | self.verify_messages(0 if enable_plugin else 2) 16 | 17 | @pytest.mark.parametrize('enable_plugin', [True, False]) 18 | def test_caller_yield_fixture(self, enable_plugin): 19 | self.run_linter(enable_plugin) 20 | self.verify_messages(0 if enable_plugin else 1) 21 | 22 | @pytest.mark.parametrize('enable_plugin', [True, False]) 23 | def test_caller_not_a_test_func(self, enable_plugin): 24 | self.run_linter(enable_plugin) 25 | self.verify_messages(1) 26 | 27 | @pytest.mark.parametrize('enable_plugin', [True, False]) 28 | def test_args_and_kwargs(self, enable_plugin): 29 | self.run_linter(enable_plugin) 30 | self.verify_messages(2) 31 | -------------------------------------------------------------------------------- /tests/test_unused_import.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pylint.checkers.variables import VariablesChecker 3 | from base_tester import BasePytestTester 4 | from pylint_pytest.checkers.fixture import FixtureChecker 5 | 6 | 7 | class TestUnusedImport(BasePytestTester): 8 | CHECKER_CLASS = FixtureChecker 9 | IMPACTED_CHECKER_CLASSES = [VariablesChecker] 10 | MSG_ID = 'unused-import' 11 | 12 | @pytest.mark.parametrize('enable_plugin', [True, False]) 13 | def test_smoke(self, enable_plugin): 14 | self.run_linter(enable_plugin) 15 | self.verify_messages(0 if enable_plugin else 1) 16 | 17 | @pytest.mark.parametrize('enable_plugin', [True, False]) 18 | def test_caller_yield_fixture(self, enable_plugin): 19 | self.run_linter(enable_plugin) 20 | self.verify_messages(0 if enable_plugin else 1) 21 | 22 | @pytest.mark.parametrize('enable_plugin', [True, False]) 23 | def test_same_name_arg(self, enable_plugin): 24 | '''an unused import (not a fixture) just happened to have the same 25 | name as fixture - should still raise unused-import warning''' 26 | self.run_linter(enable_plugin) 27 | self.verify_messages(1) 28 | 29 | @pytest.mark.parametrize('enable_plugin', [True, False]) 30 | def test_same_name_decorator(self, enable_plugin): 31 | '''an unused import (not a fixture) just happened to have the same 32 | name as fixture - should still raise unused-import warning''' 33 | self.run_linter(enable_plugin) 34 | self.verify_messages(1) 35 | 36 | @pytest.mark.parametrize('enable_plugin', [True, False]) 37 | def test_conftest(self, enable_plugin): 38 | '''fixtures are defined in different modules and imported to conftest 39 | for pytest to do its magic''' 40 | self.run_linter(enable_plugin) 41 | self.verify_messages(0 if enable_plugin else 1) 42 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37,py38,py39 3 | skipsdist = True 4 | 5 | [testenv] 6 | commands = 7 | pip install ./ --upgrade 8 | pytest {posargs:tests} 9 | --------------------------------------------------------------------------------