From 189fe4017bd98849ffbf7d7b1fa06d89cc9d4237 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Fri, 24 Feb 2023 11:23:22 +0200 Subject: [PATCH 01/27] CI: Modernize, MOAR Pythons, GH-Actions Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- .github/workflows/run-tests.yaml | 49 ++++++++++++++++++++++++++++++++ .travis.yml | 4 +++ README.md | 1 + setup.py | 2 ++ tox.ini | 2 +- 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/run-tests.yaml diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml new file mode 100644 index 0000000..2a416d0 --- /dev/null +++ b/.github/workflows/run-tests.yaml @@ -0,0 +1,49 @@ +name: Python package + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: setup.py + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox + + test-36: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.6 + uses: actions/setup-python@v4 + with: + python-version: 3.6 + cache: 'pip' + cache-dependency-path: setup.py + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Test with tox + run: tox -e py36 diff --git a/.travis.yml b/.travis.yml index d08855f..26dac9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,10 @@ matrix: env: TOX_ENV=py38 - python: 3.9 env: TOX_ENV=py39 + - python: 3.10 + env: TOX_ENV=py310 + - python: 3.11 + env: TOX_ENV=py311 install: - pip install tox diff --git a/README.md b/README.md index 2cfcc6c..2799f88 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # pylint-pytest +[![Python package](https://github.com/stdedos/pylint-pytest/actions/workflows/run-tests.yaml/badge.svg)](https://github.com/stdedos/pylint-pytest/actions/workflows/run-tests.yaml) [![PyPI version fury.io](https://badge.fury.io/py/pylint-pytest.svg)](https://pypi.python.org/pypi/pylint-pytest/) [![Travis CI](https://travis-ci.org/reverbc/pylint-pytest.svg?branch=master)](https://travis-ci.org/reverbc/pylint-pytest) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/reverbc/pylint-pytest?branch=master&svg=true)](https://ci.appveyor.com/project/reverbc/pylint-pytest) diff --git a/setup.py b/setup.py index ae9e869..847b1c9 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,8 @@ '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', diff --git a/tox.ini b/tox.ini index a7cfb5d..63013e5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,py39 +envlist = py36,py37,py38,py39,py310,py311 skipsdist = True [testenv] From c5af2e5cc0b7bf03f4aeeccc1818468de5f6370d Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Sat, 14 Oct 2023 10:18:23 +0300 Subject: [PATCH 02/27] MOAR Improvements: Cross-OS testing, `.gitignore` updates, supporting only `pylint<3`, coverage at Codecov, drop other CI Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- .github/workflows/run-tests.yaml | 61 ++++++----- .gitignore | 171 ++++++++++++++++++++++++++++++- .travis.yml | 37 ------- README.md | 2 +- appveyor.yml | 36 ------- setup.cfg | 45 +++++++- setup.py | 4 +- tox.ini | 11 +- 8 files changed, 258 insertions(+), 109 deletions(-) delete mode 100644 .travis.yml delete mode 100644 appveyor.yml diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 2a416d0..3a1e534 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -1,49 +1,62 @@ -name: Python package +name: Testing on: push: - branches: - - master pull_request: - branches: - - master jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + os: + - ubuntu-latest + - windows-latest + - macos-latest + python-version: + - '3.6' + - '3.7' + - '3.8' + - '3.9' + - '3.10' + - '3.11' + # - '3.12' # FixMe: https://github.com/pylint-dev/pylint-pytest/issues/3 + # Python 3.6 is not available in `ubuntu-latest`. + exclude: + - python-version: '3.6' + os: ubuntu-latest + include: + - python-version: '3.6' + os: ubuntu-20.04 + + defaults: + run: + shell: ${{ matrix.os == 'windows-latest' && 'pwsh' || '/bin/bash --noprofile --norc -Eeuxo pipefail {0}' }} steps: - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: 'pip' cache-dependency-path: setup.py - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox tox-gh-actions - - name: Test with tox - run: tox - test-36: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.6 - uses: actions/setup-python@v4 - with: - python-version: 3.6 - cache: 'pip' - cache-dependency-path: setup.py - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install tox + - name: Test with tox - run: tox -e py36 + env: + FORCE_COLOR: 1 + run: tox ${{ matrix.python-version == '3.6' && '--skip-missing-interpreters=true' || '' }} + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + files: test_artifacts/cobertura.xml diff --git a/.gitignore b/.gitignore index e99db73..4012e01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,169 @@ -*.pyc +sandbox/**/*.py +/test_artifacts/ + +## Mostly complete version from https://github.com/github/gitignore/blob/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Python.gitignore + +# Byte-compiled / optimized / DLL files __pycache__/ -.tox/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python build/ -sandbox/**/*.py -Pipfile* +develop-eggs/ dist/ -Makefile +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +cache/* + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# VS code +.vscode/launch.json diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 26dac9c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: python - -matrix: - include: - - python: 3.6 - env: TOX_ENV=py36 - - python: 3.7 - env: TOX_ENV=py37 - - python: 3.8 - env: TOX_ENV=py38 - - python: 3.9 - env: TOX_ENV=py39 - - python: 3.10 - env: TOX_ENV=py310 - - python: 3.11 - env: TOX_ENV=py311 - -install: - - pip install tox - -script: - - tox -e $TOX_ENV - -before_cache: - - rm -rf $HOME/.cache/pip/log - -cache: - directories: - - $HOME/.cache/pip - -deploy: - provider: pypi - user: __token__ - on: - tags: true - condition: "$TOX_ENV = py38" - distributions: bdist_wheel diff --git a/README.md b/README.md index 2799f88..47d0a2f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pylint-pytest -[![Python package](https://github.com/stdedos/pylint-pytest/actions/workflows/run-tests.yaml/badge.svg)](https://github.com/stdedos/pylint-pytest/actions/workflows/run-tests.yaml) +[![Python package](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml/badge.svg)](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml) [![PyPI version fury.io](https://badge.fury.io/py/pylint-pytest.svg)](https://pypi.python.org/pypi/pylint-pytest/) [![Travis CI](https://travis-ci.org/reverbc/pylint-pytest.svg?branch=master)](https://travis-ci.org/reverbc/pylint-pytest) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/reverbc/pylint-pytest?branch=master&svg=true)](https://ci.appveyor.com/project/reverbc/pylint-pytest) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 7b1a8b3..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,36 +0,0 @@ -# What Python version is installed where: -# http://www.appveyor.com/docs/installed-software#python - -image: Visual Studio 2019 - -environment: - matrix: - - PYTHON: "C:\\Python36" - TOX_ENV: "py36" - - - PYTHON: "C:\\Python37" - TOX_ENV: "py37" - - - PYTHON: "C:\\Python38" - TOX_ENV: "py38" - - - PYTHON: "C:\\Python39" - TOX_ENV: "py39" - -init: - - "%PYTHON%/python -V" - - "%PYTHON%/python -c \"import struct;print( 8 * struct.calcsize(\'P\'))\"" - -install: - - "%PYTHON%/Scripts/easy_install -U pip" - - "%PYTHON%/Scripts/pip install tox" - - "%PYTHON%/Scripts/pip install wheel" - - "%PYTHON%/Scripts/pip install psutil" - -build: false # Not a C# project, build stuff at the test step instead. - -test_script: - - "%PYTHON%/Scripts/tox -e %TOX_ENV%" - -#on_success: -# - TODO: upload the content of dist/*.whl to a public wheelhouse diff --git a/setup.cfg b/setup.cfg index 2a2ef5b..ce0146f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,8 +2,51 @@ test = pytest [tool:pytest] -addopts = --verbose +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/setup.py b/setup.py index 847b1c9..03a8589 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ long_description_content_type='text/markdown', packages=find_packages(exclude=['tests', 'sandbox']), install_requires=[ - 'pylint', + 'pylint<3', 'pytest>=4.6', ], python_requires='>=3.6', @@ -45,6 +45,6 @@ 'Operating System :: OS Independent', 'License :: OSI Approved :: MIT License', ], - tests_require=['pytest', 'pylint'], + tests_require=['pytest', 'pytest-cov', 'pylint'], keywords=['pylint', 'pytest', 'plugin'], ) diff --git a/tox.ini b/tox.ini index 63013e5..deb1a2b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,13 @@ [tox] -envlist = py36,py37,py38,py39,py310,py311 +envlist = py36,py37,py38,py39,py310,py311,py312 skipsdist = True +passenv = + FORCE_COLOR [testenv] +deps = + pytest + pytest-cov commands = - pip install ./ --upgrade - pytest {posargs:tests} + pip install --upgrade . + pytest --cov --cov-config=setup.cfg --cov-append --junitxml=test_artifacts/test_report.xml {tty:--color=yes} {posargs:tests} From 56fa1d1a359b740598240bae8ed694bacbb37691 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 14 Oct 2023 12:44:20 +0200 Subject: [PATCH 03/27] Add a release job to publish on pypi on github tag creation --- .github/workflows/release.yml | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5fb9cbe --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: Release + +on: + release: + types: + - published + +env: + DEFAULT_PYTHON: "3.11" + +permissions: + contents: read + +jobs: + release-pypi: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: + name: PyPI + url: https://pypi.org/project/pylint-pytest/ + steps: + - name: Check out code from Github + uses: actions/checkout@v4.1.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.7.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Install requirements + run: | + # Remove dist, build, and pylint.egg-info + # when building locally for testing! + python -m pip install twine build + - name: Build distributions + run: | + python -m build + - name: Upload to PyPI + if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags') + env: + TWINE_REPOSITORY: pypi + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + twine upload --verbose dist/* 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 04/27] 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 05/27] `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 06/27] 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 From ae436acdf36010e12c8e419e0fa294a00ca40e4d Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 14 Oct 2023 13:41:30 +0200 Subject: [PATCH 07/27] Use ruff instead of flake8 pyupgrade autoflake and isort --- .pre-commit-config.yaml | 44 +++------------------ pylint_pytest/checkers/class_attr_loader.py | 2 +- pylint_pytest/checkers/fixture.py | 3 +- pylint_pytest/utils.py | 2 +- pyproject.toml | 14 +------ tests/test_cannot_enumerate_fixtures.py | 1 - 6 files changed, 11 insertions(+), 55 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9365c0d..a15a151 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,26 +8,12 @@ repos: - id: pretty-format-json args: [ "--no-sort-keys", "--autofix", "--indent=4" ] exclude: ^.vscode/ - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.292 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 + - id: ruff + args: ["--fix"] + exclude: "tests/input/" - repo: https://github.com/psf/black rev: 23.9.1 hooks: @@ -37,7 +23,7 @@ repos: hooks: - id: blacken-docs additional_dependencies: - - black==22.6.0 + - black==23.9.1 - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: @@ -52,24 +38,6 @@ repos: - 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: diff --git a/pylint_pytest/checkers/class_attr_loader.py b/pylint_pytest/checkers/class_attr_loader.py index d741cbe..f00a1df 100644 --- a/pylint_pytest/checkers/class_attr_loader.py +++ b/pylint_pytest/checkers/class_attr_loader.py @@ -49,6 +49,6 @@ def visit_assignattr(self, node): # hack class locals self.class_node.locals[node.attrname] = [assign_node.value] - except: # pylint: disable=bare-except + except Exception: # pylint: disable=broad-except # cannot find valid assign expr, skipping the entire attribute pass diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index 59f53a3..cca224b 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -62,7 +62,8 @@ class FixtureChecker(BasePytestChecker): "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" + "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.", diff --git a/pylint_pytest/utils.py b/pylint_pytest/utils.py index 05a6c8d..41f8817 100644 --- a/pylint_pytest/utils.py +++ b/pylint_pytest/utils.py @@ -115,6 +115,6 @@ def _is_same_module(fixtures, import_node, fixture_name): ).file ): return True - except: # pylint: disable=bare-except + except Exception: # pylint: disable=broad-except pass return False diff --git a/pyproject.toml b/pyproject.toml index 11ffae0..17a7d80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,6 @@ [tool.black] line-length = 100 -[tool.isort] -profile = "black" - [tool.coverage] run.branch = true run.data_file = "test_artifacts/.coverage" @@ -38,15 +35,6 @@ paths.source = [ "pylint_pytest/", ] -[tool.flake8] -# Black compatible settings -max-line-length = 100 -extend-ignore = ["E203"] -extend-select = ["TC", "TC1", "B902", "B903"] - -noqa-require-code = true -noqa-include-name = true - [tool.mypy] python_version = "3.7" check_untyped_defs = true @@ -93,7 +81,7 @@ select = [ "W", # pycodestyle "B", # bugbear "I", # isort - "RUF", # ruff +# "RUF", # ruff "UP", # pyupgrade ] diff --git a/tests/test_cannot_enumerate_fixtures.py b/tests/test_cannot_enumerate_fixtures.py index 1904b73..4ad241e 100644 --- a/tests/test_cannot_enumerate_fixtures.py +++ b/tests/test_cannot_enumerate_fixtures.py @@ -1,6 +1,5 @@ import pytest from base_tester import BasePytestTester -from pylint.checkers.variables import VariablesChecker from pylint_pytest.checkers.fixture import FixtureChecker From 1c2613b8e86a5875ecda1cc02664adb4a81ea5ae Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 15 Oct 2023 11:48:36 +0200 Subject: [PATCH 08/27] Upgrade the changelog for 1.1.3a0 release (#10) --- CHANGELOG.md | 6 ++++++ setup.py | 8 +++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59758b8..3ea16bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +## [1.1.3] - 2023-10-15 + +### Fixed +- The continuous integration was fixed, as a new maintenance team was assembled. + + ## [1.1.2] - 2021-04-19 ### Fixed - Fix #18 plugin crash when test case is marked with a non-pytest.mark decorator diff --git a/setup.py b/setup.py index 667465c..0384661 100644 --- a/setup.py +++ b/setup.py @@ -11,11 +11,9 @@ 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", + version="1.1.3a0", + author="Stavros Ntentos", + author_email="133706+stdedos@users.noreply.github.com", license="MIT", url="https://github.com/reverbc/pylint-pytest", description="A Pylint plugin to suppress pytest-related false positives.", From ae3d8ca8b3bad2b82a7d825c2c7d1ec9ab30f3fd Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 14 Oct 2023 14:52:56 +0200 Subject: [PATCH 09/27] Activate ruff, ignore the typing of classvar --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 17a7d80..7f6f071 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,10 +81,14 @@ select = [ "W", # pycodestyle "B", # bugbear "I", # isort -# "RUF", # ruff + "RUF", # ruff "UP", # pyupgrade ] +ignore = [ + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` +] + [tool.ruff.pydocstyle] convention = "google" From ec514087609055465513dea24b4cca56214a3b66 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 14 Oct 2023 14:38:11 +0200 Subject: [PATCH 10/27] [pylint] Add pylint in the continuous integration and pre-commit --- .github/workflows/checks.yaml | 116 +++++++++ .pre-commit-config.yaml | 21 +- .pylintrc | 272 ++------------------ pylint_pytest/checkers/class_attr_loader.py | 2 +- pylint_pytest/checkers/fixture.py | 5 +- tests/base_tester.py | 8 +- 6 files changed, 153 insertions(+), 271 deletions(-) create mode 100644 .github/workflows/checks.yaml diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..54c30db --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,116 @@ +name: Checks + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + CACHE_VERSION: 1 + KEY_PREFIX: base-venv + DEFAULT_PYTHON: "3.11" + PRE_COMMIT_CACHE: ~/.cache/pre-commit + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + prepare-base: + name: Prepare base dependencies + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} + pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.7.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "key=${{ env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test.txt', + 'requirements_test_min.txt', 'requirements_test_pre_commit.txt') }}" >> + $GITHUB_OUTPUT + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.3.2 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python -m pip install -U pip setuptools wheel + pip install . + pip install pre-commit + - name: Generate pre-commit restore key + id: generate-pre-commit-key + run: >- + echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{ + hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT + - name: Restore pre-commit environment + id: cache-precommit + uses: actions/cache@v3.3.2 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} + - name: Install pre-commit dependencies + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + . venv/bin/activate + pre-commit install --install-hooks + + pylint: + name: pylint + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.7.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.3.2 + with: + path: venv + fail-on-cache-miss: true + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} + - name: Restore pre-commit environment + id: cache-precommit + uses: actions/cache@v3.3.2 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + fail-on-cache-miss: true + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Run pylint checks + run: | + . venv/bin/activate + pip install . + pre-commit run pylint --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a15a151..6e19fb2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +ci: + skip: [pylint] + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 @@ -42,12 +45,12 @@ repos: # 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 + - repo: local + hooks: + - id: pylint + name: pylint + entry: bash -c 'test -d .venv && . .venv/bin/activate ; pylint "$@"' - + language: system + types: [ python ] + exclude: "tests/input/" + args: ["-sn", "-rn"] diff --git a/.pylintrc b/.pylintrc index 4bb1398..7e84d35 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,9 +1,4 @@ -[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 +[main] load-plugins= pylint_pytest, @@ -30,255 +25,20 @@ load-plugins= 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 + unspecified-encoding, + fixme, + missing-docstring, + attribute-defined-outside-init, + use-maxsplit-arg, + used-before-assignment, + too-many-arguments, + too-many-nested-blocks, + too-many-try-statements, + redefined-loop-name, + docstring-first-line-empty, + confusing-consecutive-elif, + too-complex, + cannot-enumerate-pytest-fixtures, + duplicate-code, diff --git a/pylint_pytest/checkers/class_attr_loader.py b/pylint_pytest/checkers/class_attr_loader.py index f00a1df..34ce714 100644 --- a/pylint_pytest/checkers/class_attr_loader.py +++ b/pylint_pytest/checkers/class_attr_loader.py @@ -32,7 +32,7 @@ def visit_assign(self, node): 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)) + self.request_cls = set(t.name for t in node.targets) def visit_assignattr(self, node): if ( diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index cca224b..33c1544 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -20,6 +20,9 @@ # TODO: support pytest python_files configuration FILE_NAME_PATTERNS = ("test_*.py", "*_test.py") +ARGUMENT_ARE_KEYWORD_ONLY = ( + "https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only" +) class FixtureCollector: @@ -56,7 +59,7 @@ class FixtureChecker(BasePytestChecker): "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" + f"Take a look at: {ARGUMENT_ARE_KEYWORD_ONLY}" ), ), "F6401": ( diff --git a/tests/base_tester.py b/tests/base_tester.py index 2ad64c5..5653cfd 100644 --- a/tests/base_tester.py +++ b/tests/base_tester.py @@ -23,7 +23,7 @@ class BasePytestTester: CHECKER_CLASS = BaseChecker IMPACTED_CHECKER_CLASSES = [] MSG_ID = None - MESSAGES = None + msgs = None CONFIG = {} enable_plugin = True @@ -42,18 +42,18 @@ def run_linter(self, enable_plugin, file_path=None): module.file = fin.name self.walk(module) # run all checkers - self.MESSAGES = self.linter.release_messages() + self.msgs = self.linter.release_messages() def verify_messages(self, msg_count, msg_id=None): msg_id = msg_id or self.MSG_ID matched_count = 0 - for message in self.MESSAGES: + for message in self.msgs: # only care about ID and count, not the content if message.msg_id == msg_id: matched_count += 1 - pprint(self.MESSAGES) + pprint(self.msgs) assert matched_count == msg_count, f"expecting {msg_count}, actual {matched_count}" def setup_method(self): From 0a4331d3d58b7a89eff50bf2104451116e66fdd7 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 14 Oct 2023 20:24:24 +0200 Subject: [PATCH 11/27] Move the pylint configuration to pyproject.toml --- .pre-commit-config.yaml | 1 - .pylintrc | 44 ------------------------------------- pyproject.toml | 48 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 45 deletions(-) delete mode 100644 .pylintrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e19fb2..399a480 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,5 +52,4 @@ repos: entry: bash -c 'test -d .venv && . .venv/bin/activate ; pylint "$@"' - language: system types: [ python ] - exclude: "tests/input/" args: ["-sn", "-rn"] diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 7e84d35..0000000 --- a/.pylintrc +++ /dev/null @@ -1,44 +0,0 @@ -[main] - -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, - -disable= - unspecified-encoding, - fixme, - missing-docstring, - attribute-defined-outside-init, - use-maxsplit-arg, - used-before-assignment, - too-many-arguments, - too-many-nested-blocks, - too-many-try-statements, - redefined-loop-name, - docstring-first-line-empty, - confusing-consecutive-elif, - too-complex, - cannot-enumerate-pytest-fixtures, - duplicate-code, diff --git a/pyproject.toml b/pyproject.toml index 7f6f071..0b65117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,3 +107,51 @@ convention = "google" "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes "PLR2004", # Magic value used in comparison ] + +[tool.pylint] + +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", # highly opinionated +] +disable=[ + "unspecified-encoding", + "fixme", + "missing-docstring", + "attribute-defined-outside-init", + "use-maxsplit-arg", + "used-before-assignment", + "too-many-arguments", + "too-many-nested-blocks", + "too-many-try-statements", + "redefined-loop-name", + "docstring-first-line-empty", + "confusing-consecutive-elif", + "too-complex", + "cannot-enumerate-pytest-fixtures", # our own message, fix first + "duplicate-code", +] +ignore-paths="tests/input" From 7feeb56d5953585deec1cfaea7bc02ca2e36366d Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Sun, 15 Oct 2023 13:31:16 +0300 Subject: [PATCH 12/27] Minor re-ordering of the `tool.pylint`, with some comments Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- pyproject.toml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b65117..a935a08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,6 +110,8 @@ convention = "google" [tool.pylint] +ignore-paths="tests/input" # Ignore test inputs + load-plugins= [ "pylint_pytest", "pylint.extensions.bad_builtin", @@ -138,20 +140,21 @@ load-plugins= [ # "pylint.extensions.magic_value", # highly opinionated ] disable=[ - "unspecified-encoding", - "fixme", - "missing-docstring", + "docstring-first-line-empty", # C0199; not-an-issue + + # Temporary disables + "cannot-enumerate-pytest-fixtures", # ToDo: Our own message, fix first + "fixme", # needs-work, and probably regex "attribute-defined-outside-init", - "use-maxsplit-arg", - "used-before-assignment", + "confusing-consecutive-elif", + "duplicate-code", + "missing-docstring", + "redefined-loop-name", + "too-complex", "too-many-arguments", "too-many-nested-blocks", "too-many-try-statements", - "redefined-loop-name", - "docstring-first-line-empty", - "confusing-consecutive-elif", - "too-complex", - "cannot-enumerate-pytest-fixtures", # our own message, fix first - "duplicate-code", + "unspecified-encoding", + "use-maxsplit-arg", + "used-before-assignment", ] -ignore-paths="tests/input" From a444a6105a1a36c4d4cbb9970341fd0253566326 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Tue, 17 Oct 2023 22:15:48 +0300 Subject: [PATCH 13/27] Carry over some `.pylintrc` configuration Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- pylint_pytest/__init__.py | 1 - pylint_pytest/checkers/fixture.py | 2 +- pyproject.toml | 12 ++++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pylint_pytest/__init__.py b/pylint_pytest/__init__.py index 3245018..93e4690 100644 --- a/pylint_pytest/__init__.py +++ b/pylint_pytest/__init__.py @@ -6,7 +6,6 @@ from .checkers import BasePytestChecker -# pylint: disable=protected-access def register(linter): """auto discover pylint checker classes""" dirname = os.path.dirname(__file__) diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index 33c1544..3ff7400 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -209,7 +209,7 @@ def visit_functiondef(self, node): for arg in node.args.args: self._invoked_with_func_args.add(arg.name) - # pylint: disable=protected-access,bad-staticmethod-argument + # pylint: disable=bad-staticmethod-argument @staticmethod def patch_add_message( self, msgid, line=None, node=None, args=None, confidence=None, col_offset=None diff --git a/pyproject.toml b/pyproject.toml index a935a08..68ad0a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,3 +158,15 @@ disable=[ "use-maxsplit-arg", "used-before-assignment", ] + +[tool.pylint.design] +max-args = 7 + +[tool.pylint.reports] +output-format = "colorized" + +[tool.pylint.variables] +ignored-argument-names = "_.*" + +[tool.pylint."messages control"] +enable = ["useless-suppression"] From 122c79998383549df4731e67927a310f74c7583c Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 14 Oct 2023 20:09:50 +0200 Subject: [PATCH 14/27] [mypy] Fix Item "None" of "Optional[Module]" has no attribute "__file__" --- pylint_pytest/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pylint_pytest/utils.py b/pylint_pytest/utils.py index 41f8817..6c66e6e 100644 --- a/pylint_pytest/utils.py +++ b/pylint_pytest/utils.py @@ -108,12 +108,11 @@ def _is_same_module(fixtures, import_node, fixture_name): 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 - ): + module = inspect.getmodule(fixture.func) + parent_import = import_from.parent.import_module( + import_from.modname, False, import_from.level + ) + if module is not None and module.__file__ == parent_import.file: return True except Exception: # pylint: disable=broad-except pass From 33d1313d368c7551ba6c42efcb903b57fced4341 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 14 Oct 2023 20:10:01 +0200 Subject: [PATCH 15/27] Activate mypy in pre-commit --- .pre-commit-config.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 399a480..50be1c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,10 +41,11 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - id: text-unicode-replacement-char - # - repo: https://github.com/pre-commit/mirrors-mypy - # rev: v1.6.0 - # hooks: - # - id: mypy + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.6.0 + hooks: + - id: mypy + exclude: "tests/input/" - repo: local hooks: - id: pylint From b65e9796f6d86db31e36f16edd682d3560ed8fbd Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Wed, 18 Oct 2023 01:17:26 +0300 Subject: [PATCH 16/27] Fix mypy issues Most of the issues are "almost clear-cut", except the astroid stuff - `pylint_pytest/checkers/class_attr_loader.py`. Those were mostly placated (but hopefully done right). Additionally, add some sanity features to `BasePytestTester`: * Marked class as `ABC` * Enforce subclasses to initialize `MSG_ID` Mostly both of those changes are "aesthetical", and do not contribute to strict guarantees of behavior, or type assertions. But at least `pytest` cries loudly when used wrong - so that's something. Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- pylint_pytest/checkers/class_attr_loader.py | 18 +++++++---- pylint_pytest/checkers/fixture.py | 34 ++++++++++--------- pylint_pytest/checkers/types.py | 15 +++++++++ pyproject.toml | 5 +++ tests/base_tester.py | 36 +++++++++++++-------- tests/base_tester_test.py | 28 ++++++++++++++++ tests/test_pytest_yield_fixture.py | 1 - 7 files changed, 100 insertions(+), 37 deletions(-) create mode 100644 pylint_pytest/checkers/types.py create mode 100644 tests/base_tester_test.py diff --git a/pylint_pytest/checkers/class_attr_loader.py b/pylint_pytest/checkers/class_attr_loader.py index 34ce714..f9b5557 100644 --- a/pylint_pytest/checkers/class_attr_loader.py +++ b/pylint_pytest/checkers/class_attr_loader.py @@ -1,4 +1,6 @@ -import astroid +from __future__ import annotations + +from astroid import Assign, Attribute, ClassDef, Name from pylint.interfaces import IAstroidChecker from ..utils import _can_use_fixture, _is_class_autouse_fixture @@ -10,8 +12,8 @@ class ClassAttrLoader(BasePytestChecker): msgs = {"E6400": ("", "pytest-class-attr-loader", "")} in_setup = False - request_cls = set() - class_node = None + request_cls: set[str] = set() + class_node: ClassDef | None = None def visit_functiondef(self, node): """determine if a method is a class setup method""" @@ -23,12 +25,13 @@ def visit_functiondef(self, node): self.in_setup = True self.class_node = node.parent - def visit_assign(self, node): + def visit_assign(self, node: Assign): """store the aliases for `cls`""" if ( self.in_setup - and isinstance(node.value, astroid.Attribute) + and isinstance(node.value, Attribute) and node.value.attrname == "cls" + and isinstance(node.value.expr, Name) and node.value.expr.name == "request" ): # storing the aliases for cls from request.cls @@ -37,14 +40,15 @@ def visit_assign(self, node): def visit_assignattr(self, node): if ( self.in_setup - and isinstance(node.expr, astroid.Name) + and isinstance(node.expr, Name) and node.expr.name in self.request_cls + and self.class_node is not None and node.attrname not in self.class_node.locals ): try: # find Assign node which contains the source "value" assign_node = node - while not isinstance(assign_node, astroid.Assign): + while not isinstance(assign_node, Assign): assign_node = assign_node.parent # hack class locals diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index 3ff7400..b41201f 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import fnmatch import os import sys @@ -17,17 +19,19 @@ _is_same_module, ) from . import BasePytestChecker +from .types import FixtureDict, replacement_add_message # TODO: support pytest python_files configuration -FILE_NAME_PATTERNS = ("test_*.py", "*_test.py") +FILE_NAME_PATTERNS: tuple[str, ...] = ("test_*.py", "*_test.py") ARGUMENT_ARE_KEYWORD_ONLY = ( "https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only" ) class FixtureCollector: - fixtures = {} - errors = set() + # Same as ``_pytest.fixtures.FixtureManager._arg2fixturedefs``. + fixtures: FixtureDict = {} + errors: set[pytest.CollectReport] = set() def pytest_sessionfinish(self, session): # pylint: disable=protected-access @@ -73,10 +77,13 @@ class FixtureChecker(BasePytestChecker): ), } - _pytest_fixtures = {} - _invoked_with_func_args = set() - _invoked_with_usefixtures = set() - _original_add_message = callable + # Store all fixtures discovered by pytest session + _pytest_fixtures: FixtureDict = {} + # Stores all used function arguments + _invoked_with_func_args: set[str] = set() + # Stores all invoked fixtures through @pytest.mark.usefixture(...) + _invoked_with_usefixtures: set[str] = set() + _original_add_message = replacement_add_message def open(self): # patch VariablesChecker.add_message @@ -87,7 +94,7 @@ def close(self): """restore & reset class attr for testing""" # restore add_message VariablesChecker.add_message = FixtureChecker._original_add_message - FixtureChecker._original_add_message = callable + FixtureChecker._original_add_message = replacement_add_message # reset fixture info storage FixtureChecker._pytest_fixtures = {} @@ -100,14 +107,9 @@ def visit_module(self, node): - 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]] - - # storing all used function arguments - FixtureChecker._invoked_with_func_args = set() # Set[str] - - # storing all invoked fixtures through @pytest.mark.usefixture(...) - FixtureChecker._invoked_with_usefixtures = set() # Set[str] + FixtureChecker._pytest_fixtures = {} + FixtureChecker._invoked_with_func_args = set() + FixtureChecker._invoked_with_usefixtures = set() is_test_module = False for pattern in FILE_NAME_PATTERNS: diff --git a/pylint_pytest/checkers/types.py b/pylint_pytest/checkers/types.py new file mode 100644 index 0000000..7fd5708 --- /dev/null +++ b/pylint_pytest/checkers/types.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import sys +from pprint import pprint +from typing import Any, Dict, List + +from _pytest.fixtures import FixtureDef + +FixtureDict = Dict[str, List[FixtureDef[Any]]] + + +def replacement_add_message(*args, **kwargs): + print("Called un-initialized _original_add_message with:", file=sys.stderr) + pprint(args, sys.stderr) + pprint(kwargs, sys.stderr) diff --git a/pyproject.toml b/pyproject.toml index 68ad0a8..9f4abc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,9 @@ ignore = [ "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` ] +# py36, but ruff does not support it :/ +target-version = "py37" + [tool.ruff.pydocstyle] convention = "google" @@ -110,6 +113,8 @@ convention = "google" [tool.pylint] +py-version = "3.6" + ignore-paths="tests/input" # Ignore test inputs load-plugins= [ diff --git a/tests/base_tester.py b/tests/base_tester.py index 5653cfd..4d6498c 100644 --- a/tests/base_tester.py +++ b/tests/base_tester.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import os import sys +from abc import ABC from pprint import pprint +from typing import Any import astroid -from pylint.testutils import UnittestLinter +from pylint.testutils import MessageTest, UnittestLinter try: from pylint.utils import ASTWalker @@ -15,30 +19,36 @@ import pylint_pytest.checkers.fixture -# XXX: allow all file name +# XXX: allow all file names pylint_pytest.checkers.fixture.FILE_NAME_PATTERNS = ("*",) -class BasePytestTester: +class BasePytestTester(ABC): CHECKER_CLASS = BaseChecker - IMPACTED_CHECKER_CLASSES = [] - MSG_ID = None - msgs = None - CONFIG = {} + IMPACTED_CHECKER_CLASSES: list[BaseChecker] = [] + MSG_ID: str + msgs: list[MessageTest] = [] + CONFIG: dict[str, Any] = {} + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + if not hasattr(cls, "MSG_ID") or not isinstance(cls.MSG_ID, str) or not cls.MSG_ID: + raise TypeError("Subclasses must define a non-empty MSG_ID of type str") enable_plugin = True - def run_linter(self, enable_plugin, file_path=None): + def run_linter(self, enable_plugin): self.enable_plugin = enable_plugin - # 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") + # pylint: disable-next=protected-access + target_test_file = sys._getframe(1).f_code.co_name.replace("test_", "", 1) + file_path = os.path.join( + os.getcwd(), "tests", "input", self.MSG_ID, target_test_file + ".py" + ) with open(file_path) as fin: content = fin.read() - module = astroid.parse(content, module_name=module) + module = astroid.parse(content, module_name=target_test_file) module.file = fin.name self.walk(module) # run all checkers diff --git a/tests/base_tester_test.py b/tests/base_tester_test.py new file mode 100644 index 0000000..dc1a48a --- /dev/null +++ b/tests/base_tester_test.py @@ -0,0 +1,28 @@ +import pytest +from base_tester import BasePytestTester + +# pylint: disable=unused-variable + + +def test_init_subclass_valid_msg_id(): + some_string = "some_string" + + class ValidSubclass(BasePytestTester): + MSG_ID = some_string + + assert ValidSubclass.MSG_ID == some_string + + +def test_init_subclass_no_msg_id(): + with pytest.raises(TypeError): + + class NoMsgIDSubclass(BasePytestTester): + pass + + +@pytest.mark.parametrize("msg_id", [123, None, ""], ids=lambda msg_id: f"{msg_id=}") +def test_init_subclass_invalid_msg_id_type(msg_id): + with pytest.raises(TypeError): + + class Subclass(BasePytestTester): + MSG_ID = msg_id diff --git a/tests/test_pytest_yield_fixture.py b/tests/test_pytest_yield_fixture.py index 4408fca..359dfb8 100644 --- a/tests/test_pytest_yield_fixture.py +++ b/tests/test_pytest_yield_fixture.py @@ -5,7 +5,6 @@ class TestDeprecatedPytestYieldFixture(BasePytestTester): CHECKER_CLASS = FixtureChecker - IMPACTED_CHECKER_CLASSES = [] MSG_ID = "deprecated-pytest-yield-fixture" def test_smoke(self): From b8ce4adaade3031a11368a7ec2e5da067531b5f3 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Wed, 18 Oct 2023 01:00:45 +0300 Subject: [PATCH 17/27] Roll back `from __future__ import annotations` for Python 3.6 ... and `f{msg_id=}` Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- pylint_pytest/checkers/class_attr_loader.py | 6 +++--- pylint_pytest/checkers/fixture.py | 11 +++++------ pylint_pytest/checkers/types.py | 2 -- tests/base_tester.py | 10 ++++------ tests/base_tester_test.py | 2 +- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/pylint_pytest/checkers/class_attr_loader.py b/pylint_pytest/checkers/class_attr_loader.py index f9b5557..6c9b4e9 100644 --- a/pylint_pytest/checkers/class_attr_loader.py +++ b/pylint_pytest/checkers/class_attr_loader.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from typing import Optional, Set from astroid import Assign, Attribute, ClassDef, Name from pylint.interfaces import IAstroidChecker @@ -12,8 +12,8 @@ class ClassAttrLoader(BasePytestChecker): msgs = {"E6400": ("", "pytest-class-attr-loader", "")} in_setup = False - request_cls: set[str] = set() - class_node: ClassDef | None = None + request_cls: Set[str] = set() + class_node: Optional[ClassDef] = None def visit_functiondef(self, node): """determine if a method is a class setup method""" diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index b41201f..32ec894 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -1,9 +1,8 @@ -from __future__ import annotations - import fnmatch import os import sys from pathlib import Path +from typing import Set, Tuple import astroid import pylint @@ -22,7 +21,7 @@ from .types import FixtureDict, replacement_add_message # TODO: support pytest python_files configuration -FILE_NAME_PATTERNS: tuple[str, ...] = ("test_*.py", "*_test.py") +FILE_NAME_PATTERNS: Tuple[str, ...] = ("test_*.py", "*_test.py") ARGUMENT_ARE_KEYWORD_ONLY = ( "https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only" ) @@ -31,7 +30,7 @@ class FixtureCollector: # Same as ``_pytest.fixtures.FixtureManager._arg2fixturedefs``. fixtures: FixtureDict = {} - errors: set[pytest.CollectReport] = set() + errors: Set[pytest.CollectReport] = set() def pytest_sessionfinish(self, session): # pylint: disable=protected-access @@ -80,9 +79,9 @@ class FixtureChecker(BasePytestChecker): # Store all fixtures discovered by pytest session _pytest_fixtures: FixtureDict = {} # Stores all used function arguments - _invoked_with_func_args: set[str] = set() + _invoked_with_func_args: Set[str] = set() # Stores all invoked fixtures through @pytest.mark.usefixture(...) - _invoked_with_usefixtures: set[str] = set() + _invoked_with_usefixtures: Set[str] = set() _original_add_message = replacement_add_message def open(self): diff --git a/pylint_pytest/checkers/types.py b/pylint_pytest/checkers/types.py index 7fd5708..c4c43e8 100644 --- a/pylint_pytest/checkers/types.py +++ b/pylint_pytest/checkers/types.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import sys from pprint import pprint from typing import Any, Dict, List diff --git a/tests/base_tester.py b/tests/base_tester.py index 4d6498c..12d2e63 100644 --- a/tests/base_tester.py +++ b/tests/base_tester.py @@ -1,10 +1,8 @@ -from __future__ import annotations - import os import sys from abc import ABC from pprint import pprint -from typing import Any +from typing import Any, Dict, List import astroid from pylint.testutils import MessageTest, UnittestLinter @@ -25,10 +23,10 @@ class BasePytestTester(ABC): CHECKER_CLASS = BaseChecker - IMPACTED_CHECKER_CLASSES: list[BaseChecker] = [] + IMPACTED_CHECKER_CLASSES: List[BaseChecker] = [] MSG_ID: str - msgs: list[MessageTest] = [] - CONFIG: dict[str, Any] = {} + msgs: List[MessageTest] = [] + CONFIG: Dict[str, Any] = {} def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) diff --git a/tests/base_tester_test.py b/tests/base_tester_test.py index dc1a48a..4cf43ff 100644 --- a/tests/base_tester_test.py +++ b/tests/base_tester_test.py @@ -20,7 +20,7 @@ class NoMsgIDSubclass(BasePytestTester): pass -@pytest.mark.parametrize("msg_id", [123, None, ""], ids=lambda msg_id: f"{msg_id=}") +@pytest.mark.parametrize("msg_id", [123, None, ""], ids=lambda x: f"msg_id={x}") def test_init_subclass_invalid_msg_id_type(msg_id): with pytest.raises(TypeError): From 163ac3a61832a8a27ee04fd55d2b3a2b8c365912 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:50:15 +0300 Subject: [PATCH 18/27] Fix the repository badges Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7d0c4f2..1749343 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # pylint-pytest -[![Python package](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml/badge.svg)](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml) -[![PyPI version fury.io](https://badge.fury.io/py/pylint-pytest.svg)](https://pypi.python.org/pypi/pylint-pytest/) -[![Travis CI](https://travis-ci.org/reverbc/pylint-pytest.svg?branch=master)](https://travis-ci.org/reverbc/pylint-pytest) -[![AppVeyor](https://ci.appveyor.com/api/projects/status/github/reverbc/pylint-pytest?branch=master&svg=true)](https://ci.appveyor.com/project/reverbc/pylint-pytest) +[![Github - Testing](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml/badge.svg)](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml) +![PyPI - Downloads](https://img.shields.io/pypi/dd/pylint-pytest) +![PyPI - Version](https://img.shields.io/pypi/v/pylint-pytest) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pylint-pytest) A Pylint plugin to suppress pytest-related false positives. From 0b1bb863937e9b25cd40912091864c6ed74873b3 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:05:25 +0300 Subject: [PATCH 19/27] Improvements for `.github/workflows/run-tests.yaml` * Configure action `concurrency:` * Use `codecov/codecov-action@v3` flags (might ... help codecov, somehow) * Upload testing artifacts * Add the Codecov badge in the README.md Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- .github/workflows/run-tests.yaml | 12 ++++++++++++ README.md | 1 + 2 files changed, 13 insertions(+) diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 2a428e0..c0948ce 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -4,6 +4,10 @@ on: push: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ${{ matrix.os }} @@ -59,5 +63,13 @@ jobs: uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} + flags: ${{ matrix.os }},${{ matrix.python-version }} fail_ci_if_error: true files: test_artifacts/cobertura.xml + + - name: Create artifacts + uses: actions/upload-artifact@v3 + if: ${{ !cancelled() }} + with: + name: test-artifacts_${{ matrix.os }}_${{ matrix.python-version }} + path: test_artifacts/ diff --git a/README.md b/README.md index 1749343..d891922 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ ![PyPI - Downloads](https://img.shields.io/pypi/dd/pylint-pytest) ![PyPI - Version](https://img.shields.io/pypi/v/pylint-pytest) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pylint-pytest) +[![codecov](https://codecov.io/gh/pylint-dev/pylint-pytest/graph/badge.svg?token=NhZDLKmomd)](https://codecov.io/gh/pylint-dev/pylint-pytest) A Pylint plugin to suppress pytest-related false positives. From c208abb39023d733cf2b067ecf8753bf67c666de Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:23:34 +0300 Subject: [PATCH 20/27] Fix testing + coverage * Drop `py312` to combat https://github.com/pylint-dev/pylint-pytest/issues/16 * Install `--editable`, so that Python uses the correct source files for coverage * Ignore `tests/input/**/*.py` from coverage - they are not ran properly * Again improve on artifact naming Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- .codecov.yml | 2 ++ .github/workflows/run-tests.yaml | 5 ++++- tox.ini | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..f6947b3 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,2 @@ +ignore: + - tests/input/**/*.py diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index c0948ce..d44d33e 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -41,6 +41,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Slugify GITHUB_REPOSITORY + run: echo "GITHUB_REPOSITORY_SLUG=${GITHUB_REPOSITORY////_}" >> $GITHUB_ENV + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -71,5 +74,5 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ !cancelled() }} with: - name: test-artifacts_${{ matrix.os }}_${{ matrix.python-version }} + name: ${{ env.GITHUB_REPOSITORY_SLUG }}_test-artifacts_${{ github.event_name }}_${{ github.event.pull_request.number || github.sha }}_${{ matrix.os }}_py${{ matrix.python-version }} path: test_artifacts/ diff --git a/tox.ini b/tox.ini index 59810ac..09dd02e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,py39,py310,py311,py312 +envlist = py36,py37,py38,py39,py310,py311 skipsdist = True passenv = FORCE_COLOR @@ -9,5 +9,5 @@ deps = pytest pytest-cov commands = - pip install --upgrade . + pip install --upgrade --editable . pytest --cov --cov-append {env:PYTEST_CI_ARGS:} {tty:--color=yes} {posargs:tests} From 4971561e517517869e424d2f9a8d3e388bb93fc5 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:55:29 +0300 Subject: [PATCH 21/27] Release v1.1.3rc0 (#17) Additionally, fixes https://github.com/pylint-dev/pylint-pytest/issues/13 Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- CHANGELOG.md | 23 ++++++++++++++++++++++- README.md | 12 ++++++++++-- setup.py | 11 +++++++++-- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea16bb..7f5531a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,32 @@ ## [Unreleased] -## [1.1.3] - 2023-10-15 +## [1.1.3] - 2023-10-23 + +This is the first release after maintenance was assumed by https://github.com/stdedos. + +The focus of this release was to improve automation: +* Fix the continuous integration, +* Run tests as part of branches and PRs, +* Use `.pre-commit-config.yaml` file to upkeep the code quality, and +* Automate the release process + +There should be no functional changes in this release, although there are changes in the source code. + +A heartfelt thank you to https://github.com/Pierre-Sassoulas for his invaluable contributions to the continued maintenance of this project! ### Fixed + - The continuous integration was fixed, as a new maintenance team was assembled. +### Added + +- Added an extensive `.pre-commit-config.yaml` file to upkeep the code quality. + It includes, among others, `black`, `mypy` (in non-strict mode yet), `ruff`, and `pylint`. + +### Changed + +- Redirected all repository URLs to the https://github.com/pylint-dev/pylint-pytest. ## [1.1.2] - 2021-04-19 ### Fixed diff --git a/README.md b/README.md index d891922..8e57bce 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,19 @@ # pylint-pytest -[![Github - Testing](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml/badge.svg)](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml) -![PyPI - Downloads](https://img.shields.io/pypi/dd/pylint-pytest) ![PyPI - Version](https://img.shields.io/pypi/v/pylint-pytest) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pylint-pytest) +![PyPI - Downloads](https://img.shields.io/pypi/dd/pylint-pytest) +![PyPI - License](https://img.shields.io/pypi/l/pylint-pytest) + +[![Github - Testing](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml/badge.svg)](https://github.com/pylint-dev/pylint-pytest/actions/workflows/run-tests.yaml) [![codecov](https://codecov.io/gh/pylint-dev/pylint-pytest/graph/badge.svg?token=NhZDLKmomd)](https://codecov.io/gh/pylint-dev/pylint-pytest) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) + +[![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/stdedos) + A Pylint plugin to suppress pytest-related false positives. ## Installation diff --git a/setup.py b/setup.py index 0384661..25ce836 100644 --- a/setup.py +++ b/setup.py @@ -11,11 +11,18 @@ setup( name="pylint-pytest", - version="1.1.3a0", + version="1.1.3rc0", author="Stavros Ntentos", author_email="133706+stdedos@users.noreply.github.com", license="MIT", - url="https://github.com/reverbc/pylint-pytest", + url="https://github.com/pylint-dev/pylint-pytest", + project_urls={ + "Changelog": "https://github.com/pylint-dev/pylint-pytest/blob/master/CHANGELOG.md", + "Documentation": "https://github.com/pylint-dev/pylint-pytest#readme", + "Say Thanks!": "https://saythanks.io/to/stdedos", + "Source": "https://github.com/pylint-dev/pylint-pytest", + "Tracker": "https://github.com/pylint-dev/pylint-pytest/issues", + }, description="A Pylint plugin to suppress pytest-related false positives.", long_description=long_description, long_description_content_type="text/markdown", From 85dd45798fbe5f5fa7c3d43f7cf095818bc05c54 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:41:38 +0300 Subject: [PATCH 22/27] Improve packaging + Release v1.1.3rc1 * Drop `tests/*` both from `sdist` and `wheel` * Drop `universal` wheel (we don't support Python2) Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- CHANGELOG.md | 1 + MANIFEST.in | 1 + setup.cfg | 3 --- setup.py | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 MANIFEST.in diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5531a..6a62d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ A heartfelt thank you to https://github.com/Pierre-Sassoulas for his invaluable - Added an extensive `.pre-commit-config.yaml` file to upkeep the code quality. It includes, among others, `black`, `mypy` (in non-strict mode yet), `ruff`, and `pylint`. +- Added an automated release process ### Changed diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..cbd2698 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +prune tests/ diff --git a/setup.cfg b/setup.cfg index cf4e2a4..31ad82b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,2 @@ [aliases] test = pytest - -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index 25ce836..aaeb2f8 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="pylint-pytest", - version="1.1.3rc0", + version="1.1.3rc1", author="Stavros Ntentos", author_email="133706+stdedos@users.noreply.github.com", license="MIT", @@ -26,7 +26,7 @@ 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"]), + packages=find_packages(exclude=["tests*", "sandbox"]), install_requires=[ "pylint<3", "pytest>=4.6", From 630172b32d9e5bafde1c3e36ad7e6132a53c43d6 Mon Sep 17 00:00:00 2001 From: Anis Campos Date: Tue, 6 Dec 2022 13:54:35 +0100 Subject: [PATCH 23/27] test direct import --- .../redefined-outer-name/direct_import.py | 28 +++++++++++++++++++ tests/test_redefined_outer_name.py | 6 ++++ 2 files changed, 34 insertions(+) create mode 100644 tests/input/redefined-outer-name/direct_import.py diff --git a/tests/input/redefined-outer-name/direct_import.py b/tests/input/redefined-outer-name/direct_import.py new file mode 100644 index 0000000..d926094 --- /dev/null +++ b/tests/input/redefined-outer-name/direct_import.py @@ -0,0 +1,28 @@ +from pytest import fixture + + +@fixture +def simple_decorator(): + """the fixture using the decorator without executing it""" + + +def test_simple_fixture(simple_decorator): + assert True + + +@fixture() +def un_configured_decorated(): + """the decorated is called without argument, like scope""" + + +def test_un_configured_decorated(un_configured_decorated): + assert True + + +@fixture(scope="function") +def configured_decorated(): + """the decorated is called with argument, like scope""" + + +def test_un_configured_decorated(configured_decorated): + assert True diff --git a/tests/test_redefined_outer_name.py b/tests/test_redefined_outer_name.py index 11e48a0..aeda6fc 100644 --- a/tests/test_redefined_outer_name.py +++ b/tests/test_redefined_outer_name.py @@ -29,3 +29,9 @@ def test_caller_not_a_test_func(self, enable_plugin): def test_args_and_kwargs(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(2) + + @pytest.mark.parametrize('enable_plugin', [True, False]) + def test_direct_import(self, enable_plugin): + """the fixture method is directly imported """ + self.run_linter(enable_plugin) + self.verify_messages(0 if enable_plugin else 3) From d5cd8737cf868c03c140cc2e7cc1c63d8173628f Mon Sep 17 00:00:00 2001 From: Anis Date: Tue, 6 Dec 2022 12:26:41 +0100 Subject: [PATCH 24/27] Fixing _is_pytest_fixture currently, both fixture in this example are flagged has not fixtures ```python from pytest import fixture @fixture def my_fixture(): pass @fixture(name="foo") def my_fixture_2(): pass ``` This is due to the different semantics in astroid when using `import pytest` or `from pytest import ...` --- pylint_pytest/utils.py | 32 ++++++++++++++++++++++++------ tests/test_redefined_outer_name.py | 4 ++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/pylint_pytest/utils.py b/pylint_pytest/utils.py index 6c66e6e..c9fa02f 100644 --- a/pylint_pytest/utils.py +++ b/pylint_pytest/utils.py @@ -31,7 +31,6 @@ def _is_pytest_mark(decorator): def _is_pytest_fixture(decorator, fixture=True, yield_fixture=True): - attr = None to_check = set() if fixture: @@ -40,17 +39,38 @@ def _is_pytest_fixture(decorator, fixture=True, yield_fixture=True): if yield_fixture: to_check.add("yield_fixture") + def _check_attribute(attr): + """ + handle astroid.Attribute, i.e., when the fixture function is + used by importing the pytest module + """ + return attr.attrname in to_check and attr.expr.name == "pytest" + + def _check_name(name_): + """ + handle astroid.Name, i.e., when the fixture function is + directly imported + """ + function_name = name_.name + module = decorator.root().globals.get(function_name, [None])[0] + module_name = module.modname if module else None + return function_name in to_check and module_name == "pytest" + try: + if isinstance(decorator, astroid.Name): + # expecting @fixture + return _check_name(decorator) if isinstance(decorator, astroid.Attribute): # expecting @pytest.fixture - attr = decorator - + return _check_attribute(decorator) if isinstance(decorator, astroid.Call): + func = decorator.func + if isinstance(func, astroid.Name): + # expecting @fixture(scope=...) + return _check_name(func) # expecting @pytest.fixture(scope=...) - attr = decorator.func + return _check_attribute(func) - if attr and attr.attrname in to_check and attr.expr.name == "pytest": - return True except AttributeError: pass diff --git a/tests/test_redefined_outer_name.py b/tests/test_redefined_outer_name.py index aeda6fc..ec0beef 100644 --- a/tests/test_redefined_outer_name.py +++ b/tests/test_redefined_outer_name.py @@ -30,8 +30,8 @@ def test_args_and_kwargs(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(2) - @pytest.mark.parametrize('enable_plugin', [True, False]) + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_direct_import(self, enable_plugin): - """the fixture method is directly imported """ + """the fixture method is directly imported""" self.run_linter(enable_plugin) self.verify_messages(0 if enable_plugin else 3) From 0a47a49accb248bd324b310d1eff184781f3ddca Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Mon, 23 Oct 2023 08:50:52 +0300 Subject: [PATCH 25/27] Release v1.1.3 Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index aaeb2f8..f30541f 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="pylint-pytest", - version="1.1.3rc1", + version="1.1.3", author="Stavros Ntentos", author_email="133706+stdedos@users.noreply.github.com", license="MIT", From f6a343d72b425e632db1a02b9b065d07fba93b72 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:27:08 +0300 Subject: [PATCH 26/27] =?UTF-8?q?=F0=9F=90=9B=20Ignore=20collection=20fail?= =?UTF-8?q?ures=20in=20non-tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change applies the pre-existing patterns to identify if the files with collection problems are tests. It is then used to eliminate the false-positives of F6401 (cannot-enumerate-pytest-fixtures). As a side effect, this patch also includes precise file paths that may be used to reproduce the problem. Fixes https://github.com/reverbc/pylint-pytest/issues/20 Fixes https://github.com/reverbc/pylint-pytest/issues/21 Signed-off-by: Sviatoslav Sydorenko _Replayed; Source PR: https://github.com/reverbc/pylint-pytest/pull/22_ Additionally, satisfied the https://github.com/pylint-dev/pylint-pytest's `.pre-commit-config.yaml` Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- pylint_pytest/checkers/fixture.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index 32ec894..9af22a7 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -68,8 +68,8 @@ class FixtureChecker(BasePytestChecker): "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" + "Please run `pytest --fixtures --collect-only %s` 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.", @@ -143,8 +143,23 @@ 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) + legitimate_failure_paths = set( + collection_report.nodeid + for collection_report in fixture_collector.errors + if any( + fnmatch.fnmatch( + Path(collection_report.nodeid).name, + pattern, + ) + for pattern in FILE_NAME_PATTERNS + ) + ) + if (ret != pytest.ExitCode.OK or legitimate_failure_paths) and is_test_module: + self.add_message( + "cannot-enumerate-pytest-fixtures", + args=" ".join(legitimate_failure_paths | {node.file}), + node=node, + ) finally: # restore output devices sys.stdout, sys.stderr = stdout, stderr From abc9f2220d9407e252a520f596344341b5915dcb Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:14:31 +0300 Subject: [PATCH 27/27] Improve `F6401`:`cannot-enumerate-pytest-fixtures`: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Capture and return `stdout` and `stderr` (instead of trashing them) * Fix the 'duplicate-path' error by `relative`-izing the paths before `union`ing them * Update tests to test for both conditions Additionally, fix two `used-before-assignment` pylint issues (`stdout`, `stderr`) 🤪 Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- pylint_pytest/checkers/fixture.py | 22 +++++++++++----- tests/test_cannot_enumerate_fixtures.py | 34 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index 9af22a7..79b9fb8 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -1,5 +1,5 @@ import fnmatch -import os +import io import sys from pathlib import Path from typing import Set, Tuple @@ -69,7 +69,7 @@ class FixtureChecker(BasePytestChecker): ( "pylint-pytest plugin cannot enumerate and collect pytest fixtures. " "Please run `pytest --fixtures --collect-only %s` and resolve " - "any potential syntax error or package dependency issues" + "any potential syntax error or package dependency issues. stdout: %s. stderr: %s." ), "cannot-enumerate-pytest-fixtures", "Used when pylint-pytest has been unable to enumerate and collect pytest fixtures.", @@ -116,11 +116,12 @@ def visit_module(self, node): is_test_module = True break + stdout, stderr = sys.stdout, sys.stderr try: - with open(os.devnull, "w") as devnull: + with io.StringIO() as captured_stdout, io.StringIO() as captured_stderr: # suppress any future output from pytest - stdout, stderr = sys.stdout, sys.stderr - sys.stderr = sys.stdout = devnull + sys.stderr = captured_stderr + sys.stdout = captured_stdout # run pytest session with customized plugin to collect fixtures fixture_collector = FixtureCollector() @@ -155,9 +156,18 @@ def visit_module(self, node): ) ) if (ret != pytest.ExitCode.OK or legitimate_failure_paths) and is_test_module: + files_to_report = { + str(Path(x).absolute().relative_to(Path.cwd())) + for x in legitimate_failure_paths | {node.file} + } + self.add_message( "cannot-enumerate-pytest-fixtures", - args=" ".join(legitimate_failure_paths | {node.file}), + args=( + " ".join(files_to_report), + captured_stdout.getvalue(), + captured_stderr.getvalue(), + ), node=node, ) finally: diff --git a/tests/test_cannot_enumerate_fixtures.py b/tests/test_cannot_enumerate_fixtures.py index 4ad241e..362ab2a 100644 --- a/tests/test_cannot_enumerate_fixtures.py +++ b/tests/test_cannot_enumerate_fixtures.py @@ -1,3 +1,5 @@ +import re + import pytest from base_tester import BasePytestTester @@ -13,7 +15,39 @@ def test_no_such_package(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(1 if enable_plugin else 0) + if enable_plugin: + msg = self.msgs[0] + + # Asserts/Fixes duplicate filenames in output: + # https://github.com/reverbc/pylint-pytest/pull/22/files#r698204470 + filename_arg = msg.args[0] + assert len(re.findall(r"\.py", filename_arg)) == 1 + + # Asserts that path is relative (usually to the root of the repository). + assert filename_arg[0] != "/" + + # Assert `stdout` is non-empty. + assert msg.args[1] + # Assert `stderr` is empty (pytest runs stably, even though fixture collection fails). + assert not msg.args[2] + @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) + + if enable_plugin: + msg = self.msgs[0] + + # ... somehow, since `import_corrupted_module.py` imports `no_such_package.py` + # both of their names are returned in the message. + filename_arg = msg.args[0] + assert len(re.findall(r"\.py", filename_arg)) == 2 + + # Asserts that paths are relative (usually to the root of the repository). + assert not [x for x in filename_arg.split(" ") if x[0] == "/"] + + # Assert `stdout` is non-empty. + assert msg.args[1] + # Assert `stderr` is empty (pytest runs stably, even though fixture collection fails). + assert not msg.args[2]