From 005174fb8a8abea72329bf3c701d66daec314e8d Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Sat, 14 Oct 2023 12:58:18 +0300 Subject: [PATCH 1/3] Add `.pre-commit-config.yaml` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Store all tools' configurations in `pyproject.toml`, even if right now only the `setup.py` file is used. Keep `.pylintrc` separately. It is 300 lines; default `--generate-toml-config` gives 600 😅 Comment-out hooks that need >0 work to be green. Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- .github/workflows/run-tests.yaml | 1 + .pre-commit-config.yaml | 85 +++++++++ .pylintrc | 284 +++++++++++++++++++++++++++++++ pyproject.toml | 92 ++++++++++ setup.cfg | 47 ----- tox.ini | 2 +- 6 files changed, 463 insertions(+), 48 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100644 pyproject.toml diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 3a1e534..2a428e0 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -52,6 +52,7 @@ jobs: - name: Test with tox env: FORCE_COLOR: 1 + PYTEST_CI_ARGS: --cov-report=xml --cov-report=html --junitxml=test_artifacts/test_report.xml --color=yes run: tox ${{ matrix.python-version == '3.6' && '--skip-missing-interpreters=true' || '' }} - name: Upload coverage reports to Codecov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bb3da74 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,85 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + exclude: ^.idea/ + - id: trailing-whitespace + - id: pretty-format-json + args: [ "--no-sort-keys", "--autofix", "--indent=4" ] + exclude: ^.vscode/ + # - repo: https://github.com/asottile/pyupgrade + # rev: v3.13.0 + # hooks: + # - id: pyupgrade + # args: + # - --py36-plus + - repo: https://github.com/PyCQA/autoflake + rev: v2.2.1 + hooks: + - id: autoflake + # - repo: https://github.com/astral-sh/ruff-pre-commit + # rev: v0.0.292 + # hooks: + # - id: ruff + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: + - --filter-files + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/asottile/blacken-docs + rev: 1.16.0 + hooks: + - id: blacken-docs + additional_dependencies: + - black==22.6.0 + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-use-type-annotations + - id: python-check-blanket-noqa + - id: python-check-blanket-type-ignore + - id: python-check-mock-methods + - id: python-no-eval + - id: python-no-log-warn + - id: python-use-type-annotations + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal + - id: text-unicode-replacement-char + # - repo: https://github.com/pycqa/flake8 + # rev: 5.0.4 + # hooks: + # - id: flake8 + # additional_dependencies: + # - flake8-bandit + # - flake8-bugbear + # - flake8-class-attributes-order + # - flake8-comprehensions + # # - flake8-docstrings # it is a mess to clean up; let's only warn user's IDEs instead. + # - flake8-future-annotations + # - flake8-noqa + # - flake8-print + # - flake8-pyproject + # - flake8-pytest-style + # - flake8-type-checking + # - flake8-variables-names + # - pep8-naming + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.6.0 + # hooks: + # - id: mypy + # - repo: local + # hooks: + # - id: pylint + # name: pylint + # entry: bash -c 'test -d .venv && . .venv/bin/activate ; pylint ${CI:+--reports=yes} "$@"' - + # language: system + # types: [ python ] + # args: + # - --disable=R,C diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..4bb1398 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,284 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list=jq + +load-plugins= + pylint_pytest, + pylint.extensions.bad_builtin, + pylint.extensions.broad_try_clause, + pylint.extensions.check_elif, + pylint.extensions.code_style, + pylint.extensions.comparetozero, + pylint.extensions.comparison_placement, + pylint.extensions.confusing_elif, + # pylint.extensions.consider_ternary_expression, # Not a pretty refactoring + pylint.extensions.docparams, + pylint.extensions.docstyle, + pylint.extensions.emptystring, + pylint.extensions.eq_without_hash, + pylint.extensions.for_any_all, + pylint.extensions.mccabe, + pylint.extensions.no_self_use, + pylint.extensions.overlapping_exceptions, + pylint.extensions.redefined_loop_name, + pylint.extensions.redefined_variable_type, + pylint.extensions.typing, + # pylint.extensions.while_used, # highly opinionated + pylint.extensions.dict_init_mutate, + pylint.extensions.dunder, + pylint.extensions.typing, + pylint.extensions.magic_value, + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=4 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=ex,Run,_,i,j,k, # Defaults + rc, # Return variable of `subprocess.xxx` methods + df, # Panda's DataFrame variable + cd, # Method/Context function that does `cd`. `cwd` is not much better + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=7 + +# Argument names that match this expression will be ignored. +# Defaults to name with leading underscore +ignored-argument-names=_.* + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + +extension-pkg-allow-list=jq + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MAGIC-VALUE] + +# List of valid magic values that `magic-value-compare` will not detect. +# Supports integers, floats, negative numbers, for empty string enter ``''``, +# for backslash values just use one backslash e.g \n. +valid-magic-values=0,-1,1,,__main__ + + +[MESSAGES CONTROL] + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable= + docstring-first-line-empty, # C0199 + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + useless-suppression + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[REFACTORING] + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=colorized + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=no + +# Ignore docstrings when computing similarities. +ignore-docstrings=no + +# Ignore signatures when computing similarities. +ignore-signatures=yes diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a7456f7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,92 @@ +# Only a configuration storage, for now + +[tool.black] +line-length = 120 + +[tool.isort] +profile = "black" + +[tool.coverage] +run.branch = true +run.data_file = "test_artifacts/.coverage" +xml.output = "test_artifacts/cobertura.xml" +html.directory = "test_artifacts/htmlcov" +report.exclude_lines = [ + # Have to re-enable the standard pragma + 'pragma: no cover', + # Don't complain about missing debug-only code: + 'def __repr__', + 'if self\.debug', + 'if settings.DEBUG', + # Don't complain if tests don't hit defensive assertion code: + 'raise AssertionError', + 'raise NotImplementedError', + # Don't complain if non-runnable code isn't run: + 'if 0:', + 'if __name__ == .__main__.:', + # Don't complain about abstract methods, they aren't run: + '@(abc\.)?abstractmethod', + 'class .*\bProtocol\):', + ## Defaults must be re-listed; we cannot `extend_exclude_lines` + + # Ignore type-checking blocks + 'if TYPE_CHECKING:', + # Defensive programming does not need to be covered + 'raise UnreachableCodeException', +] +paths.source = [ + "pylint_pytest/", +] + +[tool.flake8] +# Black compatible settings +max-line-length = 120 +extend-ignore = ["E203"] +extend-select = ["TC", "TC1", "B902", "B903"] + +noqa-require-code = true +noqa-include-name = true + +[tool.mypy] +python_version = "3.6" +mypy_path = "src/" +check_untyped_defs = true +explicit_package_bases = true +namespace_packages = true +show_error_codes = true +strict_optional = true +warn_redundant_casts = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true +exclude = [ + "^.venv", # Ignore installed packages + "^.cache", # Ignore CI-defined .cache +] + +[[tool.mypy.overrides]] +module = [ + "tests.*", +] +check_untyped_defs = true + +[tool.pytest.ini_options] +addopts = "--verbose --cov-config=pyproject.toml" + +[tool.ruff] +# ruff is less lenient than pylint and does not make any exceptions +# (for docstrings, strings and comments in particular). +line-length = 120 + +select = [ + "E", # pycodestyle + "F", # pyflakes + "W", # pycodestyle + "B", # bugbear + "I", # isort + "RUF", # ruff + "UP", # pyupgrade +] + +[tool.ruff.pydocstyle] +convention = "google" diff --git a/setup.cfg b/setup.cfg index ce0146f..cf4e2a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,52 +1,5 @@ [aliases] test = pytest -[tool:pytest] -addopts = --verbose --cov-report=xml --cov-report=html -python_files = tests/test_*.py - [bdist_wheel] universal = 1 - -[coverage:run] -branch = True - -[coverage:paths] -source = - pylint_pytest/ - -[coverage:report] -; Regexes for lines to exclude from consideration -exclude_also = - ; Have to re-enable the standard pragma - pragma: no cover - - ; Don't complain about missing debug-only code: - def __repr__ - if self\.debug - if settings.DEBUG - - ; Don't complain if tests don't hit defensive assertion code: - raise AssertionError - raise NotImplementedError - - ; Don't complain if non-runnable code isn't run: - if 0: - if __name__ == .__main__.: - - ; Don't complain about abstract methods, they aren't run: - @(abc\.)?abstractmethod - class .*\bProtocol\): - - ; Ignore type-checking blocks - if TYPE_CHECKING: - ; Defensive programming does not need to be covered - raise UnreachableCodeException - -ignore_errors = True - -[coverage:xml] -output = test_artifacts/cobertura.xml - -[coverage:html] -directory = test_artifacts/htmlcov diff --git a/tox.ini b/tox.ini index deb1a2b..59810ac 100644 --- a/tox.ini +++ b/tox.ini @@ -10,4 +10,4 @@ deps = pytest-cov commands = pip install --upgrade . - pytest --cov --cov-config=setup.cfg --cov-append --junitxml=test_artifacts/test_report.xml {tty:--color=yes} {posargs:tests} + pytest --cov --cov-append {env:PYTEST_CI_ARGS:} {tty:--color=yes} {posargs:tests} From 85d7e422b36fb86e22990ede8c92f7ec95ac43ec Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Sat, 14 Oct 2023 14:15:58 +0300 Subject: [PATCH 2/3] `pre-commit run -a` Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- README.md | 25 ++- pylint_pytest/__init__.py | 22 +-- pylint_pytest/checkers/__init__.py | 2 +- pylint_pytest/checkers/class_attr_loader.py | 25 ++- pylint_pytest/checkers/fixture.py | 160 ++++++++++-------- pylint_pytest/utils.py | 47 ++--- pyproject.toml | 39 ++++- setup.py | 67 ++++---- tests/base_tester.py | 15 +- .../no_such_package.py | 1 + tests/input/conftest.py | 2 +- .../with_args_scope.py | 4 +- .../with_kwargs_scope.py | 4 +- .../without_scope.py | 2 +- .../deprecated-pytest-yield-fixture/func.py | 2 +- tests/input/no-member/assign_attr_of_attr.py | 4 +- tests/input/no-member/fixture.py | 4 +- tests/input/no-member/from_unpack.py | 4 +- tests/input/no-member/inheritance.py | 4 +- tests/input/no-member/not_using_cls.py | 4 +- tests/input/no-member/yield_fixture.py | 4 +- .../unused-import/same_name_decorator.py | 3 +- .../not_pytest_marker.py | 7 +- .../other_marks_using_for_fixture.py | 1 + tests/test_cannot_enumerate_fixtures.py | 9 +- tests/test_no_member.py | 17 +- ...est_pytest_fixture_positional_arguments.py | 3 +- tests/test_pytest_mark_for_fixtures.py | 3 +- tests/test_pytest_yield_fixture.py | 3 +- tests/test_redefined_outer_name.py | 13 +- tests/test_regression.py | 20 ++- tests/test_unused_argument.py | 13 +- tests/test_unused_import.py | 27 +-- 34 files changed, 325 insertions(+), 237 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d0fce9b..2130f9e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,7 +12,7 @@ A clear and concise description of what the bug is. **To Reproduce** Package versions -- pylint +- pylint - pytest - pylint-pytest diff --git a/README.md b/README.md index 47d0a2f..7d0c4f2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,10 @@ def test_something(conftest_fixture): # <- Unused argument 'conftest_fixture' FP when an imported fixture is used in an applicable function, e.g. ```python -from fixture_collections import imported_fixture # <- Unused imported_fixture imported from fixture_collections +from fixture_collections import ( + imported_fixture, +) # <- Unused imported_fixture imported from fixture_collections + def test_something(imported_fixture): ... @@ -64,7 +67,10 @@ FP when an imported/declared fixture is used in an applicable function, e.g. ```python from fixture_collections import imported_fixture -def test_something(imported_fixture): # <- Redefining name 'imported_fixture' from outer scope (line 1) + +def test_something( + imported_fixture, +): # <- Redefining name 'imported_fixture' from outer scope (line 1) ... ``` @@ -75,15 +81,18 @@ FP when class attributes are defined in setup fixtures ```python import pytest + class TestClass(object): @staticmethod - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) def setup_class(request): cls = request.cls cls.defined_in_setup_class = True def test_foo(self): - assert self.defined_in_setup_class # <- Instance of 'TestClass' has no 'defined_in_setup_class' member + assert ( + self.defined_in_setup_class + ) # <- Instance of 'TestClass' has no 'defined_in_setup_class' member ``` ## Raise new warning(s) @@ -95,6 +104,7 @@ Raise when using deprecated `@pytest.yield_fixture` decorator ([ref](https://doc ```python import pytest + @pytest.yield_fixture # <- Using a deprecated @pytest.yield_fixture decorator def yield_fixture(): yield @@ -107,12 +117,16 @@ Raise when using every `@pytest.mark.*` for the fixture ([ref](https://docs.pyte ```python import pytest + @pytest.fixture def awesome_fixture(): ... + @pytest.fixture -@pytest.mark.usefixtures("awesome_fixture") # <- Using useless `@pytest.mark.*` decorator for fixtures +@pytest.mark.usefixtures( + "awesome_fixture" +) # <- Using useless `@pytest.mark.*` decorator for fixtures def another_awesome_fixture(): ... ``` @@ -124,6 +138,7 @@ Raise when using deprecated positional arguments for fixture decorator ([ref](ht ```python import pytest + @pytest.fixture("module") # <- Using a deprecated positional arguments for fixture def awesome_fixture(): ... diff --git a/pylint_pytest/__init__.py b/pylint_pytest/__init__.py index 983d575..3245018 100644 --- a/pylint_pytest/__init__.py +++ b/pylint_pytest/__init__.py @@ -1,29 +1,31 @@ -import os -import inspect -import importlib import glob +import importlib +import inspect +import os from .checkers import BasePytestChecker # pylint: disable=protected-access def register(linter): - '''auto discover pylint checker classes''' + """auto discover pylint checker classes""" dirname = os.path.dirname(__file__) - for module in glob.glob(os.path.join(dirname, 'checkers', '*.py')): + for module in glob.glob(os.path.join(dirname, "checkers", "*.py")): # trim file extension module = os.path.splitext(module)[0] # use relative path only - module = module.replace(dirname, '', 1) + module = module.replace(dirname, "", 1) # translate file path into module import path - module = module.replace(os.sep, '.') + module = module.replace(os.sep, ".") checker = importlib.import_module(module, package=os.path.basename(dirname)) for attr_name in dir(checker): attr_val = getattr(checker, attr_name) - if attr_val != BasePytestChecker and \ - inspect.isclass(attr_val) and \ - issubclass(attr_val, BasePytestChecker): + if ( + attr_val != BasePytestChecker + and inspect.isclass(attr_val) + and issubclass(attr_val, BasePytestChecker) + ): linter.register_checker(attr_val(linter)) diff --git a/pylint_pytest/checkers/__init__.py b/pylint_pytest/checkers/__init__.py index 543eb8e..d74c770 100644 --- a/pylint_pytest/checkers/__init__.py +++ b/pylint_pytest/checkers/__init__.py @@ -2,4 +2,4 @@ class BasePytestChecker(BaseChecker): - name = 'pylint-pytest' + name = "pylint-pytest" diff --git a/pylint_pytest/checkers/class_attr_loader.py b/pylint_pytest/checkers/class_attr_loader.py index 1cfda6a..d741cbe 100644 --- a/pylint_pytest/checkers/class_attr_loader.py +++ b/pylint_pytest/checkers/class_attr_loader.py @@ -1,19 +1,20 @@ import astroid from pylint.interfaces import IAstroidChecker + from ..utils import _can_use_fixture, _is_class_autouse_fixture from . import BasePytestChecker class ClassAttrLoader(BasePytestChecker): __implements__ = IAstroidChecker - msgs = {'E6400': ('', 'pytest-class-attr-loader', '')} + msgs = {"E6400": ("", "pytest-class-attr-loader", "")} in_setup = False request_cls = set() class_node = None def visit_functiondef(self, node): - '''determine if a method is a class setup method''' + """determine if a method is a class setup method""" self.in_setup = False self.request_cls = set() self.class_node = None @@ -23,17 +24,23 @@ def visit_functiondef(self, node): self.class_node = node.parent def visit_assign(self, node): - '''store the aliases for `cls`''' - if self.in_setup and isinstance(node.value, astroid.Attribute) and \ - node.value.attrname == 'cls' and \ - node.value.expr.name == 'request': + """store the aliases for `cls`""" + if ( + self.in_setup + and isinstance(node.value, astroid.Attribute) + and node.value.attrname == "cls" + and node.value.expr.name == "request" + ): # storing the aliases for cls from request.cls self.request_cls = set(map(lambda t: t.name, node.targets)) def visit_assignattr(self, node): - if self.in_setup and isinstance(node.expr, astroid.Name) and \ - node.expr.name in self.request_cls and \ - node.attrname not in self.class_node.locals: + if ( + self.in_setup + and isinstance(node.expr, astroid.Name) + and node.expr.name in self.request_cls + and node.attrname not in self.class_node.locals + ): try: # find Assign node which contains the source "value" assign_node = node diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index c2a00a0..59f53a3 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -1,24 +1,25 @@ +import fnmatch import os import sys from pathlib import Path -import fnmatch import astroid import pylint +import pytest from pylint.checkers.variables import VariablesChecker from pylint.interfaces import IAstroidChecker -import pytest + from ..utils import ( _can_use_fixture, + _is_pytest_fixture, _is_pytest_mark, _is_pytest_mark_usefixtures, - _is_pytest_fixture, _is_same_module, ) from . import BasePytestChecker # TODO: support pytest python_files configuration -FILE_NAME_PATTERNS = ('test_*.py', '*_test.py') +FILE_NAME_PATTERNS = ("test_*.py", "*_test.py") class FixtureCollector: @@ -37,34 +38,34 @@ def pytest_collectreport(self, report): class FixtureChecker(BasePytestChecker): __implements__ = IAstroidChecker msgs = { - 'W6401': ( - 'Using a deprecated @pytest.yield_fixture decorator', - 'deprecated-pytest-yield-fixture', - 'Used when using a deprecated pytest decorator that has been deprecated in pytest-3.0' + "W6401": ( + "Using a deprecated @pytest.yield_fixture decorator", + "deprecated-pytest-yield-fixture", + "Used when using a deprecated pytest decorator that has been deprecated in pytest-3.0", ), - 'W6402': ( - 'Using useless `@pytest.mark.*` decorator for fixtures', - 'useless-pytest-mark-decorator', + "W6402": ( + "Using useless `@pytest.mark.*` decorator for fixtures", + "useless-pytest-mark-decorator", ( - '@pytest.mark.* decorators can\'t by applied to fixtures. ' - 'Take a look at: https://docs.pytest.org/en/stable/reference.html#marks' + "@pytest.mark.* decorators can't by applied to fixtures. " + "Take a look at: https://docs.pytest.org/en/stable/reference.html#marks" ), ), - 'W6403': ( - 'Using a deprecated positional arguments for fixture', - 'deprecated-positional-argument-for-pytest-fixture', + "W6403": ( + "Using a deprecated positional arguments for fixture", + "deprecated-positional-argument-for-pytest-fixture", ( - 'Pass scope as a kwarg, not positional arg, which is deprecated in future pytest. ' - 'Take a look at: https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only' + "Pass scope as a kwarg, not positional arg, which is deprecated in future pytest. " + "Take a look at: https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only" ), ), - 'F6401': ( + "F6401": ( ( - 'pylint-pytest plugin cannot enumerate and collect pytest fixtures. ' - 'Please run `pytest --fixtures --collect-only path/to/current/module.py` and resolve any potential syntax error or package dependency issues' + "pylint-pytest plugin cannot enumerate and collect pytest fixtures. " + "Please run `pytest --fixtures --collect-only path/to/current/module.py` and resolve any potential syntax error or package dependency issues" ), - 'cannot-enumerate-pytest-fixtures', - 'Used when pylint-pytest has been unable to enumerate and collect pytest fixtures.', + "cannot-enumerate-pytest-fixtures", + "Used when pylint-pytest has been unable to enumerate and collect pytest fixtures.", ), } @@ -79,7 +80,7 @@ def open(self): VariablesChecker.add_message = FixtureChecker.patch_add_message def close(self): - '''restore & reset class attr for testing''' + """restore & reset class attr for testing""" # restore add_message VariablesChecker.add_message = FixtureChecker._original_add_message FixtureChecker._original_add_message = callable @@ -90,11 +91,11 @@ def close(self): FixtureChecker._invoked_with_usefixtures = set() def visit_module(self, node): - ''' + """ - only run once per module - invoke pytest session to collect available fixtures - create containers for the module to store args and fixtures - ''' + """ # storing all fixtures discovered by pytest session FixtureChecker._pytest_fixtures = {} # Dict[List[_pytest.fixtures.FixtureDef]] @@ -111,7 +112,7 @@ def visit_module(self, node): break try: - with open(os.devnull, 'w') as devnull: + with open(os.devnull, "w") as devnull: # suppress any future output from pytest stdout, stderr = sys.stdout, sys.stderr sys.stderr = sys.stdout = devnull @@ -124,8 +125,10 @@ def visit_module(self, node): ret = pytest.main( [ - node.file, '--fixtures', '--collect-only', - '--pythonwarnings=ignore:Module already imported:pytest.PytestWarning', + node.file, + "--fixtures", + "--collect-only", + "--pythonwarnings=ignore:Module already imported:pytest.PytestWarning", ], plugins=[fixture_collector], ) @@ -136,7 +139,7 @@ def visit_module(self, node): FixtureChecker._pytest_fixtures = fixture_collector.fixtures if (ret != pytest.ExitCode.OK or fixture_collector.errors) and is_test_module: - self.add_message('cannot-enumerate-pytest-fixtures', node=node) + self.add_message("cannot-enumerate-pytest-fixtures", node=node) finally: # restore output devices sys.stdout, sys.stderr = stdout, stderr @@ -164,9 +167,14 @@ def visit_decorators(self, node): uses_fixture_deco, uses_mark_deco = False, False for decorator in node.nodes: try: - if _is_pytest_fixture(decorator) and isinstance(decorator, astroid.Call) and decorator.args: + if ( + _is_pytest_fixture(decorator) + and isinstance(decorator, astroid.Call) + and decorator.args + ): self.add_message( - 'deprecated-positional-argument-for-pytest-fixture', node=decorator + "deprecated-positional-argument-for-pytest-fixture", + node=decorator, ) uses_fixture_deco |= _is_pytest_fixture(decorator) uses_mark_deco |= _is_pytest_mark(decorator) @@ -177,10 +185,10 @@ def visit_decorators(self, node): self.add_message("useless-pytest-mark-decorator", node=node) def visit_functiondef(self, node): - ''' + """ - save invoked fixtures for later use - save used function arguments for later use - ''' + """ if _can_use_fixture(node): if node.decorators: # check all decorators @@ -189,72 +197,88 @@ def visit_functiondef(self, node): # save all visited fixtures for arg in decorator.args: self._invoked_with_usefixtures.add(arg.value) - if int(pytest.__version__.split('.')[0]) >= 3 and \ - _is_pytest_fixture(decorator, fixture=False): + if int(pytest.__version__.split(".")[0]) >= 3 and _is_pytest_fixture( + decorator, fixture=False + ): # raise deprecated warning for @pytest.yield_fixture - self.add_message('deprecated-pytest-yield-fixture', node=node) + self.add_message("deprecated-pytest-yield-fixture", node=node) for arg in node.args.args: self._invoked_with_func_args.add(arg.name) # pylint: disable=protected-access,bad-staticmethod-argument @staticmethod - def patch_add_message(self, msgid, line=None, node=None, args=None, - confidence=None, col_offset=None): - ''' + def patch_add_message( + self, msgid, line=None, node=None, args=None, confidence=None, col_offset=None + ): + """ - intercept and discard unwanted warning messages - ''' + """ # check W0611 unused-import - if msgid == 'unused-import': + if msgid == "unused-import": # actual attribute name is not passed as arg so...dirty hack # message is usually in the form of '%s imported from %s (as %)' message_tokens = args.split() fixture_name = message_tokens[0] # ignoring 'import %s' message - if message_tokens[0] == 'import' and len(message_tokens) == 2: + if message_tokens[0] == "import" and len(message_tokens) == 2: pass # fixture is defined in other modules and being imported to # conftest for pytest magic - elif isinstance(node.parent, astroid.Module) \ - and node.parent.name.split('.')[-1] == 'conftest' \ - and fixture_name in FixtureChecker._pytest_fixtures: + elif ( + isinstance(node.parent, astroid.Module) + and node.parent.name.split(".")[-1] == "conftest" + and fixture_name in FixtureChecker._pytest_fixtures + ): return # imported fixture is referenced in test/fixture func - elif fixture_name in FixtureChecker._invoked_with_func_args \ - and fixture_name in FixtureChecker._pytest_fixtures: - if _is_same_module(fixtures=FixtureChecker._pytest_fixtures, - import_node=node, - fixture_name=fixture_name): + elif ( + fixture_name in FixtureChecker._invoked_with_func_args + and fixture_name in FixtureChecker._pytest_fixtures + ): + if _is_same_module( + fixtures=FixtureChecker._pytest_fixtures, + import_node=node, + fixture_name=fixture_name, + ): return # fixture is referenced in @pytest.mark.usefixtures - elif fixture_name in FixtureChecker._invoked_with_usefixtures \ - and fixture_name in FixtureChecker._pytest_fixtures: - if _is_same_module(fixtures=FixtureChecker._pytest_fixtures, - import_node=node, - fixture_name=fixture_name): + elif ( + fixture_name in FixtureChecker._invoked_with_usefixtures + and fixture_name in FixtureChecker._pytest_fixtures + ): + if _is_same_module( + fixtures=FixtureChecker._pytest_fixtures, + import_node=node, + fixture_name=fixture_name, + ): return # check W0613 unused-argument - if msgid == 'unused-argument' and \ - _can_use_fixture(node.parent.parent) and \ - isinstance(node.parent, astroid.Arguments) and \ - node.name in FixtureChecker._pytest_fixtures: + if ( + msgid == "unused-argument" + and _can_use_fixture(node.parent.parent) + and isinstance(node.parent, astroid.Arguments) + and node.name in FixtureChecker._pytest_fixtures + ): return # check W0621 redefined-outer-name - if msgid == 'redefined-outer-name' and \ - _can_use_fixture(node.parent.parent) and \ - isinstance(node.parent, astroid.Arguments) and \ - node.name in FixtureChecker._pytest_fixtures: + if ( + msgid == "redefined-outer-name" + and _can_use_fixture(node.parent.parent) + and isinstance(node.parent, astroid.Arguments) + and node.name in FixtureChecker._pytest_fixtures + ): return - if int(pylint.__version__.split('.')[0]) >= 2: + if int(pylint.__version__.split(".")[0]) >= 2: FixtureChecker._original_add_message( - self, msgid, line, node, args, confidence, col_offset) + self, msgid, line, node, args, confidence, col_offset + ) else: # python2 + pylint1.9 backward compatibility - FixtureChecker._original_add_message( - self, msgid, line, node, args, confidence) + FixtureChecker._original_add_message(self, msgid, line, node, args, confidence) diff --git a/pylint_pytest/utils.py b/pylint_pytest/utils.py index 7dac65f..05a6c8d 100644 --- a/pylint_pytest/utils.py +++ b/pylint_pytest/utils.py @@ -1,14 +1,17 @@ import inspect + import astroid def _is_pytest_mark_usefixtures(decorator): # expecting @pytest.mark.usefixture(...) try: - if isinstance(decorator, astroid.Call) and \ - decorator.func.attrname == 'usefixtures' and \ - decorator.func.expr.attrname == 'mark' and \ - decorator.func.expr.expr.name == 'pytest': + if ( + isinstance(decorator, astroid.Call) + and decorator.func.attrname == "usefixtures" + and decorator.func.expr.attrname == "mark" + and decorator.func.expr.expr.name == "pytest" + ): return True except AttributeError: pass @@ -20,7 +23,7 @@ def _is_pytest_mark(decorator): deco = decorator # as attribute `@pytest.mark.trylast` if isinstance(decorator, astroid.Call): deco = decorator.func # as function `@pytest.mark.skipif(...)` - if deco.expr.attrname == 'mark' and deco.expr.expr.name == 'pytest': + if deco.expr.attrname == "mark" and deco.expr.expr.name == "pytest": return True except AttributeError: pass @@ -32,10 +35,10 @@ def _is_pytest_fixture(decorator, fixture=True, yield_fixture=True): to_check = set() if fixture: - to_check.add('fixture') + to_check.add("fixture") if yield_fixture: - to_check.add('yield_fixture') + to_check.add("yield_fixture") try: if isinstance(decorator, astroid.Attribute): @@ -46,8 +49,7 @@ def _is_pytest_fixture(decorator, fixture=True, yield_fixture=True): # expecting @pytest.fixture(scope=...) attr = decorator.func - if attr and attr.attrname in to_check \ - and attr.expr.name == 'pytest': + if attr and attr.attrname in to_check and attr.expr.name == "pytest": return True except AttributeError: pass @@ -61,15 +63,17 @@ def _is_class_autouse_fixture(function): if isinstance(decorator, astroid.Call): func = decorator.func - if func and func.attrname in ('fixture', 'yield_fixture') \ - and func.expr.name == 'pytest': - + if ( + func + and func.attrname in ("fixture", "yield_fixture") + and func.expr.name == "pytest" + ): is_class = is_autouse = False for kwarg in decorator.keywords or []: - if kwarg.arg == 'scope' and kwarg.value.value == 'class': + if kwarg.arg == "scope" and kwarg.value.value == "class": is_class = True - if kwarg.arg == 'autouse' and kwarg.value.value is True: + if kwarg.arg == "autouse" and kwarg.value.value is True: is_autouse = True if is_class and is_autouse: @@ -82,9 +86,8 @@ def _is_class_autouse_fixture(function): def _can_use_fixture(function): if isinstance(function, astroid.FunctionDef): - # test_*, *_test - if function.name.startswith('test_') or function.name.endswith('_test'): + if function.name.startswith("test_") or function.name.endswith("_test"): return True if function.decorators: @@ -101,14 +104,16 @@ def _can_use_fixture(function): def _is_same_module(fixtures, import_node, fixture_name): - '''Comparing pytest fixture node with astroid.ImportFrom''' + """Comparing pytest fixture node with astroid.ImportFrom""" try: for fixture in fixtures[fixture_name]: for import_from in import_node.root().globals[fixture_name]: - if inspect.getmodule(fixture.func).__file__ == \ - import_from.parent.import_module(import_from.modname, - False, - import_from.level).file: + if ( + inspect.getmodule(fixture.func).__file__ + == import_from.parent.import_module( + import_from.modname, False, import_from.level + ).file + ): return True except: # pylint: disable=bare-except pass diff --git a/pyproject.toml b/pyproject.toml index a7456f7..11ffae0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ # Only a configuration storage, for now [tool.black] -line-length = 120 +line-length = 100 [tool.isort] profile = "black" @@ -40,7 +40,7 @@ paths.source = [ [tool.flake8] # Black compatible settings -max-line-length = 120 +max-line-length = 100 extend-ignore = ["E203"] extend-select = ["TC", "TC1", "B902", "B903"] @@ -48,8 +48,7 @@ noqa-require-code = true noqa-include-name = true [tool.mypy] -python_version = "3.6" -mypy_path = "src/" +python_version = "3.7" check_untyped_defs = true explicit_package_bases = true namespace_packages = true @@ -60,10 +59,20 @@ warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true exclude = [ - "^.venv", # Ignore installed packages - "^.cache", # Ignore CI-defined .cache + "^.venv", # Ignore installed packages + "^.tox", # Ignore tox virtualenvs + "^.cache", # Ignore CI-defined .cache + "^tests/input/" # Ignore test inputs ] +[[tool.mypy.overrides]] +module = [ + "astroid", + "pylint.*", + "setuptools", +] +ignore_missing_imports = true + [[tool.mypy.overrides]] module = [ "tests.*", @@ -76,7 +85,7 @@ addopts = "--verbose --cov-config=pyproject.toml" [tool.ruff] # ruff is less lenient than pylint and does not make any exceptions # (for docstrings, strings and comments in particular). -line-length = 120 +line-length = 100 select = [ "E", # pycodestyle @@ -90,3 +99,19 @@ select = [ [tool.ruff.pydocstyle] convention = "google" + +[tool.ruff.extend-per-file-ignores] +"tests/**/test_*.py" = [ + "S101", # pytest works with `assert`s + "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() + # The below are debateable + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "PLR2004", # Magic value used in comparison +] +"tests/**/*_test.py" = [ + "S101", # pytest works with `assert`s + "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() + # The below are debateable + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "PLR2004", # Magic value used in comparison +] diff --git a/setup.py b/setup.py index 03a8589..667465c 100644 --- a/setup.py +++ b/setup.py @@ -1,50 +1,49 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- from os import path -from setuptools import setup, find_packages +from setuptools import find_packages, setup here = path.abspath(path.dirname(__file__)) -with open(path.join(here, 'README.md')) as fin: +with open(path.join(here, "README.md")) as fin: long_description = fin.read() setup( - name='pylint-pytest', - version='1.1.2', - author='Reverb Chu', - author_email='pylint-pytest@reverbc.tw', - maintainer='Reverb Chu', - maintainer_email='pylint-pytest@reverbc.tw', - license='MIT', - url='https://github.com/reverbc/pylint-pytest', - description='A Pylint plugin to suppress pytest-related false positives.', + name="pylint-pytest", + version="1.1.2", + author="Reverb Chu", + author_email="pylint-pytest@reverbc.tw", + maintainer="Reverb Chu", + maintainer_email="pylint-pytest@reverbc.tw", + license="MIT", + url="https://github.com/reverbc/pylint-pytest", + description="A Pylint plugin to suppress pytest-related false positives.", long_description=long_description, - long_description_content_type='text/markdown', - packages=find_packages(exclude=['tests', 'sandbox']), + long_description_content_type="text/markdown", + packages=find_packages(exclude=["tests", "sandbox"]), install_requires=[ - 'pylint<3', - 'pytest>=4.6', + "pylint<3", + "pytest>=4.6", ], - python_requires='>=3.6', + python_requires=">=3.6", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: Quality Assurance', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: Implementation :: CPython', - 'Operating System :: OS Independent', - 'License :: OSI Approved :: MIT License', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Quality Assurance", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", ], - tests_require=['pytest', 'pytest-cov', 'pylint'], - keywords=['pylint', 'pytest', 'plugin'], + tests_require=["pytest", "pytest-cov", "pylint"], + keywords=["pylint", "pytest", "plugin"], ) diff --git a/tests/base_tester.py b/tests/base_tester.py index cf8c178..2ad64c5 100644 --- a/tests/base_tester.py +++ b/tests/base_tester.py @@ -1,23 +1,25 @@ -import sys import os +import sys from pprint import pprint import astroid from pylint.testutils import UnittestLinter + try: from pylint.utils import ASTWalker except ImportError: # for pylint 1.9 from pylint.utils import PyLintASTWalker as ASTWalker + from pylint.checkers import BaseChecker import pylint_pytest.checkers.fixture # XXX: allow all file name -pylint_pytest.checkers.fixture.FILE_NAME_PATTERNS = ('*', ) +pylint_pytest.checkers.fixture.FILE_NAME_PATTERNS = ("*",) -class BasePytestTester(object): +class BasePytestTester: CHECKER_CLASS = BaseChecker IMPACTED_CHECKER_CLASSES = [] MSG_ID = None @@ -31,9 +33,8 @@ def run_linter(self, enable_plugin, file_path=None): # pylint: disable=protected-access if file_path is None: - module = sys._getframe(1).f_code.co_name.replace('test_', '', 1) - file_path = os.path.join( - os.getcwd(), 'tests', 'input', self.MSG_ID, module + '.py') + module = sys._getframe(1).f_code.co_name.replace("test_", "", 1) + file_path = os.path.join(os.getcwd(), "tests", "input", self.MSG_ID, module + ".py") with open(file_path) as fin: content = fin.read() @@ -53,7 +54,7 @@ def verify_messages(self, msg_count, msg_id=None): matched_count += 1 pprint(self.MESSAGES) - assert matched_count == msg_count, f'expecting {msg_count}, actual {matched_count}' + assert matched_count == msg_count, f"expecting {msg_count}, actual {matched_count}" def setup_method(self): self.linter = UnittestLinter() diff --git a/tests/input/cannot-enumerate-pytest-fixtures/no_such_package.py b/tests/input/cannot-enumerate-pytest-fixtures/no_such_package.py index da18ef0..23cbe82 100644 --- a/tests/input/cannot-enumerate-pytest-fixtures/no_such_package.py +++ b/tests/input/cannot-enumerate-pytest-fixtures/no_such_package.py @@ -6,5 +6,6 @@ def fixture(): pass + def test_something(fixture): pass diff --git a/tests/input/conftest.py b/tests/input/conftest.py index 0965d19..6824751 100644 --- a/tests/input/conftest.py +++ b/tests/input/conftest.py @@ -6,6 +6,6 @@ def conftest_fixture_attr(): return True -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def conftest_fixture_func(): return True diff --git a/tests/input/deprecated-positional-argument-for-pytest-fixture/with_args_scope.py b/tests/input/deprecated-positional-argument-for-pytest-fixture/with_args_scope.py index 239a960..95fa0b6 100644 --- a/tests/input/deprecated-positional-argument-for-pytest-fixture/with_args_scope.py +++ b/tests/input/deprecated-positional-argument-for-pytest-fixture/with_args_scope.py @@ -1,6 +1,6 @@ import pytest -@pytest.fixture('function') +@pytest.fixture("function") def some_fixture(): - return 'ok' + return "ok" diff --git a/tests/input/deprecated-positional-argument-for-pytest-fixture/with_kwargs_scope.py b/tests/input/deprecated-positional-argument-for-pytest-fixture/with_kwargs_scope.py index 57a510b..1ec1f5c 100644 --- a/tests/input/deprecated-positional-argument-for-pytest-fixture/with_kwargs_scope.py +++ b/tests/input/deprecated-positional-argument-for-pytest-fixture/with_kwargs_scope.py @@ -1,6 +1,6 @@ import pytest -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def some_fixture(): - return 'ok' + return "ok" diff --git a/tests/input/deprecated-positional-argument-for-pytest-fixture/without_scope.py b/tests/input/deprecated-positional-argument-for-pytest-fixture/without_scope.py index 82187f0..60d3841 100644 --- a/tests/input/deprecated-positional-argument-for-pytest-fixture/without_scope.py +++ b/tests/input/deprecated-positional-argument-for-pytest-fixture/without_scope.py @@ -3,4 +3,4 @@ @pytest.fixture def some_fixture(): - return 'ok' + return "ok" diff --git a/tests/input/deprecated-pytest-yield-fixture/func.py b/tests/input/deprecated-pytest-yield-fixture/func.py index 8f5867d..9d853bc 100644 --- a/tests/input/deprecated-pytest-yield-fixture/func.py +++ b/tests/input/deprecated-pytest-yield-fixture/func.py @@ -6,6 +6,6 @@ def yield_fixture(): yield -@pytest.yield_fixture(scope='session') +@pytest.yield_fixture(scope="session") def yield_fixture_session(): yield diff --git a/tests/input/no-member/assign_attr_of_attr.py b/tests/input/no-member/assign_attr_of_attr.py index 9b2ddc0..79c54fb 100644 --- a/tests/input/no-member/assign_attr_of_attr.py +++ b/tests/input/no-member/assign_attr_of_attr.py @@ -1,9 +1,9 @@ import pytest -class TestClass(object): +class TestClass: @staticmethod - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) def setup_class(request): cls = request.cls cls.defined_in_setup_class = object() diff --git a/tests/input/no-member/fixture.py b/tests/input/no-member/fixture.py index 4293624..76fe6b1 100644 --- a/tests/input/no-member/fixture.py +++ b/tests/input/no-member/fixture.py @@ -1,9 +1,9 @@ import pytest -class TestClass(object): +class TestClass: @staticmethod - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) def setup_class(request): cls = request.cls cls.defined_in_setup_class = 123 diff --git a/tests/input/no-member/from_unpack.py b/tests/input/no-member/from_unpack.py index 6132fbd..e88c635 100644 --- a/tests/input/no-member/from_unpack.py +++ b/tests/input/no-member/from_unpack.py @@ -5,9 +5,9 @@ def meh(): return True, False -class TestClass(object): +class TestClass: @staticmethod - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) def setup_class(request): cls = request.cls cls.defined_in_setup_class, _ = meh() diff --git a/tests/input/no-member/inheritance.py b/tests/input/no-member/inheritance.py index e4355f1..9e86916 100644 --- a/tests/input/no-member/inheritance.py +++ b/tests/input/no-member/inheritance.py @@ -1,9 +1,9 @@ import pytest -class TestClass(object): +class TestClass: @staticmethod - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) def setup_class(request): cls = request.cls cls.defined_in_setup_class = 123 diff --git a/tests/input/no-member/not_using_cls.py b/tests/input/no-member/not_using_cls.py index b2ad9e6..95438af 100644 --- a/tests/input/no-member/not_using_cls.py +++ b/tests/input/no-member/not_using_cls.py @@ -1,9 +1,9 @@ import pytest -class TestClass(object): +class TestClass: @staticmethod - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) def setup_class(request): clls = request.cls clls.defined_in_setup_class = 123 diff --git a/tests/input/no-member/yield_fixture.py b/tests/input/no-member/yield_fixture.py index 3bc5deb..c2f823b 100644 --- a/tests/input/no-member/yield_fixture.py +++ b/tests/input/no-member/yield_fixture.py @@ -1,9 +1,9 @@ import pytest -class TestClass(object): +class TestClass: @staticmethod - @pytest.yield_fixture(scope='class', autouse=True) + @pytest.yield_fixture(scope="class", autouse=True) def setup_class(request): cls = request.cls cls.defined_in_setup_class = 123 diff --git a/tests/input/unused-import/same_name_decorator.py b/tests/input/unused-import/same_name_decorator.py index 322dc08..cc32ed6 100644 --- a/tests/input/unused-import/same_name_decorator.py +++ b/tests/input/unused-import/same_name_decorator.py @@ -1,8 +1,9 @@ import pytest + # an actual unused import, just happened to have the same name as fixture from _same_name_module import conftest_fixture_attr -@pytest.mark.usefixtures('conftest_fixture_attr') +@pytest.mark.usefixtures("conftest_fixture_attr") def test_conftest_fixture_attr(): assert True diff --git a/tests/input/useless-pytest-mark-decorator/not_pytest_marker.py b/tests/input/useless-pytest-mark-decorator/not_pytest_marker.py index 0efad86..595601d 100644 --- a/tests/input/useless-pytest-mark-decorator/not_pytest_marker.py +++ b/tests/input/useless-pytest-mark-decorator/not_pytest_marker.py @@ -6,14 +6,11 @@ def noop(func): @functools.wraps(func) def wrapper_noop(*args, **kwargs): return func(*args, **kwargs) + return wrapper_noop -PYTEST = SimpleNamespace( - MARK=SimpleNamespace( - noop=noop - ) -) +PYTEST = SimpleNamespace(MARK=SimpleNamespace(noop=noop)) @noop diff --git a/tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py b/tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py index b269f61..562f04b 100644 --- a/tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py +++ b/tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py @@ -1,4 +1,5 @@ import os + import pytest diff --git a/tests/test_cannot_enumerate_fixtures.py b/tests/test_cannot_enumerate_fixtures.py index c931359..1904b73 100644 --- a/tests/test_cannot_enumerate_fixtures.py +++ b/tests/test_cannot_enumerate_fixtures.py @@ -1,19 +1,20 @@ import pytest -from pylint.checkers.variables import VariablesChecker from base_tester import BasePytestTester +from pylint.checkers.variables import VariablesChecker + from pylint_pytest.checkers.fixture import FixtureChecker class TestCannotEnumerateFixtures(BasePytestTester): CHECKER_CLASS = FixtureChecker - MSG_ID = 'cannot-enumerate-pytest-fixtures' + MSG_ID = "cannot-enumerate-pytest-fixtures" - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_no_such_package(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(1 if enable_plugin else 0) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_import_corrupted_module(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(1 if enable_plugin else 0) diff --git a/tests/test_no_member.py b/tests/test_no_member.py index 4c7bad7..9d9019f 100644 --- a/tests/test_no_member.py +++ b/tests/test_no_member.py @@ -1,40 +1,41 @@ import pytest +from base_tester import BasePytestTester from pylint.checkers.typecheck import TypeChecker + from pylint_pytest.checkers.class_attr_loader import ClassAttrLoader -from base_tester import BasePytestTester class TestNoMember(BasePytestTester): CHECKER_CLASS = ClassAttrLoader IMPACTED_CHECKER_CLASSES = [TypeChecker] - MSG_ID = 'no-member' + MSG_ID = "no-member" - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_fixture(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_yield_fixture(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_not_using_cls(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_inheritance(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_from_unpack(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_assign_attr_of_attr(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) diff --git a/tests/test_pytest_fixture_positional_arguments.py b/tests/test_pytest_fixture_positional_arguments.py index 6208d9f..3913c9d 100644 --- a/tests/test_pytest_fixture_positional_arguments.py +++ b/tests/test_pytest_fixture_positional_arguments.py @@ -1,10 +1,11 @@ from base_tester import BasePytestTester + from pylint_pytest.checkers.fixture import FixtureChecker class TestDeprecatedPytestFixtureScopeAsPositionalParam(BasePytestTester): CHECKER_CLASS = FixtureChecker - MSG_ID = 'deprecated-positional-argument-for-pytest-fixture' + MSG_ID = "deprecated-positional-argument-for-pytest-fixture" def test_with_args_scope(self): self.run_linter(enable_plugin=True) diff --git a/tests/test_pytest_mark_for_fixtures.py b/tests/test_pytest_mark_for_fixtures.py index 2854126..df5f747 100644 --- a/tests/test_pytest_mark_for_fixtures.py +++ b/tests/test_pytest_mark_for_fixtures.py @@ -1,10 +1,11 @@ from base_tester import BasePytestTester + from pylint_pytest.checkers.fixture import FixtureChecker class TestPytestMarkUsefixtures(BasePytestTester): CHECKER_CLASS = FixtureChecker - MSG_ID = 'useless-pytest-mark-decorator' + MSG_ID = "useless-pytest-mark-decorator" def test_mark_usefixture_using_for_test(self): self.run_linter(enable_plugin=True) diff --git a/tests/test_pytest_yield_fixture.py b/tests/test_pytest_yield_fixture.py index 0102b89..4408fca 100644 --- a/tests/test_pytest_yield_fixture.py +++ b/tests/test_pytest_yield_fixture.py @@ -1,11 +1,12 @@ from base_tester import BasePytestTester + from pylint_pytest.checkers.fixture import FixtureChecker class TestDeprecatedPytestYieldFixture(BasePytestTester): CHECKER_CLASS = FixtureChecker IMPACTED_CHECKER_CLASSES = [] - MSG_ID = 'deprecated-pytest-yield-fixture' + MSG_ID = "deprecated-pytest-yield-fixture" def test_smoke(self): self.run_linter(enable_plugin=True) diff --git a/tests/test_redefined_outer_name.py b/tests/test_redefined_outer_name.py index 2de88b4..11e48a0 100644 --- a/tests/test_redefined_outer_name.py +++ b/tests/test_redefined_outer_name.py @@ -1,30 +1,31 @@ import pytest -from pylint.checkers.variables import VariablesChecker from base_tester import BasePytestTester +from pylint.checkers.variables import VariablesChecker + from pylint_pytest.checkers.fixture import FixtureChecker class TestRedefinedOuterName(BasePytestTester): CHECKER_CLASS = FixtureChecker IMPACTED_CHECKER_CLASSES = [VariablesChecker] - MSG_ID = 'redefined-outer-name' + MSG_ID = "redefined-outer-name" - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_smoke(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_caller_yield_fixture(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_caller_not_a_test_func(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_args_and_kwargs(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(2) diff --git a/tests/test_regression.py b/tests/test_regression.py index 2cc9233..e53f999 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -1,24 +1,26 @@ import pylint import pytest -from pylint.checkers.variables import VariablesChecker from base_tester import BasePytestTester +from pylint.checkers.variables import VariablesChecker + from pylint_pytest.checkers.fixture import FixtureChecker class TestRegression(BasePytestTester): - '''Covering some behaviors that shouldn't get impacted by the plugin''' + """Covering some behaviors that shouldn't get impacted by the plugin""" + CHECKER_CLASS = FixtureChecker IMPACTED_CHECKER_CLASSES = [VariablesChecker] - MSG_ID = 'regression' + MSG_ID = "regression" - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_import_twice(self, enable_plugin): - '''catch a coding error when using fixture + if + inline import''' + """catch a coding error when using fixture + if + inline import""" self.run_linter(enable_plugin) - if int(pylint.__version__.split('.')[0]) < 2: + if int(pylint.__version__.split(".")[0]) < 2: # for some reason pylint 1.9.5 does not raise unused-import for inline import - self.verify_messages(1, msg_id='unused-import') + self.verify_messages(1, msg_id="unused-import") else: - self.verify_messages(2, msg_id='unused-import') - self.verify_messages(1, msg_id='redefined-outer-name') + self.verify_messages(2, msg_id="unused-import") + self.verify_messages(1, msg_id="redefined-outer-name") diff --git a/tests/test_unused_argument.py b/tests/test_unused_argument.py index 7ab2747..7ca2307 100644 --- a/tests/test_unused_argument.py +++ b/tests/test_unused_argument.py @@ -1,30 +1,31 @@ import pytest -from pylint.checkers.variables import VariablesChecker from base_tester import BasePytestTester +from pylint.checkers.variables import VariablesChecker + from pylint_pytest.checkers.fixture import FixtureChecker class TestUnusedArgument(BasePytestTester): CHECKER_CLASS = FixtureChecker IMPACTED_CHECKER_CLASSES = [VariablesChecker] - MSG_ID = 'unused-argument' + MSG_ID = "unused-argument" - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_smoke(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 2) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_caller_yield_fixture(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_caller_not_a_test_func(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_args_and_kwargs(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(2) diff --git a/tests/test_unused_import.py b/tests/test_unused_import.py index 888e432..1f2bd4b 100644 --- a/tests/test_unused_import.py +++ b/tests/test_unused_import.py @@ -1,41 +1,42 @@ import pytest -from pylint.checkers.variables import VariablesChecker from base_tester import BasePytestTester +from pylint.checkers.variables import VariablesChecker + from pylint_pytest.checkers.fixture import FixtureChecker class TestUnusedImport(BasePytestTester): CHECKER_CLASS = FixtureChecker IMPACTED_CHECKER_CLASSES = [VariablesChecker] - MSG_ID = 'unused-import' + MSG_ID = "unused-import" - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_smoke(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_caller_yield_fixture(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_same_name_arg(self, enable_plugin): - '''an unused import (not a fixture) just happened to have the same - name as fixture - should still raise unused-import warning''' + """an unused import (not a fixture) just happened to have the same + name as fixture - should still raise unused-import warning""" self.run_linter(enable_plugin) self.verify_messages(1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_same_name_decorator(self, enable_plugin): - '''an unused import (not a fixture) just happened to have the same - name as fixture - should still raise unused-import warning''' + """an unused import (not a fixture) just happened to have the same + name as fixture - should still raise unused-import warning""" self.run_linter(enable_plugin) self.verify_messages(1) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_conftest(self, enable_plugin): - '''fixtures are defined in different modules and imported to conftest - for pytest to do its magic''' + """fixtures are defined in different modules and imported to conftest + for pytest to do its magic""" self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 1) From ff18b5870c93228b2a145052a4f27121c71a6c5a Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Sat, 14 Oct 2023 13:29:05 +0300 Subject: [PATCH 3/3] Enable https://github.com/asottile/pyupgrade + `.git-blame-ignore-revs` Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- .git-blame-ignore-revs | 2 ++ .pre-commit-config.yaml | 12 ++++++------ .../other_marks_using_for_fixture.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..c08b6d4 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Add `.pre-commit-config.yaml` + `pre-commit run -a` +85d7e422b36fb86e22990ede8c92f7ec95ac43ec diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb3da74..9365c0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,12 +8,12 @@ repos: - id: pretty-format-json args: [ "--no-sort-keys", "--autofix", "--indent=4" ] exclude: ^.vscode/ - # - repo: https://github.com/asottile/pyupgrade - # rev: v3.13.0 - # hooks: - # - id: pyupgrade - # args: - # - --py36-plus + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: + - --py36-plus - repo: https://github.com/PyCQA/autoflake rev: v2.2.1 hooks: diff --git a/tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py b/tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py index 562f04b..96e9ed1 100644 --- a/tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py +++ b/tests/input/useless-pytest-mark-decorator/other_marks_using_for_fixture.py @@ -12,7 +12,7 @@ def fixture(): @pytest.mark.parametrize("id", range(2)) @pytest.fixture def fixture_with_params(id): - return "{} not OK".format(id) + return f"{id} not OK" @pytest.mark.custom_mark