diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 38fd6c7a77..e10646a7c9 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -21,14 +21,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 with: # `towncrier check` runs `git diff --name-only origin/main...`, which # needs a non-shallow clone. fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -41,7 +41,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index e2d36616ec..4b305cd2c6 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -33,10 +33,10 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -49,7 +49,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -71,7 +71,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -89,16 +89,16 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -107,7 +107,7 @@ jobs: needs.prepare-base.outputs.python-key }} - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -130,16 +130,16 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -158,16 +158,16 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv fail-on-cache-miss: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 26a7ee9ac3..68d8d84988 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/primer-test.yaml b/.github/workflows/primer-test.yaml index fbc577d5fd..a6fa2a8753 100644 --- a/.github/workflows/primer-test.yaml +++ b/.github/workflows/primer-test.yaml @@ -35,10 +35,10 @@ jobs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -51,7 +51,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -75,16 +75,16 @@ jobs: python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv fail-on-cache-miss: true diff --git a/.github/workflows/primer_comment.yaml b/.github/workflows/primer_comment.yaml index 00b1ee70fc..77bd262665 100644 --- a/.github/workflows/primer_comment.yaml +++ b/.github/workflows/primer_comment.yaml @@ -30,10 +30,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -41,7 +41,7 @@ jobs: # Restore cached Python environment - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: diff --git a/.github/workflows/primer_run_main.yaml b/.github/workflows/primer_run_main.yaml index afe188c2ea..9334d96f39 100644 --- a/.github/workflows/primer_run_main.yaml +++ b/.github/workflows/primer_run_main.yaml @@ -34,10 +34,10 @@ jobs: batchIdx: [0, 1, 2, 3] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -45,7 +45,7 @@ jobs: # Create a re-usable virtual environment - name: Create Python virtual environment cache id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: @@ -71,7 +71,7 @@ jobs: echo "commitstring=$output" >> $GITHUB_OUTPUT - name: Restore projects cache id: cache-projects - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: tests/.pylint_primer_tests/ key: >- @@ -83,7 +83,7 @@ jobs: . venv/bin/activate python tests/primer/__main__.py prepare --clone - name: Upload commit string - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 if: matrix.batchIdx == 0 with: name: primer_commitstring_${{ matrix.python-version }} @@ -104,7 +104,7 @@ jobs: then echo "::warning ::$WARNINGS" fi - name: Upload output - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: primer_output_main_${{ matrix.python-version }}_batch${{ matrix.batchIdx }} diff --git a/.github/workflows/primer_run_pr.yaml b/.github/workflows/primer_run_pr.yaml index 72bbbb9fdd..14c3e8892c 100644 --- a/.github/workflows/primer_run_pr.yaml +++ b/.github/workflows/primer_run_pr.yaml @@ -43,12 +43,12 @@ jobs: batchIdx: [0, 1, 2, 3] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -56,7 +56,7 @@ jobs: # Restore cached Python environment - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: @@ -140,7 +140,7 @@ jobs: echo "commitstring=$output" >> $GITHUB_OUTPUT - name: Restore projects cache id: cache-projects - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: tests/.pylint_primer_tests/ key: >- @@ -178,7 +178,7 @@ jobs: then echo "::warning ::$WARNINGS" fi - name: Upload output of PR - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: primer_output_pr_${{ matrix.python-version }}_batch${{ matrix.batchIdx }} @@ -186,7 +186,7 @@ jobs: tests/.pylint_primer_tests/output_${{ matrix.python-version }}_pr_batch${{ matrix.batchIdx }}.txt - name: Upload output of 'main' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: primer_output_main_${{ matrix.python-version }}_batch${{ matrix.batchIdx }} @@ -199,7 +199,7 @@ jobs: - name: Upload PR number if: startsWith(steps.python.outputs.python-version, '3.8') && matrix.batchIdx == 0 - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: pr_number path: pr_number.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6ac290765..0c36cb0cf2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,10 +20,10 @@ jobs: url: https://pypi.org/project/pylint/ steps: - name: Check out code from Github - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e362c6ee7e..86edc29880 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -36,10 +36,10 @@ jobs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -52,7 +52,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -76,7 +76,7 @@ jobs: pip list | grep 'astroid\|pylint' python -m pytest -vv --minimal-messages-config tests/test_functional.py - name: Upload coverage artifact - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }} path: .coverage @@ -88,16 +88,16 @@ jobs: needs: tests-linux steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python 3.12 id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: "3.12" check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -105,13 +105,13 @@ jobs: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.tests-linux.outputs.python-key }} - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v4.1.7 - name: Combine coverage results run: | . venv/bin/activate coverage combine coverage*/.coverage coverage xml - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -128,16 +128,16 @@ jobs: python-version: ["3.12"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -160,7 +160,7 @@ jobs: run: >- echo "datetime="$(date "+%Y%m%d_%H%M") >> $GITHUB_OUTPUT - name: Upload benchmark artifact - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: benchmark-${{ runner.os }}-${{ matrix.python-version }}_${{ @@ -182,10 +182,10 @@ jobs: # Workaround to set correct temp directory on Windows # https://github.com/actions/virtual-environments/issues/712 - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -197,7 +197,7 @@ jobs: }}" >> $env:GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -228,10 +228,10 @@ jobs: python-version: [3.8] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -243,7 +243,7 @@ jobs: }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -272,10 +272,10 @@ jobs: python-version: ["pypy-3.8", "pypy-3.9"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -287,7 +287,7 @@ jobs: }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 845f083c45..104267c522 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace exclude: tests(/\w*)*/functional/t/trailing_whitespaces.py|tests/pyreverse/data/.*.html|doc/data/messages/t/trailing-whitespace/bad.py @@ -17,7 +17,7 @@ repos: doc/data/messages/m/missing-final-newline/bad/crlf.py )$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.2.2" + rev: "v0.4.4" hooks: - id: ruff args: ["--fix"] @@ -39,7 +39,7 @@ repos: - id: isort exclude: doc/data/messages/ - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.4.2 hooks: - id: black args: [--safe, --quiet] @@ -87,7 +87,15 @@ repos: entry: pylint language: system types: [python] - args: ["-rn", "-sn", "--rcfile=pylintrc", "--fail-on=I", "--spelling-dict=en"] + args: + [ + "-rn", + "-sn", + "--rcfile=pylintrc", + "--fail-on=I", + "--spelling-dict=en", + "--output-format=github", + ] exclude: tests(/\w*)*/functional/|tests/input|tests(/\w*)*data/|doc/ stages: [manual] - id: sphinx-generated-doc @@ -112,7 +120,7 @@ repos: files: ^(doc/(.*/)*.*\.rst) additional_dependencies: [Sphinx==5.0.1] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.10.0 hooks: - id: mypy name: mypy @@ -131,7 +139,7 @@ repos: ] exclude: tests(/\w*)*/functional/|tests/input|tests(/.*)+/conftest.py|doc/data/messages|tests(/\w*)*data/ - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + rev: v4.0.0-alpha.8 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] @@ -158,7 +166,7 @@ repos: setup.cfg )$ - repo: https://github.com/PyCQA/bandit - rev: 1.7.7 + rev: 1.7.8 hooks: - id: bandit args: ["-r", "-lll"] diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt index fd4fed00c3..78d861aea3 100644 --- a/.pyenchant_pylint_custom_dict.txt +++ b/.pyenchant_pylint_custom_dict.txt @@ -300,6 +300,7 @@ spammy sqlalchemy src starargs +stateful staticmethod stderr stdin @@ -335,6 +336,7 @@ testoptions tmp tokencheckers tokeninfo +tokenization tokenize tokenizer toml diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index d7cfb58901..24959e454d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -114,6 +114,7 @@ contributors: - Glenn Matthews : * autogenerated documentation for optional extensions, * bug fixes and enhancements for docparams (née check_docs) extension +- crazybolillo - Vlad Temian : redundant-unittest-assert and the JSON reporter. - Julien Jehannet - Boris Feld @@ -157,7 +158,6 @@ contributors: * Added new useless-return checker, * Added new try-except-raise checker - theirix -- crazybolillo - Téo Bouvard - Stavros Ntentos <133706+stdedos@users.noreply.github.com> - Nicolas Boulenguez @@ -174,6 +174,7 @@ contributors: - Andreas Freimuth : fix indentation checking with tabs - Alexandru Coman - jpkotta +- Thomas Grainger - Takahide Nojima - Taewon D. Kim - Sneaky Pete @@ -254,7 +255,7 @@ contributors: - Wes Turner (Google): added new check 'inconsistent-quotes' - Tyler Thieding - Tobias Hernstig <30827238+thernstig@users.noreply.github.com> -- Thomas Grainger +- Sviatoslav Sydorenko - Smixi - Simu Toni - Sergei Lebedev <185856+superbobry@users.noreply.github.com> @@ -262,6 +263,7 @@ contributors: - Saugat Pachhai - Samuel FORESTIER - Rémi Cardona +- Ryan Ozawa - Raphael Gaschignard - Ram Rachum (cool-RR) - Radostin Stoyanov @@ -281,6 +283,7 @@ contributors: - Maarten ter Huurne - Lefteris Karapetsas - LCD 47 +- Jérome Perrin - Justin Li - John Kirkham - Jens H. Nielsen @@ -351,6 +354,7 @@ contributors: - cherryblossom <31467609+cherryblossom000@users.noreply.github.com> - bluesheeptoken - anatoly techtonik +- akirchhoff-modular - agutole - Zeckie <49095968+Zeckie@users.noreply.github.com> - Zeb Nicholls @@ -373,6 +377,7 @@ contributors: - Victor Jiajunsu <16359131+jiajunsu@users.noreply.github.com> - ViRuSTriNiTy - Val Lorentz +- Ulrich Eckhardt - Udi Fuchs - Trevor Bekolay * Added --list-msgs-enabled command @@ -404,7 +409,6 @@ contributors: - Santiago Castro - Samuel Freilich (sfreilich) - Sam Vermeiren <88253337+PaaEl@users.noreply.github.com> -- Ryan Ozawa - Ryan McGuire - Ry4an Brase - Ruro @@ -488,7 +492,6 @@ contributors: - Kayran Schmidt <59456929+yumasheta@users.noreply.github.com> - Karthik Nadig - Jürgen Hermann -- Jérome Perrin - Josselin Feist - Jonathan Kotta - John Paraskevopoulos : add 'differing-param-doc' and 'differing-type-doc' @@ -505,6 +508,7 @@ contributors: - Jared Garst - Jared Deckard - Janne Rönkkö +- Jamie Scott - James Sinclair - James M. Allen - James Lingard @@ -598,9 +602,11 @@ contributors: - Alok Singh <8325708+alok@users.noreply.github.com> - Allan Chandler <95424144+allanc65@users.noreply.github.com> (allanc65) * Fixed issue 5452, false positive missing-param-doc for multi-line Google-style params +- Alex Waygood - Alex Mor <5476113+nashcontrol@users.noreply.github.com> - Alex Jurkiewicz - Alex Hearn +- Alex Fortin - Aleksander Mamla - Alan Evangelista - Alan Chan diff --git a/MANIFEST.in b/MANIFEST.in index 354ce64d9d..694393065f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,7 @@ include README.rst +include requirements_test_min.txt +include requirements_test_pre_commit.txt +include requirements_test.txt include tox.ini graft doc graft examples diff --git a/doc/conf.py b/doc/conf.py index 096af48c82..50d891eb93 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -106,8 +106,8 @@ # The encoding of source files. # source_encoding = 'utf-8-sig' -# The master toctree document. -master_doc = "index" +# The root toctree document. +root_doc = "index" # General information about the project. project = "Pylint" diff --git a/doc/data/messages/c/contextmanager-generator-missing-cleanup/bad.py b/doc/data/messages/c/contextmanager-generator-missing-cleanup/bad.py new file mode 100644 index 0000000000..b2072f7b19 --- /dev/null +++ b/doc/data/messages/c/contextmanager-generator-missing-cleanup/bad.py @@ -0,0 +1,14 @@ +import contextlib + + +@contextlib.contextmanager +def cm(): + contextvar = "acquired context" + print("cm enter") + yield contextvar + print("cm exit") + + +def genfunc_with_cm(): + with cm() as context: # [contextmanager-generator-missing-cleanup] + yield context * 2 diff --git a/doc/data/messages/c/contextmanager-generator-missing-cleanup/details.rst b/doc/data/messages/c/contextmanager-generator-missing-cleanup/details.rst new file mode 100644 index 0000000000..50b948fb54 --- /dev/null +++ b/doc/data/messages/c/contextmanager-generator-missing-cleanup/details.rst @@ -0,0 +1,31 @@ +Instantiating and using a contextmanager inside a generator function can +result in unexpected behavior if there is an expectation that the context is only +available for the generator function. In the case that the generator is not closed or destroyed +then the context manager is held suspended as is. + +This message warns on the generator function instead of the contextmanager function +because the ways to use a contextmanager are many. +A contextmanager can be used as a decorator (which immediately has ``__enter__``/``__exit__`` applied) +and the use of ``as ...`` or discard of the return value also implies whether the context needs cleanup or not. +So for this message, warning the invoker of the contextmanager is important. + +The check can create false positives if ``yield`` is used inside an ``if-else`` block without custom cleanup. Use ``pylint: disable`` for these. + +.. code-block:: python + + from contextlib import contextmanager + + @contextmanager + def good_cm_no_cleanup(): + contextvar = "acquired context" + print("cm enter") + if some_condition: + yield contextvar + else: + yield contextvar + + + def good_cm_no_cleanup_genfunc(): + # pylint: disable-next=contextmanager-generator-missing-cleanup + with good_cm_no_cleanup() as context: + yield context * 2 diff --git a/doc/data/messages/c/contextmanager-generator-missing-cleanup/good.py b/doc/data/messages/c/contextmanager-generator-missing-cleanup/good.py new file mode 100644 index 0000000000..2287e86a59 --- /dev/null +++ b/doc/data/messages/c/contextmanager-generator-missing-cleanup/good.py @@ -0,0 +1,61 @@ +import contextlib + + +@contextlib.contextmanager +def good_cm_except(): + contextvar = "acquired context" + print("good cm enter") + try: + yield contextvar + except GeneratorExit: + print("good cm exit") + + +def genfunc_with_cm(): + with good_cm_except() as context: + yield context * 2 + + +def genfunc_with_discard(): + with good_cm_except(): + yield "discarded" + + +@contextlib.contextmanager +def good_cm_yield_none(): + print("good cm enter") + yield + print("good cm exit") + + +def genfunc_with_none_yield(): + with good_cm_yield_none() as var: + print(var) + yield "constant yield" + + +@contextlib.contextmanager +def good_cm_finally(): + contextvar = "acquired context" + print("good cm enter") + try: + yield contextvar + finally: + print("good cm exit") + + +def good_cm_finally_genfunc(): + with good_cm_finally() as context: + yield context * 2 + + +@contextlib.contextmanager +def good_cm_no_cleanup(): + contextvar = "acquired context" + print("cm enter") + yield contextvar + + +def good_cm_no_cleanup_genfunc(): + with good_cm_no_cleanup() as context: + yield context * 2 diff --git a/doc/data/messages/c/contextmanager-generator-missing-cleanup/related.rst b/doc/data/messages/c/contextmanager-generator-missing-cleanup/related.rst new file mode 100644 index 0000000000..aacc968cd5 --- /dev/null +++ b/doc/data/messages/c/contextmanager-generator-missing-cleanup/related.rst @@ -0,0 +1,2 @@ +- `Rationale `_ +- `CPython Issue `_ diff --git a/doc/data/messages/p/possibly-used-before-assignment/bad.py b/doc/data/messages/p/possibly-used-before-assignment/bad.py new file mode 100644 index 0000000000..8e7f0cb02a --- /dev/null +++ b/doc/data/messages/p/possibly-used-before-assignment/bad.py @@ -0,0 +1,4 @@ +def check_lunchbox(items: list[str]): + if not items: + empty = True + print(empty) # [possibly-used-before-assignment] diff --git a/doc/data/messages/p/possibly-used-before-assignment/details.rst b/doc/data/messages/p/possibly-used-before-assignment/details.rst new file mode 100644 index 0000000000..4737d26685 --- /dev/null +++ b/doc/data/messages/p/possibly-used-before-assignment/details.rst @@ -0,0 +1,15 @@ +If you rely on a pattern like: + +.. sourcecode:: python + + if guarded(): + var = 1 + + if guarded(): + print(var) # emits possibly-used-before-assignment + +you may be concerned that ``possibly-used-before-assignment`` is not totally useful +in this instance. However, consider that pylint, as a static analysis tool, does +not know if ``guarded()`` is deterministic or talks to +a database. (Likewise, for ``guarded`` instead of ``guarded()``, any other +part of your program may have changed its value in the meantime.) diff --git a/doc/data/messages/p/possibly-used-before-assignment/good.py b/doc/data/messages/p/possibly-used-before-assignment/good.py new file mode 100644 index 0000000000..6bb478f5f8 --- /dev/null +++ b/doc/data/messages/p/possibly-used-before-assignment/good.py @@ -0,0 +1,5 @@ +def check_lunchbox(items: list[str]): + empty = False + if not items: + empty = True + print(empty) diff --git a/doc/data/messages/s/singledispatch-method/bad.py b/doc/data/messages/s/singledispatch-method/bad.py index 49e545b92d..27df8d2ba0 100644 --- a/doc/data/messages/s/singledispatch-method/bad.py +++ b/doc/data/messages/s/singledispatch-method/bad.py @@ -3,17 +3,14 @@ class Board: @singledispatch # [singledispatch-method] - @classmethod - def convert_position(cls, position): + def convert_position(self, position): pass @convert_position.register # [singledispatch-method] - @classmethod - def _(cls, position: str) -> tuple: + def _(self, position: str) -> tuple: position_a, position_b = position.split(",") return (int(position_a), int(position_b)) @convert_position.register # [singledispatch-method] - @classmethod - def _(cls, position: tuple) -> str: + def _(self, position: tuple) -> str: return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/singledispatch-method/good.py b/doc/data/messages/s/singledispatch-method/good.py index f38047cd13..36e623d1e0 100644 --- a/doc/data/messages/s/singledispatch-method/good.py +++ b/doc/data/messages/s/singledispatch-method/good.py @@ -1,19 +1,17 @@ from functools import singledispatch -class Board: - @singledispatch - @staticmethod - def convert_position(position): - pass +@singledispatch +def convert_position(position): + print(position) - @convert_position.register - @staticmethod - def _(position: str) -> tuple: - position_a, position_b = position.split(",") - return (int(position_a), int(position_b)) - @convert_position.register - @staticmethod - def _(position: tuple) -> str: - return f"{position[0]},{position[1]}" +@convert_position.register +def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + +@convert_position.register +def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/singledispatchmethod-function/bad.py b/doc/data/messages/s/singledispatchmethod-function/bad.py index d2255f8659..861d3a20e9 100644 --- a/doc/data/messages/s/singledispatchmethod-function/bad.py +++ b/doc/data/messages/s/singledispatchmethod-function/bad.py @@ -1,19 +1,17 @@ from functools import singledispatchmethod -class Board: - @singledispatchmethod # [singledispatchmethod-function] - @staticmethod - def convert_position(position): - pass +@singledispatchmethod # [singledispatchmethod-function] +def convert_position(position): + print(position) - @convert_position.register # [singledispatchmethod-function] - @staticmethod - def _(position: str) -> tuple: - position_a, position_b = position.split(",") - return (int(position_a), int(position_b)) - @convert_position.register # [singledispatchmethod-function] - @staticmethod - def _(position: tuple) -> str: - return f"{position[0]},{position[1]}" +@convert_position.register # [singledispatchmethod-function] +def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + +@convert_position.register # [singledispatchmethod-function] +def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/syntax-error/details.rst b/doc/data/messages/s/syntax-error/details.rst new file mode 100644 index 0000000000..2f02260bc8 --- /dev/null +++ b/doc/data/messages/s/syntax-error/details.rst @@ -0,0 +1,2 @@ +The python's ast builtin module cannot parse your code if there's a syntax error, so +if there's a syntax error other messages won't be available at all. diff --git a/doc/data/messages/s/syntax-error/related.rst b/doc/data/messages/s/syntax-error/related.rst new file mode 100644 index 0000000000..fc1fdf7f67 --- /dev/null +++ b/doc/data/messages/s/syntax-error/related.rst @@ -0,0 +1 @@ +- `Why can't pylint recover from a syntax error ? `_ diff --git a/doc/data/messages/t/too-many-branches/good.py b/doc/data/messages/t/too-many-branches/good.py index 785c386352..10932adac0 100644 --- a/doc/data/messages/t/too-many-branches/good.py +++ b/doc/data/messages/t/too-many-branches/good.py @@ -4,8 +4,8 @@ def num_to_word(x): 1: "one", 2: "two", 3: "three", - 4: "for", - 5: "fie", + 4: "four", + 5: "five", 6: "six", 7: "seven", 8: "eight", diff --git a/doc/requirements.txt b/doc/requirements.txt index c72ed192d5..f4373c3deb 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ -Sphinx==7.2.6 +Sphinx==7.3.7 sphinx-reredirects<1 towncrier~=23.11 -furo==2024.1.29 +furo==2024.5.6 -e . diff --git a/doc/test_messages_documentation.py b/doc/test_messages_documentation.py index f024e034b1..96d98ed6c5 100644 --- a/doc/test_messages_documentation.py +++ b/doc/test_messages_documentation.py @@ -140,7 +140,7 @@ def get_expected_messages(stream: TextIO) -> MessageCounter: line = match.group("line") if line is None: lineno = i + 1 - elif line.startswith("+") or line.startswith("-"): + elif line.startswith(("+", "-")): lineno = i + 1 + int(line) else: lineno = int(line) diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index 4da29bbc46..95462f9218 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -118,7 +118,7 @@ Confusing Elif checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :confusing-consecutive-elif (R5601): *Consecutive elif with differing indentation level, consider creating a function to separate the inner elif* Used when an elif statement follows right after an indented block which - itself ends with if or elif. It may not be ovious if the elif statement was + itself ends with if or elif. It may not be obvious if the elif statement was willingly or mistakenly unindented. Extracting the indented if statement into a separate function might avoid confusion and prevent errors. diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index 7dce5c43b8..cb63930a01 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -166,6 +166,9 @@ Basic checker Messages This is a particular case of W0104 with its own message so you can easily disable it if you're using those strings as documentation, instead of comments. +:contextmanager-generator-missing-cleanup (W0135): *The context used in function %r will not be exited.* + Used when a contextmanager is used inside a generator function and the + cleanup is not handled. :unnecessary-pass (W0107): *Unnecessary pass statement* Used when a "pass" statement can be removed without affecting the behaviour of the code. @@ -1374,6 +1377,9 @@ Variables checker Messages Used when an invalid (non-string) object occurs in __all__. :no-name-in-module (E0611): *No name %r in module %r* Used when a name cannot be found in a module. +:possibly-used-before-assignment (E0606): *Possibly using variable %r before assignment* + Emitted when a local variable is accessed before its assignment took place in + both branches of an if/else switch. :undefined-variable (E0602): *Undefined variable %r* Used when an undefined variable is accessed. :undefined-all-variable (E0603): *Undefined variable name %r in __all__* diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index 94d2c1775e..eb7d72f2a3 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -120,7 +120,7 @@ Standard Checkers --ignored-modules """"""""""""""""" -*List of module names for which member attributes should not be checked (useful for modules/projects where namespaces are manipulated during runtime and thus existing member attributes cannot be deduced by static analysis). It supports qualified module names, as well as Unix pattern matching.* +*List of module names for which member attributes should not be checked and will not be imported (useful for modules/projects where namespaces are manipulated during runtime and thus existing member attributes cannot be deduced by static analysis). It supports qualified module names, as well as Unix pattern matching.* **Default:** ``()`` @@ -167,6 +167,13 @@ Standard Checkers **Default:** ``True`` +--prefer-stubs +"""""""""""""" +*Resolve imports to .pyi stubs if available. May reduce no-member messages and increase not-an-iterable messages.* + +**Default:** ``False`` + + --py-version """""""""""" *Minimum Python version to use for version dependent checks. Will default to the version used to run pylint.* @@ -271,6 +278,8 @@ Standard Checkers persistent = true + prefer-stubs = false + py-version = "sys.version_info[:2]" recursive = false diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index e928ac1a24..99cf238480 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -139,6 +139,7 @@ All messages in the error category: error/not-in-loop error/notimplemented-raised error/positional-only-arguments-expected + error/possibly-used-before-assignment error/potential-index-error error/raising-bad-type error/raising-non-exception @@ -227,6 +228,7 @@ All messages in the warning category: warning/comparison-with-callable warning/confusing-with-statement warning/consider-ternary-expression + warning/contextmanager-generator-missing-cleanup warning/dangerous-default-value warning/deprecated-argument warning/deprecated-attribute diff --git a/doc/whatsnew/1/1.5.rst b/doc/whatsnew/1/1.5.rst index 1f33b21694..f8ce036060 100644 --- a/doc/whatsnew/1/1.5.rst +++ b/doc/whatsnew/1/1.5.rst @@ -349,7 +349,7 @@ Release date: 2015-11-29 * Remove the rest of interface checks: interface-is-not-class, missing-interface-method, unresolved-interface. The reason is that - its better to start recommending ABCs instead of the old Zope era + it's better to start recommending ABCs instead of the old Zope era of interfaces. One side effect of this change is that ignore-iface-methods becomes a noop, it's deprecated and it will be removed at some time. diff --git a/doc/whatsnew/2/2.11/full.rst b/doc/whatsnew/2/2.11/full.rst index e26405a5cf..323d28eaed 100644 --- a/doc/whatsnew/2/2.11/full.rst +++ b/doc/whatsnew/2/2.11/full.rst @@ -14,7 +14,7 @@ What's New in Pylint 2.11.0? ---------------------------- Release date: 2021-09-16 -* The python3 porting mode checker and it's ``py3k`` option were removed. You can still find it in older pylint +* The python3 porting mode checker and its ``py3k`` option were removed. You can still find it in older pylint versions. * ``raising-bad-type`` is now properly emitted when raising a string diff --git a/doc/whatsnew/2/2.11/summary.rst b/doc/whatsnew/2/2.11/summary.rst index efbeea5220..75f9a5566c 100644 --- a/doc/whatsnew/2/2.11/summary.rst +++ b/doc/whatsnew/2/2.11/summary.rst @@ -41,7 +41,7 @@ New checkers Removed checkers ================ -* The python3 porting mode checker and it's ``py3k`` option were removed. You can still find it in older pylint +* The python3 porting mode checker and its ``py3k`` option were removed. You can still find it in older pylint versions. Extensions diff --git a/doc/whatsnew/3/3.1/index.rst b/doc/whatsnew/3/3.1/index.rst index 2580c7377e..1abe832dcb 100644 --- a/doc/whatsnew/3/3.1/index.rst +++ b/doc/whatsnew/3/3.1/index.rst @@ -6,17 +6,40 @@ .. toctree:: :maxdepth: 2 -:Release:3.1 +:Release: 3.1 :Date: 2024-02-25 Summary -- Release highlights ============================= -Two new checks--``use-yield-from``, ``deprecated-attribute``-- +Two new checks -- ``use-yield-from``, ``deprecated-attribute`` -- and a smattering of bug fixes. .. towncrier release notes start +What's new in Pylint 3.1.1? +--------------------------- +Release date: 2024-05-13 + + +False Positives Fixed +--------------------- + +- Treat `attrs.define` and `attrs.frozen` as dataclass decorators in + `too-few-public-methods` check. + + Closes #9345 (`#9345 `_) + +- Fix a false positive with ``singledispatchmethod-function`` when a method is decorated with both ``functools.singledispatchmethod`` and ``staticmethod``. + + Closes #9531 (`#9531 `_) + +- Fix a false positive for ``consider-using-dict-items`` when iterating using ``keys()`` and then deleting an item using the key as a lookup. + + Closes #9554 (`#9554 `_) + + + What's new in Pylint 3.1.0? --------------------------- Release date: 2024-02-25 diff --git a/doc/whatsnew/3/3.2/index.rst b/doc/whatsnew/3/3.2/index.rst new file mode 100644 index 0000000000..c71ae72197 --- /dev/null +++ b/doc/whatsnew/3/3.2/index.rst @@ -0,0 +1,175 @@ + +*************************** + What's New in Pylint 3.2 +*************************** + +.. toctree:: + :maxdepth: 2 + +:Release: 3.2 +:Date: TBA + +Summary -- Release highlights +============================= + +.. towncrier release notes start + +What's new in Pylint 3.2.2? +--------------------------- +Release date: 2024-05-20 + + +False Positives Fixed +--------------------- + +- Fix multiple false positives for generic class syntax added in Python 3.12 (PEP 695). + + Closes #9406 (`#9406 `_) + +- Exclude context manager without cleanup from + ``contextmanager-generator-missing-cleanup`` checks. + + Closes #9625 (`#9625 `_) + + + +What's new in Pylint 3.2.1? +--------------------------- +Release date: 2024-05-18 + + +False Positives Fixed +--------------------- + +- Exclude if/else branches containing terminating functions (e.g. `sys.exit()`) + from `possibly-used-before-assignment` checks. + + Closes #9627 (`#9627 `_) + +- Don't emit ``typevar-name-incorrect-variance`` warnings for PEP 695 style TypeVars. + The variance is inferred automatically by the type checker. + Adding ``_co`` or ``_contra`` suffix can help to reason about TypeVar. + + Refs #9638 (`#9638 `_) + +- Fix a false positive for `possibly-used-before-assignment` when using + `typing.assert_never()` (3.11+) to indicate exhaustiveness. + + Closes #9643 (`#9643 `_) + + + +Other Bug Fixes +--------------- + +- Fix a false negative for ``--ignore-patterns`` when the directory to be linted is specified using a dot(``.``) and all files are ignored instead of only the files whose name begin with a dot. + + Closes #9273 (`#9273 `_) + +- Restore "errors / warnings by module" section to report output (with `-ry`). + + Closes #9145 (`#9145 `_) + +- ``trailing-comma-tuple`` should now be correctly emitted when it was disabled globally + but enabled via local message control, after removal of an over-optimisation. + + Refs #9608. (`#9608 `_) + +- Add `--prefer-stubs=yes` option to opt-in to the astroid 3.2 feature + that prefers `.pyi` stubs over same-named `.py` files. This has the + potential to reduce `no-member` errors but at the cost of more errors + such as `not-an-iterable` from function bodies appearing as `...`. + + Defaults to `no`. + + Closes #9626 + Closes #9623 (`#9626 `_) + + + +Internal Changes +---------------- + +- Update astroid version to 3.2.1. This solves some reports of ``RecursionError`` + and also makes the *prefer .pyi stubs* feature in astroid 3.2.0 *opt-in* + with the aforementioned ``--prefer-stubs=y`` option. + + Refs #9139 (`#9139 `_) + + + +What's new in Pylint 3.2.0? +--------------------------- +Release date: 2024-05-14 + + +New Features +------------ + +- Understand `six.PY2` and `six.PY3` for conditional imports. + + Closes #3501 (`#3501 `_) + +- A new `github` reporter has been added. This reporter returns the output of `pylint` in a format that + Github can use to automatically annotate code. Use it with `pylint --output-format=github` on your Github Workflows. + + Closes #9443. (`#9443 `_) + + + +New Checks +---------- + +- Add check ``possibly-used-before-assignment`` when relying on names after an ``if/else`` + switch when one branch failed to define the name, raise, or return. + + Closes #1727 (`#1727 `_) + +- Checks for generators that use contextmanagers that don't handle cleanup properly. + Is meant to raise visibility on the case that a generator is not fully exhausted and the contextmanager is not cleaned up properly. + A contextmanager must yield a non-constant value and not handle cleanup for GeneratorExit. + The using generator must attempt to use the yielded context value `with x() as y` and not just `with x()`. + + Closes #2832 (`#2832 `_) + + + +False Negatives Fixed +--------------------- + +- If and Try nodes are now checked for useless return statements as well. + + Closes #9449. (`#9449 `_) + +- Fix false negative for ``property-with-parameters`` in the case of parameters which are ``positional-only``, ``keyword-only``, ``variadic positional`` or ``variadic keyword``. + + Closes #9584 (`#9584 `_) + +False Positives Fixed +--------------------- + +pylint now understands the ``@overload`` decorator return values better. + +Closes #4696 (`#4696 `_) +Refs #9606 (`#9606 `_) + +Performance Improvements +------------------------ + + +- Ignored modules are now not checked at all, instead of being checked and then + ignored. This should speed up the analysis of large codebases which have + ignored modules. + + Closes #9442 (`#9442 `_) (`#9442 `_) + + +- ImportChecker's logic has been modified to avoid context files when possible. This makes it possible + to cache module searches on astroid and reduce execution times. + + Refs #9310. (`#9310 `_) + +- An internal check for ``trailing-comma-tuple`` being enabled for a file or not is now + done once per file instead of once for each token. + + Refs #9608. (`#9608 `_) diff --git a/doc/whatsnew/3/index.rst b/doc/whatsnew/3/index.rst index a64c5eca81..5c44fa39a6 100644 --- a/doc/whatsnew/3/index.rst +++ b/doc/whatsnew/3/index.rst @@ -6,5 +6,6 @@ This is the full list of change in pylint 3.x minors, by categories. .. toctree:: :maxdepth: 2 + 3.2/index 3.1/index 3.0/index diff --git a/examples/custom_raw.py b/examples/custom_raw.py index 68e685504b..5adb777783 100644 --- a/examples/custom_raw.py +++ b/examples/custom_raw.py @@ -11,8 +11,8 @@ class MyRawChecker(BaseRawFileChecker): - """Check for line continuations with '\' instead of using triple - quoted string or parenthesis + r"""Check for line continuations with '\' instead of using triple + quoted string or parenthesis. """ name = "custom_raw" diff --git a/examples/pylintrc b/examples/pylintrc index d47800516d..0a917e9cef 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -59,10 +59,11 @@ ignore-paths= # Emacs file locks ignore-patterns=^\.# -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. +# List of module names for which member attributes should not be checked and +# will not be imported (useful for modules/projects where namespaces are +# manipulated during runtime and thus existing member attributes cannot be +# deduced by static analysis). It supports qualified module names, as well as +# Unix pattern matching. ignored-modules= # Python code to execute, usually for sys.path manipulation such as @@ -86,6 +87,10 @@ load-plugins= # Pickle collected data for later comparisons. persistent=yes +# Resolve imports to .pyi stubs if available. May reduce no-member messages +# and increase not-an-iterable messages. +prefer-stubs=no + # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. py-version=3.10 diff --git a/examples/pyproject.toml b/examples/pyproject.toml index a8ec9a7ecb..68e8c66737 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -49,10 +49,11 @@ ignore = ["CVS"] # file locks ignore-patterns = ["^\\.#"] -# List of module names for which member attributes should not be checked (useful -# for modules/projects where namespaces are manipulated during runtime and thus -# existing member attributes cannot be deduced by static analysis). It supports -# qualified module names, as well as Unix pattern matching. +# List of module names for which member attributes should not be checked and will +# not be imported (useful for modules/projects where namespaces are manipulated +# during runtime and thus existing member attributes cannot be deduced by static +# analysis). It supports qualified module names, as well as Unix pattern +# matching. # ignored-modules = # Python code to execute, usually for sys.path manipulation such as @@ -76,6 +77,10 @@ limit-inference-results = 100 # Pickle collected data for later comparisons. persistent = true +# Resolve imports to .pyi stubs if available. May reduce no-member messages +# and increase not-an-iterable messages. +prefer-stubs = false + # Minimum Python version to use for version dependent checks. Will default to the # version used to run pylint. py-version = "3.10" diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 06884a7f1e..a898f5f7bd 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "3.1.0" +__version__ = "3.2.2" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/pylint/checkers/base/__init__.py b/pylint/checkers/base/__init__.py index c9067b405e..a3e6071c4c 100644 --- a/pylint/checkers/base/__init__.py +++ b/pylint/checkers/base/__init__.py @@ -23,6 +23,7 @@ from pylint.checkers.base.basic_error_checker import BasicErrorChecker from pylint.checkers.base.comparison_checker import ComparisonChecker from pylint.checkers.base.docstring_checker import DocStringChecker +from pylint.checkers.base.function_checker import FunctionChecker from pylint.checkers.base.name_checker import ( KNOWN_NAME_TYPES_WITH_STYLE, AnyStyle, @@ -46,3 +47,4 @@ def register(linter: PyLinter) -> None: linter.register_checker(DocStringChecker(linter)) linter.register_checker(PassChecker(linter)) linter.register_checker(ComparisonChecker(linter)) + linter.register_checker(FunctionChecker(linter)) diff --git a/pylint/checkers/base/comparison_checker.py b/pylint/checkers/base/comparison_checker.py index 14d40c7d6d..6fb053e2e1 100644 --- a/pylint/checkers/base/comparison_checker.py +++ b/pylint/checkers/base/comparison_checker.py @@ -89,7 +89,6 @@ def _check_singleton_comparison( checking_for_absence: bool = False, ) -> None: """Check if == or != is being used to compare a singleton value.""" - if utils.is_singleton_const(left_value): singleton, other_value = left_value.value, right_value elif utils.is_singleton_const(right_value): diff --git a/pylint/checkers/base/function_checker.py b/pylint/checkers/base/function_checker.py new file mode 100644 index 0000000000..f7d92a4649 --- /dev/null +++ b/pylint/checkers/base/function_checker.py @@ -0,0 +1,149 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Function checker for Python code.""" + +from __future__ import annotations + +from itertools import chain + +from astroid import nodes + +from pylint.checkers import utils +from pylint.checkers.base.basic_checker import _BasicChecker + + +class FunctionChecker(_BasicChecker): + """Check if a function definition handles possible side effects.""" + + msgs = { + "W0135": ( + "The context used in function %r will not be exited.", + "contextmanager-generator-missing-cleanup", + "Used when a contextmanager is used inside a generator function" + " and the cleanup is not handled.", + ) + } + + @utils.only_required_for_messages("contextmanager-generator-missing-cleanup") + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + self._check_contextmanager_generator_missing_cleanup(node) + + @utils.only_required_for_messages("contextmanager-generator-missing-cleanup") + def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None: + self._check_contextmanager_generator_missing_cleanup(node) + + def _check_contextmanager_generator_missing_cleanup( + self, node: nodes.FunctionDef + ) -> None: + """Check a FunctionDef to find if it is a generator + that uses a contextmanager internally. + + If it is, check if the contextmanager is properly cleaned up. Otherwise, add message. + + :param node: FunctionDef node to check + :type node: nodes.FunctionDef + """ + # if function does not use a Yield statement, it cant be a generator + with_nodes = list(node.nodes_of_class(nodes.With)) + if not with_nodes: + return + # check for Yield inside the With statement + yield_nodes = list( + chain.from_iterable( + with_node.nodes_of_class(nodes.Yield) for with_node in with_nodes + ) + ) + if not yield_nodes: + return + + # infer the call that yields a value, and check if it is a contextmanager + for with_node in with_nodes: + for call, held in with_node.items: + if held is None: + # if we discard the value, then we can skip checking it + continue + + # safe infer is a generator + inferred_node = getattr(utils.safe_infer(call), "parent", None) + if not isinstance(inferred_node, nodes.FunctionDef): + continue + if self._node_fails_contextmanager_cleanup(inferred_node, yield_nodes): + self.add_message( + "contextmanager-generator-missing-cleanup", + node=with_node, + args=(node.name,), + ) + + @staticmethod + def _node_fails_contextmanager_cleanup( + node: nodes.FunctionDef, yield_nodes: list[nodes.Yield] + ) -> bool: + """Check if a node fails contextmanager cleanup. + + Current checks for a contextmanager: + - only if the context manager yields a non-constant value + - only if the context manager lacks a finally, or does not catch GeneratorExit + - only if some statement follows the yield, some manually cleanup happens + + :param node: Node to check + :type node: nodes.FunctionDef + :return: True if fails, False otherwise + :param yield_nodes: List of Yield nodes in the function body + :type yield_nodes: list[nodes.Yield] + :rtype: bool + """ + + def check_handles_generator_exceptions(try_node: nodes.Try) -> bool: + # needs to handle either GeneratorExit, Exception, or bare except + for handler in try_node.handlers: + if handler.type is None: + # handles all exceptions (bare except) + return True + inferred = utils.safe_infer(handler.type) + if inferred and inferred.qname() in { + "builtins.GeneratorExit", + "builtins.Exception", + }: + return True + return False + + # if context manager yields a non-constant value, then continue checking + if any( + yield_node.value is None or isinstance(yield_node.value, nodes.Const) + for yield_node in yield_nodes + ): + return False + + # Check if yield expression is last statement + yield_nodes = list(node.nodes_of_class(nodes.Yield)) + if len(yield_nodes) == 1: + n = yield_nodes[0].parent + while n is not node: + if n.next_sibling() is not None: + break + n = n.parent + else: + # No next statement found + return False + + # if function body has multiple Try, filter down to the ones that have a yield node + try_with_yield_nodes = [ + try_node + for try_node in node.nodes_of_class(nodes.Try) + if any(try_node.nodes_of_class(nodes.Yield)) + ] + if not try_with_yield_nodes: + # no try blocks at all, so checks after this line do not apply + return True + # if the contextmanager has a finally block, then it is fine + if all(try_node.finalbody for try_node in try_with_yield_nodes): + return False + # if the contextmanager catches GeneratorExit, then it is fine + if all( + check_handles_generator_exceptions(try_node) + for try_node in try_with_yield_nodes + ): + return False + return True diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 82ff725369..68f5767206 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -60,6 +60,7 @@ class TypeVarVariance(Enum): covariant = auto() contravariant = auto() double_variant = auto() + inferred = auto() def _get_properties(config: argparse.Namespace) -> tuple[set[str], set[str]]: @@ -623,6 +624,7 @@ def _assigns_typealias(node: nodes.NodeNG | None) -> bool: def _check_typevar(self, name: str, node: nodes.AssignName) -> None: """Check for TypeVar lint violations.""" + variance: TypeVarVariance = TypeVarVariance.invariant if isinstance(node.parent, nodes.Assign): keywords = node.assign_type().value.keywords args = node.assign_type().value.args @@ -634,8 +636,8 @@ def _check_typevar(self, name: str, node: nodes.AssignName) -> None: else: # PEP 695 generic type nodes keywords = () args = () + variance = TypeVarVariance.inferred - variance = TypeVarVariance.invariant name_arg = None for kw in keywords: if variance == TypeVarVariance.double_variant: @@ -659,7 +661,12 @@ def _check_typevar(self, name: str, node: nodes.AssignName) -> None: if name_arg is None and args and isinstance(args[0], nodes.Const): name_arg = args[0].value - if variance == TypeVarVariance.double_variant: + if variance == TypeVarVariance.inferred: + # Ignore variance check for PEP 695 type parameters. + # The variance is inferred by the type checker. + # Adding _co or _contra suffix can help to reason about TypeVar. + pass + elif variance == TypeVarVariance.double_variant: self.add_message( "typevar-double-variance", node=node, @@ -688,7 +695,7 @@ def _check_typevar(self, name: str, node: nodes.AssignName) -> None: confidence=interfaces.INFERENCE, ) elif variance == TypeVarVariance.invariant and ( - name.endswith("_co") or name.endswith("_contra") + name.endswith(("_co", "_contra")) ): suggest_name = re.sub("_contra$|_co$", "", name) self.add_message( diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py index d06572ab4b..6d577e0bdd 100644 --- a/pylint/checkers/base_checker.py +++ b/pylint/checkers/base_checker.py @@ -68,7 +68,7 @@ def __gt__(self, other: Any) -> bool: return not self_is_builtin return self.name > other.name - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Permit to assert Checkers are equal.""" if not isinstance(other, BaseChecker): return False diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index 996c59dcc2..ffe47ab156 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -454,7 +454,6 @@ def _is_attribute_property(name: str, klass: nodes.ClassDef) -> bool: Returns ``True`` if the name is a property in the given klass, ``False`` otherwise. """ - try: attributes = klass.getattr(name) except astroid.NotFoundError: @@ -748,7 +747,6 @@ def __init__(self) -> None: def set_accessed(self, node: _AccessNodes) -> None: """Set the given node as accessed.""" - frame = node_frame_class(node) if frame is None: # The node does not live in a class. @@ -1410,12 +1408,11 @@ def form_annotations(arguments: nodes.Arguments) -> list[str]: def _check_property_with_parameters(self, node: nodes.FunctionDef) -> None: if ( - node.args.args - and len(node.args.args) > 1 + len(node.args.arguments) > 1 and decorated_with_property(node) and not is_property_setter(node) ): - self.add_message("property-with-parameters", node=node) + self.add_message("property-with-parameters", node=node, confidence=HIGH) def _check_invalid_overridden_method( self, @@ -1947,7 +1944,6 @@ def _is_class_or_instance_attribute(name: str, klass: nodes.ClassDef) -> bool: Returns ``True`` if the name is a property in the given klass, ``False`` otherwise. """ - if utils.is_class_attr(name, klass): return True diff --git a/pylint/checkers/deprecated.py b/pylint/checkers/deprecated.py index 3428e736f8..028dc13f38 100644 --- a/pylint/checkers/deprecated.py +++ b/pylint/checkers/deprecated.py @@ -236,7 +236,6 @@ def check_deprecated_method(self, node: nodes.Call, inferred: nodes.NodeNG) -> N This method should be called from the checker implementing this mixin. """ - # Reject nodes which aren't of interest to us. if not isinstance(inferred, ACCEPTABLE_NODES): return @@ -272,7 +271,6 @@ def check_deprecated_class( self, node: nodes.NodeNG, mod_name: str, class_names: Iterable[str] ) -> None: """Checks if the class is deprecated.""" - for class_name in class_names: if class_name in self.deprecated_classes(mod_name): self.add_message( @@ -281,7 +279,6 @@ def check_deprecated_class( def check_deprecated_class_in_call(self, node: nodes.Call) -> None: """Checks if call the deprecated class.""" - if isinstance(node.func, nodes.Attribute) and isinstance( node.func.expr, nodes.Name ): diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index 8ff26eca15..78378e92c9 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -92,6 +92,8 @@ SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") DATACLASSES_DECORATORS = frozenset({"dataclass", "attrs"}) DATACLASS_IMPORT = "dataclasses" +ATTRS_DECORATORS = frozenset({"define", "frozen"}) +ATTRS_IMPORT = "attrs" TYPING_NAMEDTUPLE = "typing.NamedTuple" TYPING_TYPEDDICT = "typing.TypedDict" TYPING_EXTENSIONS_TYPEDDICT = "typing_extensions.TypedDict" @@ -183,7 +185,6 @@ def _is_exempt_from_public_methods(node: astroid.ClassDef) -> bool: """Check if a class is exempt from too-few-public-methods.""" - # If it's a typing.Namedtuple, typing.TypedDict or an Enum for ancestor in node.ancestors(): if is_enum(ancestor): @@ -214,6 +215,10 @@ def _is_exempt_from_public_methods(node: astroid.ClassDef) -> bool: or DATACLASS_IMPORT in root_locals ): return True + if name in ATTRS_DECORATORS and ( + root_locals.intersection(ATTRS_DECORATORS) or ATTRS_IMPORT in root_locals + ): + return True return False diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index bde463820f..afef0277ee 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -686,7 +686,6 @@ def _check_misplaced_future(self, node: nodes.ImportFrom) -> None: isinstance(prev, nodes.ImportFrom) and prev.modname == "__future__" ): self.add_message("misplaced-future", node=node) - return def _check_same_line_imports(self, node: nodes.ImportFrom) -> None: # Detect duplicate imports on the same line. @@ -1046,9 +1045,12 @@ def _add_imported_module(self, node: ImportNode, importedmodname: str) -> None: base = os.path.splitext(os.path.basename(module_file))[0] try: - importedmodname = astroid.modutils.get_module_part( - importedmodname, module_file - ) + if isinstance(node, nodes.ImportFrom) and node.level: + importedmodname = astroid.modutils.get_module_part( + importedmodname, module_file + ) + else: + importedmodname = astroid.modutils.get_module_part(importedmodname) except ImportError: pass @@ -1077,7 +1079,6 @@ def _add_imported_module(self, node: ImportNode, importedmodname: str) -> None: def _check_preferred_module(self, node: ImportNode, mod_path: str) -> None: """Check if the module has a preferred replacement.""" - mod_compare = [mod_path] # build a comparison list of possible names using importfrom if isinstance(node, astroid.nodes.node_classes.ImportFrom): diff --git a/pylint/checkers/non_ascii_names.py b/pylint/checkers/non_ascii_names.py index 825db1b11a..693d8529f5 100644 --- a/pylint/checkers/non_ascii_names.py +++ b/pylint/checkers/non_ascii_names.py @@ -65,7 +65,6 @@ class NonAsciiNameChecker(base_checker.BaseChecker): def _check_name(self, node_type: str, name: str | None, node: nodes.NodeNG) -> None: """Check whether a name is using non-ASCII characters.""" - if name is None: # For some nodes i.e. *kwargs from a dict, the name will be empty return diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 617406613f..c5b19e1a55 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -113,7 +113,6 @@ def _check_use_maxsplit_arg(self, node: nodes.Call) -> None: """Add message when accessing first or last elements of a str.split() or str.rsplit(). """ - # Check if call is split() or rsplit() if not ( isinstance(node.func, nodes.Attribute) @@ -308,6 +307,10 @@ def _check_consider_using_dict_items(self, node: nodes.For) -> None: # Ignore this subscript if it is the target of an assignment # Early termination as dict index lookup is necessary return + if isinstance(subscript.parent, nodes.Delete): + # Ignore this subscript if the index is used to delete a + # dictionary item. + return self.add_message("consider-using-dict-items", node=node) return diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index a9acf47748..94e722b177 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -586,7 +586,6 @@ def _check_simplifiable_if(self, node: nodes.If) -> None: the result of the statement's test, then this can be reduced to `bool(test)` without losing any functionality. """ - if self._is_actual_elif(node): # Not interested in if statements with multiple branches. return @@ -649,9 +648,33 @@ def _check_simplifiable_if(self, node: nodes.If) -> None: self.add_message("simplifiable-if-statement", node=node, args=(reduced_to,)) def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: + # Optimization flag because '_is_trailing_comma' is costly + trailing_comma_tuple_enabled_for_file = self.linter.is_message_enabled( + "trailing-comma-tuple" + ) + trailing_comma_tuple_enabled_once: bool = trailing_comma_tuple_enabled_for_file # Process tokens and look for 'if' or 'elif' for index, token in enumerate(tokens): token_string = token[1] + if ( + not trailing_comma_tuple_enabled_once + and token_string.startswith("#") + # We have at least 1 '#' (one char) at the start of the token + and "pylint:" in token_string[1:] + # We have at least '#' 'pylint' ( + ':') (8 chars) at the start of the token + and "enable" in token_string[8:] + # We have at least '#', 'pylint', ( + ':'), 'enable' (+ '=') (15 chars) at + # the start of the token + and any( + c in token_string[15:] for c in ("trailing-comma-tuple", "R1707") + ) + ): + # Way to not have to check if "trailing-comma-tuple" is enabled or + # disabled on each line: Any enable for it during tokenization and + # we'll start using the costly '_is_trailing_comma' to check if we + # need to raise the message. We still won't raise if it's disabled + # again due to the usual generic message control handling later. + trailing_comma_tuple_enabled_once = True if token_string == "elif": # AST exists by the time process_tokens is called, so # it's safe to assume tokens[index+1] exists. @@ -660,10 +683,17 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: # token[2] is the actual position and also is # reported by IronPython. self._elifs.extend([token[2], tokens[index + 1][2]]) - elif self.linter.is_message_enabled( - "trailing-comma-tuple" + elif ( + trailing_comma_tuple_enabled_for_file + or trailing_comma_tuple_enabled_once ) and _is_trailing_comma(tokens, index): - self.add_message("trailing-comma-tuple", line=token.start[0]) + # If "trailing-comma-tuple" is enabled globally we always check _is_trailing_comma + # it might be for nothing if there's a local disable, or if the message control is + # not enabling 'trailing-comma-tuple', but the alternative is having to check if + # it's enabled for a line each line (just to avoid calling '_is_trailing_comma'). + self.add_message( + "trailing-comma-tuple", line=token.start[0], confidence=HIGH + ) @utils.only_required_for_messages("consider-using-with") def leave_module(self, _: nodes.Module) -> None: @@ -2078,12 +2108,18 @@ def _check_return_at_the_end(self, node: nodes.FunctionDef) -> None: Per its implementation and PEP8 we can have a "return None" at the end of the function body if there are other return statements before that! """ - if len(self._return_nodes[node.name]) > 1: + if len(self._return_nodes[node.name]) != 1: return - if len(node.body) <= 1: + if not node.body: return last = node.body[-1] + if isinstance(last, nodes.Return) and len(node.body) == 1: + return + + while isinstance(last, (nodes.If, nodes.Try, nodes.ExceptHandler)): + last = last.last_child() + if isinstance(last, nodes.Return): # e.g. "return" if last.value is None: diff --git a/pylint/checkers/similar.py b/pylint/checkers/similar.py index 4f6d41efc6..b06b9d707f 100644 --- a/pylint/checkers/similar.py +++ b/pylint/checkers/similar.py @@ -44,7 +44,6 @@ from itertools import chain from typing import ( TYPE_CHECKING, - Any, Dict, List, NamedTuple, @@ -136,7 +135,7 @@ def __init__(self, fileid: str, num_line: int, *lines: Iterable[str]) -> None: self._hash: int = sum(hash(lin) for lin in lines) """The hash of some consecutive lines.""" - def __eq__(self, o: Any) -> bool: + def __eq__(self, o: object) -> bool: if not isinstance(o, LinesChunk): return NotImplemented return self._hash == o._hash @@ -195,7 +194,7 @@ def __repr__(self) -> str: f">" ) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, LineSetStartCouple): return NotImplemented return ( @@ -612,7 +611,6 @@ def _get_functions( """Recursively get all functions including nested in the classes from the tree. """ - for node in tree.body: if isinstance(node, (nodes.FunctionDef, nodes.AsyncFunctionDef)): functions.append(node) @@ -648,10 +646,10 @@ def _get_functions( line = line.strip() if ignore_docstrings: if not docstring: - if line.startswith('"""') or line.startswith("'''"): + if line.startswith(('"""', "'''")): docstring = line[:3] line = line[3:] - elif line.startswith('r"""') or line.startswith("r'''"): + elif line.startswith(('r"""', "r'''")): docstring = line[1:4] line = line[4:] if docstring: @@ -717,7 +715,7 @@ def __lt__(self, other: LineSet) -> bool: def __hash__(self) -> int: return id(self) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, LineSet): return False return self.__dict__ == other.__dict__ diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index df8b271bf7..10c1d54bfc 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -673,8 +673,9 @@ def visit_boolop(self, node: nodes.BoolOp) -> None: "singledispatchmethod-function", ) def visit_functiondef(self, node: nodes.FunctionDef) -> None: - if node.decorators and isinstance(node.parent, nodes.ClassDef): - self._check_lru_cache_decorators(node) + if node.decorators: + if isinstance(node.parent, nodes.ClassDef): + self._check_lru_cache_decorators(node) self._check_dispatch_decorators(node) def _check_lru_cache_decorators(self, node: nodes.FunctionDef) -> None: @@ -733,16 +734,14 @@ def _check_dispatch_decorators(self, node: nodes.FunctionDef) -> None: interfaces.INFERENCE, ) - if "singledispatch" in decorators_map and "classmethod" in decorators_map: - self.add_message( - "singledispatch-method", - node=decorators_map["singledispatch"][0], - confidence=decorators_map["singledispatch"][1], - ) - elif ( - "singledispatchmethod" in decorators_map - and "staticmethod" in decorators_map - ): + if node.is_method(): + if "singledispatch" in decorators_map: + self.add_message( + "singledispatch-method", + node=decorators_map["singledispatch"][0], + confidence=decorators_map["singledispatch"][1], + ) + elif "singledispatchmethod" in decorators_map: self.add_message( "singledispatchmethod-function", node=decorators_map["singledispatchmethod"][0], diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 814c392f46..56bd729c18 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1228,7 +1228,6 @@ def _get_nomember_msgid_hint( ) def visit_assign(self, node: nodes.Assign) -> None: """Process assignments in the AST.""" - self._check_assignment_from_function_call(node) self._check_dundername_is_string(node) @@ -1309,7 +1308,6 @@ def _is_builtin_no_return(node: nodes.Assign) -> bool: def _check_dundername_is_string(self, node: nodes.Assign) -> None: """Check a string is assigned to self.__name__.""" - # Check the left-hand side of the assignment is .__name__ lhs = node.targets[0] if not isinstance(lhs, nodes.AssignAttr): @@ -1926,7 +1924,6 @@ def visit_with(self, node: nodes.With) -> None: @only_required_for_messages("invalid-unary-operand-type") def visit_unaryop(self, node: nodes.UnaryOp) -> None: """Detect TypeErrors for unary operands.""" - for error in node.type_errors(): # Let the error customize its output. self.add_message("invalid-unary-operand-type", args=str(error), node=node) diff --git a/pylint/checkers/unicode.py b/pylint/checkers/unicode.py index 30ae0afd30..c90ace9710 100644 --- a/pylint/checkers/unicode.py +++ b/pylint/checkers/unicode.py @@ -165,7 +165,6 @@ def _map_positions_to_result( Also takes care of encodings for which the length of an encoded code point does not default to 8 Bit. """ - result: dict[int, _BadChar] = {} for search_for, char in search_dict.items(): @@ -248,7 +247,7 @@ def _cached_encode_search(string: str, encoding: str) -> bytes: def _fix_utf16_32_line_stream(steam: Iterable[bytes], codec: str) -> Iterable[bytes]: - """Handle line ending for UTF16 and UTF32 correctly. + r"""Handle line ending for UTF16 and UTF32 correctly. Currently, Python simply strips the required zeros after \n after the line ending. Leading to lines that can't be decoded properly @@ -375,7 +374,7 @@ class UnicodeChecker(checkers.BaseRawFileChecker): @staticmethod def _is_invalid_codec(codec: str) -> bool: - return codec.startswith("utf-16") or codec.startswith("utf-32") + return codec.startswith(("utf-16", "utf-32")) @staticmethod def _is_unicode(codec: str) -> bool: diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 3b6a26db35..a3e6496519 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1484,7 +1484,6 @@ def node_type(node: nodes.NodeNG) -> SuccessfulInferenceResult | None: def is_registered_in_singledispatch_function(node: nodes.FunctionDef) -> bool: """Check if the given function node is a singledispatch function.""" - singledispatch_qnames = ( "functools.singledispatch", "singledispatch.singledispatch", @@ -1540,7 +1539,6 @@ def find_inferred_fn_from_register(node: nodes.NodeNG) -> nodes.FunctionDef | No def is_registered_in_singledispatchmethod_function(node: nodes.FunctionDef) -> bool: """Check if the given function node is a singledispatchmethod function.""" - singledispatchmethod_qnames = ( "functools.singledispatchmethod", "singledispatch.singledispatchmethod", @@ -1831,7 +1829,11 @@ def is_sys_guard(node: nodes.If) -> bool: and value.as_string() == "sys.version_info" ): return True - + elif isinstance(node.test, nodes.Attribute) and node.test.as_string() in { + "six.PY2", + "six.PY3", + }: + return True return False @@ -2272,7 +2274,6 @@ def is_enum_member(node: nodes.AssignName) -> bool: """Return `True` if `node` is an Enum member (is an item of the `__members__` container). """ - frame = node.frame() if ( not isinstance(frame, nodes.ClassDef) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index e38bec03e3..6c33a05556 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -32,7 +32,7 @@ is_sys_guard, overridden_method, ) -from pylint.constants import PY39_PLUS, TYPING_NEVER, TYPING_NORETURN +from pylint.constants import PY39_PLUS, PY311_PLUS, TYPING_NEVER, TYPING_NORETURN from pylint.interfaces import CONTROL_FLOW, HIGH, INFERENCE, INFERENCE_FAILURE from pylint.typing import MessageDefinitionTuple @@ -403,6 +403,12 @@ def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) -> "invalid-all-format", "Used when __all__ has an invalid format.", ), + "E0606": ( + "Possibly using variable %r before assignment", + "possibly-used-before-assignment", + "Emitted when a local variable is accessed before its assignment took place " + "in both branches of an if/else switch.", + ), "E0611": ( "No name %r in module %r", "no-name-in-module", @@ -537,6 +543,8 @@ def __init__(self, node: nodes.NodeNG, scope_type: str) -> None: copy.copy(node.locals), {}, collections.defaultdict(list), scope_type ) self.node = node + self.names_under_always_false_test: set[str] = set() + self.names_defined_under_one_branch_only: set[str] = set() def __repr__(self) -> str: _to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()] @@ -636,13 +644,6 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: if VariablesChecker._comprehension_between_frame_and_node(node): return found_nodes - # Filter out assignments guarded by always false conditions - if found_nodes: - uncertain_nodes = self._uncertain_nodes_in_false_tests(found_nodes, node) - self.consumed_uncertain[node.name] += uncertain_nodes - uncertain_nodes_set = set(uncertain_nodes) - found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] - # Filter out assignments in ExceptHandlers that node is not contained in if found_nodes: found_nodes = [ @@ -652,6 +653,13 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: or n.statement().parent_of(node) ] + # Filter out assignments guarded by always false conditions + if found_nodes: + uncertain_nodes = self._uncertain_nodes_if_tests(found_nodes, node) + self.consumed_uncertain[node.name] += uncertain_nodes + uncertain_nodes_set = set(uncertain_nodes) + found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] + # Filter out assignments in an Except clause that the node is not # contained in, assuming they may fail if found_nodes: @@ -688,8 +696,9 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: return found_nodes - @staticmethod - def _inferred_to_define_name_raise_or_return(name: str, node: nodes.NodeNG) -> bool: + def _inferred_to_define_name_raise_or_return( + self, name: str, node: nodes.NodeNG + ) -> bool: """Return True if there is a path under this `if_node` that is inferred to define `name`, raise, or return. """ @@ -716,8 +725,8 @@ def _inferred_to_define_name_raise_or_return(name: str, node: nodes.NodeNG) -> b if not isinstance(node, nodes.If): return False - # Be permissive if there is a break - if any(node.nodes_of_class(nodes.Break)): + # Be permissive if there is a break or a continue + if any(node.nodes_of_class(nodes.Break, nodes.Continue)): return True # Is there an assignment in this node itself, e.g. in named expression? @@ -739,17 +748,18 @@ def _inferred_to_define_name_raise_or_return(name: str, node: nodes.NodeNG) -> b # Only search else branch when test condition is inferred to be false if all_inferred and only_search_else: - return NamesConsumer._branch_handles_name(name, node.orelse) - # Only search if branch when test condition is inferred to be true - if all_inferred and only_search_if: - return NamesConsumer._branch_handles_name(name, node.body) + self.names_under_always_false_test.add(name) + return self._branch_handles_name(name, node.orelse) # Search both if and else branches - return NamesConsumer._branch_handles_name( - name, node.body - ) or NamesConsumer._branch_handles_name(name, node.orelse) - - @staticmethod - def _branch_handles_name(name: str, body: Iterable[nodes.NodeNG]) -> bool: + if_branch_handles = self._branch_handles_name(name, node.body) + else_branch_handles = self._branch_handles_name(name, node.orelse) + if if_branch_handles ^ else_branch_handles: + self.names_defined_under_one_branch_only.add(name) + elif name in self.names_defined_under_one_branch_only: + self.names_defined_under_one_branch_only.remove(name) + return if_branch_handles and else_branch_handles + + def _branch_handles_name(self, name: str, body: Iterable[nodes.NodeNG]) -> bool: return any( NamesConsumer._defines_name_raises_or_returns(name, if_body_stmt) or isinstance( @@ -762,17 +772,15 @@ def _branch_handles_name(name: str, body: Iterable[nodes.NodeNG]) -> bool: nodes.While, ), ) - and NamesConsumer._inferred_to_define_name_raise_or_return( - name, if_body_stmt - ) + and self._inferred_to_define_name_raise_or_return(name, if_body_stmt) for if_body_stmt in body ) - def _uncertain_nodes_in_false_tests( + def _uncertain_nodes_if_tests( self, found_nodes: list[nodes.NodeNG], node: nodes.NodeNG ) -> list[nodes.NodeNG]: - """Identify nodes of uncertain execution because they are defined under - tests that evaluate false. + """Identify nodes of uncertain execution because they are defined under if + tests. Don't identify a node if there is a path that is inferred to define the name, raise, or return (e.g. any executed if/elif/else branch). @@ -808,7 +816,7 @@ def _uncertain_nodes_in_false_tests( continue # Name defined in the if/else control flow - if NamesConsumer._inferred_to_define_name_raise_or_return(name, outer_if): + if self._inferred_to_define_name_raise_or_return(name, outer_if): continue uncertain_nodes.append(other_node) @@ -930,8 +938,17 @@ def _uncertain_nodes_in_except_blocks( @staticmethod def _defines_name_raises_or_returns(name: str, node: nodes.NodeNG) -> bool: - if isinstance(node, (nodes.Raise, nodes.Assert, nodes.Return)): + if isinstance(node, (nodes.Raise, nodes.Assert, nodes.Return, nodes.Continue)): return True + if isinstance(node, nodes.Expr) and isinstance(node.value, nodes.Call): + if utils.is_terminating_func(node.value): + return True + if ( + PY311_PLUS + and isinstance(node.value.func, nodes.Name) + and node.value.func.name == "assert_never" + ): + return True if ( isinstance(node, nodes.AnnAssign) and node.value @@ -1009,7 +1026,6 @@ def _check_loop_finishes_via_except( the loop can depend on it being assigned. Example: - for _ in range(3): try: do_something() @@ -1993,11 +2009,19 @@ def _report_unfound_name_definition( ): return - confidence = ( - CONTROL_FLOW if node.name in current_consumer.consumed_uncertain else HIGH - ) + confidence = HIGH + if node.name in current_consumer.names_under_always_false_test: + confidence = INFERENCE + elif node.name in current_consumer.consumed_uncertain: + confidence = CONTROL_FLOW + + if node.name in current_consumer.names_defined_under_one_branch_only: + msg = "possibly-used-before-assignment" + else: + msg = "used-before-assignment" + self.add_message( - "used-before-assignment", + msg, args=node.name, node=node, confidence=confidence, diff --git a/pylint/config/arguments_manager.py b/pylint/config/arguments_manager.py index b99c9476ff..aca8f5f878 100644 --- a/pylint/config/arguments_manager.py +++ b/pylint/config/arguments_manager.py @@ -149,7 +149,7 @@ def _add_parser_option( metavar=argument.metavar, choices=argument.choices, ) - # We add the old name as hidden option to make it's default value gets loaded when + # We add the old name as hidden option to make its default value get loaded when # argparse initializes all options from the checker assert argument.kwargs["old_names"] for old_name in argument.kwargs["old_names"]: diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py index 6fa7b6b895..9656ea5647 100644 --- a/pylint/config/config_initialization.py +++ b/pylint/config/config_initialization.py @@ -141,7 +141,7 @@ def _config_initialization( linter._parse_error_mode() # Link the base Namespace object on the current directory - linter._directory_namespaces[Path(".").resolve()] = (linter.config, {}) + linter._directory_namespaces[Path().resolve()] = (linter.config, {}) # parsed_args_list should now only be a list of inputs to lint. # All other options have been removed from the list. @@ -169,7 +169,7 @@ def _order_all_first(config_args: list[str], *, joined: bool) -> list[str]: all_action = "" for i, arg in enumerate(config_args): - if joined and (arg.startswith("--enable=") or arg.startswith("--disable=")): + if joined and arg.startswith(("--enable=", "--disable=")): value = arg.split("=")[1] elif arg in {"--enable", "--disable"}: value = config_args[i + 1] diff --git a/pylint/constants.py b/pylint/constants.py index e51022e654..f147e5189a 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -17,6 +17,7 @@ PY38_PLUS = sys.version_info[:2] >= (3, 8) PY39_PLUS = sys.version_info[:2] >= (3, 9) PY310_PLUS = sys.version_info[:2] >= (3, 10) +PY311_PLUS = sys.version_info[:2] >= (3, 11) PY312_PLUS = sys.version_info[:2] >= (3, 12) IS_PYPY = platform.python_implementation() == "PyPy" diff --git a/pylint/extensions/confusing_elif.py b/pylint/extensions/confusing_elif.py index 546b644b30..287547eaae 100644 --- a/pylint/extensions/confusing_elif.py +++ b/pylint/extensions/confusing_elif.py @@ -27,7 +27,7 @@ class ConfusingConsecutiveElifChecker(BaseChecker): " elif", "confusing-consecutive-elif", "Used when an elif statement follows right after an indented block which itself ends with if or elif. " - "It may not be ovious if the elif statement was willingly or mistakenly unindented. " + "It may not be obvious if the elif statement was willingly or mistakenly unindented. " "Extracting the indented if statement into a separate function might avoid confusion and prevent " "errors.", ) diff --git a/pylint/extensions/empty_comment.py b/pylint/extensions/empty_comment.py index 61e257ffd6..7f54322ae1 100644 --- a/pylint/extensions/empty_comment.py +++ b/pylint/extensions/empty_comment.py @@ -16,7 +16,6 @@ def is_line_commented(line: bytes) -> bool: """Checks if a `# symbol that is not part of a string was found in line.""" - comment_idx = line.find(b"#") if comment_idx == -1: return False @@ -27,7 +26,6 @@ def is_line_commented(line: bytes) -> bool: def comment_part_of_string(line: bytes, comment_idx: int) -> bool: """Checks if the symbol at comment_idx is part of a string.""" - if ( line[:comment_idx].count(b"'") % 2 == 1 and line[comment_idx:].count(b"'") % 2 == 1 diff --git a/pylint/lint/__init__.py b/pylint/lint/__init__.py index adc920708d..1c0c6d9f58 100644 --- a/pylint/lint/__init__.py +++ b/pylint/lint/__init__.py @@ -4,15 +4,15 @@ """Pylint [options] modules_or_packages. - Check that module(s) satisfy a coding standard (and more !). +Check that module(s) satisfy a coding standard (and more !). - pylint --help +pylint --help - Display this help message and exit. +Display this help message and exit. - pylint --help-msg [,] +pylint --help-msg [,] - Display help messages about given message identifiers and exit. +Display help messages about given message identifiers and exit. """ import sys diff --git a/pylint/lint/base_options.py b/pylint/lint/base_options.py index 3d5ba5d0db..59a811d9c6 100644 --- a/pylint/lint/base_options.py +++ b/pylint/lint/base_options.py @@ -384,7 +384,8 @@ def _make_linter_options(linter: PyLinter) -> Options: "type": "csv", "metavar": "", "help": "List of module names for which member attributes " - "should not be checked (useful for modules/projects " + "should not be checked and will not be imported " + "(useful for modules/projects " "where namespaces are manipulated during runtime and " "thus existing member attributes cannot be " "deduced by static analysis). It supports qualified " @@ -414,6 +415,17 @@ def _make_linter_options(linter: PyLinter) -> Options: "Useful if running pylint in a server-like mode.", }, ), + ( + "prefer-stubs", + { + "default": False, + "type": "yn", + "metavar": "", + "help": "Resolve imports to .pyi stubs if available. May " + "reduce no-member messages and increase not-an-iterable " + "messages.", + }, + ), ) diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index d42c798c9d..04e7018843 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -7,6 +7,7 @@ import os import sys from collections.abc import Sequence +from pathlib import Path from re import Pattern from astroid import modutils @@ -58,7 +59,7 @@ def _is_ignored_file( ignore_list_paths_re: list[Pattern[str]], ) -> bool: element = os.path.normpath(element) - basename = os.path.basename(element) + basename = Path(element).absolute().name return ( basename in ignore_list or _is_in_ignore_list_re(basename, ignore_list_re) diff --git a/pylint/lint/message_state_handler.py b/pylint/lint/message_state_handler.py index 26028f0fab..2ddd7d4db3 100644 --- a/pylint/lint/message_state_handler.py +++ b/pylint/lint/message_state_handler.py @@ -305,12 +305,14 @@ def is_message_enabled( line: int | None = None, confidence: interfaces.Confidence | None = None, ) -> bool: - """Return whether this message is enabled for the current file, line and - confidence level. + """Is this message enabled for the current file ? - This function can't be cached right now as the line is the line of - the currently analysed file (self.file_state), if it changes, then the - result for the same msg_descr/line might need to change. + Optionally, is it enabled for this line and confidence level ? + + The current file is implicit and mandatory. As a result this function + can't be cached right now as the line is the line of the currently + analysed file (self.file_state), if it changes, then the result for + the same msg_descr/line might need to change. :param msg_descr: Either the msgid or the symbol for a MessageDefinition :param line: The line of the currently analysed file diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 30250154e6..eff15cc444 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -1073,6 +1073,8 @@ def open(self) -> None: MANAGER.always_load_extensions = self.config.unsafe_load_any_extension MANAGER.max_inferable_values = self.config.limit_inference_results MANAGER.extension_package_whitelist.update(self.config.extension_pkg_allow_list) + MANAGER.module_denylist.update(self.config.ignored_modules) + MANAGER.prefer_stubs = self.config.prefer_stubs if self.config.extension_pkg_whitelist: MANAGER.extension_package_whitelist.update( self.config.extension_pkg_whitelist diff --git a/pylint/lint/report_functions.py b/pylint/lint/report_functions.py index da7ab5fbc6..72734e4688 100644 --- a/pylint/lint/report_functions.py +++ b/pylint/lint/report_functions.py @@ -6,9 +6,11 @@ import collections from collections import defaultdict +from typing import cast from pylint import checkers, exceptions from pylint.reporters.ureports.nodes import Section, Table +from pylint.typing import MessageTypesFullName from pylint.utils import LinterStats @@ -54,6 +56,7 @@ def report_messages_by_module_stats( raise exceptions.EmptyReportError() by_mod: defaultdict[str, dict[str, int | float]] = collections.defaultdict(dict) for m_type in ("fatal", "error", "warning", "refactor", "convention"): + m_type = cast(MessageTypesFullName, m_type) total = stats.get_global_message_count(m_type) for module in module_stats.keys(): mod_total = stats.get_module_message_count(module, m_type) diff --git a/pylint/message/message_definition.py b/pylint/message/message_definition.py index eebcd5daed..a318cc83f7 100644 --- a/pylint/message/message_definition.py +++ b/pylint/message/message_definition.py @@ -5,7 +5,7 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from astroid import nodes @@ -59,7 +59,7 @@ def check_msgid(msgid: str) -> None: if msgid[0] not in MSG_TYPES: raise InvalidMessageError(f"Bad message type {msgid[0]} in {msgid!r}") - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: return ( isinstance(other, MessageDefinition) and self.msgid == other.msgid diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 3b76948238..88aea482ed 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -222,7 +222,6 @@ def get_diadefs(self, project: Project, linker: Linker) -> list[ClassDiagram]: :returns: The list of diagram definitions :rtype: list(:class:`pylint.pyreverse.diagrams.ClassDiagram`) """ - # read and interpret diagram definitions (Diadefs) diagrams = [] generator = ClassDiadefGenerator(linker, self) diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py index cf2a0ab6a7..bdd28dc7c3 100644 --- a/pylint/pyreverse/utils.py +++ b/pylint/pyreverse/utils.py @@ -218,7 +218,6 @@ def infer_node(node: nodes.AssignAttr | nodes.AssignName) -> set[InferenceResult """Return a set containing the node annotation if it exists otherwise return a set of the inferred types using the NodeNG.infer method. """ - ann = get_annotation(node) try: if ann: diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index 462ea48fe2..0e3577199a 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -255,9 +255,35 @@ def handle_message(self, msg: Message) -> None: self.write_message(msg) +class GithubReporter(TextReporter): + """Report messages in GitHub's special format to annotate code in its user + interface. + """ + + name = "github" + line_format = "::{category} file={path},line={line},endline={end_line},col={column},title={msg_id}::{msg}" + category_map = { + "F": "error", + "E": "error", + "W": "warning", + "C": "notice", + "R": "notice", + "I": "notice", + } + + def write_message(self, msg: Message) -> None: + self_dict = asdict(msg) + for key in ("end_line", "end_column"): + self_dict[key] = self_dict[key] or "" + + self_dict["category"] = self.category_map.get(msg.C) or "error" + self.writeln(self._fixed_template.format(**self_dict)) + + def register(linter: PyLinter) -> None: linter.register_reporter(TextReporter) linter.register_reporter(NoHeaderReporter) linter.register_reporter(ParseableTextReporter) linter.register_reporter(VSTextReporter) linter.register_reporter(ColorizedTextReporter) + linter.register_reporter(GithubReporter) diff --git a/pylint/testutils/lint_module_test.py b/pylint/testutils/lint_module_test.py index b578e3162c..48ee5a0b2f 100644 --- a/pylint/testutils/lint_module_test.py +++ b/pylint/testutils/lint_module_test.py @@ -172,7 +172,7 @@ def get_expected_messages(stream: TextIO) -> MessageCounter: line = match.group("line") if line is None: lineno = i + 1 - elif line.startswith("+") or line.startswith("-"): + elif line.startswith(("+", "-")): lineno = i + 1 + int(line) else: lineno = int(line) diff --git a/pylint/utils/linterstats.py b/pylint/utils/linterstats.py index e7a088b7bb..53afbcfe21 100644 --- a/pylint/utils/linterstats.py +++ b/pylint/utils/linterstats.py @@ -292,9 +292,11 @@ def get_global_message_count(self, type_name: str) -> int: """Get a global message count.""" return getattr(self, type_name, 0) - def get_module_message_count(self, modname: str, type_name: str) -> int: + def get_module_message_count( + self, modname: str, type_name: MessageTypesFullName + ) -> int: """Get a module message count.""" - return getattr(self.by_module[modname], type_name, 0) + return self.by_module[modname].get(type_name, 0) def increase_single_message_count(self, type_name: str, increase: int) -> None: """Increase the message type count of an individual message type.""" diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 91f1cdd8b1..73e9e6a5f3 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -52,6 +52,7 @@ "suggestion-mode", "analyse-fallback-blocks", "allow-global-unused-variables", + "prefer-stubs", ] GLOBAL_OPTION_INT = Literal["max-line-length", "docstring-min-length"] GLOBAL_OPTION_LIST = Literal["ignored-modules"] @@ -97,14 +98,12 @@ def normalize_text( # py3k has no more cmp builtin -def cmp(a: int | float, b: int | float) -> int: +def cmp(a: float, b: float) -> int: return (a > b) - (a < b) -def diff_string(old: int | float, new: int | float) -> str: - """Given an old and new int value, return a string representing the - difference. - """ +def diff_string(old: float, new: float) -> str: + """Given an old and new value, return a string representing the difference.""" diff = abs(old - new) diff_str = f"{CMPS[cmp(old, new)]}{diff and f'{diff:.2f}' or ''}" return diff_str @@ -211,7 +210,7 @@ def register_plugins(linter: PyLinter, directory: str) -> None: def _splitstrip(string: str, sep: str = ",") -> list[str]: - """Return a list of stripped string by splitting the string given as + r"""Return a list of stripped string by splitting the string given as argument on `sep` (',' by default), empty strings are discarded. >>> _splitstrip('a, b, c , 4,,') @@ -256,7 +255,8 @@ def _check_csv(value: list[str] | tuple[str] | str) -> Sequence[str]: def _check_regexp_csv(value: list[str] | tuple[str] | str) -> Iterable[str]: r"""Split a comma-separated list of regexps, taking care to avoid splitting - a regex employing a comma as quantifier, as in `\d{1,2}`.""" + a regex employing a comma as quantifier, as in `\d{1,2}`. + """ if isinstance(value, (list, tuple)): yield from value else: diff --git a/pylintrc b/pylintrc index 3896d4e353..a943b1cfd3 100644 --- a/pylintrc +++ b/pylintrc @@ -109,6 +109,7 @@ disable= # We anticipate #3512 where it will become optional fixme, consider-using-assignment-expr, + possibly-used-before-assignment, [REPORTS] @@ -341,10 +342,11 @@ property-classes=abc.abstractproperty # members is set to 'yes' mixin-class-rgx=.*MixIn -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. +# List of module names for which member attributes should not be checked and +# will not be imported (useful for modules/projects where namespaces are +# manipulated during runtime and thus existing member attributes cannot be +# deduced by static analysis). It supports qualified module names, as well +# as Unix pattern matching. ignored-modules= # List of class names for which member attributes should not be checked (useful diff --git a/pyproject.toml b/pyproject.toml index 822e5efb9b..21f6fe03aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ # Also upgrade requirements_test_min.txt. # Pinned to dev of second minor update to allow editable installs and fix primer issues, # see https://github.com/pylint-dev/astroid/issues/1341 - "astroid>=3.1.0,<=3.2.0-dev0", + "astroid>=3.2.2,<=3.3.0-dev0", "isort>=4.2.5,<6,!=5.13.0", "mccabe>=0.6,<0.8", "tomli>=1.1.0;python_version<'3.11'", @@ -144,17 +144,51 @@ module = [ # (for docstrings, strings and comments in particular). line-length = 115 +[tool.ruff.lint] select = [ + "B", # bugbear + "D", # pydocstyle "E", # pycodestyle "F", # pyflakes - "W", # pycodestyle - "B", # bugbear "I", # isort - "RUF", # ruff + "PIE", # flake8-pie + "PTH", # flake8-pathlib + "PYI", # flake8-pyi "UP", # pyupgrade + "RUF", # ruff + "W", # pycodestyle ] ignore = [ "B905", # `zip()` without an explicit `strict=` parameter + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in `__init__` + "D205", # 1 blank line required between summary line and description + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "PTH100", # `os.path.abspath()` should be replaced by `Path.resolve()` + "PTH103", # `os.makedirs()` should be replaced by `Path.mkdir(parents=True)` + "PTH107", # `os.remove()` should be replaced by `Path.unlink()` + "PTH108", # `os.unlink()` should be replaced by `Path.unlink()` + "PTH109", # `os.getcwd()` should be replaced by `Path.cwd()` + "PTH110", # `os.path.exists()` should be replaced by `Path.exists()` + "PTH111", # `os.path.expanduser()` should be replaced by `Path.expanduser()` + "PTH112", # `os.path.isdir()` should be replaced by `Path.is_dir()` + "PTH113", # `os.path.isfile()` should be replaced by `Path.is_file()` + "PTH118", # `os.path.join()` should be replaced by `Path` with `/` operator + "PTH119", # `os.path.basename()` should be replaced by `Path.name` + "PTH120", # `os.path.dirname()` should be replaced by `Path.parent` + "PTH122", # `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` + "PTH123", # `open()` should be replaced by `Path.open()` + "PTH207", # Replace `glob` with `Path.glob` or `Path.rglob` "RUF012", # mutable default values in class attributes ] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/requirements_test.txt b/requirements_test.txt index 0d10b5e98d..a4a0eba040 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,10 +1,10 @@ -r requirements_test_min.txt -coverage~=7.4 +coverage~=7.5 tbump~=6.11.0 contributors-txt>=1.0.0 -pytest-cov~=4.1 +pytest-cov~=5.0 pytest-profiling~=1.7 -pytest-xdist~=3.5 +pytest-xdist~=3.6 six # Type packages for mypy types-pkg_resources==0.1.3 diff --git a/requirements_test_min.txt b/requirements_test_min.txt index f2631e6ff1..14b100c7c8 100644 --- a/requirements_test_min.txt +++ b/requirements_test_min.txt @@ -1,11 +1,11 @@ .[testutils,spelling] # astroid dependency is also defined in pyproject.toml -astroid==3.1.0 # Pinned to a specific version for tests -typing-extensions~=4.9 +astroid==3.2.2 # Pinned to a specific version for tests +typing-extensions~=4.11 py~=1.11.0 pytest~=7.4 pytest-benchmark~=4.0 -pytest-timeout~=2.2 +pytest-timeout~=2.3 towncrier~=23.11 requests # Voluntary for test purpose, not actually used in prod, see #8904 diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index af68020a57..d30e2dc055 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -772,6 +772,10 @@ "mails": ["pedro@algarvio.me"], "name": "Pedro Algarvio" }, + "perrinjerome@gmail.com": { + "mails": ["jerome@nexedi.com", "perrinjerome@gmail.com"], + "name": "Jérome Perrin" + }, "peter.kolbus@gmail.com": { "comment": " (Garmin)", "mails": ["peter.kolbus@gmail.com", "peter.kolbus@garmin.com"], diff --git a/script/create_contributor_list.py b/script/create_contributor_list.py index 656902e91d..90cf1a98a2 100644 --- a/script/create_contributor_list.py +++ b/script/create_contributor_list.py @@ -6,7 +6,7 @@ from contributors_txt import create_contributors_txt -CWD = Path(".").absolute() +CWD = Path().absolute() BASE_DIRECTORY = Path(__file__).parent.parent.absolute() ALIASES_FILE = (BASE_DIRECTORY / "script/.contributors_aliases.json").relative_to(CWD) DEFAULT_CONTRIBUTOR_PATH = (BASE_DIRECTORY / "CONTRIBUTORS.txt").relative_to(CWD) diff --git a/tbump.toml b/tbump.toml index a12e0ef93b..e8a35d2fcc 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/pylint" [version] -current = "3.1.0" +current = "3.2.2" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/checkers/unittest_design.py b/tests/checkers/unittest_design.py index 379fee5592..1f3df788a0 100644 --- a/tests/checkers/unittest_design.py +++ b/tests/checkers/unittest_design.py @@ -19,7 +19,6 @@ def test_too_many_ancestors_ignored_parents_are_skipped(self) -> None: """Make sure that classes listed in ``ignored-parents`` aren't counted by the too-many-ancestors message. """ - node = astroid.extract_node( """ class Aaaa(object): diff --git a/tests/checkers/unittest_format.py b/tests/checkers/unittest_format.py index dfaf037b99..c0659ad763 100644 --- a/tests/checkers/unittest_format.py +++ b/tests/checkers/unittest_format.py @@ -157,7 +157,7 @@ def test_encoding_token(self) -> None: def test_disable_global_option_end_of_line() -> None: """Test for issue with disabling tokenizer messages - that extend beyond the scope of the ast tokens + that extend beyond the scope of the ast tokens. """ file_ = tempfile.NamedTemporaryFile("w", delete=False) with file_: diff --git a/tests/checkers/unittest_imports.py b/tests/checkers/unittest_imports.py index 4a8d6e5536..2a5547d388 100644 --- a/tests/checkers/unittest_imports.py +++ b/tests/checkers/unittest_imports.py @@ -117,9 +117,7 @@ def test_wildcard_import_non_init(self) -> None: @staticmethod def test_preferred_module(capsys: CaptureFixture[str]) -> None: - """ - Tests preferred-module configuration option - """ + """Tests preferred-module configuration option.""" # test preferred-modules case with base module import Run( [ @@ -212,7 +210,6 @@ def test_preferred_module(capsys: CaptureFixture[str]) -> None: @staticmethod def test_allow_reexport_package(capsys: CaptureFixture[str]) -> None: """Test --allow-reexport-from-package option.""" - # Option disabled - useless-import-alias should always be emitted Run( [ diff --git a/tests/checkers/unittest_spelling.py b/tests/checkers/unittest_spelling.py index 08c1fa0515..71fed57900 100644 --- a/tests/checkers/unittest_spelling.py +++ b/tests/checkers/unittest_spelling.py @@ -335,7 +335,8 @@ def test_skip_sphinx_directives_2(self) -> None: ) def test_tool_directives_handling(self, prefix: str, suffix: str) -> None: """We're not raising when the directive is at the beginning of comments, - but we raise if a directive appears later in comment.""" + but we raise if a directive appears later in comment. + """ full_comment = f"# {prefix}{suffix} {prefix}" args = ( prefix, diff --git a/tests/checkers/unittest_unicode/unittest_bad_chars.py b/tests/checkers/unittest_unicode/unittest_bad_chars.py index e5cdcfe04c..1009eba422 100644 --- a/tests/checkers/unittest_unicode/unittest_bad_chars.py +++ b/tests/checkers/unittest_unicode/unittest_bad_chars.py @@ -219,7 +219,7 @@ def test_bad_chars_that_would_currently_crash_python( codec_and_msg: tuple[str, tuple[pylint.testutils.MessageTest]], ) -> None: """Special test for a file containing chars that lead to - Python or Astroid crashes (which causes Pylint to exit early) + Python or Astroid crashes (which causes Pylint to exit early). """ codec, start_msg = codec_and_msg # Create file that will fail loading in astroid. diff --git a/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py b/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py index 95b2b4602b..91e4f36852 100644 --- a/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py +++ b/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py @@ -27,11 +27,10 @@ class TestBidirectionalUnicodeChecker(pylint.testutils.CheckerTestCase): def test_finds_bidirectional_unicode_that_currently_not_parsed(self) -> None: """Test an example from https://github.com/nickboucher/trojan-source/tree/main/Python - that is currently not working Python but producing a syntax error + that is currently not working Python but producing a syntax error. So we test this to make sure it stays like this """ - test_file = UNICODE_TESTS / "invisible_function.txt" with pytest.raises(astroid.AstroidSyntaxError): diff --git a/tests/checkers/unittest_utils.py b/tests/checkers/unittest_utils.py index 9d90d4c015..44fa13552a 100644 --- a/tests/checkers/unittest_utils.py +++ b/tests/checkers/unittest_utils.py @@ -400,9 +400,19 @@ def test_if_sys_guard() -> None: if sys.some_other_function > (3, 8): #@ pass + + import six + if six.PY2: #@ + pass + + if six.PY3: #@ + pass + + if six.something_else: #@ + pass """ ) - assert isinstance(code, list) and len(code) == 3 + assert isinstance(code, list) and len(code) == 6 assert isinstance(code[0], nodes.If) assert utils.is_sys_guard(code[0]) is True @@ -412,6 +422,14 @@ def test_if_sys_guard() -> None: assert isinstance(code[2], nodes.If) assert utils.is_sys_guard(code[2]) is False + assert isinstance(code[3], nodes.If) + assert utils.is_sys_guard(code[3]) is True + assert isinstance(code[4], nodes.If) + assert utils.is_sys_guard(code[4]) is True + + assert isinstance(code[5], nodes.If) + assert utils.is_sys_guard(code[5]) is False + def test_if_typing_guard() -> None: code = astroid.extract_node( diff --git a/tests/checkers/unittest_variables.py b/tests/checkers/unittest_variables.py index 858873a363..f43a712b10 100644 --- a/tests/checkers/unittest_variables.py +++ b/tests/checkers/unittest_variables.py @@ -164,7 +164,7 @@ class MyObject(object): def test_nested_lambda(self) -> None: """Make sure variables from parent lambdas - aren't noted as undefined + aren't noted as undefined. https://github.com/pylint-dev/pylint/issues/760 """ @@ -179,7 +179,7 @@ def test_nested_lambda(self) -> None: @set_config(ignored_argument_names=re.compile("arg")) def test_ignored_argument_names_no_message(self) -> None: """Make sure is_ignored_argument_names properly ignores - function arguments + function arguments. """ node = astroid.parse( """ diff --git a/tests/config/test_argparse_config.py b/tests/config/test_argparse_config.py index dfa0fd4ddd..b818b7af1d 100644 --- a/tests/config/test_argparse_config.py +++ b/tests/config/test_argparse_config.py @@ -2,7 +2,7 @@ # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt -"""Test for the (new) implementation of option parsing with argparse""" +"""Test for the (new) implementation of option parsing with argparse.""" import re from os.path import abspath, dirname, join diff --git a/tests/config/test_find_default_config_files.py b/tests/config/test_find_default_config_files.py index 6a8095c5d7..ae879a10d0 100644 --- a/tests/config/test_find_default_config_files.py +++ b/tests/config/test_find_default_config_files.py @@ -24,7 +24,7 @@ @pytest.fixture def pop_pylintrc() -> None: - """Remove the PYLINTRC environment variable""" + """Remove the PYLINTRC environment variable.""" os.environ.pop("PYLINTRC", None) @@ -166,7 +166,7 @@ def test_pylintrc_toml_parentdir() -> None: @pytest.mark.usefixtures("pop_pylintrc") def test_pyproject_toml_parentdir() -> None: - """Test the search of pyproject.toml file in parent directories""" + """Test the search of pyproject.toml file in parent directories.""" with tempdir() as chroot: with fake_home(): chroot_path = Path(chroot) diff --git a/tests/extensions/test_private_import.py b/tests/extensions/test_private_import.py index a10384eacd..156025eafd 100644 --- a/tests/extensions/test_private_import.py +++ b/tests/extensions/test_private_import.py @@ -2,7 +2,7 @@ # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt -"""Tests the local module directory comparison logic which requires mocking file directories""" +"""Tests the local module directory comparison logic which requires mocking file directories.""" from unittest.mock import MagicMock, patch @@ -14,7 +14,7 @@ class TestPrivateImport(CheckerTestCase): - """The mocked dirname is the directory of the file being linted, the node is code inside that file""" + """The mocked dirname is the directory of the file being linted, the node is code inside that file.""" CHECKER_CLASS = private_import.PrivateImportChecker diff --git a/tests/functional/a/access/access_attr_before_def_false_positive.py b/tests/functional/a/access/access_attr_before_def_false_positive.py index 3d74b302f9..0f22d924fa 100644 --- a/tests/functional/a/access/access_attr_before_def_false_positive.py +++ b/tests/functional/a/access/access_attr_before_def_false_positive.py @@ -79,7 +79,7 @@ def __init__(self): pass class DefinedOutsideInit: - """use_attr is seen as the method defining attr because its in + """use_attr is seen as the method defining attr because it's in first position """ def __init__(self): diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index 7fd74814fc..16f73d1dcd 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -1,6 +1,10 @@ """Emit a message for iteration through dict keys and subscripting dict with key.""" + # pylint: disable=line-too-long,missing-docstring,unsubscriptable-object,too-few-public-methods,redefined-outer-name,use-dict-literal,modified-iterating-dict +import os + + def bad(): a_dict = {1: 1, 2: 2, 3: 3} for k in a_dict: # [consider-using-dict-items] @@ -15,12 +19,15 @@ def good(): for k in a_dict: print(k) + out_of_scope_dict = dict() + def another_bad(): for k in out_of_scope_dict: # [consider-using-dict-items] print(out_of_scope_dict[k]) + def another_good(): for k in out_of_scope_dict: k = 1 @@ -47,9 +54,11 @@ def another_good(): for k4 in b_dict.keys(): # [consider-iterating-dictionary,consider-using-dict-items] val = b_dict[k4] + class Foo: c_dict = {} + # Should emit warning when iterating over a dict attribute of a class for k5 in Foo.c_dict: # [consider-using-dict-items] val = Foo.c_dict[k5] @@ -88,18 +97,25 @@ class Foo: # Test false positive described in #4630 # (https://github.com/pylint-dev/pylint/issues/4630) -d = {'key': 'value'} +d = {"key": "value"} for k in d: # this is fine, with the reassignment of d[k], d[k] is necessary - d[k] += '123' - if '1' in d[k]: # index lookup necessary here, do not emit error - print('found 1') + d[k] += "123" + if "1" in d[k]: # index lookup necessary here, do not emit error + print("found 1") for k in d: # if this gets rewritten to d.items(), we are back to the above problem d[k] = d[k] + 1 - if '1' in d[k]: # index lookup necessary here, do not emit error - print('found 1') + if "1" in d[k]: # index lookup necessary here, do not emit error + print("found 1") for k in d: # [consider-using-dict-items] - if '1' in d[k]: # index lookup necessary here, do not emit error - print('found 1') + if "1" in d[k]: # index lookup necessary here, do not emit error + print("found 1") + + +# False positive in issue #9554 +# https://github.com/pylint-dev/pylint/issues/9554 +for var in os.environ.keys(): # [consider-iterating-dictionary] + if var.startswith("foo_"): + del os.environ[var] # index lookup necessary here, do not emit error diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index 280ffecf37..63f684cc6a 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -1,16 +1,17 @@ -consider-using-dict-items:6:4:7:24:bad:Consider iterating with .items():UNDEFINED -consider-using-dict-items:9:4:10:30:bad:Consider iterating with .items():UNDEFINED -consider-using-dict-items:21:4:22:35:another_bad:Consider iterating with .items():UNDEFINED -consider-using-dict-items:40:0:42:18::Consider iterating with .items():UNDEFINED -consider-using-dict-items:44:0:45:20::Consider iterating with .items():UNDEFINED -consider-iterating-dictionary:47:10:47:23::Consider iterating the dictionary directly instead of calling .keys():INFERENCE -consider-using-dict-items:47:0:48:20::Consider iterating with .items():UNDEFINED -consider-using-dict-items:54:0:55:24::Consider iterating with .items():UNDEFINED -consider-using-dict-items:67:0:None:None::Consider iterating with .items():UNDEFINED -consider-using-dict-items:68:0:None:None::Consider iterating with .items():UNDEFINED -consider-using-dict-items:71:0:None:None::Consider iterating with .items():UNDEFINED -consider-using-dict-items:72:0:None:None::Consider iterating with .items():UNDEFINED -consider-using-dict-items:75:0:None:None::Consider iterating with .items():UNDEFINED -consider-iterating-dictionary:86:25:86:42::Consider iterating the dictionary directly instead of calling .keys():INFERENCE -consider-using-dict-items:86:0:None:None::Consider iterating with .items():UNDEFINED -consider-using-dict-items:103:0:105:24::Consider iterating with .items():UNDEFINED +consider-using-dict-items:10:4:11:24:bad:Consider iterating with .items():UNDEFINED +consider-using-dict-items:13:4:14:30:bad:Consider iterating with .items():UNDEFINED +consider-using-dict-items:27:4:28:35:another_bad:Consider iterating with .items():UNDEFINED +consider-using-dict-items:47:0:49:18::Consider iterating with .items():UNDEFINED +consider-using-dict-items:51:0:52:20::Consider iterating with .items():UNDEFINED +consider-iterating-dictionary:54:10:54:23::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-using-dict-items:54:0:55:20::Consider iterating with .items():UNDEFINED +consider-using-dict-items:63:0:64:24::Consider iterating with .items():UNDEFINED +consider-using-dict-items:76:0:None:None::Consider iterating with .items():UNDEFINED +consider-using-dict-items:77:0:None:None::Consider iterating with .items():UNDEFINED +consider-using-dict-items:80:0:None:None::Consider iterating with .items():UNDEFINED +consider-using-dict-items:81:0:None:None::Consider iterating with .items():UNDEFINED +consider-using-dict-items:84:0:None:None::Consider iterating with .items():UNDEFINED +consider-iterating-dictionary:95:25:95:42::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-using-dict-items:95:0:None:None::Consider iterating with .items():UNDEFINED +consider-using-dict-items:112:0:114:24::Consider iterating with .items():UNDEFINED +consider-iterating-dictionary:119:11:119:28::Consider iterating the dictionary directly instead of calling .keys():INFERENCE diff --git a/tests/functional/c/consider/consider_using_with.py b/tests/functional/c/consider/consider_using_with.py index e8e1623374..9ff70e08b2 100644 --- a/tests/functional/c/consider/consider_using_with.py +++ b/tests/functional/c/consider/consider_using_with.py @@ -186,9 +186,7 @@ def test_futures(): pass -global_pool = ( - multiprocessing.Pool() -) # must not trigger, will be used in nested scope +global_pool = multiprocessing.Pool() # must not trigger, will be used in nested scope def my_nested_function(): diff --git a/tests/functional/c/consider/consider_using_with.txt b/tests/functional/c/consider/consider_using_with.txt index 455762f57d..864a0784c1 100644 --- a/tests/functional/c/consider/consider_using_with.txt +++ b/tests/functional/c/consider/consider_using_with.txt @@ -20,9 +20,9 @@ consider-using-with:140:8:140:30:test_multiprocessing:Consider using 'with' for consider-using-with:145:4:145:19:test_multiprocessing:Consider using 'with' for resource-allocating operations:UNDEFINED consider-using-with:150:4:150:19:test_multiprocessing:Consider using 'with' for resource-allocating operations:UNDEFINED consider-using-with:156:8:156:30:test_popen:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:212:4:212:26::Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:213:4:213:26::Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:218:4:218:26::Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:224:4:224:26::Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:240:18:240:40:test_subscript_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:242:24:242:46:test_subscript_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:210:4:210:26::Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:211:4:211:26::Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:216:4:216:26::Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:222:4:222:26::Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:238:18:238:40:test_subscript_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:240:24:240:46:test_subscript_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED diff --git a/tests/functional/c/consider/consider_using_with_open.py b/tests/functional/c/consider/consider_using_with_open.py index dd58426879..b76765cf89 100644 --- a/tests/functional/c/consider/consider_using_with_open.py +++ b/tests/functional/c/consider/consider_using_with_open.py @@ -1,5 +1,6 @@ # pylint: disable=missing-function-docstring, missing-module-docstring, invalid-name, import-outside-toplevel # pylint: disable=missing-class-docstring, too-few-public-methods, unused-variable, multiple-statements, line-too-long +# pylint: disable=contextmanager-generator-missing-cleanup """ Previously, open was uninferable on PyPy so we moved all functional tests to a separate file. This is no longer the case but the files remain split. diff --git a/tests/functional/c/consider/consider_using_with_open.txt b/tests/functional/c/consider/consider_using_with_open.txt index 3819e266dd..57aaff736b 100644 --- a/tests/functional/c/consider/consider_using_with_open.txt +++ b/tests/functional/c/consider/consider_using_with_open.txt @@ -1,7 +1,7 @@ -consider-using-with:10:9:10:43::Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:14:9:14:43:test_open:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:44:4:44:33:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:45:14:45:43:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:50:8:50:37:test_open_inside_with_block:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:118:26:120:13:TestControlFlow.test_triggers_if_reassigned_after_if_else:Consider using 'with' for resource-allocating operations:UNDEFINED -used-before-assignment:139:12:139:23:TestControlFlow.test_defined_in_try_and_finally:Using variable 'file_handle' before assignment:CONTROL_FLOW +consider-using-with:11:9:11:43::Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:15:9:15:43:test_open:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:45:4:45:33:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:46:14:46:43:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:51:8:51:37:test_open_inside_with_block:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:119:26:121:13:TestControlFlow.test_triggers_if_reassigned_after_if_else:Consider using 'with' for resource-allocating operations:UNDEFINED +used-before-assignment:140:12:140:23:TestControlFlow.test_defined_in_try_and_finally:Using variable 'file_handle' before assignment:CONTROL_FLOW diff --git a/tests/functional/c/contextmanager_generator_missing_cleanup.py b/tests/functional/c/contextmanager_generator_missing_cleanup.py new file mode 100644 index 0000000000..cb77e1610c --- /dev/null +++ b/tests/functional/c/contextmanager_generator_missing_cleanup.py @@ -0,0 +1,189 @@ +# pylint: disable = missing-docstring, unused-variable, bare-except, broad-exception-caught +from collections import namedtuple +import contextlib +from contextlib import contextmanager + +# Positive + + +@contextlib.contextmanager +def cm(): + contextvar = "acquired context" + print("cm enter") + yield contextvar + print("cm exit") + + +def genfunc_with_cm(): + with cm() as context: # [contextmanager-generator-missing-cleanup] + yield context * 2 + + +@contextmanager +def name_cm(): + contextvar = "acquired context" + print("cm enter") + yield contextvar + print("cm exit") + + +def genfunc_with_name_cm(): + with name_cm() as context: # [contextmanager-generator-missing-cleanup] + yield context * 2 + + +def genfunc_with_cm_after(): + with after_cm() as context: # [contextmanager-generator-missing-cleanup] + yield context * 2 + + +@contextlib.contextmanager +def after_cm(): + contextvar = "acquired context" + print("cm enter") + yield contextvar + print("cm exit") + + +@contextmanager +def cm_with_improper_handling(): + contextvar = "acquired context" + print("cm enter") + try: + yield contextvar + except ValueError: + pass + print("cm exit") + + +def genfunc_with_cm_improper(): + with cm_with_improper_handling() as context: # [contextmanager-generator-missing-cleanup] + yield context * 2 + + +# Negative + + +class Enterable: + def __enter__(self): + print("enter") + return self + + def __exit__(self, *args): + print("exit") + + +def genfunc_with_enterable(): + enter = Enterable() + with enter as context: + yield context * 2 + + +def genfunc_with_enterable_attr(): + EnterableTuple = namedtuple("EnterableTuple", ["attr"]) + t = EnterableTuple(Enterable()) + with t.attr as context: + yield context.attr * 2 + + +@contextlib.contextmanager +def good_cm_except(): + contextvar = "acquired context" + print("good cm enter") + try: + yield contextvar + except GeneratorExit: + print("good cm exit") + + +def good_genfunc_with_cm(): + with good_cm_except() as context: + yield context * 2 + + +def genfunc_with_discard(): + with good_cm_except(): + yield "discarded" + + +@contextlib.contextmanager +def good_cm_yield_none(): + print("good cm enter") + yield + print("good cm exit") + + +def genfunc_with_none_yield(): + with good_cm_yield_none() as var: + print(var) + yield "discarded" + + +@contextlib.contextmanager +def good_cm_finally(): + contextvar = "acquired context" + print("good cm enter") + try: + yield contextvar + finally: + print("good cm exit") + + +def good_cm_finally_genfunc(): + with good_cm_finally() as context: + yield context * 2 + + +def genfunc_with_cm_finally_odd_body(): + with good_cm_finally() as context: + if context: + yield context * 2 + else: + yield context * 3 + + +@cm_with_improper_handling +def genfunc_wrapped(): + yield "wrapped" + + +@contextmanager +def cm_bare_handler(): + contextvar = "acquired context" + print("cm enter") + try: + yield contextvar + except: + print("cm exit") + + +@contextmanager +def cm_base_exception_handler(): + contextvar = "acquired context" + print("cm enter") + try: + yield contextvar + except Exception: + print("cm exit") + + +def genfunc_with_cm_bare_handler(): + with cm_bare_handler() as context: + yield context * 2 + + +def genfunc_with_cm_base_exception_handler(): + with cm_base_exception_handler() as context: + yield context * 2 + + +@contextlib.contextmanager +def good_cm_no_cleanup(): + contextvar = "acquired context" + print("cm enter") + yield contextvar + + +def good_cm_no_cleanup_genfunc(): + with good_cm_no_cleanup() as context: + yield context * 2 diff --git a/tests/functional/c/contextmanager_generator_missing_cleanup.txt b/tests/functional/c/contextmanager_generator_missing_cleanup.txt new file mode 100644 index 0000000000..0c6b5e15cf --- /dev/null +++ b/tests/functional/c/contextmanager_generator_missing_cleanup.txt @@ -0,0 +1,4 @@ +contextmanager-generator-missing-cleanup:18:4:19:25:genfunc_with_cm:The context used in function 'genfunc_with_cm' will not be exited.:UNDEFINED +contextmanager-generator-missing-cleanup:31:4:32:25:genfunc_with_name_cm:The context used in function 'genfunc_with_name_cm' will not be exited.:UNDEFINED +contextmanager-generator-missing-cleanup:36:4:37:25:genfunc_with_cm_after:The context used in function 'genfunc_with_cm_after' will not be exited.:UNDEFINED +contextmanager-generator-missing-cleanup:60:4:61:25:genfunc_with_cm_improper:The context used in function 'genfunc_with_cm_improper' will not be exited.:UNDEFINED diff --git a/tests/functional/e/.#emacs_file_lock.py b/tests/functional/e/.#emacs_file_lock.py index a21e28482d..780fcc77db 100644 --- a/tests/functional/e/.#emacs_file_lock.py +++ b/tests/functional/e/.#emacs_file_lock.py @@ -1,4 +1,4 @@ # The name is invalid, but we should not analyse this file -# Because its filename reseambles an Emacs file lock ignored by default +# Because its filename resembles an Emacs file lock ignored by default # https://www.gnu.org/software/emacs/manual/html_node/elisp/File-Locks.html # See https://github.com/pylint-dev/pylint/issues/367 diff --git a/tests/functional/g/generic_class_syntax.py b/tests/functional/g/generic_class_syntax.py new file mode 100644 index 0000000000..b7305965ac --- /dev/null +++ b/tests/functional/g/generic_class_syntax.py @@ -0,0 +1,38 @@ +# pylint: disable=missing-docstring,too-few-public-methods +from typing import Generic, TypeVar, Optional + +_T = TypeVar("_T") + + +class Entity(Generic[_T]): + last_update: Optional[int] = None + + def __init__(self, data: _T) -> None: + self.data = data + + +class Sensor(Entity[int]): + def __init__(self, data: int) -> None: + super().__init__(data) + + def async_update(self) -> None: + self.data = 2 + + if self.last_update is None: + pass + self.last_update = 2 + + +class Switch(Entity[int]): + def __init__(self, data: int) -> None: + Entity.__init__(self, data) + + +class Parent(Generic[_T]): + def __init__(self): + self.update_interval = 0 + + +class Child(Parent[_T]): + def func(self): + self.update_interval = None diff --git a/tests/functional/g/generic_class_syntax_py312.py b/tests/functional/g/generic_class_syntax_py312.py new file mode 100644 index 0000000000..bbfff1c6ad --- /dev/null +++ b/tests/functional/g/generic_class_syntax_py312.py @@ -0,0 +1,33 @@ +# pylint: disable=missing-docstring,too-few-public-methods +class Entity[_T: float]: + last_update: int | None = None + + def __init__(self, data: _T) -> None: # [undefined-variable] # false-positive + self.data = data + + +class Sensor(Entity[int]): + def __init__(self, data: int) -> None: + super().__init__(data) + + def async_update(self) -> None: + self.data = 2 + + if self.last_update is None: + pass + self.last_update = 2 + + +class Switch(Entity[int]): + def __init__(self, data: int) -> None: + Entity.__init__(self, data) + + +class Parent[_T]: + def __init__(self): + self.update_interval = 0 + + +class Child[_T](Parent[_T]): # [undefined-variable] # false-positive + def func(self): + self.update_interval = None diff --git a/tests/functional/g/generic_class_syntax_py312.rc b/tests/functional/g/generic_class_syntax_py312.rc new file mode 100644 index 0000000000..9c966d4bda --- /dev/null +++ b/tests/functional/g/generic_class_syntax_py312.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.12 diff --git a/tests/functional/g/generic_class_syntax_py312.txt b/tests/functional/g/generic_class_syntax_py312.txt new file mode 100644 index 0000000000..bd5fbbe7ee --- /dev/null +++ b/tests/functional/g/generic_class_syntax_py312.txt @@ -0,0 +1,2 @@ +undefined-variable:5:29:5:31:Entity.__init__:Undefined variable '_T':UNDEFINED +undefined-variable:31:23:31:25:Child:Undefined variable '_T':UNDEFINED diff --git a/tests/functional/i/inconsistent/inconsistent_returns.py b/tests/functional/i/inconsistent/inconsistent_returns.py index bcdfd76e58..932c1791b0 100644 --- a/tests/functional/i/inconsistent/inconsistent_returns.py +++ b/tests/functional/i/inconsistent/inconsistent_returns.py @@ -1,5 +1,5 @@ #pylint: disable=missing-docstring, no-else-return, no-else-break, invalid-name, unused-variable, superfluous-parens, try-except-raise -#pylint: disable=disallowed-name,too-few-public-methods,no-member,useless-else-on-loop +#pylint: disable=disallowed-name,too-few-public-methods,no-member,useless-else-on-loop,useless-return """Testing inconsistent returns""" import math import sys diff --git a/tests/functional/m/missing/missing_kwoa.py b/tests/functional/m/missing/missing_kwoa.py index 15df710bfd..15943254c4 100644 --- a/tests/functional/m/missing/missing_kwoa.py +++ b/tests/functional/m/missing/missing_kwoa.py @@ -2,6 +2,7 @@ import contextlib import typing + def target(pos, *, keyword): return pos + keyword @@ -13,18 +14,19 @@ def forwarding_kwds(pos, **kwds): def forwarding_args(*args, keyword): target(*args, keyword=keyword) + def forwarding_conversion(*args, **kwargs): target(*args, **dict(kwargs)) def not_forwarding_kwargs(*args, **kwargs): - target(*args) # [missing-kwoa] + target(*args) # [missing-kwoa] target(1, keyword=2) PARAM = 1 -target(2, PARAM) # [too-many-function-args, missing-kwoa] +target(2, PARAM) # [too-many-function-args, missing-kwoa] def some_function(*, param): @@ -39,9 +41,8 @@ def other_function(**kwargs): class Parent: - @typing.overload - def __init__( self, *, first, second, third): + def __init__(self, *, first, second, third): pass @typing.overload @@ -53,22 +54,19 @@ def __init__(self, *, first): pass def __init__( - self, - *, - first, - second: typing.Optional[str] = None, - third: typing.Optional[str] = None): + self, + *, + first, + second: typing.Optional[str] = None, + third: typing.Optional[str] = None, + ): self._first = first self._second = second self._third = third class Child(Parent): - def __init__( - self, - *, - first, - second): + def __init__(self, *, first, second): super().__init__(first=first, second=second) self._first = first + second @@ -77,6 +75,7 @@ def __init__( def run(*, a): yield + def test_context_managers(**kw): run(**kw) @@ -89,4 +88,5 @@ def test_context_managers(**kw): with run(**kw), run(): # [missing-kwoa] pass + test_context_managers(a=1) diff --git a/tests/functional/m/missing/missing_kwoa.txt b/tests/functional/m/missing/missing_kwoa.txt index fc1694ed20..e31249cfb8 100644 --- a/tests/functional/m/missing/missing_kwoa.txt +++ b/tests/functional/m/missing/missing_kwoa.txt @@ -1,4 +1,4 @@ -missing-kwoa:21:4:21:17:not_forwarding_kwargs:Missing mandatory keyword argument 'keyword' in function call:INFERENCE -missing-kwoa:27:0:27:16::Missing mandatory keyword argument 'keyword' in function call:INFERENCE -too-many-function-args:27:0:27:16::Too many positional arguments for function call:UNDEFINED -missing-kwoa:89:20:89:25:test_context_managers:Missing mandatory keyword argument 'a' in function call:INFERENCE +missing-kwoa:23:4:23:17:not_forwarding_kwargs:Missing mandatory keyword argument 'keyword' in function call:INFERENCE +missing-kwoa:29:0:29:16::Missing mandatory keyword argument 'keyword' in function call:INFERENCE +too-many-function-args:29:0:29:16::Too many positional arguments for function call:UNDEFINED +missing-kwoa:88:20:88:25:test_context_managers:Missing mandatory keyword argument 'a' in function call:INFERENCE diff --git a/tests/functional/p/property_with_parameters.py b/tests/functional/p/property_with_parameters.py index b210bb5015..599d744b4c 100644 --- a/tests/functional/p/property_with_parameters.py +++ b/tests/functional/p/property_with_parameters.py @@ -4,8 +4,24 @@ class Cls: @property - def attribute(self, param, param1): # [property-with-parameters] - return param + param1 + def a(self, arg): # [property-with-parameters] + return arg + + @property + def b(self, arg, /): # [property-with-parameters] + return arg + + @property + def c(self, *, arg): # [property-with-parameters] + return arg + + @property + def d(self, *args): # [property-with-parameters] + return args + + @property + def e(self, **kwargs): # [property-with-parameters] + return kwargs class MyClassBase(metaclass=ABCMeta): diff --git a/tests/functional/p/property_with_parameters.txt b/tests/functional/p/property_with_parameters.txt index bc07bc6d19..5360e90c0f 100644 --- a/tests/functional/p/property_with_parameters.txt +++ b/tests/functional/p/property_with_parameters.txt @@ -1 +1,5 @@ -property-with-parameters:7:4:7:17:Cls.attribute:Cannot have defined parameters for properties:UNDEFINED +property-with-parameters:7:4:7:9:Cls.a:Cannot have defined parameters for properties:HIGH +property-with-parameters:11:4:11:9:Cls.b:Cannot have defined parameters for properties:HIGH +property-with-parameters:15:4:15:9:Cls.c:Cannot have defined parameters for properties:HIGH +property-with-parameters:19:4:19:9:Cls.d:Cannot have defined parameters for properties:HIGH +property-with-parameters:23:4:23:9:Cls.e:Cannot have defined parameters for properties:HIGH diff --git a/tests/functional/r/redefined/redefined_except_handler.txt b/tests/functional/r/redefined/redefined_except_handler.txt index a0ccc6b9b3..1184bdd816 100644 --- a/tests/functional/r/redefined/redefined_except_handler.txt +++ b/tests/functional/r/redefined/redefined_except_handler.txt @@ -1,4 +1,4 @@ redefined-outer-name:11:4:12:12::Redefining name 'err' from outer scope (line 8):UNDEFINED redefined-outer-name:57:8:58:16::Redefining name 'err' from outer scope (line 51):UNDEFINED -used-before-assignment:69:14:69:29:func:Using variable 'CustomException' before assignment:CONTROL_FLOW +used-before-assignment:69:14:69:29:func:Using variable 'CustomException' before assignment:HIGH redefined-outer-name:71:4:72:12:func:Redefining name 'CustomException' from outer scope (line 62):UNDEFINED diff --git a/tests/functional/r/regression_02/regression_4660.py b/tests/functional/r/regression_02/regression_4660.py index b3dee058f5..6a051f63ae 100644 --- a/tests/functional/r/regression_02/regression_4660.py +++ b/tests/functional/r/regression_02/regression_4660.py @@ -14,7 +14,6 @@ def my_print(*args: Any) -> None: return -# ---- This is OK ---- class MyClass: def my_method(self, option: Literal["mandatory"]) -> Callable[..., Any]: return my_print @@ -23,7 +22,6 @@ def my_method(self, option: Literal["mandatory"]) -> Callable[..., Any]: c = MyClass().my_method("mandatory") c(1, "foo") -# ---- This runs OK but pylint reports an error ---- class MyClass1: @overload def my_method(self, option: Literal["mandatory"]) -> Callable[..., Any]: @@ -42,4 +40,4 @@ def my_method( d = MyClass1().my_method("mandatory") -d(1, "bar") # [not-callable] +d(1, "bar") diff --git a/tests/functional/r/regression_02/regression_4660.txt b/tests/functional/r/regression_02/regression_4660.txt deleted file mode 100644 index 59b48ecacb..0000000000 --- a/tests/functional/r/regression_02/regression_4660.txt +++ /dev/null @@ -1 +0,0 @@ -not-callable:45:0:45:11::d is not callable:UNDEFINED diff --git a/tests/functional/r/regression_02/regression_node_statement.py b/tests/functional/r/regression_02/regression_node_statement.py index bd982480be..668ff05893 100644 --- a/tests/functional/r/regression_02/regression_node_statement.py +++ b/tests/functional/r/regression_02/regression_node_statement.py @@ -1,5 +1,5 @@ """Test to see we don't crash on this code in pandas. -See: https://github.com/pandas-dev/pandas/blob/master/pandas/core/arrays/sparse/array.py +See: https://github.com/pandas-dev/pandas/blob/main/pandas/core/arrays/sparse/array.py Code written by Guido van Rossum here: https://github.com/python/typing/issues/684""" # pylint: disable=no-member, redefined-builtin, invalid-name, missing-class-docstring diff --git a/tests/functional/r/regression_02/regression_node_statement_two.py b/tests/functional/r/regression_02/regression_node_statement_two.py index ad4afd9472..2c50ab0f44 100644 --- a/tests/functional/r/regression_02/regression_node_statement_two.py +++ b/tests/functional/r/regression_02/regression_node_statement_two.py @@ -1,5 +1,5 @@ """Test to see we don't crash on this code in pandas. -See: https://github.com/pandas-dev/pandas/blob/master/pandas/core/indexes/period.py +See: https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexes/period.py Reported in https://github.com/pylint-dev/pylint/issues/5382 """ # pylint: disable=missing-function-docstring, missing-class-docstring, unused-argument diff --git a/tests/functional/s/singledispatch/singledispatch_method.py b/tests/functional/s/singledispatch/singledispatch_method.py new file mode 100644 index 0000000000..789abc1f84 --- /dev/null +++ b/tests/functional/s/singledispatch/singledispatch_method.py @@ -0,0 +1,72 @@ +"""Tests for singledispatch-method""" +# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods + + +from functools import singledispatch + + +class Board1: + @singledispatch # [singledispatch-method] + def convert_position(self, position): + pass + + @convert_position.register # [singledispatch-method] + def _(self, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + def _(self, position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +class Board2: + @singledispatch # [singledispatch-method] + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" + + + +class Board3: + @singledispatch # [singledispatch-method] + @staticmethod + def convert_position(position): + pass + + @convert_position.register # [singledispatch-method] + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +# Do not emit `singledispatch-method`: +@singledispatch +def convert_position(position): + print(position) + +@convert_position.register +def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + +@convert_position.register +def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch/singledispatch_method.txt b/tests/functional/s/singledispatch/singledispatch_method.txt index c747fb6a84..794355121f 100644 --- a/tests/functional/s/singledispatch/singledispatch_method.txt +++ b/tests/functional/s/singledispatch/singledispatch_method.txt @@ -1,3 +1,9 @@ -singledispatch-method:26:5:26:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH -singledispatch-method:31:5:31:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE -singledispatch-method:37:5:37:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:9:5:9:19:Board1.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:13:5:13:30:Board1._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:18:5:18:30:Board1._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:24:5:24:19:Board2.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:29:5:29:30:Board2._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:35:5:35:30:Board2._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:43:5:43:19:Board3.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:48:5:48:30:Board3._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:54:5:54:30:Board3._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatch/singledispatch_method_py37.py b/tests/functional/s/singledispatch/singledispatch_method_py37.py deleted file mode 100644 index c9269f7bf1..0000000000 --- a/tests/functional/s/singledispatch/singledispatch_method_py37.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Tests for singledispatch-method""" -# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods - - -from functools import singledispatch - - -class Board: - @singledispatch # [singledispatch-method] - @classmethod - def convert_position(cls, position): - pass - - @convert_position.register # [singledispatch-method] - @classmethod - def _(cls, position: str) -> tuple: - position_a, position_b = position.split(",") - return (int(position_a), int(position_b)) - - @convert_position.register # [singledispatch-method] - @classmethod - def _(cls, position: tuple) -> str: - return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch/singledispatch_method_py37.rc b/tests/functional/s/singledispatch/singledispatch_method_py37.rc deleted file mode 100644 index 77eb3be645..0000000000 --- a/tests/functional/s/singledispatch/singledispatch_method_py37.rc +++ /dev/null @@ -1,2 +0,0 @@ -[main] -py-version=3.7 diff --git a/tests/functional/s/singledispatch/singledispatch_method_py37.txt b/tests/functional/s/singledispatch/singledispatch_method_py37.txt deleted file mode 100644 index 111bc47225..0000000000 --- a/tests/functional/s/singledispatch/singledispatch_method_py37.txt +++ /dev/null @@ -1,3 +0,0 @@ -singledispatch-method:9:5:9:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH -singledispatch-method:14:5:14:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE -singledispatch-method:20:5:20:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatch/singledispatch_method_py38.py b/tests/functional/s/singledispatch/singledispatch_method_py38.py deleted file mode 100644 index ad8eea1dd8..0000000000 --- a/tests/functional/s/singledispatch/singledispatch_method_py38.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Tests for singledispatch-method""" -# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods - - -from functools import singledispatch, singledispatchmethod - - -class BoardRight: - @singledispatchmethod - @classmethod - def convert_position(cls, position): - pass - - @convert_position.register - @classmethod - def _(cls, position: str) -> tuple: - position_a, position_b = position.split(",") - return (int(position_a), int(position_b)) - - @convert_position.register - def _(self, position: tuple) -> str: - return f"{position[0]},{position[1]}" - - -class Board: - @singledispatch # [singledispatch-method] - @classmethod - def convert_position(cls, position): - pass - - @convert_position.register # [singledispatch-method] - @classmethod - def _(cls, position: str) -> tuple: - position_a, position_b = position.split(",") - return (int(position_a), int(position_b)) - - @convert_position.register # [singledispatch-method] - @classmethod - def _(cls, position: tuple) -> str: - return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch/singledispatch_method_py38.txt b/tests/functional/s/singledispatch/singledispatch_method_py38.txt deleted file mode 100644 index c747fb6a84..0000000000 --- a/tests/functional/s/singledispatch/singledispatch_method_py38.txt +++ /dev/null @@ -1,3 +0,0 @@ -singledispatch-method:26:5:26:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH -singledispatch-method:31:5:31:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE -singledispatch-method:37:5:37:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatch/singledispatchmethod_function.py b/tests/functional/s/singledispatch/singledispatchmethod_function.py new file mode 100644 index 0000000000..1a3bf8db9b --- /dev/null +++ b/tests/functional/s/singledispatch/singledispatchmethod_function.py @@ -0,0 +1,71 @@ +"""Tests for singledispatchmethod-function""" +# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods + + +from functools import singledispatchmethod + + +# Emit `singledispatchmethod-function` when functions are decorated with `singledispatchmethod` +@singledispatchmethod # [singledispatchmethod-function] +def convert_position2(position): + print(position) + +@convert_position2.register # [singledispatchmethod-function] +def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + +@convert_position2.register # [singledispatchmethod-function] +def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +class Board1: + @singledispatchmethod + def convert_position(self, position): + pass + + @convert_position.register + def _(self, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + def _(self, position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +class Board2: + @singledispatchmethod + @staticmethod + def convert_position(position): + pass + + @convert_position.register + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +class Board3: + @singledispatchmethod + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch/singledispatchmethod_function.txt b/tests/functional/s/singledispatch/singledispatchmethod_function.txt new file mode 100644 index 0000000000..c25f70cf53 --- /dev/null +++ b/tests/functional/s/singledispatch/singledispatchmethod_function.txt @@ -0,0 +1,3 @@ +singledispatchmethod-function:9:1:9:21:convert_position2:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:HIGH +singledispatchmethod-function:13:1:13:27:_:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:INFERENCE +singledispatchmethod-function:18:1:18:27:_:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:INFERENCE diff --git a/tests/functional/s/singledispatch/singledispatchmethod_function_py38.py b/tests/functional/s/singledispatch/singledispatchmethod_function_py38.py deleted file mode 100644 index ef44f71c15..0000000000 --- a/tests/functional/s/singledispatch/singledispatchmethod_function_py38.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Tests for singledispatchmethod-function""" -# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods - - -from functools import singledispatch, singledispatchmethod - - -class BoardRight: - @singledispatch - @staticmethod - def convert_position(position): - pass - - @convert_position.register - @staticmethod - def _(position: str) -> tuple: - position_a, position_b = position.split(",") - return (int(position_a), int(position_b)) - - @convert_position.register - @staticmethod - def _(position: tuple) -> str: - return f"{position[0]},{position[1]}" - - -class Board: - @singledispatchmethod # [singledispatchmethod-function] - @staticmethod - def convert_position(position): - pass - - @convert_position.register # [singledispatchmethod-function] - @staticmethod - def _(position: str) -> tuple: - position_a, position_b = position.split(",") - return (int(position_a), int(position_b)) - - @convert_position.register # [singledispatchmethod-function] - @staticmethod - def _(position: tuple) -> str: - return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch/singledispatchmethod_function_py38.txt b/tests/functional/s/singledispatch/singledispatchmethod_function_py38.txt deleted file mode 100644 index 4c236b3466..0000000000 --- a/tests/functional/s/singledispatch/singledispatchmethod_function_py38.txt +++ /dev/null @@ -1,3 +0,0 @@ -singledispatchmethod-function:27:5:27:25:Board.convert_position:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:HIGH -singledispatchmethod-function:32:5:32:30:Board._:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:INFERENCE -singledispatchmethod-function:38:5:38:30:Board._:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:INFERENCE diff --git a/tests/functional/t/too/too_few_public_methods_37.py b/tests/functional/t/too/too_few_public_methods_37.py index db9c9f171e..3d3a12517b 100644 --- a/tests/functional/t/too/too_few_public_methods_37.py +++ b/tests/functional/t/too/too_few_public_methods_37.py @@ -8,6 +8,9 @@ import typing from dataclasses import dataclass +import attrs # pylint: disable=import-error +from attrs import define, frozen # pylint: disable=import-error + @dataclasses.dataclass class ScheduledTxSearchModel: @@ -40,3 +43,27 @@ class Point: def to_array(self): """Convert to a NumPy array `np.array((x, y, z))`.""" return self.attr1 + + +@define +class AttrsBarePoint: + x: float + y: float + + +@frozen +class AttrsBareFrozenPoint: + x: float + y: float + + +@attrs.define +class AttrsQualifiedPoint: + x: float + y: float + + +@attrs.frozen +class AttrsQualifiedFrozenPoint: + x: float + y: float diff --git a/tests/functional/t/trailing_comma_tuple.py b/tests/functional/t/trailing_comma_tuple.py index de60184cab..8effe475ec 100644 --- a/tests/functional/t/trailing_comma_tuple.py +++ b/tests/functional/t/trailing_comma_tuple.py @@ -48,3 +48,13 @@ def some_other_func(): JJJ = some_func(0, 0) + +# pylint: disable-next=trailing-comma-tuple +AAA = 1, +BBB = "aaaa", # [trailing-comma-tuple] +# pylint: disable=trailing-comma-tuple +CCC="aaa", +III = some_func(0, + 0), +# pylint: enable=trailing-comma-tuple +FFF=['f'], # [trailing-comma-tuple] diff --git a/tests/functional/t/trailing_comma_tuple.txt b/tests/functional/t/trailing_comma_tuple.txt index 9984e5afb7..d65ad72ed8 100644 --- a/tests/functional/t/trailing_comma_tuple.txt +++ b/tests/functional/t/trailing_comma_tuple.txt @@ -1,9 +1,11 @@ -trailing-comma-tuple:3:0:None:None::Disallow trailing comma tuple:UNDEFINED -trailing-comma-tuple:4:0:None:None::Disallow trailing comma tuple:UNDEFINED -trailing-comma-tuple:5:0:None:None::Disallow trailing comma tuple:UNDEFINED -trailing-comma-tuple:6:0:None:None::Disallow trailing comma tuple:UNDEFINED -trailing-comma-tuple:31:0:None:None::Disallow trailing comma tuple:UNDEFINED -trailing-comma-tuple:34:0:None:None::Disallow trailing comma tuple:UNDEFINED -trailing-comma-tuple:38:0:None:None::Disallow trailing comma tuple:UNDEFINED -trailing-comma-tuple:41:0:None:None::Disallow trailing comma tuple:UNDEFINED -trailing-comma-tuple:47:0:None:None::Disallow trailing comma tuple:UNDEFINED +trailing-comma-tuple:3:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:4:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:5:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:6:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:31:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:34:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:38:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:41:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:47:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:54:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:60:0:None:None::Disallow trailing comma tuple:HIGH diff --git a/tests/functional/t/trailing_comma_tuple_9608.py b/tests/functional/t/trailing_comma_tuple_9608.py new file mode 100644 index 0000000000..6ca408c2b9 --- /dev/null +++ b/tests/functional/t/trailing_comma_tuple_9608.py @@ -0,0 +1,24 @@ +"""Check trailing comma tuple optimization.""" +# pylint: disable=missing-docstring + +AAA = 1, +BBB = "aaaa", +CCC="aaa", +FFF=['f'], + +def some_func(first, second): + if first: + return first, + if second: + return (first, second,) + return first, second, + +#pylint:enable = trailing-comma-tuple +AAA = 1, # [trailing-comma-tuple] +BBB = "aaaa", # [trailing-comma-tuple] +# pylint: disable=trailing-comma-tuple +CCC="aaa", +III = some_func(0, + 0), +# pylint: enable=trailing-comma-tuple +FFF=['f'], # [trailing-comma-tuple] diff --git a/tests/functional/t/trailing_comma_tuple_9608.rc b/tests/functional/t/trailing_comma_tuple_9608.rc new file mode 100644 index 0000000000..80157090eb --- /dev/null +++ b/tests/functional/t/trailing_comma_tuple_9608.rc @@ -0,0 +1,5 @@ +[MAIN] +disable=trailing-comma-tuple + +[testoptions] +exclude_from_minimal_messages_config=True diff --git a/tests/functional/t/trailing_comma_tuple_9608.txt b/tests/functional/t/trailing_comma_tuple_9608.txt new file mode 100644 index 0000000000..b6ea91784b --- /dev/null +++ b/tests/functional/t/trailing_comma_tuple_9608.txt @@ -0,0 +1,3 @@ +trailing-comma-tuple:17:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:18:0:None:None::Disallow trailing comma tuple:HIGH +trailing-comma-tuple:24:0:None:None::Disallow trailing comma tuple:HIGH diff --git a/tests/functional/t/type/typevar_naming_style_py312.py b/tests/functional/t/type/typevar_naming_style_py312.py index 378290219f..9f04080c0d 100644 --- a/tests/functional/t/type/typevar_naming_style_py312.py +++ b/tests/functional/t/type/typevar_naming_style_py312.py @@ -1,4 +1,10 @@ """PEP 695 generic typing nodes""" +from collections.abc import Sequence + type Point[T] = tuple[T, ...] type Point[t] = tuple[t, ...] # [invalid-name] + +# Don't report typevar-name-incorrect-variance for type parameter +# The variance is determined by the type checker +type Array[T_co] = Sequence[T_co] diff --git a/tests/functional/t/type/typevar_naming_style_py312.txt b/tests/functional/t/type/typevar_naming_style_py312.txt index babe2a57bb..239378379b 100644 --- a/tests/functional/t/type/typevar_naming_style_py312.txt +++ b/tests/functional/t/type/typevar_naming_style_py312.txt @@ -1 +1 @@ -invalid-name:4:11:4:12::"Type variable name ""t"" doesn't conform to predefined naming style":HIGH +invalid-name:6:11:6:12::"Type variable name ""t"" doesn't conform to predefined naming style":HIGH diff --git a/tests/functional/u/undefined/undefined_variable.txt b/tests/functional/u/undefined/undefined_variable.txt index b41f9ae46f..ea707c910a 100644 --- a/tests/functional/u/undefined/undefined_variable.txt +++ b/tests/functional/u/undefined/undefined_variable.txt @@ -27,7 +27,7 @@ undefined-variable:166:4:166:13::Undefined variable 'unicode_2':UNDEFINED undefined-variable:171:4:171:13::Undefined variable 'unicode_3':UNDEFINED undefined-variable:226:25:226:37:LambdaClass4.:Undefined variable 'LambdaClass4':UNDEFINED undefined-variable:234:25:234:37:LambdaClass5.:Undefined variable 'LambdaClass5':UNDEFINED -used-before-assignment:255:26:255:34:func_should_fail:Using variable 'datetime' before assignment:CONTROL_FLOW +used-before-assignment:255:26:255:34:func_should_fail:Using variable 'datetime' before assignment:INFERENCE undefined-variable:291:18:291:24:not_using_loop_variable_accordingly:Undefined variable 'iteree':UNDEFINED undefined-variable:308:27:308:28:undefined_annotation:Undefined variable 'x':UNDEFINED used-before-assignment:309:7:309:8:undefined_annotation:Using variable 'x' before assignment:HIGH diff --git a/tests/functional/u/undefined/undefined_variable_py38.txt b/tests/functional/u/undefined/undefined_variable_py38.txt index 1674707a57..5a3533dc93 100644 --- a/tests/functional/u/undefined/undefined_variable_py38.txt +++ b/tests/functional/u/undefined/undefined_variable_py38.txt @@ -7,4 +7,4 @@ undefined-variable:106:6:106:19::Undefined variable 'else_assign_2':INFERENCE used-before-assignment:141:10:141:16:type_annotation_used_improperly_after_comprehension:Using variable 'my_int' before assignment:HIGH used-before-assignment:148:10:148:16:type_annotation_used_improperly_after_comprehension_2:Using variable 'my_int' before assignment:HIGH used-before-assignment:186:9:186:10::Using variable 'z' before assignment:HIGH -used-before-assignment:193:6:193:19::Using variable 'NEVER_DEFINED' before assignment:CONTROL_FLOW +used-before-assignment:193:6:193:19::Using variable 'NEVER_DEFINED' before assignment:INFERENCE diff --git a/tests/functional/u/used/used_before_assignment.py b/tests/functional/u/used/used_before_assignment.py index 16f33939cb..c706af043f 100644 --- a/tests/functional/u/used/used_before_assignment.py +++ b/tests/functional/u/used/used_before_assignment.py @@ -1,6 +1,7 @@ """Miscellaneous used-before-assignment cases""" # pylint: disable=consider-using-f-string, missing-function-docstring import datetime +import sys MSG = "hello %s" % MSG # [used-before-assignment] @@ -60,7 +61,7 @@ def redefine_time_import_with_global(): pass else: VAR4 = False -if VAR4: # [used-before-assignment] +if VAR4: # [possibly-used-before-assignment] pass if FALSE: @@ -70,7 +71,7 @@ def redefine_time_import_with_global(): VAR5 = True else: VAR5 = True -if VAR5: +if VAR5: # [possibly-used-before-assignment] pass if FALSE: @@ -116,7 +117,13 @@ def redefine_time_import_with_global(): VAR11 = num if VAR11: VAR12 = False -print(VAR12) +print(VAR12) # [possibly-used-before-assignment] + +if input("This tests terminating functions: "): + sys.exit() +else: + VAR13 = 1 +print(VAR13) def turn_on2(**kwargs): """https://github.com/pylint-dev/pylint/issues/7873""" @@ -180,3 +187,21 @@ def give_me_none(): class T: # pylint: disable=invalid-name, too-few-public-methods, undefined-variable '''Issue #8754, no crash from unexpected assignment between attribute and variable''' T.attr = attr + + +if outer(): + NOT_ALWAYS_DEFINED = True +print(NOT_ALWAYS_DEFINED) # [used-before-assignment] + + +def inner_if_continues_outer_if_has_no_other_statements(): + for i in range(5): + if isinstance(i, int): + # Testing no assignment here, before the inner if + if i % 2 == 0: + order = None + else: + continue + else: + order = None + print(order) diff --git a/tests/functional/u/used/used_before_assignment.txt b/tests/functional/u/used/used_before_assignment.txt index 37b25ab49b..6f97f4324c 100644 --- a/tests/functional/u/used/used_before_assignment.txt +++ b/tests/functional/u/used/used_before_assignment.txt @@ -1,12 +1,15 @@ -used-before-assignment:5:19:5:22::Using variable 'MSG' before assignment:HIGH -used-before-assignment:7:20:7:24::Using variable 'MSG2' before assignment:HIGH -used-before-assignment:10:4:10:9:outer:Using variable 'inner' before assignment:HIGH -used-before-assignment:19:20:19:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH -used-before-assignment:23:0:23:9::Using variable 'calculate' before assignment:HIGH -used-before-assignment:31:10:31:14:redefine_time_import:Using variable 'time' before assignment:HIGH -used-before-assignment:45:3:45:7::Using variable 'VAR2' before assignment:CONTROL_FLOW -used-before-assignment:63:3:63:7::Using variable 'VAR4' before assignment:CONTROL_FLOW -used-before-assignment:78:3:78:7::Using variable 'VAR6' before assignment:CONTROL_FLOW -used-before-assignment:113:6:113:11::Using variable 'VAR10' before assignment:CONTROL_FLOW -used-before-assignment:144:10:144:14::Using variable 'SALE' before assignment:CONTROL_FLOW -used-before-assignment:176:10:176:18::Using variable 'ALL_DONE' before assignment:CONTROL_FLOW +used-before-assignment:6:19:6:22::Using variable 'MSG' before assignment:HIGH +used-before-assignment:8:20:8:24::Using variable 'MSG2' before assignment:HIGH +used-before-assignment:11:4:11:9:outer:Using variable 'inner' before assignment:HIGH +used-before-assignment:20:20:20:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH +used-before-assignment:24:0:24:9::Using variable 'calculate' before assignment:HIGH +used-before-assignment:32:10:32:14:redefine_time_import:Using variable 'time' before assignment:HIGH +used-before-assignment:46:3:46:7::Using variable 'VAR2' before assignment:INFERENCE +possibly-used-before-assignment:64:3:64:7::Possibly using variable 'VAR4' before assignment:INFERENCE +possibly-used-before-assignment:74:3:74:7::Possibly using variable 'VAR5' before assignment:INFERENCE +used-before-assignment:79:3:79:7::Using variable 'VAR6' before assignment:INFERENCE +used-before-assignment:114:6:114:11::Using variable 'VAR10' before assignment:INFERENCE +possibly-used-before-assignment:120:6:120:11::Possibly using variable 'VAR12' before assignment:CONTROL_FLOW +used-before-assignment:151:10:151:14::Using variable 'SALE' before assignment:INFERENCE +used-before-assignment:183:10:183:18::Using variable 'ALL_DONE' before assignment:INFERENCE +used-before-assignment:194:6:194:24::Using variable 'NOT_ALWAYS_DEFINED' before assignment:INFERENCE diff --git a/tests/functional/u/used/used_before_assignment_else_return.py b/tests/functional/u/used/used_before_assignment_else_return.py index 8dcd21337d..5129f4e65b 100644 --- a/tests/functional/u/used/used_before_assignment_else_return.py +++ b/tests/functional/u/used/used_before_assignment_else_return.py @@ -1,5 +1,5 @@ """If the else block returns, it is generally safe to rely on assignments in the except.""" -# pylint: disable=missing-function-docstring, invalid-name +# pylint: disable=missing-function-docstring, invalid-name, useless-return import sys def valid(): diff --git a/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.txt b/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.txt index 5f2be351dd..4cac0253f9 100644 --- a/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.txt +++ b/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.txt @@ -1,7 +1,7 @@ used-before-assignment:16:14:16:29:function:Using variable 'failure_message' before assignment:CONTROL_FLOW used-before-assignment:120:10:120:13:func_invalid1:Using variable 'msg' before assignment:CONTROL_FLOW used-before-assignment:131:10:131:13:func_invalid2:Using variable 'msg' before assignment:CONTROL_FLOW -used-before-assignment:150:10:150:13:func_invalid3:Using variable 'msg' before assignment:CONTROL_FLOW +used-before-assignment:150:10:150:13:func_invalid3:Using variable 'msg' before assignment:INFERENCE used-before-assignment:163:10:163:13:func_invalid4:Using variable 'msg' before assignment:CONTROL_FLOW used-before-assignment:175:10:175:13:func_invalid5:Using variable 'msg' before assignment:CONTROL_FLOW used-before-assignment:187:10:187:13:func_invalid6:Using variable 'msg' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/used/used_before_assignment_issue1081.py b/tests/functional/u/used/used_before_assignment_issue1081.py index d478bdeecc..cd317671f1 100644 --- a/tests/functional/u/used/used_before_assignment_issue1081.py +++ b/tests/functional/u/used/used_before_assignment_issue1081.py @@ -16,7 +16,7 @@ def used_before_assignment_2(a): def used_before_assignment_3(a): - if x == a: # [used-before-assignment] + if x == a: # [possibly-used-before-assignment] if x > 3: x = 2 # [redefined-outer-name] diff --git a/tests/functional/u/used/used_before_assignment_issue1081.txt b/tests/functional/u/used/used_before_assignment_issue1081.txt index 857c4826ba..81b38f17dc 100644 --- a/tests/functional/u/used/used_before_assignment_issue1081.txt +++ b/tests/functional/u/used/used_before_assignment_issue1081.txt @@ -2,6 +2,6 @@ used-before-assignment:7:7:7:8:used_before_assignment_1:Using variable 'x' befor redefined-outer-name:8:12:8:13:used_before_assignment_1:Redefining name 'x' from outer scope (line 3):UNDEFINED used-before-assignment:13:7:13:8:used_before_assignment_2:Using variable 'x' before assignment:HIGH redefined-outer-name:15:4:15:5:used_before_assignment_2:Redefining name 'x' from outer scope (line 3):UNDEFINED -used-before-assignment:19:7:19:8:used_before_assignment_3:Using variable 'x' before assignment:HIGH +possibly-used-before-assignment:19:7:19:8:used_before_assignment_3:Possibly using variable 'x' before assignment:CONTROL_FLOW redefined-outer-name:21:12:21:13:used_before_assignment_3:Redefining name 'x' from outer scope (line 3):UNDEFINED redefined-outer-name:30:4:30:5:not_used_before_assignment_2:Redefining name 'x' from outer scope (line 3):UNDEFINED diff --git a/tests/functional/u/used/used_before_assignment_issue2615.txt b/tests/functional/u/used/used_before_assignment_issue2615.txt index 567f562305..419770fcb5 100644 --- a/tests/functional/u/used/used_before_assignment_issue2615.txt +++ b/tests/functional/u/used/used_before_assignment_issue2615.txt @@ -1,3 +1,3 @@ -used-before-assignment:12:14:12:17:main:Using variable 'res' before assignment:CONTROL_FLOW +used-before-assignment:12:14:12:17:main:Using variable 'res' before assignment:INFERENCE used-before-assignment:30:18:30:35:nested_except_blocks:Using variable 'more_bad_division' before assignment:CONTROL_FLOW -used-before-assignment:31:18:31:21:nested_except_blocks:Using variable 'res' before assignment:CONTROL_FLOW +used-before-assignment:31:18:31:21:nested_except_blocks:Using variable 'res' before assignment:INFERENCE diff --git a/tests/functional/u/used/used_before_assignment_issue626.txt b/tests/functional/u/used/used_before_assignment_issue626.txt index 3d0e572463..1ee575ba3e 100644 --- a/tests/functional/u/used/used_before_assignment_issue626.txt +++ b/tests/functional/u/used/used_before_assignment_issue626.txt @@ -1,5 +1,5 @@ unused-variable:5:4:6:12:main1:Unused variable 'e':UNDEFINED -used-before-assignment:8:10:8:11:main1:Using variable 'e' before assignment:CONTROL_FLOW +used-before-assignment:8:10:8:11:main1:Using variable 'e' before assignment:HIGH unused-variable:21:4:22:12:main3:Unused variable 'e':UNDEFINED unused-variable:31:4:32:12:main4:Unused variable 'e':UNDEFINED -used-before-assignment:44:10:44:11:main4:Using variable 'e' before assignment:CONTROL_FLOW +used-before-assignment:44:10:44:11:main4:Using variable 'e' before assignment:HIGH diff --git a/tests/functional/u/used/used_before_assignment_postponed_evaluation.txt b/tests/functional/u/used/used_before_assignment_postponed_evaluation.txt index 88a9587369..15681c6dba 100644 --- a/tests/functional/u/used/used_before_assignment_postponed_evaluation.txt +++ b/tests/functional/u/used/used_before_assignment_postponed_evaluation.txt @@ -1 +1 @@ -used-before-assignment:10:6:10:9::Using variable 'var' before assignment:CONTROL_FLOW +used-before-assignment:10:6:10:9::Using variable 'var' before assignment:INFERENCE diff --git a/tests/functional/u/used/used_before_assignment_py311.py b/tests/functional/u/used/used_before_assignment_py311.py new file mode 100644 index 0000000000..2e46ff5fd6 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_py311.py @@ -0,0 +1,21 @@ +"""assert_never() introduced in 3.11""" +from enum import Enum +from typing import assert_never + + +class MyEnum(Enum): + """A lovely enum.""" + VAL1 = 1 + VAL2 = 2 + + +def do_thing(val: MyEnum) -> None: + """Do a thing.""" + if val is MyEnum.VAL1: + note = 'got 1' + elif val is MyEnum.VAL2: + note = 'got 2' + else: + assert_never(val) + + print('Note:', note) diff --git a/tests/functional/u/used/used_before_assignment_py311.rc b/tests/functional/u/used/used_before_assignment_py311.rc new file mode 100644 index 0000000000..56e6770585 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_py311.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.11 diff --git a/tests/functional/u/used/used_before_assignment_scoping.txt b/tests/functional/u/used/used_before_assignment_scoping.txt index 32b6d3e1bc..007f59a27c 100644 --- a/tests/functional/u/used/used_before_assignment_scoping.txt +++ b/tests/functional/u/used/used_before_assignment_scoping.txt @@ -1,2 +1,2 @@ -used-before-assignment:10:13:10:21:func_two:Using variable 'datetime' before assignment:CONTROL_FLOW -used-before-assignment:16:12:16:20:func:Using variable 'datetime' before assignment:CONTROL_FLOW +used-before-assignment:10:13:10:21:func_two:Using variable 'datetime' before assignment:INFERENCE +used-before-assignment:16:12:16:20:func:Using variable 'datetime' before assignment:INFERENCE diff --git a/tests/functional/u/used/used_before_assignment_typing.py b/tests/functional/u/used/used_before_assignment_typing.py index 9ec040ff62..9316285cda 100644 --- a/tests/functional/u/used/used_before_assignment_typing.py +++ b/tests/functional/u/used/used_before_assignment_typing.py @@ -167,32 +167,32 @@ class ConditionalImportGuardedWhenUsed: # pylint: disable=too-few-public-method class TypeCheckingMultiBranch: # pylint: disable=too-few-public-methods,unused-variable """Test for defines in TYPE_CHECKING if/elif/else branching""" - def defined_in_elif_branch(self) -> calendar.Calendar: - print(bisect) + def defined_in_elif_branch(self) -> calendar.Calendar: # [possibly-used-before-assignment] + print(bisect) # [possibly-used-before-assignment] return calendar.Calendar() def defined_in_else_branch(self) -> urlopen: - print(zoneinfo) + print(zoneinfo) # [used-before-assignment] print(pprint()) print(collections()) return urlopen - def defined_in_nested_if_else(self) -> heapq: + def defined_in_nested_if_else(self) -> heapq: # [possibly-used-before-assignment] print(heapq) return heapq - def defined_in_try_except(self) -> array: - print(types) - print(copy) - print(numbers) + def defined_in_try_except(self) -> array: # [used-before-assignment] + print(types) # [used-before-assignment] + print(copy) # [used-before-assignment] + print(numbers) # [used-before-assignment] return array - def defined_in_loops(self) -> json: - print(email) - print(mailbox) - print(mimetypes) + def defined_in_loops(self) -> json: # [used-before-assignment] + print(email) # [used-before-assignment] + print(mailbox) # [used-before-assignment] + print(mimetypes) # [used-before-assignment] return json - def defined_in_with(self) -> base64: - print(binascii) + def defined_in_with(self) -> base64: # [used-before-assignment] + print(binascii) # [used-before-assignment] return base64 diff --git a/tests/functional/u/used/used_before_assignment_typing.txt b/tests/functional/u/used/used_before_assignment_typing.txt index 12794f0e95..24900a3f95 100644 --- a/tests/functional/u/used/used_before_assignment_typing.txt +++ b/tests/functional/u/used/used_before_assignment_typing.txt @@ -1,5 +1,19 @@ undefined-variable:69:21:69:28:MyClass.incorrect_typing_method:Undefined variable 'MyClass':UNDEFINED undefined-variable:74:26:74:33:MyClass.incorrect_nested_typing_method:Undefined variable 'MyClass':UNDEFINED undefined-variable:79:20:79:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED -used-before-assignment:140:35:140:39:MyFourthClass.is_close:Using variable 'math' before assignment:CONTROL_FLOW -used-before-assignment:153:20:153:28:VariableAnnotationsGuardedByTypeChecking:Using variable 'datetime' before assignment:CONTROL_FLOW +used-before-assignment:140:35:140:39:MyFourthClass.is_close:Using variable 'math' before assignment:INFERENCE +used-before-assignment:153:20:153:28:VariableAnnotationsGuardedByTypeChecking:Using variable 'datetime' before assignment:INFERENCE +possibly-used-before-assignment:170:40:170:48:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'calendar' before assignment:INFERENCE +possibly-used-before-assignment:171:14:171:20:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'bisect' before assignment:INFERENCE +used-before-assignment:175:14:175:22:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'zoneinfo' before assignment:INFERENCE +possibly-used-before-assignment:180:43:180:48:TypeCheckingMultiBranch.defined_in_nested_if_else:Possibly using variable 'heapq' before assignment:INFERENCE +used-before-assignment:184:39:184:44:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'array' before assignment:INFERENCE +used-before-assignment:185:14:185:19:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'types' before assignment:INFERENCE +used-before-assignment:186:14:186:18:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'copy' before assignment:INFERENCE +used-before-assignment:187:14:187:21:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'numbers' before assignment:INFERENCE +used-before-assignment:190:34:190:38:TypeCheckingMultiBranch.defined_in_loops:Using variable 'json' before assignment:INFERENCE +used-before-assignment:191:14:191:19:TypeCheckingMultiBranch.defined_in_loops:Using variable 'email' before assignment:INFERENCE +used-before-assignment:192:14:192:21:TypeCheckingMultiBranch.defined_in_loops:Using variable 'mailbox' before assignment:INFERENCE +used-before-assignment:193:14:193:23:TypeCheckingMultiBranch.defined_in_loops:Using variable 'mimetypes' before assignment:INFERENCE +used-before-assignment:196:33:196:39:TypeCheckingMultiBranch.defined_in_with:Using variable 'base64' before assignment:INFERENCE +used-before-assignment:197:14:197:22:TypeCheckingMultiBranch.defined_in_with:Using variable 'binascii' before assignment:INFERENCE diff --git a/tests/functional/u/useless/useless_return.py b/tests/functional/u/useless/useless_return.py index e7537353ef..122f172f6f 100644 --- a/tests/functional/u/useless/useless_return.py +++ b/tests/functional/u/useless/useless_return.py @@ -13,3 +13,60 @@ def mymethod(self): # [useless-return] # These are not emitted def item_at(self): return None + + +def function2(parameter): # [useless-return] + if parameter: + pass + return + + +def function3(parameter): # [useless-return] + if parameter: + pass + else: + return + + +def function4(parameter): # [useless-return] + try: + parameter.do() + except RuntimeError: + parameter.other() + return + + +def function5(parameter): # [useless-return] + try: + parameter.do() + except RuntimeError: + return + + +def code_after_return(param): + try: + param.kaboom() + except RuntimeError: + param.other() + return + + param.something_else() + param.state = "good" + + +def code_after_else(obj): + if obj.k: + pass + else: + return + + obj.do() + + +def return_in_loop(obj): + for _ in range(10): + obj.do() + if obj.k: + return + + return diff --git a/tests/functional/u/useless/useless_return.txt b/tests/functional/u/useless/useless_return.txt index 035e951ab2..40213d6b90 100644 --- a/tests/functional/u/useless/useless_return.txt +++ b/tests/functional/u/useless/useless_return.txt @@ -1,2 +1,6 @@ useless-return:4:0:4:10:myfunc:Useless return at end of function or method:UNDEFINED useless-return:9:4:9:16:SomeClass.mymethod:Useless return at end of function or method:UNDEFINED +useless-return:18:0:18:13:function2:Useless return at end of function or method:UNDEFINED +useless-return:24:0:24:13:function3:Useless return at end of function or method:UNDEFINED +useless-return:31:0:31:13:function4:Useless return at end of function or method:UNDEFINED +useless-return:39:0:39:13:function5:Useless return at end of function or method:UNDEFINED diff --git a/tests/lint/test_pylinter.py b/tests/lint/test_pylinter.py index 1e4f40a002..bc12455354 100644 --- a/tests/lint/test_pylinter.py +++ b/tests/lint/test_pylinter.py @@ -10,7 +10,7 @@ from pytest import CaptureFixture -from pylint.lint.pylinter import PyLinter +from pylint.lint.pylinter import MANAGER, PyLinter from pylint.utils import FileState @@ -48,3 +48,23 @@ def test_crash_during_linting( assert len(files) == 1 assert "pylint-crash-20" in str(files[0]) assert any(m.symbol == "astroid-error" for m in linter.reporter.messages) + + +def test_open_pylinter_denied_modules(linter: PyLinter) -> None: + """Test PyLinter open() adds ignored modules to Astroid manager deny list.""" + MANAGER.module_denylist = {"mod1"} + try: + linter.config.ignored_modules = ["mod2", "mod3"] + linter.open() + assert MANAGER.module_denylist == {"mod1", "mod2", "mod3"} + finally: + MANAGER.module_denylist = set() + + +def test_open_pylinter_prefer_stubs(linter: PyLinter) -> None: + try: + linter.config.prefer_stubs = True + linter.open() + assert MANAGER.prefer_stubs + finally: + MANAGER.prefer_stubs = False diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py index 00e410b469..18305b73a8 100644 --- a/tests/lint/unittest_lint.py +++ b/tests/lint/unittest_lint.py @@ -1049,7 +1049,7 @@ def test_by_module_statement_value(initialized_linter: PyLinter) -> None: def test_finds_pyi_file() -> None: run = Run( - [join(REGRTEST_DATA_DIR, "pyi")], + ["--prefer-stubs=y", join(REGRTEST_DATA_DIR, "pyi")], exit=False, ) assert run.linter.current_file is not None @@ -1061,6 +1061,8 @@ def test_recursive_finds_pyi_file() -> None: [ "--recursive", "y", + "--prefer-stubs", + "y", join(REGRTEST_DATA_DIR, "pyi"), ], exit=False, @@ -1069,6 +1071,20 @@ def test_recursive_finds_pyi_file() -> None: assert run.linter.current_file.endswith("foo.pyi") +def test_no_false_positive_from_pyi_stub() -> None: + run = Run( + [ + "--recursive", + "y", + "--prefer-stubs", + "n", + join(REGRTEST_DATA_DIR, "uses_module_with_stub.py"), + ], + exit=False, + ) + assert not run.linter.stats.by_msg + + @pytest.mark.parametrize( "ignore_parameter,ignore_parameter_value", [ @@ -1171,7 +1187,7 @@ def test_globbing() -> None: def test_relative_imports(initialized_linter: PyLinter) -> None: - """Regression test for https://github.com/pylint-dev/pylint/issues/3651""" + """Regression test for https://github.com/pylint-dev/pylint/issues/3651.""" linter = initialized_linter with tempdir() as tmpdir: create_files(["x/y/__init__.py", "x/y/one.py", "x/y/two.py"], tmpdir) @@ -1204,7 +1220,8 @@ def test_relative_imports(initialized_linter: PyLinter) -> None: def test_import_sibling_module_from_namespace(initialized_linter: PyLinter) -> None: """If the parent directory above `namespace` is on sys.path, ensure that - modules under `namespace` can import each other without raising `import-error`.""" + modules under `namespace` can import each other without raising `import-error`. + """ linter = initialized_linter with tempdir() as tmpdir: create_files(["namespace/submodule1.py", "namespace/submodule2.py"]) @@ -1226,7 +1243,7 @@ def test_import_sibling_module_from_namespace(initialized_linter: PyLinter) -> N def test_lint_namespace_package_under_dir(initialized_linter: PyLinter) -> None: - """Regression test for https://github.com/pylint-dev/pylint/issues/1667""" + """Regression test for https://github.com/pylint-dev/pylint/issues/1667.""" linter = initialized_linter with tempdir(): create_files(["outer/namespace/__init__.py", "outer/namespace/module.py"]) @@ -1236,7 +1253,8 @@ def test_lint_namespace_package_under_dir(initialized_linter: PyLinter) -> None: def test_lint_namespace_package_under_dir_on_path(initialized_linter: PyLinter) -> None: """If the directory above a namespace package is on sys.path, - the namespace module under it is linted.""" + the namespace module under it is linted. + """ linter = initialized_linter with tempdir() as tmpdir: create_files(["namespace_on_path/submodule1.py"]) diff --git a/tests/primer/packages_to_prime.json b/tests/primer/packages_to_prime.json index b36a598ef0..a0d1b38c0c 100644 --- a/tests/primer/packages_to_prime.json +++ b/tests/primer/packages_to_prime.json @@ -34,7 +34,7 @@ }, "pandas": { "branch": "main", - "directories": ["pandas"], + "directories": ["pandas/core"], "pylint_additional_args": ["--ignore-patterns=\"test_"], "url": "https://github.com/pandas-dev/pandas" }, diff --git a/tests/pyreverse/test_diadefs.py b/tests/pyreverse/test_diadefs.py index cdcdea7c51..2b8bd5e324 100644 --- a/tests/pyreverse/test_diadefs.py +++ b/tests/pyreverse/test_diadefs.py @@ -160,7 +160,7 @@ def test_functional_relation_extraction( self, default_config: PyreverseConfig, get_project: GetProjectCallable ) -> None: """Functional test of relations extraction; - different classes possibly in different modules + different classes possibly in different modules. """ # XXX should be catching pyreverse environment problem but doesn't # pyreverse doesn't extract the relations but this test ok diff --git a/tests/pyreverse/test_main.py b/tests/pyreverse/test_main.py index 1721d89ccf..e8e46df2c1 100644 --- a/tests/pyreverse/test_main.py +++ b/tests/pyreverse/test_main.py @@ -59,7 +59,7 @@ def setup_path(request: SubRequest) -> Iterator[None]: @pytest.mark.usefixtures("setup_path") def test_project_root_in_sys_path() -> None: """Test the context manager adds the project root directory to sys.path. - This should happen when pyreverse is run from any directory + This should happen when pyreverse is run from any directory. """ with augmented_sys_path([discover_package_path(TEST_DATA_DIR, [])]): assert sys.path == [PROJECT_ROOT_DIR] diff --git a/tests/pyreverse/test_utils.py b/tests/pyreverse/test_utils.py index ef843fd294..6a6afc1b48 100644 --- a/tests/pyreverse/test_utils.py +++ b/tests/pyreverse/test_utils.py @@ -107,7 +107,7 @@ def test_get_annotation_label_of_return_type( @patch("astroid.nodes.NodeNG.infer", side_effect=astroid.InferenceError) def test_infer_node_1(mock_infer: Any, mock_get_annotation: Any) -> None: """Return set() when astroid.InferenceError is raised and an annotation has - not been returned + not been returned. """ mock_get_annotation.return_value = None node = astroid.extract_node("a: str = 'mystr'") @@ -120,7 +120,7 @@ def test_infer_node_1(mock_infer: Any, mock_get_annotation: Any) -> None: @patch("astroid.nodes.NodeNG.infer") def test_infer_node_2(mock_infer: Any, mock_get_annotation: Any) -> None: """Return set(node.infer()) when InferenceError is not raised and an - annotation has not been returned + annotation has not been returned. """ mock_get_annotation.return_value = None node = astroid.extract_node("a: str = 'mystr'") @@ -131,7 +131,7 @@ def test_infer_node_2(mock_infer: Any, mock_get_annotation: Any) -> None: def test_infer_node_3() -> None: """Return a set containing a nodes.ClassDef object when the attribute - has a type annotation + has a type annotation. """ node = astroid.extract_node( """ @@ -150,7 +150,7 @@ def __init__(self, component: Component): def test_infer_node_4() -> None: """Verify the label for an argument with a typehint of the type - nodes.Subscript + nodes.Subscript. """ node = astroid.extract_node( """ diff --git a/tests/regrtest_data/ignore_pattern/.hidden/module.py b/tests/regrtest_data/ignore_pattern/.hidden/module.py new file mode 100644 index 0000000000..21b405d8c2 --- /dev/null +++ b/tests/regrtest_data/ignore_pattern/.hidden/module.py @@ -0,0 +1 @@ +import os diff --git a/tests/regrtest_data/ignore_pattern/module.py b/tests/regrtest_data/ignore_pattern/module.py new file mode 100644 index 0000000000..21b405d8c2 --- /dev/null +++ b/tests/regrtest_data/ignore_pattern/module.py @@ -0,0 +1 @@ +import os diff --git a/tests/regrtest_data/pyi/foo.py b/tests/regrtest_data/pyi/foo.py new file mode 100644 index 0000000000..c49f18ee09 --- /dev/null +++ b/tests/regrtest_data/pyi/foo.py @@ -0,0 +1,2 @@ +def three_item_iterable(): + return [1, 2, 3] diff --git a/tests/regrtest_data/pyi/foo.pyi b/tests/regrtest_data/pyi/foo.pyi index c4e5bcc800..a84058c7c1 100644 --- a/tests/regrtest_data/pyi/foo.pyi +++ b/tests/regrtest_data/pyi/foo.pyi @@ -1 +1,4 @@ foo = 1 + +def three_item_iterable(): + ... diff --git a/tests/regrtest_data/uses_module_with_stub.py b/tests/regrtest_data/uses_module_with_stub.py new file mode 100644 index 0000000000..d7cbf63d9c --- /dev/null +++ b/tests/regrtest_data/uses_module_with_stub.py @@ -0,0 +1,5 @@ +"""If the stub is preferred over the .py, this might emit not-an-iterable""" +from pyi.foo import three_item_iterable + +for val in three_item_iterable(): + print(val) diff --git a/tests/reporters/unittest_reporting.py b/tests/reporters/unittest_reporting.py index 6273f70111..015401a057 100644 --- a/tests/reporters/unittest_reporting.py +++ b/tests/reporters/unittest_reporting.py @@ -84,7 +84,7 @@ def test_template_option_end_line(linter: PyLinter) -> None: def test_template_option_non_existing(linter: PyLinter) -> None: """Test the msg-template option with non-existent options. This makes sure that this option remains backwards compatible as new - parameters do not break on previous versions + parameters do not break on previous versions. """ output = StringIO() linter.reporter.out = output @@ -309,8 +309,7 @@ def test_multi_format_output(tmp_path: Path) -> None: def test_multi_reporter_independant_messages() -> None: - """Messages should not be modified by multiple reporters""" - + """Messages should not be modified by multiple reporters.""" check_message = "Not modified" class ReporterModify(BaseReporter): diff --git a/tests/test_check_parallel.py b/tests/test_check_parallel.py index 0ae3b1ae10..969fde3d82 100644 --- a/tests/test_check_parallel.py +++ b/tests/test_check_parallel.py @@ -263,7 +263,7 @@ def test_worker_check_single_file_no_checkers(self) -> None: assert stats.warning == 0 def test_linter_with_unpickleable_plugins_is_pickleable(self) -> None: - """The linter needs to be pickle-able in order to be passed between workers""" + """The linter needs to be pickle-able in order to be passed between workers.""" linter = PyLinter(reporter=Reporter()) # We load an extension that we know is not pickle-safe linter.load_plugin_modules(["pylint.extensions.overlapping_exceptions"]) @@ -479,7 +479,6 @@ def test_compare_workers_to_single_proc( This test becomes more important if we want to change how we parameterize the checkers, for example if we aim to batch the files across jobs. """ - # define the stats we expect to get back from the runs, these should only vary # with the number of files. expected_stats = LinterStats( @@ -572,7 +571,6 @@ def test_map_reduce(self, num_files: int, num_jobs: int, num_checkers: int) -> N Checks regression of https://github.com/pylint-dev/pylint/issues/4118 """ - # define the stats we expect to get back from the runs, these should only vary # with the number of files. file_infos = _gen_file_datas(num_files) diff --git a/tests/test_func.py b/tests/test_func.py index d74e7ac6f1..99805d160e 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -29,7 +29,7 @@ def exception_str( self: Exception, ex: Exception # pylint: disable=unused-argument ) -> str: """Function used to replace default __str__ method of exception instances - This function is not typed because it is legacy code + This function is not typed because it is legacy code. """ return f"in {ex.file}\n:: {', '.join(ex.args)}" # type: ignore[attr-defined] # Defined in the caller @@ -44,8 +44,7 @@ class LintTestUsingModule: output: str | None = None def _test_functionality(self) -> None: - if self.module: - tocheck = [self.package + "." + self.module] + tocheck = [self.package + "." + self.module] if self.module else [] if self.depends: tocheck += [ self.package + f".{name.replace('.py', '')}" for name, _ in self.depends diff --git a/tests/test_functional.py b/tests/test_functional.py index ef0a373def..13087cfd6b 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -7,13 +7,16 @@ from __future__ import annotations import sys +from collections.abc import Iterator from pathlib import Path +from typing import TYPE_CHECKING import pytest from _pytest.config import Config from pylint import testutils from pylint.constants import PY312_PLUS +from pylint.lint.pylinter import MANAGER from pylint.testutils import UPDATE_FILE, UPDATE_OPTION from pylint.testutils.functional import ( FunctionalTestFile, @@ -22,6 +25,9 @@ ) from pylint.utils import HAS_ISORT_5 +if TYPE_CHECKING: + from pylint.lint import PyLinter + FUNCTIONAL_DIR = Path(__file__).parent.resolve() / "functional" @@ -40,6 +46,15 @@ ] +@pytest.fixture +def revert_stateful_config_changes(linter: PyLinter) -> Iterator[PyLinter]: + yield linter + # Revert any stateful configuration changes. + MANAGER.brain["module_denylist"] = set() + MANAGER.brain["prefer_stubs"] = False + + +@pytest.mark.usefixtures("revert_stateful_config_changes") @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES) def test_functional(test_file: FunctionalTestFile, pytestconfig: Config) -> None: __tracebackhide__ = True # pylint: disable=unused-variable diff --git a/tests/test_regr.py b/tests/test_regr.py index 19fc009a67..850dfa56e8 100644 --- a/tests/test_regr.py +++ b/tests/test_regr.py @@ -3,7 +3,7 @@ # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Non regression tests for pylint, which requires a too specific configuration -to be incorporated in the automatic functional test framework +to be incorporated in the automatic functional test framework. """ # pylint: disable=redefined-outer-name diff --git a/tests/test_self.py b/tests/test_self.py index e45b524ac9..1c72bc95f8 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -33,7 +33,7 @@ from pylint.message import Message from pylint.reporters import BaseReporter from pylint.reporters.json_reporter import JSON2Reporter -from pylint.reporters.text import ColorizedTextReporter, TextReporter +from pylint.reporters.text import ColorizedTextReporter, GithubReporter, TextReporter from pylint.testutils._run import _add_rcfile_default_pylintrc from pylint.testutils._run import _Run as Run from pylint.testutils.utils import ( @@ -189,6 +189,7 @@ def test_all(self) -> None: TextReporter(StringIO()), ColorizedTextReporter(StringIO()), JSON2Reporter(StringIO()), + GithubReporter(StringIO()), ] self._runtest( [join(HERE, "functional", "a", "arguments.py")], @@ -884,7 +885,8 @@ def test_modify_sys_path() -> None: @staticmethod def test_plugin_that_imports_from_open() -> None: """Test that a plugin that imports a source file from a checker open() - function (ala pylint_django) does not raise an exception.""" + function (ala pylint_django) does not raise an exception. + """ with _test_sys_path(): # Enable --load-plugins=importing_plugin sys.path.append(join(HERE, "regrtest_data", "importing_plugin")) @@ -986,6 +988,11 @@ def test_can_list_directories_without_dunder_init(tmp_path: Path) -> None: stderr=subprocess.PIPE, ) + def test_warnings_by_module(self) -> None: + path = join(HERE, "regrtest_data", "unused_variable.py") + expected = "errors / warnings by module" + self._test_output([path, "-ry"], expected_output=expected) + @pytest.mark.needs_two_cores def test_jobs_score(self) -> None: path = join(HERE, "regrtest_data", "unused_variable.py") @@ -1139,7 +1146,7 @@ def test_load_text_repoter_if_not_provided() -> None: def test_regex_paths_csv_validator() -> None: """Test to see if _regexp_paths_csv_validator works. Previously the validator crashed when encountering already validated values. - Reported in https://github.com/pylint-dev/pylint/issues/5437 + Reported in https://github.com/pylint-dev/pylint/issues/5437. """ with pytest.raises(SystemExit) as ex: args = _add_rcfile_default_pylintrc( @@ -1164,14 +1171,14 @@ def test_max_inferred_for_complicated_class_hierarchy() -> None: assert not ex.value.code % 2 def test_recursive(self) -> None: - """Tests if running linter over directory using --recursive=y""" + """Tests if running linter over directory using --recursive=y.""" self._runtest( [join(HERE, "regrtest_data", "directory", "subdirectory"), "--recursive=y"], code=0, ) def test_recursive_globbing(self) -> None: - """Tests if running linter over directory using --recursive=y and globbing""" + """Tests if running linter over directory using --recursive=y and globbing.""" self._runtest( [join(HERE, "regrtest_data", "d?rectory", "subd*"), "--recursive=y"], code=0, @@ -1200,6 +1207,27 @@ def test_ignore_pattern_recursive(self, ignore_pattern_value: str) -> None: code=0, ) + @pytest.mark.parametrize("ignore_pattern_value", ["^\\.", "^\\..+", "^\\..*"]) + def test_ignore_pattern_recursive_rel_path(self, ignore_pattern_value: str) -> None: + """Test that ``--ignore-patterns`` strictly only ignores files + whose names begin with a "." when a dot is used to specify the + current directory. + """ + expected = "module.py:1:0: W0611: Unused import os (unused-import)" + unexpected = ".hidden/module.py:1:0: W0611: Unused import os (unused-import)" + + with _test_cwd(): + os.chdir(join(HERE, "regrtest_data", "ignore_pattern")) + self._test_output( + [ + ".", + "--recursive=y", + f"--ignore-patterns={ignore_pattern_value}", + ], + expected_output=expected, + unexpected_output=unexpected, + ) + def test_ignore_pattern_from_stdin(self) -> None: """Test if linter ignores standard input if the filename matches the ignore pattern.""" with mock.patch("pylint.lint.pylinter._read_stdin", return_value="import os\n"): @@ -1243,7 +1271,7 @@ def test_recursive_current_dir(self) -> None: ) def test_ignore_path_recursive_current_dir(self) -> None: - """Tests that path is normalized before checked that is ignored. GitHub issue #6964""" + """Tests that path is normalized before checked that is ignored. GitHub issue #6964.""" with _test_sys_path(): # pytest is including directory HERE/regrtest_data to sys.path which causes # astroid to believe that directory is a package. @@ -1283,7 +1311,7 @@ def test_encoding(self, module_name: str, expected_output: str) -> None: ) def test_line_too_long_useless_suppression(self) -> None: - """A test that demonstrates a known false positive for useless-suppression + """A test that demonstrates a known false positive for useless-suppression. See https://github.com/pylint-dev/pylint/issues/3368 @@ -1314,7 +1342,8 @@ def test_output_no_header(self) -> None: def test_no_name_in_module(self) -> None: """Test that a package with both a variable name `base` and a module `base` - does not emit a no-name-in-module msg.""" + does not emit a no-name-in-module msg. + """ module = join(HERE, "regrtest_data", "test_no_name_in_module.py") unexpected = "No name 'errors' in module 'list' (no-name-in-module)" self._test_output( @@ -1508,7 +1537,8 @@ def test_errors_only() -> None: @staticmethod def test_errors_only_functions_as_disable() -> None: """--errors-only functions as a shortcut for --disable=W,C,R,I; - it no longer enables any messages.""" + it no longer enables any messages. + """ run = Run( [str(UNNECESSARY_LAMBDA), "--disable=import-error", "--errors-only"], exit=False, diff --git a/tests/test_similar.py b/tests/test_similar.py index 8a09b45008..4c12d4366c 100644 --- a/tests/test_similar.py +++ b/tests/test_similar.py @@ -225,7 +225,8 @@ def test_duplicate_code_raw_strings_disable_scope_double(self) -> None: def test_duplicate_code_raw_strings_disable_scope_function(self) -> None: """Tests disabling duplicate-code at an inner scope level with another scope with - similarity.""" + similarity. + """ path = join(DATA, "raw_strings_disable_scope_second_function") expected_output = "Similar lines in 2 files" self._test_output( diff --git a/tests/testutils/_primer/test_primer.py b/tests/testutils/_primer/test_primer.py index 869ae6ad19..798b5a4d8c 100644 --- a/tests/testutils/_primer/test_primer.py +++ b/tests/testutils/_primer/test_primer.py @@ -2,7 +2,7 @@ # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt -"""Test the primer commands. """ +"""Test the primer commands.""" from __future__ import annotations import sys @@ -55,7 +55,8 @@ class TestPrimer: def test_compare(self, directory: Path) -> None: """Test for the standard case. - Directory in 'fixtures/' with 'main.json', 'pr.json' and 'expected.txt'.""" + Directory in 'fixtures/' with 'main.json', 'pr.json' and 'expected.txt'. + """ self.__assert_expected(directory) def test_compare_batched(self) -> None: diff --git a/towncrier.toml b/towncrier.toml index 08a78d762c..8df0009ec2 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,7 +1,7 @@ [tool.towncrier] -version = "3.1.0" +version = "3.2.2" directory = "doc/whatsnew/fragments" -filename = "doc/whatsnew/3/3.1/index.rst" +filename = "doc/whatsnew/3/3.2/index.rst" template = "doc/whatsnew/fragments/_template.rst" issue_format = "`#{issue} `_" wrap = false # doesn't wrap links correctly if beginning with indentation