diff --git a/.coveragerc b/.coveragerc index faa494f8e6..ddfcbd3348 100644 --- a/.coveragerc +++ b/.coveragerc @@ -24,5 +24,5 @@ exclude_lines = if TYPE_CHECKING: @overload - # Abstract methods are not exectued during pytest runs + # Abstract methods are not executed during pytest runs raise NotImplementedError() diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 9a701d7f09..0000000000 --- a/.flake8 +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -ignore = - E203, W503, # Incompatible with black see https://github.com/ambv/black/issues/315 - E501, # Lot of lines too long right now - -max-line-length=88 -max-complexity=39 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..a3a7609a78 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +tests/**/functional/** -text +tests/input/** -text +tests/**/data/** -text +tests/regrtest_data/** -text diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..5083128913 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,35 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in the repo. +# Right now there is not default owner to avoid spam +# * @pierre-sassoulas @DanielNoord @cdce8p @jacobtylerwalls @hippo91 + +# Order is important. The last matching pattern has the most precedence. + +### Core components + +# internal message handling +pylint/message/* @pierre-sassoulas +tests/message/* @pierre-sassoulas + +# typing +pylint/typing.py @DanielNoord + +# multiprocessing (doublethefish is not yet a contributor with write access) +# pylint/lint/parallel.py @doublethefish +# tests/test_check_parallel.py @doublethefish + +### Pyreverse +pylint/pyreverse/* @DudeNr33 +tests/pyreverse/* @DudeNr33 + +### Extensions + +# CodeStyle +pylint/extensions/code_style.* @cdce8p +tests/functional/ext/code_style/* @cdce8p + +# Typing +pylint/extensions/typing.* @cdce8p +tests/functional/ext/typing/* @cdce8p diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0fe17f8600..9231d293db 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,2 +1,2 @@ Please read the -[contribute doc](https://github.com/PyCQA/pylint/blob/main/doc/development_guide/contribute.rst). +[contribute doc](https://pylint.pycqa.org/en/latest/development_guide/contributor_guide/contribute.html). diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 29063e2e07..8b88c8fcd5 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,17 @@ # These are supported funding model platforms tidelift: "pypi/pylint" -github: [Pierre-Sassoulas, cdce8p, hippo91] +github: + [ + PCManticore, + Pierre-Sassoulas, + cdce8p, + hippo91, + AWhetter, + DanielNoord, + areveny, + DudeNr33, + jacobtylerwalls, + matusvalo, + yushao2, + ] diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 640c1bfe39..e7a9f168c5 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -1,6 +1,6 @@ name: 🐛 Bug report description: Report a bug in pylint -labels: [bug, needs triage] +labels: ["Needs triage :inbox_tray:"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml index ea04db0959..a2b9b52e04 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml @@ -1,6 +1,6 @@ name: ✨ Feature request description: Suggest an idea for pylint -labels: [enhancement, needs triage] +labels: ["Needs triage :inbox_tray:"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yml b/.github/ISSUE_TEMPLATE/QUESTION.yml index 235c61edc6..4af829fa38 100644 --- a/.github/ISSUE_TEMPLATE/QUESTION.yml +++ b/.github/ISSUE_TEMPLATE/QUESTION.yml @@ -1,6 +1,6 @@ name: 🤔 Support question description: Questions about pylint that are not covered in the documentation -labels: [question, needs triage, documentation] +labels: ["Needs triage :inbox_tray:", "Question", "Documentation :green_book:"] body: - type: markdown attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 90a6b12c3a..dfa06dbbb7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,10 +4,10 @@ Thank you for submitting a PR to pylint! To ease the process of reviewing your PR, do make sure to complete the following boxes. - [ ] Write a good description on what the PR does. -- [ ] Add an entry to the change log describing the change in - `doc/whatsnew/2/2.15/index.rst` (or ``doc/whatsnew/2/2.14/full.rst`` - if the change needs backporting in 2.14). If necessary you can write - details or offer examples on how the new change is supposed to work. +- [ ] Create a news fragment with `towncrier create .` which will be + included in the changelog. `` can be one of: breaking, user_action, feature, + new_check, removed_check, extension, false_positive, false_negative, bugfix, other, internal. + If necessary you can write details or offer examples on how the new change is supposed to work. - [ ] If you used multiple emails or multiple names when contributing, add your mails and preferred name in ``script/.contributors_aliases.json`` --> diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000..4c2639b503 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,29 @@ +name: Backport +on: + pull_request_target: + types: + - closed + - labeled + +permissions: + pull-requests: write + contents: write + +jobs: + backport: + name: Backport + runs-on: ubuntu-latest + # Only react to merged PRs for security reasons. + # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. + if: > + github.event.pull_request.merged && ( + github.event.action == 'closed' + || ( + github.event.action == 'labeled' + && contains(github.event.label.name, 'backport') + ) + ) + steps: + - uses: tibdex/backport@2e217641d82d02ba0603f46b1aeedefb258890ac # v2.0.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000000..328f057294 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,66 @@ +name: changelog + +on: + pull_request: + types: [opened, synchronize, labeled, unlabeled, reopened] + branches-ignore: + - "maintenance/**" +env: + CACHE_VERSION: 1 + KEY_PREFIX: base-venv + DEFAULT_PYTHON: "3.11" + +permissions: + contents: read + +jobs: + check-changelog: + if: contains(github.event.pull_request.labels.*.name, 'skip news :mute:') != true + name: Changelog Entry Check + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.3.0 + 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@v4.5.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "key=${{ env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test.txt', + 'requirements_test_min.txt', 'requirements_test_pre_commit.txt') }}" >> + $GITHUB_OUTPUT + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.2.4 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python -m pip install -U pip setuptools wheel + pip install -U -r requirements_test.txt + pip install -U -r doc/requirements.txt + - name: Emit warning if news fragment is missing + env: + BASE_BRANCH: ${{ github.base_ref }} + run: | + # Fetch the pull request' base branch so towncrier will be able to + # compare the current branch with the base branch. + git fetch --no-tags origin +refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH} + . venv/bin/activate + towncrier check --compare-with origin/${{ github.base_ref }} diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index fd894701a9..45f1f00d79 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -4,19 +4,25 @@ on: push: branches: - main - - 2.* - pull_request: ~ + - "maintenance/**" + pull_request: + branches: + - main + - "maintenance/**" env: - # Also change CACHE_VERSION in the other workflows - CACHE_VERSION: 8 - DEFAULT_PYTHON: 3.8 + CACHE_VERSION: 1 + KEY_PREFIX: base-venv + DEFAULT_PYTHON: "3.11" PRE_COMMIT_CACHE: ~/.cache/pre-commit concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: prepare-base: name: Prepare base dependencies @@ -27,28 +33,28 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt') - }}" + echo "key=${{ env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test.txt', + 'requirements_test_min.txt', 'requirements_test_pre_commit.txt') }}" >> + $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -60,17 +66,15 @@ jobs: - name: Generate pre-commit restore key id: generate-pre-commit-key run: >- - echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ - hashFiles('.pre-commit-config.yaml') }}" + echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{ + hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}- - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -84,44 +88,38 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv + fail-on-cache-miss: true key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} + fail-on-cache-miss: true key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} - - name: Fail job if pre-commit cache restore failed - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - echo "Failed to restore pre-commit environment from cache" - exit 1 - name: Install enchant and aspell run: | sudo apt-get update - sudo apt-get install enchant aspell-en + sudo apt-get install enchant-2 aspell-en - name: Run pylint checks run: | . venv/bin/activate pip install -e . + pip list | grep 'astroid\|pylint' pre-commit run --hook-stage manual pylint-with-spelling --all-files spelling: @@ -131,25 +129,22 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv + fail-on-cache-miss: true key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - name: Run spelling checks run: | . venv/bin/activate @@ -158,33 +153,32 @@ jobs: documentation: name: documentation runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv + fail-on-cache-miss: true key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + - name: Install tox run: | - echo "Failed to restore Python venv from cache" - exit 1 + pip install -U tox - name: Run checks on documentation code examples run: | - . venv/bin/activate - pytest doc/test_messages_documentation.py + tox -e test_doc - name: Check documentation build and links run: | . venv/bin/activate diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1cce434b2f..550ec973e9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,6 +20,9 @@ on: schedule: - cron: "44 16 * * 4" +permissions: + contents: read + jobs: analyze: name: Analyze @@ -39,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/primer-test.yaml b/.github/workflows/primer-test.yaml index 72d1dd0cfc..07e9f401db 100644 --- a/.github/workflows/primer-test.yaml +++ b/.github/workflows/primer-test.yaml @@ -4,22 +4,25 @@ on: push: branches: - main - - 2.* pull_request: paths: - "pylint/**" - "tests/primer/**" - "requirements*" - ".github/workflows/primer-test.yaml" - + branches: + - main env: - # Also change CACHE_VERSION in the CI workflow - CACHE_VERSION: 6 + CACHE_VERSION: 1 + KEY_PREFIX: venv concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: prepare-tests-linux: name: prepare / ${{ matrix.python-version }} / Linux @@ -27,33 +30,33 @@ jobs: timeout-minutes: 5 strategy: matrix: - python-version: [3.8, 3.9, "3.10"] + python-version: [3.8, 3.9, "3.10", "3.11"] outputs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt') - }}" + echo "key=${{ env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test.txt', + 'requirements_test_min.txt', 'requirements_test_pre_commit.txt') }}" >> + $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -69,28 +72,25 @@ jobs: needs: prepare-tests-linux strategy: matrix: - python-version: [3.8, 3.9, "3.10"] + python-version: [3.8, 3.9, "3.10", "3.11"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv + fail-on-cache-miss: true key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.prepare-tests-linux.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - name: Run pytest run: | . venv/bin/activate @@ -104,28 +104,25 @@ jobs: needs: prepare-tests-linux strategy: matrix: - python-version: [3.8, 3.9, "3.10"] + python-version: [3.8, 3.9, "3.10", "3.11"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv + fail-on-cache-miss: true key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.prepare-tests-linux.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - name: Run pytest run: | . venv/bin/activate diff --git a/.github/workflows/primer_comment.yaml b/.github/workflows/primer_comment.yaml index 6c806fecf2..9b09342152 100644 --- a/.github/workflows/primer_comment.yaml +++ b/.github/workflows/primer_comment.yaml @@ -13,7 +13,10 @@ on: - completed env: - CACHE_VERSION: 2 + # This needs to be the SAME as in the Main and PR job + CACHE_VERSION: 1 + KEY_PREFIX: venv-primer + DEFAULT_PYTHON: "3.11" permissions: contents: read @@ -21,48 +24,35 @@ permissions: jobs: primer-comment: + # Skip job if the workflow failed + if: ${{ github.event.workflow_run.conclusion == 'success' }} name: Run runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v3 - with: - node-version: 16 - - run: npm install @octokit/rest - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 - - name: Set up Python 3.10 + uses: actions/checkout@v3.3.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: - python-version: "3.10" + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true # Restore cached Python environment - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt') - }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv - key: >- + fail-on-cache-miss: true + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- - - name: Create Python virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - python -m venv venv - . venv/bin/activate - python -m pip install -U pip setuptools wheel - pip install -U -r requirements_test.txt + env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml', + 'requirements_test.txt', 'requirements_test_min.txt', + 'requirements_test_pre_commit.txt') }} - name: Download outputs - uses: actions/github-script@v6 + uses: actions/github-script@v6.4.0 with: script: | // Download workflow pylint output @@ -111,15 +101,14 @@ jobs: - name: Compare outputs run: | . venv/bin/activate - python tests/primer/primer_tool.py compare \ + python tests/primer/__main__.py compare \ --commit=${{ github.event.workflow_run.head_sha }} \ --base-file=output_${{ steps.python.outputs.python-version }}_main.txt \ --new-file=output_${{ steps.python.outputs.python-version }}_pr.txt - name: Post comment id: post-comment - uses: actions/github-script@v6 + uses: actions/github-script@v6.4.0 with: - github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs') const comment = fs.readFileSync('tests/.pylint_primer_tests/comment.txt', { encoding: 'utf8' }) @@ -135,16 +124,8 @@ jobs: return prNumber - name: Hide old comments # Taken from mypy primer - # v0.3.0 - uses: kanga333/comment-hider@bbdf5b562fbec24e6f60572d8f712017428b92e0 + uses: kanga333/comment-hider@c12bb20b48aeb8fc098e35967de8d4f8018fffdf # v0.4.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} leave_visible: 1 issue_number: ${{ steps.post-comment.outputs.result }} - - name: Warn about failure - if: ${{ failure() }} - run: | - echo "🤖 **Comment workflow failed**. 🤖" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Please investigate:" >> $GITHUB_STEP_SUMMARY - echo "@DanielNoord" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/primer_run_main.yaml b/.github/workflows/primer_run_main.yaml index d928bb5205..1cadcbe390 100644 --- a/.github/workflows/primer_run_main.yaml +++ b/.github/workflows/primer_run_main.yaml @@ -15,48 +15,42 @@ concurrency: cancel-in-progress: true env: - CACHE_VERSION: 2 + # This needs to be the SAME as in the PR and comment job + CACHE_VERSION: 1 + KEY_PREFIX: venv-primer + +permissions: + contents: read jobs: run-primer: name: Run / ${{ matrix.python-version }} runs-on: ubuntu-latest - timeout-minutes: 45 + timeout-minutes: 60 strategy: matrix: - python-version: ["3.7", "3.10"] + python-version: ["3.7", "3.11"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - - name: Get latest astroid commit - id: get-astroid-sha - run: | - curl https://api.github.com/repos/PyCQA/astroid/commits | - python -c "import json, sys; print(json.load(sys.stdin)[0]['sha'])" > astroid_sha.txt - - # Restore cached Python environment - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt', - 'astroid_sha.txt') }}" - - name: Restore Python virtual environment + # Create a re-usable virtual environment + - name: Create Python virtual environment cache id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv - key: >- + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- + env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml', + 'requirements_test.txt', 'requirements_test_min.txt', + 'requirements_test_pre_commit.txt') }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -64,20 +58,18 @@ jobs: . venv/bin/activate python -m pip install -U pip setuptools wheel pip install -U -r requirements_test.txt - # Use bleeding-edge astroid - pip install git+https://github.com/PyCQA/astroid.git # Cache primer packages - name: Get commit string id: commitstring run: | . venv/bin/activate - python tests/primer/primer_tool.py prepare --make-commit-string - output=$(python tests/primer/primer_tool.py prepare --read-commit-string) - echo "::set-output name=commitstring::$output" + python tests/primer/__main__.py prepare --make-commit-string + output=$(python tests/primer/__main__.py prepare --read-commit-string) + echo "commitstring=$output" >> $GITHUB_OUTPUT - name: Restore projects cache id: cache-projects - uses: actions/cache@v3 + uses: actions/cache@v3.2.4 with: path: tests/.pylint_primer_tests/ key: >- @@ -86,27 +78,28 @@ jobs: - name: Regenerate cache run: | . venv/bin/activate - python tests/primer/primer_tool.py prepare --clone + python tests/primer/__main__.py prepare --clone - name: Upload output diff - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.2 with: name: primer_commitstring - path: tests/.pylint_primer_tests/commit_string.txt + path: + tests/.pylint_primer_tests/commit_string_${{ matrix.python-version }}.txt # Run primer - name: Run pylint primer run: | . venv/bin/activate pip install -e . - python tests/primer/primer_tool.py run --type=main 2>warnings.txt + python tests/primer/__main__.py run --type=main 2>warnings.txt WARNINGS=$(head -c 65000 < warnings.txt) if [[ $WARNINGS ]] then echo "::warning ::$WARNINGS" fi - name: Upload output - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.2 with: name: primer_output - path: + path: >- tests/.pylint_primer_tests/output_${{ steps.python.outputs.python-version }}_main.txt diff --git a/.github/workflows/primer_run_pr.yaml b/.github/workflows/primer_run_pr.yaml index 167c1e26ae..82e1596b0f 100644 --- a/.github/workflows/primer_run_pr.yaml +++ b/.github/workflows/primer_run_pr.yaml @@ -16,15 +16,20 @@ on: - "!.github/workflows/primer_run_main.yaml" - "!.github/workflows/primer_comment.yaml" - "!tests/primer/packages_to_prime.json" - branches-ignore: - - "maintenance/**" + branches: + - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: - CACHE_VERSION: 2 + # This needs to be the SAME as in the Main and comment job + CACHE_VERSION: 1 + KEY_PREFIX: venv-primer + +permissions: + contents: read jobs: run-primer: @@ -33,45 +38,31 @@ jobs: timeout-minutes: 120 strategy: matrix: - python-version: ["3.7", "3.10"] + python-version: ["3.7", "3.11"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} - - uses: actions/setup-node@v3 - with: - node-version: 16 - - run: npm install @octokit/rest - - - name: Get latest astroid commit - id: get-astroid-sha - run: | - curl https://api.github.com/repos/PyCQA/astroid/commits | - python -c "import json, sys; print(json.load(sys.stdin)[0]['sha'])" > astroid_sha.txt + check-latest: true # Restore cached Python environment - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt', - 'astroid_sha.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv - key: >- + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- + env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml', + 'requirements_test.txt', 'requirements_test_min.txt', + 'requirements_test_pre_commit.txt') }} + # Create environment must match step in 'Primer / Main' - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -79,24 +70,20 @@ jobs: . venv/bin/activate python -m pip install -U pip setuptools wheel pip install -U -r requirements_test.txt - # Use bleeding-edge astroid - pip install git+https://github.com/PyCQA/astroid.git # Cache primer packages - name: Download last 'main' run info id: download-main-run - uses: actions/github-script@v6 + uses: actions/github-script@v6.4.0 with: script: | // Download 'main' pylint output const fs = require('fs'); - const { Octokit } = require("@octokit/rest"); - const octokit = new Octokit({}); - const runs = await octokit.rest.actions.listWorkflowRuns({ + const runs = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: ".github/workflows/primer_run_main.yaml", - status: "completed" + status: "success" }); const lastRunMain = runs.data.workflow_runs.reduce(function(prev, current) { return (prev.run_number > current.run_number) ? prev : current @@ -134,18 +121,18 @@ jobs: - name: Copy and unzip the commit string run: | unzip primer_commitstring.zip - cp commit_string.txt tests/.pylint_primer_tests/commit_string.txt + cp commit_string_${{ matrix.python-version }}.txt tests/.pylint_primer_tests/commit_string_${{ matrix.python-version }}.txt - name: Unzip the output of 'main' run: unzip primer_output_main.zip - name: Get commit string id: commitstring run: | . venv/bin/activate - output=$(python tests/primer/primer_tool.py prepare --read-commit-string) - echo "::set-output name=commitstring::$output" + output=$(python tests/primer/__main__.py prepare --read-commit-string) + echo "commitstring=$output" >> $GITHUB_OUTPUT - name: Restore projects cache id: cache-projects - uses: actions/cache@v3 + uses: actions/cache@v3.2.4 with: path: tests/.pylint_primer_tests/ key: >- @@ -154,7 +141,7 @@ jobs: - name: Check cache run: | . venv/bin/activate - python tests/primer/primer_tool.py prepare --check + python tests/primer/__main__.py prepare --check # Merge the 'main' commit of last successful run - name: Pull 'main' @@ -169,20 +156,20 @@ jobs: run: | . venv/bin/activate pip install -e . - python tests/primer/primer_tool.py run --type=pr 2>warnings.txt + python tests/primer/__main__.py run --type=pr 2>warnings.txt WARNINGS=$(head -c 65000 < warnings.txt) if [[ $WARNINGS ]] then echo "::warning ::$WARNINGS" fi - name: Upload output of PR - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.2 with: name: primer_output_pr path: tests/.pylint_primer_tests/output_${{ steps.python.outputs.python-version }}_pr.txt - name: Upload output of 'main' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.2 with: name: primer_output_main path: output_${{ steps.python.outputs.python-version }}_main.txt @@ -192,7 +179,7 @@ jobs: run: | echo ${{ github.event.pull_request.number }} | tee pr_number.txt - name: Upload PR number - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.2 with: name: pr_number path: pr_number.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7094aa46e..ec8ebde83f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,27 +6,35 @@ on: - published env: - DEFAULT_PYTHON: 3.9 + DEFAULT_PYTHON: "3.11" + +permissions: + contents: read jobs: release-pypi: name: Upload release to PyPI runs-on: ubuntu-latest + environment: + name: PyPI + url: https://pypi.org/project/pylint/ steps: - name: Check out code from Github - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Install requirements run: | - python -m pip install -U pip twine wheel - python -m pip install -U "setuptools>=56.0.0" + # Remove dist, build, and pylint.egg-info + # when building locally for testing! + python -m pip install twine build - name: Build distributions run: | - python setup.py sdist bdist_wheel + python -m build - name: Upload to PyPI if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags') env: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1aa2ca46d7..ea963d3cd6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,14 +4,20 @@ on: push: branches: - main - - 2.* - pull_request: + - "maintenance/**" paths-ignore: - doc/data/messages/** + pull_request: + branches: + - main + - "maintenance/**" env: - # Also change CACHE_VERSION in the other workflows - CACHE_VERSION: 7 + CACHE_VERSION: 2 + KEY_PREFIX: venv + +permissions: + contents: read jobs: tests-linux: @@ -21,33 +27,33 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] outputs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt') - }}" + echo "key=${{ env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test.txt', + 'requirements_test_min.txt', 'requirements_test_pre_commit.txt') }}" >> + $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -58,13 +64,15 @@ jobs: - name: Run pytest run: | . venv/bin/activate + pip list | grep 'astroid\|pylint' pytest --durations=10 --benchmark-disable --cov --cov-report= tests/ - name: Run functional tests with minimal messages config run: | . venv/bin/activate + pip list | grep 'astroid\|pylint' pytest -vv --minimal-messages-config tests/test_functional.py - name: Upload coverage artifact - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }} path: .coverage @@ -74,45 +82,36 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 needs: tests-linux - strategy: - matrix: - python-version: [3.8] - env: - COVERAGERC_FILE: .coveragerc steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 - - name: Set up Python ${{ matrix.python-version }} + uses: actions/checkout@v3.3.0 + - name: Set up Python 3.11 id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: - python-version: ${{ matrix.python-version }} + python-version: "3.11" + check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv + fail-on-cache-miss: true key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.tests-linux.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - name: Download all coverage artifacts - uses: actions/download-artifact@v3.0.0 + uses: actions/download-artifact@v3.0.2 - name: Combine coverage results run: | . venv/bin/activate coverage combine coverage*/.coverage - coverage report --rcfile=${{ env.COVERAGERC_FILE }} - - name: Upload coverage to Coveralls - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - . venv/bin/activate - coveralls --rcfile=${{ env.COVERAGERC_FILE }} --service=github + coverage xml + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + verbose: true benchmark-linux: name: run benchmark / ${{ matrix.python-version }} / Linux @@ -122,33 +121,31 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8] + python-version: ["3.11"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv + fail-on-cache-miss: true key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.tests-linux.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - name: Run pytest run: | . venv/bin/activate pip install pygal pip install -e . + pip list | grep 'astroid\|pylint' pytest --exitfirst \ --benchmark-only \ --benchmark-autosave \ @@ -157,9 +154,9 @@ jobs: - name: Create partial artifact name suffix id: artifact-name-suffix run: >- - echo "::set-output name=datetime::"$(date "+%Y%m%d_%H%M") + echo "datetime="$(date "+%Y%m%d_%H%M") >> $GITHUB_OUTPUT - name: Upload benchmark artifact - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v3.1.2 with: name: benchmark-${{ runner.os }}-${{ matrix.python-version }}_${{ @@ -174,35 +171,34 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] steps: - name: Set temp directory run: echo "TEMP=$env:USERPROFILE\AppData\Local\Temp" >> $env:GITHUB_ENV # 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@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test_min.txt') - }}" + echo "key=venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test_min.txt') + }}" >> $env:GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -213,6 +209,7 @@ jobs: - name: Run pytest run: | . venv\\Scripts\\activate + pip list | grep 'astroid\|pylint' pytest --durations=10 --benchmark-disable tests/ tests-macos: @@ -227,38 +224,38 @@ jobs: python-version: [3.7] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test_min.txt') - }}" + echo "key=venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test_min.txt') + }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate python -m pip install -U pip setuptools wheel - pip install -U -r requirements_test.txt + pip install -U -r requirements_test_min.txt - name: Run pytest run: | . venv/bin/activate + pip list | grep 'astroid\|pylint' pytest --durations=10 --benchmark-disable tests/ tests-pypy: @@ -271,28 +268,27 @@ jobs: python-version: ["pypy-3.7", "pypy-3.8", "pypy-3.9"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test_min.txt') - }}" + echo "key=venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test_min.txt') + }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.2.4 with: path: venv key: >- ${{ runner.os }}-${{ matrix.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -303,4 +299,5 @@ jobs: - name: Run pytest run: | . venv/bin/activate + pip list | grep 'astroid\|pylint' pytest --durations=10 --benchmark-disable tests/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c2390a727..c2ba36f51c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,14 +3,19 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - exclude: "tests/functional/t/trailing_whitespaces.py|tests/pyreverse/data/.*.html" + exclude: tests(/\w*)*/functional/t/trailing_whitespaces.py|tests/pyreverse/data/.*.html|doc/data/messages/t/trailing-whitespace/bad.py - id: end-of-file-fixer - exclude: "tests/functional/m/missing/missing_final_newline.py|tests/functional/t/trailing_newlines.py" - - repo: https://github.com/myint/autoflake - rev: v1.4 + exclude: | + (?x)^( + tests(/\w*)*/functional/m/missing/missing_final_newline.py| + tests/functional/t/trailing_newlines.py| + doc/data/messages/t/trailing-newlines/bad.py| + )$ + - repo: https://github.com/PyCQA/autoflake + rev: v2.0.0 hooks: - id: autoflake exclude: &fixtures tests(/\w*)*/functional/|tests/input|doc/data/messages|tests(/\w*)*data/ @@ -28,32 +33,40 @@ repos: exclude: tests(/\w*)*/functional/|tests/input|doc/data/messages|examples/|setup.py|tests(/\w*)*data/ types: [python] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py37-plus] exclude: *fixtures - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort - exclude: doc/data/messages/(r/reimported|w/wrong-import-order|u/ungrouped-imports|m/misplaced-future)/bad.py + exclude: doc/data/messages/(r/reimported|w/wrong-import-order|u/ungrouped-imports|m/misplaced-future|m/multiple-imports)/bad.py - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.12.0 hooks: - id: black args: [--safe, --quiet] exclude: *fixtures - repo: https://github.com/Pierre-Sassoulas/black-disable-checker - rev: v1.1.0 + rev: v1.1.3 hooks: - id: black-disable-checker - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 6.0.0 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.12.0] + additional_dependencies: + [flake8-bugbear==23.1.20, flake8-typing-imports==1.14.0] exclude: *fixtures + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + name: line-length-doc + files: doc/data/messages + args: ["--config", "doc/data/.flake8"] - repo: local hooks: - id: pylint @@ -62,7 +75,7 @@ repos: language: system types: [python] args: ["-rn", "-sn", "--rcfile=pylintrc", "--fail-on=I"] - exclude: tests/functional/|tests/input|tests(/\w*)*data/|doc/ + exclude: tests(/\w*)*/functional/|tests/input|tests(/\w*)*data/|doc/ # We define an additional manual step to allow running pylint with a spelling # checker in CI. - id: pylint @@ -72,30 +85,30 @@ repos: language: system types: [python] args: ["-rn", "-sn", "--rcfile=pylintrc", "--fail-on=I", "--spelling-dict=en"] - exclude: tests/functional/|tests/input|tests(/\w*)*data/|doc/ + exclude: tests(/\w*)*/functional/|tests/input|tests(/\w*)*data/|doc/ stages: [manual] - - id: check-changelog - alias: check-changelog - name: check-changelog - types: [text] - entry: python3 script/check_changelog.py - pass_filenames: false - language: system - id: fix-documentation name: Fix documentation entry: python3 -m script.fix_documentation language: system types: [text] files: ^(doc/whatsnew/\d+\.\d+\.rst) + - id: check-newsfragments + name: Check newsfragments + entry: python3 -m script.check_newsfragments + language: system + types: [text] + files: ^(doc/whatsnew/fragments) + exclude: doc/whatsnew/fragments/_.*.rst - repo: https://github.com/rstcheck/rstcheck - rev: "v6.0.0rc3" + rev: "v6.1.1" hooks: - id: rstcheck args: ["--report-level=warning"] files: ^(doc/(.*/)*.*\.rst) - additional_dependencies: [Sphinx==4.5.0] + additional_dependencies: [Sphinx==5.0.1] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.960 + rev: v0.991 hooks: - id: mypy name: mypy @@ -104,18 +117,45 @@ repos: types: [python] args: [] require_serial: true - additional_dependencies: ["platformdirs==2.2.0", "types-pkg_resources==0.1.3"] - exclude: tests/functional/|tests/input|tests(/.*)+/conftest.py|doc/data/messages|tests(/\w*)*data/ + additional_dependencies: + [ + "isort>=5", + "platformdirs==2.2.0", + "py==1.11", + "tomlkit>=0.10.1", + "types-pkg_resources==0.1.3", + ] + exclude: tests(/\w*)*/functional/|tests/input|tests(/.*)+/conftest.py|doc/data/messages|tests(/\w*)*data/ - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.6.2 + rev: v3.0.0-alpha.4 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] exclude: tests(/\w*)*data/ - repo: https://github.com/DanielNoord/pydocstringformatter - rev: v0.6.0 + rev: v0.7.3 hooks: - id: pydocstringformatter exclude: *fixtures args: ["--max-summary-lines=2", "--linewrap-full-docstring"] files: "pylint" + - repo: https://github.com/regebro/pyroma + rev: "4.1" + hooks: + - id: pyroma + # Must be specified because of the default value in pyroma + always_run: false + files: | + (?x)^( + README.rst| + pyproject.toml| + pylint/__init__.py| + pylint/__pkginfo__.py| + setup.cfg + )$ + - repo: https://github.com/PyCQA/bandit + rev: 1.7.4 + hooks: + - id: bandit + args: ["-r", "-lll"] + exclude: *fixtures diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt index b5d832d058..a82b2e1bf0 100644 --- a/.pyenchant_pylint_custom_dict.txt +++ b/.pyenchant_pylint_custom_dict.txt @@ -21,6 +21,7 @@ async asynccontextmanager attr attrib +backport BaseChecker basename behaviour @@ -30,6 +31,7 @@ bla bom bool boolean +booleaness boolop boundmethod builtins @@ -48,6 +50,7 @@ classmethod classmethod's classname classobj +CLI cls cmp codebase @@ -71,10 +74,13 @@ cyclomatic dataclass datetime debian +deduplication deepcopy +defaultdicts defframe defstmts deleter +dependabot deque destructured destructuring @@ -102,6 +108,7 @@ epytext erroring etree expr +falsey favour filepath filestream @@ -153,6 +160,7 @@ iterables iteritems jn jpg +json jx jython # class is a reserved word @@ -208,6 +216,7 @@ mymodule mypy namedtuple namespace +newsfile newstyle nl nodename @@ -240,6 +249,7 @@ pragma's pragmas pre preorder +prepended proc py pyenchant @@ -275,6 +285,7 @@ sep setcomp shortstrings singledispatch +singledispatchmethod spammy sqlalchemy src @@ -315,8 +326,11 @@ tokenizer toml tomlkit toplevel +towncrier tp truthness +truthy +truthey tryexcept txt typecheck @@ -350,6 +364,7 @@ vcg's vectorisation virtualized wc +whitespaces xfails xml xyz diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0fec8925dd..9f7c35450e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -14,3 +14,6 @@ build: os: ubuntu-20.04 tools: python: "3.8" + jobs: + pre_build: + - towncrier build --yes --date TBA diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 1fc26311e4..47e3c05c54 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -13,18 +13,20 @@ Ex-maintainers Maintainers ----------- - - Pierre Sassoulas - Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> - Marc Mueller <30130371+cdce8p@users.noreply.github.com> -- Hippo91 - Jacob Walls +- Hippo91 +- Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> - Matus Valo - Andreas Finkler <3929834+DudeNr33@users.noreply.github.com> +- Dani Alcala <112832187+clavedeluna@users.noreply.github.com> - Łukasz Rogalski - Ashley Whetter - Bryce Guinta - Yu Shao, Pang <36848472+yushao2@users.noreply.github.com> +- Nick Drozd : performance improvements to astroid - Dimitri Prybysh * multiple-imports, not-iterable, not-a-mapping, various patches. - Roy Williams (Lyft) @@ -51,7 +53,7 @@ contributors: * wrong-spelling-in-comment * wrong-spelling-in-docstring * parallel execution on multiple CPUs -- Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> +- Julthep Nandakwang - Bruno Daniel : check_docs extension. - Sushobhit <31987769+sushobhit27@users.noreply.github.com> (sushobhit27) * Added new check 'comparison-with-itself'. @@ -75,11 +77,11 @@ contributors: * ungrouped-imports, * wrong-import-position * redefined-variable-type +- Harutaka Kawamura - Alexandre Fayolle (Logilab): TkInter gui, documentation, debian support -- Nick Drozd : performance improvements to astroid +- Ville Skyttä - Julien Cristau (Logilab): python 3 support - Adrien Di Mascio -- Frank Harrison (doublethefish) - Moisés López (Vauxoo): * Support for deprecated-modules in modules not installed, * Refactor wrong-import-order to integrate it with `isort` library @@ -88,7 +90,8 @@ contributors: * Add consider-merging-isinstance, superfluous-else-return * Fix consider-using-ternary for 'True and True and True or True' case * Add bad-docstring-quotes and docstring-first-line-empty -- Ville Skyttä + * Add missing-timeout +- Frank Harrison (doublethefish) - Pierre-Yves David - David Shea : invalid sequence and slice index - Gunung P. Wibisono <55311527+gunungpw@users.noreply.github.com> @@ -96,7 +99,9 @@ contributors: - Cezar Elnazli : deprecated-method - Joseph Young <80432516+jpy-git@users.noreply.github.com> (jpy-git) - Tim Martin +- Tushar Sadhwani (tusharsadhwani) - Nicolas Chauvat +- orSolocate <38433858+orSolocate@users.noreply.github.com> - Radu Ciorba : not-context-manager and confusing-with-statement warnings. - Holger Peters - Cosmin Poieană : unichr-builtin and improvements to bad-open-mode. @@ -107,7 +112,6 @@ contributors: * autogenerated documentation for optional extensions, * bug fixes and enhancements for docparams (née check_docs) extension - Vlad Temian : redundant-unittest-assert and the JSON reporter. -- Tushar Sadhwani (tusharsadhwani) - Julien Jehannet - Boris Feld - Anthony Sottile @@ -115,6 +119,7 @@ contributors: - Julien Palard - David Liu (david-yz-liu) - Dan Goldsmith : support for msg-template in HTML reporter. +- Buck Evan - Mariatta Wijaya * Added new check `logging-fstring-interpolation` * Documentation typo fixes @@ -122,19 +127,23 @@ contributors: - Eli Fine (eli88fine): Fixed false positive duplicate code warning for lines with symbols only - Andrew Haigh (nelfin) - Émile Crater -- orSolocate <38433858+orSolocate@users.noreply.github.com> - Pavel Roskin - David Gilman - へーさん +- Yilei "Dolee" Yang - Thomas Hisch - Marianna Polatoglou : minor contribution for wildcard import check - Manuel Vázquez Acosta - Luis Escobar (Vauxoo): Add bad-docstring-quotes and docstring-first-line-empty +- Konstantina Saketou <56515303+ksaketou@users.noreply.github.com> +- Konstantin - Jim Robertson +- Hugo van Kemenade - Ethan Leba - Enji Cooper +- Drum Ogilvie - David Lindquist : logging-format-interpolation warning. -- Buck (Yelp) +- Daniel Harding - Anthony Truchet - Alexander Todorov : * added new error conditions to 'bad-super-call', @@ -146,8 +155,8 @@ contributors: - Téo Bouvard - Mihai Balint - Mark Bell -- Konstantina Saketou <56515303+ksaketou@users.noreply.github.com> -- Hugo van Kemenade +- Levi Gruspe +- Jakub Kuczys - Hornwitser : fix import graph - Fureigh - David Douard @@ -157,13 +166,17 @@ contributors: - Andreas Freimuth : fix indentation checking with tabs - Alexandru Coman - jpkotta +- Zen Lee <53538590+zenlyj@users.noreply.github.com> - Takahide Nojima - Taewon D. Kim - Sneaky Pete - Sergey B Kirpichev +- Sandro Tosi : Debian packaging - Rene Zhang +- Paul Lichtenberger - Or Bahari - Mr. Senko +- Mike Frysinger - Martin von Gagern (Google): Added 'raising-format-tuple' warning. - Martin Vielsmaier - Martin Pool (Google): @@ -174,20 +187,20 @@ contributors: * Added new check for shallow copy of os.environ * Added new check for useless `with threading.Lock():` statement - Marcus Näslund (naslundx) +- Marco Pernigotti <7657251+mpernigo@users.noreply.github.com> - Marco Forte - Ionel Maries Cristian - Gergely Kalmár -- Daniel Harding - Damien Baty - Benjamin Drung : contributing Debian Developer - Anubhav <35621759+anubh-v@users.noreply.github.com> -- Antonio Quarta (sgheppy) -- Andrew J. Simmons (anjsimmo) +- Antonio Quarta +- Andrew J. Simmons - wtracy -- Yilei "Dolee" Yang - chohner - Tiago Honorato <61059243+tiagohonorato@users.noreply.github.com> - Steven M. Vascellaro +- Robin Tweedie <70587124+robin-wayve@users.noreply.github.com> - Roberto Leinardi : PyCharm plugin maintainer - Ricardo Gemignani - Pieter Engelbrecht @@ -195,7 +208,6 @@ contributors: - Nicolas Dickreuter - Nick Bastin - Nathaniel Manista : suspicious lambda checking -- Mike Frysinger - Maksym Humetskyi (mhumetskyi) * Fixed ignored empty functions by similarities checker with "ignore-signatures" option enabled * Ignore function decorators signatures as well by similarities checker with "ignore-signatures" option enabled @@ -203,13 +215,15 @@ contributors: - Lucas Cimon - Kylian - Konstantin Manna -- Kai Mueller <15907922+kasium@users.noreply.github.com> (kasium) +- Kai Mueller <15907922+kasium@users.noreply.github.com> - Joshua Cannon - John Leach - James Morgensen : ignored-modules option applies to import errors. - Jaehoon Hwang (jaehoonhwang) +- Huw Jones +- Gideon <87426140+GideonBear@users.noreply.github.com> - Ganden Schaffner -- Frost Ming (frostming) +- Frost Ming - Federico Bond - Erik Wright - Erik Eriksson : Added overlapping-except error check. @@ -218,23 +232,28 @@ contributors: - Aurelien Campeas - Alexander Pervakov - Alain Leufroy +- Adam Williamson - xmo-odoo -- root@clnstor.am.local +- tbennett0 - omarandlorraine <64254276+omarandlorraine@users.noreply.github.com> -- grizzly.nyo@gmail.com - craig-sh - bernie gray - Wes Turner (Google): added new check 'inconsistent-quotes' - Tyler Thieding - Tobias Hernstig <30827238+thernstig@users.noreply.github.com> - Thomas Grainger +- Stavros Ntentos <133706+stdedos@users.noreply.github.com> +- Smixi - Simu Toni -- Sergei Lebedev <185856+superbobry@users.noreply.github.com> (superbobry) +- Sergei Lebedev <185856+superbobry@users.noreply.github.com> - Scott Worley +- Saugat Pachhai - Rémi Cardona +- Rogdham - Raphael Gaschignard - Ram Rachum (cool-RR) - Radostin Stoyanov +- Peter Bittner - Paul Renvoisé - PHeanEX - Omega Weapon @@ -250,33 +269,39 @@ contributors: - Maarten ter Huurne - Lefteris Karapetsas - LCD 47 -- Justin Li (justinnhli) +- Justin Li - John Kirkham - Jens H. Nielsen +- James Addison <55152140+jayaddison@users.noreply.github.com> - Ioana Tagirta : fix bad thread instantiation check - Ikraduya Edian : Added new checks 'consider-using-generator' and 'use-a-generator'. - Hugues Bruant - Harut - Grygorii Iermolenko +- Grizzly Nyo - Gabriel R. Sezefredo : Fixed "exception-escape" false positive with generators - Filipe Brandenburger - Fantix King (UChicago) +- Eric McDonald <221418+emcd@users.noreply.github.com> - Elias Dorneles : minor adjust to config defaults and docs - Derek Harland - David Pursehouse +- Daniel Mouritzen - Daniel Miller +- Christoph Blessing <33834216+cblessing24@users.noreply.github.com> - Chris Murray - Chris Lamb - Charles Hebert - Carli Freudenberg (CarliJoy) * Fixed issue 5281, added Unicode checker * Improve non-ascii-name checker -- Buck Golemon +- Bruce Dawson - Brian Shaginaw : prevent error on exception check for functions - Benny Mueller - Ben James - Ben Green - Batuhan Taskaya +- Alvaro Frias - Alexander Kapshuna - Adam Parkin - 谭九鼎 <109224573@qq.com> @@ -286,30 +311,31 @@ contributors: - ttenhoeve-aa - thinwybk - syutbai +- sur.la.route <17788706+christopherpickering@users.noreply.github.com> - sdet_liang -- pyves@crater.logilab.fr - paschich - oittaa <8972248+oittaa@users.noreply.github.com> - nyabkun <75878387+nyabkun@users.noreply.github.com> - moxian - mar-chi-pan -- ludal@logilab.fr - lrjball <50599110+lrjball@users.noreply.github.com> - laike9m +- kriek - jaydesl <35102795+jaydesl@users.noreply.github.com> - jab - glmdgrielson <32415403+glmdgrielson@users.noreply.github.com> - glegoux - gaurikholkar - flyingbot91 +- fly - fahhem - fadedDexofan +- epenet <6771947+epenet@users.noreply.github.com> - danields - cosven - cordis-dev - bluesheeptoken - anatoly techtonik -- amdev@AMDEV-WS01.cisco.com - agutole - Zeckie <49095968+Zeckie@users.noreply.github.com> - Zeb Nicholls @@ -318,7 +344,7 @@ contributors: - Yury Gribov - Yuri Bochkarev : Added epytext support to docparams extension. - Youngsoo Sung -- Yory <39745367+yory8@users.noreply.github.com> (yory8) +- Yory <39745367+yory8@users.noreply.github.com> - Yoichi Nakayama - Yeting Li (yetingli) - Yannack @@ -327,6 +353,7 @@ contributors: - Xi Shen - Will Shanks - Viorel Știrbu : intern-builtin warning. +- VictorT - Victor Jiajunsu <16359131+jiajunsu@users.noreply.github.com> - Trevor Bekolay * Added --list-msgs-enabled command @@ -334,6 +361,7 @@ contributors: - Tomasz Magulski - Tim Hatch - Tim Gates +- Thomas Benhamou - Tanvi Moharir <74228962+tanvimoharir@users.noreply.github.com>: Fix for invalid toml config - T.Rzepka - Svetoslav Neykov @@ -343,39 +371,40 @@ contributors: - Sorin Sbarnea - Slavfox - Skip Montanaro +- Sigurd Spieckermann <2206639+sisp@users.noreply.github.com> - Shiv Venkatasubrahmanyam - Sebastian Müller -- Saugat Pachhai - Sasha Bagan - Sardorbek Imomaliev - Santiago Castro -- Sandro Tosi : Debian packaging - Samuel Freilich (sfreilich) - Samuel FORESTIER -- Sam Vermeiren <88253337+PaaEl@users.noreply.github.com> (PaaEl) +- Sam Vermeiren <88253337+PaaEl@users.noreply.github.com> - Ryan McGuire - Ry4an Brase - Ruro - Roman Ivanov -- Robin Tweedie <70587124+robin-wayve@users.noreply.github.com> - Robert Schweizer +- Robert Hofer - Reverb Chu - Renat Galimov - Rebecca Turner (9999years) - Randall Leeds +- Ramon Saraiva - Ramiro Leal-Cavazos (ramiro050): Fixed bug preventing pylint from working with Emacs tramp - Qwiddle13 <32040075+Qwiddle13@users.noreply.github.com> - Quentin Young +- Prajwal Borkar - Petr Pulc : require whitespace around annotations - Peter Dawyndt -- Peter Bittner +- Peter Dave Hello - Peter Aronoff - Paul Cochrane -- Paul Lichtenberger - Patrik - Pascal Corpet - Pablo Galindo Salgado * Fix false positive 'Non-iterable value' with async comprehensions. +- Osher De Paz - Oisín Moran - Obscuron - Noam Yorav-Raphael @@ -396,18 +425,19 @@ contributors: - Michael Kefeder - Michael Hudson-Doyle - Michael Giuffrida -- Melvin Hazeleger <31448155+melvio@users.noreply.github.com> (melvio) +- Melvin Hazeleger <31448155+melvio@users.noreply.github.com> - Matěj Grabovský - Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> - Matej Marušák - Markus Siebenhaar <41283549+siehar@users.noreply.github.com> -- Marco Pernigotti <7657251+mpernigo@users.noreply.github.com> - Marco Edward Gorelli : Documented Jupyter integration - Marcin Kurczewski (rr-) - Maik Röder +- Lumír 'Frenzy' Balhar +- Ludovic Aubry - Louis Sautier -- Lorena Buciu <46202743+lorena-b@users.noreply.github.com> (lorena-b) -- Logan Miller <14319179+komodo472@users.noreply.github.com> (komodo472) +- Lorena Buciu <46202743+lorena-b@users.noreply.github.com> +- Logan Miller <14319179+komodo472@users.noreply.github.com> - Kári Tristan Helgason - Kurian Benoy <70306694+kurianbenoy-aot@users.noreply.github.com> - Krzysztof Czapla @@ -420,8 +450,9 @@ contributors: - Kevin Phillips - Kevin Jing Qiu - Kayran Schmidt <59456929+yumasheta@users.noreply.github.com> +- Karthik Nadig - Jürgen Hermann -- Jérome Perrin (perrinjerome) +- Jérome Perrin - Josselin Feist - Jonathan Kotta - John Paraskevopoulos : add 'differing-param-doc' and 'differing-type-doc' @@ -429,60 +460,69 @@ contributors: - John Gabriele - John Belmonte - Joffrey Mander -- Jochen Preusche (iilei) -- Jeroen Seegers (jeroenseegers) +- Jochen Preusche +- Jeroen Seegers : * Fixed `toml` dependency issue -- Jeremy Fleischman (jfly) +- Jeremy Fleischman - Jason Owen - Jared Garst - Jared Deckard - Janne Rönkkö -- James Sinclair (irgeek) +- James Sinclair - James M. Allen - James Lingard - James Broadhead +- Jakub Kulík - Jakob Normark -- Jake Lishman (jakelishman) +- Jake Lishman - Jacques Kvam - Jace Browning : updated default report format with clickable paths - JT Olds -- Huw Jones (huwcbjones) -- Hayden Richards <62866982+SupImDos@users.noreply.github.com> (SupImDos) +- Hayden Richards <62866982+SupImDos@users.noreply.github.com> * Fixed "no-self-use" for async methods * Fixed "docparams" extension for async functions and methods -- Harshil <37377066+harshil21@users.noreply.github.com> (harshil21) +- Harshil <37377066+harshil21@users.noreply.github.com> - Harry +- Grégoire <96051754+gregoire-mullvad@users.noreply.github.com> - Grant Welch - Giuseppe Valente - Gary Tyler McLeod -- Felix von Drigalski (felixvd) +- Felix von Drigalski - Fabrice Douchant - Fabio Natali - Fabian Damken - Eric Froemling - Emmanuel Chaudron - Elizabeth Bott <52465744+elizabethbott@users.noreply.github.com> -- Eisuke Kawashima (e-kwsm) +- Eisuke Kawashima - Edward K. Ream - Edgemaster +- Eddie Darling - Drew Risinger - Dr. Nick - Don Jayamanne - Dmytro Kyrychuk +- DetachHead <57028336+DetachHead@users.noreply.github.com> - Denis Laxalde +- David Lawson - David Cain +- Dave Bunten - Danny Hermes - Daniele Procida - Daniela Plascencia +- Daniel Werner - Daniel R. Neal (danrneal) - Daniel Draper - Daniel Dorani (doranid) - Daniel Brookman <53625739+dbrookman@users.noreply.github.com> - Dan Garrette - Damien Nozay +- Cubicpath - Craig Citro +- Cosmo - Clément Pit-Claudel - Christopher Zurcher +- Christian Clauss - Carl Crowder : don't evaluate the value of arguments for 'dangerous-default-value' - Carey Metcalfe : demoted `try-except-raise` from error to warning - Cameron Olechowski @@ -490,12 +530,13 @@ contributors: - Caio Carrara - C.A.M. Gerlach - Bruno P. Kinoshita -- Bruce Dawson +- Brice Chardin - Brian C. Lane - Brandon W Maister - BioGeek - Benjamin Graham - Benedikt Morbach +- Ben Greiner - Banjamin Freeman - Athos Ribeiro : Fixed dict-keys-not-iterating false positive for inverse containment checks - Arun Persaud @@ -524,6 +565,7 @@ contributors: - Aditya Gupta (adityagupta1089) * Added ignore_signatures to duplicate checker - Adam Dangoor +- 243f6a88 85a308d3 <33170174+243f6a8885a308d313198a2e037@users.noreply.github.com> Co-Author diff --git a/Dockerfile b/Dockerfile index 2667145da8..976a56d475 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,6 @@ FROM python:3.10.0-alpine3.15 COPY ./ /tmp/build WORKDIR /tmp/build -RUN python setup.py install && rm -rf /tmp/build +RUN python -m pip install --no-cache-dir . && rm -rf /tmp/build ENTRYPOINT ["pylint"] diff --git a/MANIFEST.in b/MANIFEST.in index d354668948..9561fb1061 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,13 +1 @@ -recursive-exclude pylint *.rst -prune .github -prune doc -prune elisp -prune examples -prune tests -prune script -exclude .* -exclude Dockerfile -exclude README.rst -exclude pylintrc -exclude requirements_*.txt -exclude tox.ini +include README.rst diff --git a/README.rst b/README.rst index 67fd09c159..c6f212d139 100644 --- a/README.rst +++ b/README.rst @@ -8,8 +8,8 @@ .. image:: https://github.com/PyCQA/pylint/actions/workflows/tests.yaml/badge.svg?branch=main :target: https://github.com/PyCQA/pylint/actions -.. image:: https://coveralls.io/repos/github/PyCQA/pylint/badge.svg?branch=main - :target: https://coveralls.io/github/PyCQA/pylint?branch=main +.. image:: https://codecov.io/gh/PyCQA/pylint/branch/main/graph/badge.svg?token=ZETEzayrfk + :target: https://codecov.io/gh/PyCQA/pylint .. image:: https://img.shields.io/pypi/v/pylint.svg :alt: Pypi Package version @@ -29,6 +29,19 @@ :target: https://results.pre-commit.ci/latest/github/PyCQA/pylint/main :alt: pre-commit.ci status +.. image:: https://bestpractices.coreinfrastructure.org/projects/6328/badge + :target: https://bestpractices.coreinfrastructure.org/projects/6328 + :alt: CII Best Practices + +.. image:: https://api.securityscorecards.dev/projects/github.com/PyCQA/pylint/badge + :target: https://api.securityscorecards.dev/projects/github.com/PyCQA/pylint + :alt: OpenSSF Scorecard + +.. image:: https://img.shields.io/discord/825463413634891776.svg + :target: https://discord.gg/qYxpadCgkx + :alt: Discord + + What is Pylint? ================ @@ -46,12 +59,22 @@ will know that ``argparse.error(...)`` is in fact a logging call and not an argp .. _`code smells`: https://martinfowler.com/bliki/CodeSmell.html Pylint is highly configurable and permits to write plugins in order to add your -own checks (for example, for internal libraries or an internal rule). Pylint has an -ecosystem of existing plugins for popular frameworks such as `pylint-django`_ or -`pylint-i18n`_. +own checks (for example, for internal libraries or an internal rule). Pylint also has an +ecosystem of existing plugins for popular frameworks and third party libraries. +.. note:: + + Pylint supports the Python standard library out of the box. Third-party + libraries are not always supported, so a plugin might be needed. A good place + to start is ``PyPI`` which often returns a plugin by searching for + ``pylint ``. `pylint-pydantic`_, `pylint-django`_ and + `pylint-sonarjson`_ are examples of such plugins. More information about plugins + and how to load them can be found at `plugins`_. + +.. _`plugins`: https://pylint.pycqa.org/en/latest/development_guide/how_tos/plugins.html#plugins +.. _`pylint-pydantic`: https://pypi.org/project/pylint-pydantic .. _`pylint-django`: https://github.com/PyCQA/pylint-django -.. _`pylint-i18n`: https://github.com/amandasaurus/python-pylint-i18n +.. _`pylint-sonarjson`: https://github.com/omegacen/pylint-sonarjson Pylint isn't smarter than you: it may warn you about things that you have conscientiously done or check for some things that you don't care about. @@ -64,11 +87,15 @@ Pylint ships with three additional tools: - pyreverse_ (standalone tool that generates package and class diagrams.) - symilar_ (duplicate code finder that is also integrated in pylint) -- epylint_ (Emacs and Flymake compatible Pylint) .. _pyreverse: https://pylint.pycqa.org/en/latest/pyreverse.html .. _symilar: https://pylint.pycqa.org/en/latest/symilar.html + +The epylint_ Emacs package, which includes Flymake support, is now maintained +in `its own repository`_. + .. _epylint: https://pylint.pycqa.org/en/latest/user_guide/ide_integration/flymake-emacs.html +.. _its own repository: https://github.com/emacsorphanage/pylint Projects that you might want to use alongside pylint include flake8_ (faster and simpler checks with very few false positives), mypy_, pyright_ or pyre_ (typing checks), bandit_ (security @@ -76,7 +103,7 @@ oriented checks), black_ and isort_ (auto-formatting), autoflake_ (automated rem unused imports or variables), pyupgrade_ (automated upgrade to newer python syntax) and pydocstringformatter_ (automated pep257). -.. _flake8: https://gitlab.com/pycqa/flake8/ +.. _flake8: https://github.com/PyCQA/flake8 .. _bandit: https://github.com/PyCQA/bandit .. _mypy: https://github.com/python/mypy .. _pyright: https://github.com/microsoft/pyright diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..c45517a33a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + patch: + default: + target: 100% + project: + default: + target: 95% +comment: + layout: "reach, diff, flags, files" diff --git a/doc/conf.py b/doc/conf.py index 3f0c1177b4..b65cdd8a0b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. +# documentation root, use 'os.path.abspath' to make it absolute, like shown here. sys.path.append(os.path.abspath("exts")) # -- General configuration ----------------------------------------------------- @@ -131,7 +131,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ["_build", "data/**"] +exclude_patterns = ["_build", "data/**", "whatsnew/fragments"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None @@ -296,4 +296,8 @@ # through including multiple documents autosectionlabel_prefix_document = True +# Permit duplicated titles in the resulting document. +# See https://github.com/PyCQA/pylint/issues/7362#issuecomment-1256932866 +autosectionlabel_maxdepth = 2 + linkcheck_ignore = ["https://github.com/PyCQA/pylint/blob/main/pylint/extensions/.*"] diff --git a/doc/contact.rst b/doc/contact.rst index c6d0e841ec..5c3920dfc2 100644 --- a/doc/contact.rst +++ b/doc/contact.rst @@ -37,8 +37,8 @@ https://mail.python.org/mailman3/lists/code-quality.python.org/ Archives are available at https://mail.python.org/pipermail/code-quality/ -Archives before April 2013 are available at -https://lists.logilab.org/pipermail/python-projects/ +Archives before April 2013 are not available anymore. At +https://mail.python.org/pipermail/ it was under ``python-projects``. Support ------- diff --git a/doc/data/.flake8 b/doc/data/.flake8 new file mode 100644 index 0000000000..676bedc0d5 --- /dev/null +++ b/doc/data/.flake8 @@ -0,0 +1,6 @@ +[flake8] +select = + E501, +# Reading ease is drastically reduced on read the doc after 103 chars +# (Because of horizontal scrolling) +max-line-length=103 diff --git a/doc/data/messages/a/anomalous-unicode-escape-in-string/bad.py b/doc/data/messages/a/anomalous-unicode-escape-in-string/bad.py index 40a2a0caff..40275f0551 100644 --- a/doc/data/messages/a/anomalous-unicode-escape-in-string/bad.py +++ b/doc/data/messages/a/anomalous-unicode-escape-in-string/bad.py @@ -1 +1 @@ -print(b"\u{0}".format("0394")) # [anomalous-unicode-escape-in-string] +print(b"\u%b" % b"0394") # [anomalous-unicode-escape-in-string] diff --git a/doc/data/messages/a/anomalous-unicode-escape-in-string/good.py b/doc/data/messages/a/anomalous-unicode-escape-in-string/good.py index f2285d70c9..c5f4cf46b5 100644 --- a/doc/data/messages/a/anomalous-unicode-escape-in-string/good.py +++ b/doc/data/messages/a/anomalous-unicode-escape-in-string/good.py @@ -1 +1 @@ -print(b"\\u{0}".format("0394")) +print(b"\\u%b" % b"0394") diff --git a/doc/data/messages/a/astroid-error/details.rst b/doc/data/messages/a/astroid-error/details.rst index ab82045295..96e8f7ade7 100644 --- a/doc/data/messages/a/astroid-error/details.rst +++ b/doc/data/messages/a/astroid-error/details.rst @@ -1 +1,2 @@ -You can help us make the doc better `by contributing `_ ! +This is a message linked to an internal problem in pylint. There's nothing to change in your code, +but maybe in pylint's configuration or installation. diff --git a/doc/data/messages/a/astroid-error/good.py b/doc/data/messages/a/astroid-error/good.py deleted file mode 100644 index c40beb573f..0000000000 --- a/doc/data/messages/a/astroid-error/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/b/bad-builtin/pylintrc b/doc/data/messages/b/bad-builtin/pylintrc new file mode 100644 index 0000000000..d5dfb0cce8 --- /dev/null +++ b/doc/data/messages/b/bad-builtin/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins = pylint.extensions.bad_builtin diff --git a/doc/data/messages/b/bad-docstring-quotes/bad.py b/doc/data/messages/b/bad-docstring-quotes/bad.py new file mode 100644 index 0000000000..561921db00 --- /dev/null +++ b/doc/data/messages/b/bad-docstring-quotes/bad.py @@ -0,0 +1,3 @@ +def foo(): # [bad-docstring-quotes] + 'Docstring.' + return diff --git a/doc/data/messages/b/bad-docstring-quotes/details.rst b/doc/data/messages/b/bad-docstring-quotes/details.rst index ab82045295..2a3add6814 100644 --- a/doc/data/messages/b/bad-docstring-quotes/details.rst +++ b/doc/data/messages/b/bad-docstring-quotes/details.rst @@ -1 +1,2 @@ -You can help us make the doc better `by contributing `_ ! +From `PEP 257`: + "For consistency, always use ``"""triple double quotes"""`` around docstrings." diff --git a/doc/data/messages/b/bad-docstring-quotes/good.py b/doc/data/messages/b/bad-docstring-quotes/good.py index c40beb573f..e5f6ceb590 100644 --- a/doc/data/messages/b/bad-docstring-quotes/good.py +++ b/doc/data/messages/b/bad-docstring-quotes/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +def foo(): + """Docstring.""" + return diff --git a/doc/data/messages/b/bad-docstring-quotes/pylintrc b/doc/data/messages/b/bad-docstring-quotes/pylintrc new file mode 100644 index 0000000000..6acf217beb --- /dev/null +++ b/doc/data/messages/b/bad-docstring-quotes/pylintrc @@ -0,0 +1,2 @@ +[main] +load-plugins=pylint.extensions.docstyle diff --git a/doc/data/messages/b/bad-docstring-quotes/related.rst b/doc/data/messages/b/bad-docstring-quotes/related.rst new file mode 100644 index 0000000000..bec6174927 --- /dev/null +++ b/doc/data/messages/b/bad-docstring-quotes/related.rst @@ -0,0 +1 @@ +- `PEP 257 – Docstring Conventions `_ diff --git a/doc/data/messages/b/bad-dunder-name/bad.py b/doc/data/messages/b/bad-dunder-name/bad.py new file mode 100644 index 0000000000..f01f65010e --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/bad.py @@ -0,0 +1,6 @@ +class Apples: + def _init_(self): # [bad-dunder-name] + pass + + def __hello__(self): # [bad-dunder-name] + print("hello") diff --git a/doc/data/messages/b/bad-dunder-name/good.py b/doc/data/messages/b/bad-dunder-name/good.py new file mode 100644 index 0000000000..4f0adb9b62 --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/good.py @@ -0,0 +1,6 @@ +class Apples: + def __init__(self): + pass + + def hello(self): + print("hello") diff --git a/doc/data/messages/b/bad-dunder-name/pylintrc b/doc/data/messages/b/bad-dunder-name/pylintrc new file mode 100644 index 0000000000..c70980544f --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.dunder diff --git a/doc/data/messages/b/bad-exception-context/bad.py b/doc/data/messages/b/bad-exception-cause/bad.py similarity index 75% rename from doc/data/messages/b/bad-exception-context/bad.py rename to doc/data/messages/b/bad-exception-cause/bad.py index ef198cb9ac..ad4228af82 100644 --- a/doc/data/messages/b/bad-exception-context/bad.py +++ b/doc/data/messages/b/bad-exception-cause/bad.py @@ -3,5 +3,6 @@ def divide(x, y): try: result = x / y except ZeroDivisionError: - raise ValueError(f"Division by zero when dividing {x} by {y} !") from result # [bad-exception-context] + # +1: [bad-exception-cause] + raise ValueError(f"Division by zero when dividing {x} by {y} !") from result return result diff --git a/doc/data/messages/b/bad-exception-context/good.py b/doc/data/messages/b/bad-exception-cause/good.py similarity index 100% rename from doc/data/messages/b/bad-exception-context/good.py rename to doc/data/messages/b/bad-exception-cause/good.py diff --git a/doc/data/messages/b/bad-exception-context/related.rst b/doc/data/messages/b/bad-exception-cause/related.rst similarity index 100% rename from doc/data/messages/b/bad-exception-context/related.rst rename to doc/data/messages/b/bad-exception-cause/related.rst diff --git a/doc/data/messages/b/bad-format-string-key/bad.py b/doc/data/messages/b/bad-format-string-key/bad.py new file mode 100644 index 0000000000..346d02d149 --- /dev/null +++ b/doc/data/messages/b/bad-format-string-key/bad.py @@ -0,0 +1 @@ +print("%(one)d" % {"one": 1, 2: 2}) # [bad-format-string-key] diff --git a/doc/data/messages/b/bad-format-string-key/details.rst b/doc/data/messages/b/bad-format-string-key/details.rst index ab82045295..321b4a0baf 100644 --- a/doc/data/messages/b/bad-format-string-key/details.rst +++ b/doc/data/messages/b/bad-format-string-key/details.rst @@ -1 +1,6 @@ -You can help us make the doc better `by contributing `_ ! +This check only works for old-style string formatting using the '%' operator. + +This check only works if the dictionary with the values to be formatted is defined inline. +Passing a variable will not trigger the check as the other keys in this dictionary may be +used in other contexts, while an inline defined dictionary is clearly only intended to hold +the values that should be formatted. diff --git a/doc/data/messages/b/bad-format-string-key/good.py b/doc/data/messages/b/bad-format-string-key/good.py index c40beb573f..db7cfde04a 100644 --- a/doc/data/messages/b/bad-format-string-key/good.py +++ b/doc/data/messages/b/bad-format-string-key/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +print("%(one)d, %(two)d" % {"one": 1, "two": 2}) diff --git a/doc/data/messages/b/bad-inline-option/bad.py b/doc/data/messages/b/bad-inline-option/bad.py new file mode 100644 index 0000000000..b244da97a2 --- /dev/null +++ b/doc/data/messages/b/bad-inline-option/bad.py @@ -0,0 +1,2 @@ +# 2:[bad-inline-option] +# pylint: disable line-too-long diff --git a/doc/data/messages/b/bad-inline-option/details.rst b/doc/data/messages/b/bad-inline-option/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/b/bad-inline-option/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/b/bad-inline-option/good.py b/doc/data/messages/b/bad-inline-option/good.py index c40beb573f..9799fff2ce 100644 --- a/doc/data/messages/b/bad-inline-option/good.py +++ b/doc/data/messages/b/bad-inline-option/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +# pylint: disable=line-too-long diff --git a/doc/data/messages/b/bad-thread-instantiation/bad.py b/doc/data/messages/b/bad-thread-instantiation/bad.py new file mode 100644 index 0000000000..580786d853 --- /dev/null +++ b/doc/data/messages/b/bad-thread-instantiation/bad.py @@ -0,0 +1,9 @@ +import threading + + +def thread_target(n): + print(n ** 2) + + +thread = threading.Thread(lambda: None) # [bad-thread-instantiation] +thread.start() diff --git a/doc/data/messages/b/bad-thread-instantiation/details.rst b/doc/data/messages/b/bad-thread-instantiation/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/b/bad-thread-instantiation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/b/bad-thread-instantiation/good.py b/doc/data/messages/b/bad-thread-instantiation/good.py index c40beb573f..735fa4da13 100644 --- a/doc/data/messages/b/bad-thread-instantiation/good.py +++ b/doc/data/messages/b/bad-thread-instantiation/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +import threading + + +def thread_target(n): + print(n ** 2) + + +thread = threading.Thread(target=thread_target, args=(10,)) +thread.start() diff --git a/doc/data/messages/b/bare-except/bad.py b/doc/data/messages/b/bare-except/bad.py index fc88c6fd3e..33dea31287 100644 --- a/doc/data/messages/b/bare-except/bad.py +++ b/doc/data/messages/b/bare-except/bad.py @@ -1,4 +1,4 @@ try: - 1 / 0 + import platform_specific_module except: # [bare-except] - pass + platform_specific_module = None diff --git a/doc/data/messages/b/bare-except/details.rst b/doc/data/messages/b/bare-except/details.rst new file mode 100644 index 0000000000..e93c7ea01c --- /dev/null +++ b/doc/data/messages/b/bare-except/details.rst @@ -0,0 +1,3 @@ +A good rule of thumb is to limit use of bare ‘except’ clauses to two cases: +- If the exception handler will be printing out or logging the traceback; at least the user will be aware that an error has occurred. +- If the code needs to do some cleanup work, but then lets the exception propagate upwards with raise. ``try...finally`` can be a better way to handle this case. diff --git a/doc/data/messages/b/bare-except/good.py b/doc/data/messages/b/bare-except/good.py index b02b365b06..24baac945b 100644 --- a/doc/data/messages/b/bare-except/good.py +++ b/doc/data/messages/b/bare-except/good.py @@ -1,4 +1,4 @@ try: - 1 / 0 -except ZeroDivisionError: - pass + import platform_specific_module +except ImportError: + platform_specific_module = None diff --git a/doc/data/messages/b/bare-except/related.rst b/doc/data/messages/b/bare-except/related.rst new file mode 100644 index 0000000000..978b57937a --- /dev/null +++ b/doc/data/messages/b/bare-except/related.rst @@ -0,0 +1 @@ +- `Programming recommendation in PEP8 `_ diff --git a/doc/data/messages/b/broad-except/bad.py b/doc/data/messages/b/broad-except/bad.py deleted file mode 100644 index f4946093e8..0000000000 --- a/doc/data/messages/b/broad-except/bad.py +++ /dev/null @@ -1,4 +0,0 @@ -try: - 1 / 0 -except Exception: # [broad-except] - pass diff --git a/doc/data/messages/b/broad-except/good.py b/doc/data/messages/b/broad-except/good.py deleted file mode 100644 index b02b365b06..0000000000 --- a/doc/data/messages/b/broad-except/good.py +++ /dev/null @@ -1,4 +0,0 @@ -try: - 1 / 0 -except ZeroDivisionError: - pass diff --git a/doc/data/messages/b/broad-exception-caught/bad.py b/doc/data/messages/b/broad-exception-caught/bad.py new file mode 100644 index 0000000000..67d5d7b493 --- /dev/null +++ b/doc/data/messages/b/broad-exception-caught/bad.py @@ -0,0 +1,4 @@ +try: + import platform_specific_module +except Exception: # [broad-exception-caught] + platform_specific_module = None diff --git a/doc/data/messages/b/broad-exception-caught/details.rst b/doc/data/messages/b/broad-exception-caught/details.rst new file mode 100644 index 0000000000..ae7f1638cd --- /dev/null +++ b/doc/data/messages/b/broad-exception-caught/details.rst @@ -0,0 +1,3 @@ +For example, you're trying to import a library with required system dependencies and you catch +everything instead of only import errors, you will miss the error message telling you, that +your code could work if you had installed the system dependencies. diff --git a/doc/data/messages/b/broad-exception-caught/good.py b/doc/data/messages/b/broad-exception-caught/good.py new file mode 100644 index 0000000000..24baac945b --- /dev/null +++ b/doc/data/messages/b/broad-exception-caught/good.py @@ -0,0 +1,4 @@ +try: + import platform_specific_module +except ImportError: + platform_specific_module = None diff --git a/doc/data/messages/b/broad-exception-caught/related.rst b/doc/data/messages/b/broad-exception-caught/related.rst new file mode 100644 index 0000000000..23c74d2ac3 --- /dev/null +++ b/doc/data/messages/b/broad-exception-caught/related.rst @@ -0,0 +1 @@ +- `Should I always specify an exception type in 'except' statements? `_ diff --git a/doc/data/messages/b/broad-exception-raised/bad.py b/doc/data/messages/b/broad-exception-raised/bad.py new file mode 100644 index 0000000000..4c8ff3b5a3 --- /dev/null +++ b/doc/data/messages/b/broad-exception-raised/bad.py @@ -0,0 +1,4 @@ +def small_apple(apple, length): + if len(apple) < length: + raise Exception("Apple is too small!") # [broad-exception-raised] + print(f"{apple} is proper size.") diff --git a/doc/data/messages/b/broad-exception-raised/good.py b/doc/data/messages/b/broad-exception-raised/good.py new file mode 100644 index 0000000000..a63b1b3560 --- /dev/null +++ b/doc/data/messages/b/broad-exception-raised/good.py @@ -0,0 +1,4 @@ +def small_apple(apple, length): + if len(apple) < length: + raise ValueError("Apple is too small!") + print(f"{apple} is proper size.") diff --git a/doc/data/messages/b/broad-exception-raised/related.rst b/doc/data/messages/b/broad-exception-raised/related.rst new file mode 100644 index 0000000000..978b57937a --- /dev/null +++ b/doc/data/messages/b/broad-exception-raised/related.rst @@ -0,0 +1 @@ +- `Programming recommendation in PEP8 `_ diff --git a/doc/data/messages/b/broken-noreturn/bad.py b/doc/data/messages/b/broken-noreturn/bad.py new file mode 100644 index 0000000000..77baf763bf --- /dev/null +++ b/doc/data/messages/b/broken-noreturn/bad.py @@ -0,0 +1,5 @@ +from typing import NoReturn, Union + + +def exploding_apple(apple) -> Union[None, NoReturn]: # [broken-noreturn] + print(f"{apple} is about to explode") diff --git a/doc/data/messages/b/broken-noreturn/details.rst b/doc/data/messages/b/broken-noreturn/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/b/broken-noreturn/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/b/broken-noreturn/good.py b/doc/data/messages/b/broken-noreturn/good.py index c40beb573f..ce4dc6e988 100644 --- a/doc/data/messages/b/broken-noreturn/good.py +++ b/doc/data/messages/b/broken-noreturn/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +from typing import NoReturn + + +def exploding_apple(apple) -> NoReturn: + print(f"{apple} is about to explode") + raise Exception("{apple} exploded !") diff --git a/doc/data/messages/b/broken-noreturn/pylintrc b/doc/data/messages/b/broken-noreturn/pylintrc new file mode 100644 index 0000000000..eb28fc75b9 --- /dev/null +++ b/doc/data/messages/b/broken-noreturn/pylintrc @@ -0,0 +1,3 @@ +[main] +py-version=3.7 +load-plugins=pylint.extensions.typing diff --git a/doc/data/messages/c/class-variable-slots-conflict/bad.py b/doc/data/messages/c/class-variable-slots-conflict/bad.py new file mode 100644 index 0000000000..7705669620 --- /dev/null +++ b/doc/data/messages/c/class-variable-slots-conflict/bad.py @@ -0,0 +1,15 @@ +class Person: + # +1: [class-variable-slots-conflict, class-variable-slots-conflict, class-variable-slots-conflict] + __slots__ = ("age", "name", "say_hi") + name = None + + def __init__(self, age, name): + self.age = age + self.name = name + + @property + def age(self): + return self.age + + def say_hi(self): + print(f"Hi, I'm {self.name}.") diff --git a/doc/data/messages/c/class-variable-slots-conflict/details.rst b/doc/data/messages/c/class-variable-slots-conflict/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/class-variable-slots-conflict/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/class-variable-slots-conflict/good.py b/doc/data/messages/c/class-variable-slots-conflict/good.py index c40beb573f..b5324bff0f 100644 --- a/doc/data/messages/c/class-variable-slots-conflict/good.py +++ b/doc/data/messages/c/class-variable-slots-conflict/good.py @@ -1 +1,13 @@ -# This is a placeholder for correct code for this message. +class Person: + __slots__ = ("_age", "name",) + + def __init__(self, age, name): + self._age = age + self.name = name + + @property + def age(self): + return self._age + + def say_hi(self): + print(f"Hi, I'm {self.name}.") diff --git a/doc/data/messages/c/compare-to-empty-string/bad.py b/doc/data/messages/c/compare-to-empty-string/bad.py new file mode 100644 index 0000000000..1ab940de7b --- /dev/null +++ b/doc/data/messages/c/compare-to-empty-string/bad.py @@ -0,0 +1,8 @@ +x = "" +y = "hello" + +if x == "": # [compare-to-empty-string] + print("x is an empty string") + +if y != "": # [compare-to-empty-string] + print("y is not an empty string") diff --git a/doc/data/messages/c/compare-to-empty-string/details.rst b/doc/data/messages/c/compare-to-empty-string/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/compare-to-empty-string/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/compare-to-empty-string/good.py b/doc/data/messages/c/compare-to-empty-string/good.py index c40beb573f..6c4c67e36f 100644 --- a/doc/data/messages/c/compare-to-empty-string/good.py +++ b/doc/data/messages/c/compare-to-empty-string/good.py @@ -1 +1,8 @@ -# This is a placeholder for correct code for this message. +x = "" +y = "hello" + +if not x: + print("x is an empty string") + +if y: + print("y is not an empty string") diff --git a/doc/data/messages/c/compare-to-empty-string/pylintrc b/doc/data/messages/c/compare-to-empty-string/pylintrc new file mode 100644 index 0000000000..13b9afd7e2 --- /dev/null +++ b/doc/data/messages/c/compare-to-empty-string/pylintrc @@ -0,0 +1,2 @@ +[main] +load-plugins=pylint.extensions.emptystring diff --git a/doc/data/messages/c/compare-to-zero/bad.py b/doc/data/messages/c/compare-to-zero/bad.py new file mode 100644 index 0000000000..a6b64a4079 --- /dev/null +++ b/doc/data/messages/c/compare-to-zero/bad.py @@ -0,0 +1,8 @@ +x = 0 +y = 1 + +if x == 0: # [compare-to-zero] + print("x is equal to zero") + +if y != 0: # [compare-to-zero] + print("y is not equal to zero") diff --git a/doc/data/messages/c/compare-to-zero/details.rst b/doc/data/messages/c/compare-to-zero/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/compare-to-zero/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/compare-to-zero/good.py b/doc/data/messages/c/compare-to-zero/good.py index c40beb573f..bea3733e26 100644 --- a/doc/data/messages/c/compare-to-zero/good.py +++ b/doc/data/messages/c/compare-to-zero/good.py @@ -1 +1,8 @@ -# This is a placeholder for correct code for this message. +x = 0 +y = 1 + +if not x: + print("x is equal to zero") + +if y: + print("y is not equal to zero") diff --git a/doc/data/messages/c/compare-to-zero/pylintrc b/doc/data/messages/c/compare-to-zero/pylintrc new file mode 100644 index 0000000000..895291f848 --- /dev/null +++ b/doc/data/messages/c/compare-to-zero/pylintrc @@ -0,0 +1,2 @@ +[main] +load-plugins=pylint.extensions.comparetozero diff --git a/doc/data/messages/c/condition-evals-to-constant/bad.py b/doc/data/messages/c/condition-evals-to-constant/bad.py new file mode 100644 index 0000000000..f52b24fc01 --- /dev/null +++ b/doc/data/messages/c/condition-evals-to-constant/bad.py @@ -0,0 +1,2 @@ +def is_a_fruit(fruit): + return bool(fruit in {"apple", "orange"} or True) # [condition-evals-to-constant] diff --git a/doc/data/messages/c/condition-evals-to-constant/details.rst b/doc/data/messages/c/condition-evals-to-constant/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/condition-evals-to-constant/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/condition-evals-to-constant/good.py b/doc/data/messages/c/condition-evals-to-constant/good.py index c40beb573f..37e9754910 100644 --- a/doc/data/messages/c/condition-evals-to-constant/good.py +++ b/doc/data/messages/c/condition-evals-to-constant/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def is_a_fruit(fruit): + return fruit in {"apple", "orange"} diff --git a/doc/data/messages/c/config-parse-error/details.rst b/doc/data/messages/c/config-parse-error/details.rst index ab82045295..4fc0fe0768 100644 --- a/doc/data/messages/c/config-parse-error/details.rst +++ b/doc/data/messages/c/config-parse-error/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing `_ ! +This is a message linked to a problem in your configuration not your code. diff --git a/doc/data/messages/c/config-parse-error/good.py b/doc/data/messages/c/config-parse-error/good.py deleted file mode 100644 index c40beb573f..0000000000 --- a/doc/data/messages/c/config-parse-error/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/c/confusing-with-statement/bad.py b/doc/data/messages/c/confusing-with-statement/bad.py new file mode 100644 index 0000000000..d842880580 --- /dev/null +++ b/doc/data/messages/c/confusing-with-statement/bad.py @@ -0,0 +1,2 @@ +with open('file.txt', 'w') as fh1, fh2: # [confusing-with-statement] + pass diff --git a/doc/data/messages/c/confusing-with-statement/details.rst b/doc/data/messages/c/confusing-with-statement/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/confusing-with-statement/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/confusing-with-statement/good.py b/doc/data/messages/c/confusing-with-statement/good.py index c40beb573f..e8b39d5001 100644 --- a/doc/data/messages/c/confusing-with-statement/good.py +++ b/doc/data/messages/c/confusing-with-statement/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +with open('file.txt', 'w', encoding="utf8") as fh1: + with open('file.txt', 'w', encoding="utf8") as fh2: + pass diff --git a/doc/data/messages/c/consider-iterating-dictionary/bad.py b/doc/data/messages/c/consider-iterating-dictionary/bad.py new file mode 100644 index 0000000000..eb5a97ab41 --- /dev/null +++ b/doc/data/messages/c/consider-iterating-dictionary/bad.py @@ -0,0 +1,5 @@ +FRUITS = {"apple": 1, "pear": 5, "peach": 10} + + +for fruit in FRUITS.keys(): # [consider-iterating-dictionary] + print(fruit) diff --git a/doc/data/messages/c/consider-iterating-dictionary/details.rst b/doc/data/messages/c/consider-iterating-dictionary/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/consider-iterating-dictionary/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/consider-iterating-dictionary/good.py b/doc/data/messages/c/consider-iterating-dictionary/good.py index c40beb573f..c67b3997c8 100644 --- a/doc/data/messages/c/consider-iterating-dictionary/good.py +++ b/doc/data/messages/c/consider-iterating-dictionary/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +FRUITS = {"apple": 1, "pear": 5, "peach": 10} + + +for fruit in FRUITS: + print(fruit) diff --git a/doc/data/messages/c/consider-refactoring-into-while-condition/bad.py b/doc/data/messages/c/consider-refactoring-into-while-condition/bad.py new file mode 100644 index 0000000000..edb6fe31d2 --- /dev/null +++ b/doc/data/messages/c/consider-refactoring-into-while-condition/bad.py @@ -0,0 +1,7 @@ +fruit_basket = ["apple", "orange", "banana", "cherry", "guava"] + +while True: # [consider-refactoring-into-while-condition] + if len(fruit_basket) == 0: + break + fruit = fruit_basket.pop() + print(f"We removed {fruit} from the basket") diff --git a/doc/data/messages/c/consider-refactoring-into-while-condition/good.py b/doc/data/messages/c/consider-refactoring-into-while-condition/good.py new file mode 100644 index 0000000000..900b9c6131 --- /dev/null +++ b/doc/data/messages/c/consider-refactoring-into-while-condition/good.py @@ -0,0 +1,5 @@ +fruit_basket = ["apple", "orange", "banana", "cherry", "guava"] + +while len(fruit_basket) != 0: + fruit = fruit_basket.pop() + print(f"We removed {fruit} from the basket") diff --git a/doc/data/messages/c/consider-refactoring-into-while-condition/pylintrc b/doc/data/messages/c/consider-refactoring-into-while-condition/pylintrc new file mode 100644 index 0000000000..7625181398 --- /dev/null +++ b/doc/data/messages/c/consider-refactoring-into-while-condition/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.consider_refactoring_into_while_condition diff --git a/doc/data/messages/c/consider-swap-variables/bad.py b/doc/data/messages/c/consider-swap-variables/bad.py new file mode 100644 index 0000000000..2092c993d5 --- /dev/null +++ b/doc/data/messages/c/consider-swap-variables/bad.py @@ -0,0 +1,6 @@ +a = 1 +b = 2 + +temp = a # [consider-swap-variables] +a = b +b = temp diff --git a/doc/data/messages/c/consider-swap-variables/details.rst b/doc/data/messages/c/consider-swap-variables/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/consider-swap-variables/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/consider-swap-variables/good.py b/doc/data/messages/c/consider-swap-variables/good.py index c40beb573f..1b92dcb6b0 100644 --- a/doc/data/messages/c/consider-swap-variables/good.py +++ b/doc/data/messages/c/consider-swap-variables/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +a = 1 +b = 2 + +a, b = b, a diff --git a/doc/data/messages/c/consider-ternary-expression/bad.py b/doc/data/messages/c/consider-ternary-expression/bad.py new file mode 100644 index 0000000000..126b92b0ed --- /dev/null +++ b/doc/data/messages/c/consider-ternary-expression/bad.py @@ -0,0 +1,5 @@ +x, y = input(), input() +if x >= y: # [consider-ternary-expression] + maximum = x +else: + maximum = y diff --git a/doc/data/messages/c/consider-ternary-expression/details.rst b/doc/data/messages/c/consider-ternary-expression/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/consider-ternary-expression/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/consider-ternary-expression/good.py b/doc/data/messages/c/consider-ternary-expression/good.py index c40beb573f..117324054c 100644 --- a/doc/data/messages/c/consider-ternary-expression/good.py +++ b/doc/data/messages/c/consider-ternary-expression/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +x, y = input(), input() +maximum = x if x >= y else y diff --git a/doc/data/messages/c/consider-ternary-expression/pylintrc b/doc/data/messages/c/consider-ternary-expression/pylintrc new file mode 100644 index 0000000000..20f7725cd7 --- /dev/null +++ b/doc/data/messages/c/consider-ternary-expression/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.consider_ternary_expression diff --git a/doc/data/messages/c/consider-using-any-or-all/bad.py b/doc/data/messages/c/consider-using-any-or-all/bad.py new file mode 100644 index 0000000000..7fd48f6f52 --- /dev/null +++ b/doc/data/messages/c/consider-using-any-or-all/bad.py @@ -0,0 +1,14 @@ +def any_even(items): + """Return True if the list contains any even numbers""" + for item in items: # [consider-using-any-or-all] + if item % 2 == 0: + return True + return False + + +def all_even(items): + """Return True if the list contains all even numbers""" + for item in items: # [consider-using-any-or-all] + if not item % 2 == 0: + return False + return True diff --git a/doc/data/messages/c/consider-using-any-or-all/details.rst b/doc/data/messages/c/consider-using-any-or-all/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/consider-using-any-or-all/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/consider-using-any-or-all/good.py b/doc/data/messages/c/consider-using-any-or-all/good.py index c40beb573f..5acf18e745 100644 --- a/doc/data/messages/c/consider-using-any-or-all/good.py +++ b/doc/data/messages/c/consider-using-any-or-all/good.py @@ -1 +1,8 @@ -# This is a placeholder for correct code for this message. + +def any_even(items): + """Return True if the list contains any even numbers""" + return any(item % 2 == 0 for item in items) + +def all_even(items): + """Return True if the list contains all even numbers""" + return all(item % 2 == 0 for item in items) diff --git a/doc/data/messages/c/consider-using-any-or-all/pylintrc b/doc/data/messages/c/consider-using-any-or-all/pylintrc new file mode 100644 index 0000000000..2fc8207935 --- /dev/null +++ b/doc/data/messages/c/consider-using-any-or-all/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.for_any_all diff --git a/doc/data/messages/c/consider-using-assignment-expr/bad.py b/doc/data/messages/c/consider-using-assignment-expr/bad.py new file mode 100644 index 0000000000..a700537fa9 --- /dev/null +++ b/doc/data/messages/c/consider-using-assignment-expr/bad.py @@ -0,0 +1,4 @@ +apples = 2 + +if apples: # [consider-using-assignment-expr] + print("God apples!") diff --git a/doc/data/messages/c/consider-using-assignment-expr/details.rst b/doc/data/messages/c/consider-using-assignment-expr/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/consider-using-assignment-expr/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/consider-using-assignment-expr/good.py b/doc/data/messages/c/consider-using-assignment-expr/good.py index c40beb573f..a1e402701d 100644 --- a/doc/data/messages/c/consider-using-assignment-expr/good.py +++ b/doc/data/messages/c/consider-using-assignment-expr/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +if apples := 2: + print("God apples!") diff --git a/doc/data/messages/c/consider-using-assignment-expr/pylintrc b/doc/data/messages/c/consider-using-assignment-expr/pylintrc new file mode 100644 index 0000000000..14b316d488 --- /dev/null +++ b/doc/data/messages/c/consider-using-assignment-expr/pylintrc @@ -0,0 +1,3 @@ +[MAIN] +py-version=3.8 +load-plugins=pylint.extensions.code_style diff --git a/doc/data/messages/c/consider-using-augmented-assign/bad.py b/doc/data/messages/c/consider-using-augmented-assign/bad.py new file mode 100644 index 0000000000..90b8931a66 --- /dev/null +++ b/doc/data/messages/c/consider-using-augmented-assign/bad.py @@ -0,0 +1,2 @@ +x = 1 +x = x + 1 # [consider-using-augmented-assign] diff --git a/doc/data/messages/c/consider-using-augmented-assign/good.py b/doc/data/messages/c/consider-using-augmented-assign/good.py new file mode 100644 index 0000000000..3e34f6b266 --- /dev/null +++ b/doc/data/messages/c/consider-using-augmented-assign/good.py @@ -0,0 +1,2 @@ +x = 1 +x += 1 diff --git a/doc/data/messages/c/consider-using-augmented-assign/pylintrc b/doc/data/messages/c/consider-using-augmented-assign/pylintrc new file mode 100644 index 0000000000..5846022946 --- /dev/null +++ b/doc/data/messages/c/consider-using-augmented-assign/pylintrc @@ -0,0 +1,3 @@ +[MAIN] +load-plugins=pylint.extensions.code_style +enable=consider-using-augmented-assign diff --git a/doc/data/messages/c/consider-using-dict-comprehension/bad.py b/doc/data/messages/c/consider-using-dict-comprehension/bad.py index 78129c56fc..d9b02c71b0 100644 --- a/doc/data/messages/c/consider-using-dict-comprehension/bad.py +++ b/doc/data/messages/c/consider-using-dict-comprehension/bad.py @@ -1,3 +1,4 @@ NUMBERS = [1, 2, 3] -DOUBLED_NUMBERS = dict([(number, number * 2) for number in NUMBERS]) # [consider-using-dict-comprehension] +# +1: [consider-using-dict-comprehension] +DOUBLED_NUMBERS = dict([(number, number * 2) for number in NUMBERS]) diff --git a/doc/data/messages/c/consider-using-dict-comprehension/details.rst b/doc/data/messages/c/consider-using-dict-comprehension/details.rst new file mode 100644 index 0000000000..c287fc4847 --- /dev/null +++ b/doc/data/messages/c/consider-using-dict-comprehension/details.rst @@ -0,0 +1,3 @@ +pyupgrade_ can fix this issue automatically. + +.. _pyupgrade: https://github.com/asottile/pyupgrade diff --git a/doc/data/messages/c/consider-using-f-string/bad.py b/doc/data/messages/c/consider-using-f-string/bad.py index d706d08e2c..26da6a1667 100644 --- a/doc/data/messages/c/consider-using-f-string/bad.py +++ b/doc/data/messages/c/consider-using-f-string/bad.py @@ -1,10 +1,16 @@ from string import Template -menu = ('eggs', 'spam', 42.4) +menu = ("eggs", "spam", 42.4) -old_order = "%s and %s: %.2f ¤" % menu # [consider-using-f-string] +old_order = "%s and %s: %.2f ¤" % menu # [consider-using-f-string] beginner_order = menu[0] + " and " + menu[1] + ": " + str(menu[2]) + " ¤" joined_order = " and ".join(menu[:2]) -format_order = "{} and {}: {:0.2f} ¤".format(menu[0], menu[1], menu[2]) # [consider-using-f-string] -named_format_order = "{eggs} and {spam}: {price:0.2f} ¤".format(eggs=menu[0], spam=menu[1], price=menu[2]) # [consider-using-f-string] -template_order = Template('$eggs and $spam: $price ¤').substitute(eggs=menu[0], spam=menu[1], price=menu[2]) +# +1: [consider-using-f-string] +format_order = "{} and {}: {:0.2f} ¤".format(menu[0], menu[1], menu[2]) +# +1: [consider-using-f-string] +named_format_order = "{eggs} and {spam}: {price:0.2f} ¤".format( + eggs=menu[0], spam=menu[1], price=menu[2] +) +template_order = Template("$eggs and $spam: $price ¤").substitute( + eggs=menu[0], spam=menu[1], price=menu[2] +) diff --git a/doc/data/messages/c/consider-using-set-comprehension/bad.py b/doc/data/messages/c/consider-using-set-comprehension/bad.py index 657a211627..ffdc9e526f 100644 --- a/doc/data/messages/c/consider-using-set-comprehension/bad.py +++ b/doc/data/messages/c/consider-using-set-comprehension/bad.py @@ -1,3 +1,4 @@ NUMBERS = [1, 2, 2, 3, 4, 4] -UNIQUE_EVEN_NUMBERS = set([number for number in NUMBERS if number % 2 == 0]) # [consider-using-set-comprehension] +# +1: [consider-using-set-comprehension] +UNIQUE_EVEN_NUMBERS = set([number for number in NUMBERS if number % 2 == 0]) diff --git a/doc/data/messages/c/consider-using-set-comprehension/details.rst b/doc/data/messages/c/consider-using-set-comprehension/details.rst new file mode 100644 index 0000000000..c287fc4847 --- /dev/null +++ b/doc/data/messages/c/consider-using-set-comprehension/details.rst @@ -0,0 +1,3 @@ +pyupgrade_ can fix this issue automatically. + +.. _pyupgrade: https://github.com/asottile/pyupgrade diff --git a/doc/data/messages/c/consider-using-ternary/bad.py b/doc/data/messages/c/consider-using-ternary/bad.py new file mode 100644 index 0000000000..bd69aa99e7 --- /dev/null +++ b/doc/data/messages/c/consider-using-ternary/bad.py @@ -0,0 +1,2 @@ +x, y = 1, 2 +maximum = x >= y and x or y # [consider-using-ternary] diff --git a/doc/data/messages/c/consider-using-ternary/details.rst b/doc/data/messages/c/consider-using-ternary/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/c/consider-using-ternary/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/c/consider-using-ternary/good.py b/doc/data/messages/c/consider-using-ternary/good.py index c40beb573f..bcb8446bce 100644 --- a/doc/data/messages/c/consider-using-ternary/good.py +++ b/doc/data/messages/c/consider-using-ternary/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +x, y = 1, 2 +maximum = x if x >= y else y diff --git a/doc/data/messages/c/consider-using-tuple/pylintrc b/doc/data/messages/c/consider-using-tuple/pylintrc new file mode 100644 index 0000000000..8663ab085d --- /dev/null +++ b/doc/data/messages/c/consider-using-tuple/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.code_style diff --git a/doc/data/messages/c/consider-using-with/bad.py b/doc/data/messages/c/consider-using-with/bad.py index bd657299b8..f6ea102c15 100644 --- a/doc/data/messages/c/consider-using-with/bad.py +++ b/doc/data/messages/c/consider-using-with/bad.py @@ -1,3 +1,5 @@ -file = open("foo.txt", "r", encoding="utf8") # [consider-using-with] +file = open("apple.txt", "r", encoding="utf8") # [consider-using-with] contents = file.read() file.close() + +worst = open("banana.txt", "r", encoding="utf8").read() # [consider-using-with] diff --git a/doc/data/messages/c/consider-using-with/details.rst b/doc/data/messages/c/consider-using-with/details.rst index 1c990988ed..58d763fa4a 100644 --- a/doc/data/messages/c/consider-using-with/details.rst +++ b/doc/data/messages/c/consider-using-with/details.rst @@ -1,3 +1,7 @@ +Calling ``write()`` without using the ``with`` keyword or calling ``close()`` might +result in the arguments of ``write()`` not being completely written to the disk, +even if the program exits successfully. + This message applies to callables of Python's stdlib which can be replaced by a ``with`` statement. It is suppressed in the following cases: diff --git a/doc/data/messages/c/consider-using-with/good.py b/doc/data/messages/c/consider-using-with/good.py index 70e09b146c..7f677ca7e4 100644 --- a/doc/data/messages/c/consider-using-with/good.py +++ b/doc/data/messages/c/consider-using-with/good.py @@ -1,2 +1,5 @@ -with open("foo.txt", "r", encoding="utf8") as file: +with open("apple.txt", "r", encoding="utf8") as file: contents = file.read() + +with open("banana.txt", "r", encoding="utf8") as f: + best = f.read() diff --git a/doc/data/messages/c/consider-using-with/related.rst b/doc/data/messages/c/consider-using-with/related.rst index c4864e02e1..a2a209ca0d 100644 --- a/doc/data/messages/c/consider-using-with/related.rst +++ b/doc/data/messages/c/consider-using-with/related.rst @@ -1,2 +1,4 @@ +- `Python doc: Reading and writing files `_ - `PEP 343 `_ - `Context managers in Python `_ by John Lekberg +- `Rationale `_ diff --git a/doc/data/messages/c/continue-in-finally/bad.py b/doc/data/messages/c/continue-in-finally/bad.py new file mode 100644 index 0000000000..190463f5a0 --- /dev/null +++ b/doc/data/messages/c/continue-in-finally/bad.py @@ -0,0 +1,5 @@ +while True: + try: + pass + finally: + continue # [continue-in-finally] diff --git a/doc/data/messages/c/continue-in-finally/details.rst b/doc/data/messages/c/continue-in-finally/details.rst index ab82045295..2d9044ee1a 100644 --- a/doc/data/messages/c/continue-in-finally/details.rst +++ b/doc/data/messages/c/continue-in-finally/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing `_ ! +Note this message can't be emitted when using Python version 3.8 or greater. diff --git a/doc/data/messages/c/continue-in-finally/good.py b/doc/data/messages/c/continue-in-finally/good.py index c40beb573f..6b14ef4462 100644 --- a/doc/data/messages/c/continue-in-finally/good.py +++ b/doc/data/messages/c/continue-in-finally/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +while True: + try: + pass + except ValueError: + pass + else: + continue diff --git a/doc/data/messages/c/continue-in-finally/pylintrc b/doc/data/messages/c/continue-in-finally/pylintrc new file mode 100644 index 0000000000..b7a429f3d4 --- /dev/null +++ b/doc/data/messages/c/continue-in-finally/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +py-version=3.7 diff --git a/doc/data/messages/d/dangerous-default-value/bad.py b/doc/data/messages/d/dangerous-default-value/bad.py new file mode 100644 index 0000000000..6ce1223c97 --- /dev/null +++ b/doc/data/messages/d/dangerous-default-value/bad.py @@ -0,0 +1,3 @@ +def whats_on_the_telly(penguin=[]): # [dangerous-default-value] + penguin.append("property of the zoo") + return penguin diff --git a/doc/data/messages/d/dangerous-default-value/details.rst b/doc/data/messages/d/dangerous-default-value/details.rst index ab82045295..2b9a2e672c 100644 --- a/doc/data/messages/d/dangerous-default-value/details.rst +++ b/doc/data/messages/d/dangerous-default-value/details.rst @@ -1 +1,7 @@ -You can help us make the doc better `by contributing `_ ! +With a mutable default value, with each call the default value is modified, i.e.: + +.. code-block:: python + + whats_on_the_telly() # ["property of the zoo"] + whats_on_the_telly() # ["property of the zoo", "property of the zoo"] + whats_on_the_telly() # ["property of the zoo", "property of the zoo", "property of the zoo"] diff --git a/doc/data/messages/d/dangerous-default-value/good.py b/doc/data/messages/d/dangerous-default-value/good.py index c40beb573f..605c3cec39 100644 --- a/doc/data/messages/d/dangerous-default-value/good.py +++ b/doc/data/messages/d/dangerous-default-value/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +def whats_on_the_telly(penguin=None): + if penguin is None: + penguin = [] + penguin.append("property of the zoo") + return penguin diff --git a/doc/data/messages/d/dict-init-mutate/bad.py b/doc/data/messages/d/dict-init-mutate/bad.py new file mode 100644 index 0000000000..d6d1cfe189 --- /dev/null +++ b/doc/data/messages/d/dict-init-mutate/bad.py @@ -0,0 +1,3 @@ +fruit_prices = {} # [dict-init-mutate] +fruit_prices['apple'] = 1 +fruit_prices['banana'] = 10 diff --git a/doc/data/messages/d/dict-init-mutate/good.py b/doc/data/messages/d/dict-init-mutate/good.py new file mode 100644 index 0000000000..02137f2879 --- /dev/null +++ b/doc/data/messages/d/dict-init-mutate/good.py @@ -0,0 +1 @@ +fruit_prices = {"apple": 1, "banana": 10} diff --git a/doc/data/messages/d/dict-init-mutate/pylintrc b/doc/data/messages/d/dict-init-mutate/pylintrc new file mode 100644 index 0000000000..bbe6bd1f78 --- /dev/null +++ b/doc/data/messages/d/dict-init-mutate/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.dict_init_mutate, diff --git a/doc/data/messages/d/disallowed-name/bad.py b/doc/data/messages/d/disallowed-name/bad.py new file mode 100644 index 0000000000..d28892db31 --- /dev/null +++ b/doc/data/messages/d/disallowed-name/bad.py @@ -0,0 +1,2 @@ +def foo(): # [disallowed-name] + print("apples") diff --git a/doc/data/messages/d/disallowed-name/details.rst b/doc/data/messages/d/disallowed-name/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/d/disallowed-name/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/d/disallowed-name/good.py b/doc/data/messages/d/disallowed-name/good.py index c40beb573f..b584b631fd 100644 --- a/doc/data/messages/d/disallowed-name/good.py +++ b/doc/data/messages/d/disallowed-name/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def print_fruit(): + print("apples") diff --git a/doc/data/messages/d/disallowed-name/pylintrc b/doc/data/messages/d/disallowed-name/pylintrc new file mode 100644 index 0000000000..5e073119d8 --- /dev/null +++ b/doc/data/messages/d/disallowed-name/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +bad-names=foo,bar,baz diff --git a/doc/data/messages/d/duplicate-except/bad.py b/doc/data/messages/d/duplicate-except/bad.py new file mode 100644 index 0000000000..e50b4231c8 --- /dev/null +++ b/doc/data/messages/d/duplicate-except/bad.py @@ -0,0 +1,6 @@ +try: + 1 / 0 +except ZeroDivisionError: + pass +except ZeroDivisionError: # [duplicate-except] + pass diff --git a/doc/data/messages/d/duplicate-except/details.rst b/doc/data/messages/d/duplicate-except/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/d/duplicate-except/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/d/duplicate-except/good.py b/doc/data/messages/d/duplicate-except/good.py index c40beb573f..b02b365b06 100644 --- a/doc/data/messages/d/duplicate-except/good.py +++ b/doc/data/messages/d/duplicate-except/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +try: + 1 / 0 +except ZeroDivisionError: + pass diff --git a/doc/data/messages/e/else-if-used/bad.py b/doc/data/messages/e/else-if-used/bad.py new file mode 100644 index 0000000000..55cf422ac7 --- /dev/null +++ b/doc/data/messages/e/else-if-used/bad.py @@ -0,0 +1,7 @@ +if input(): + pass +else: + if len(input()) >= 10: # [else-if-used] + pass + else: + pass diff --git a/doc/data/messages/e/else-if-used/details.rst b/doc/data/messages/e/else-if-used/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/e/else-if-used/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/e/else-if-used/good.py b/doc/data/messages/e/else-if-used/good.py index c40beb573f..39eb3ba757 100644 --- a/doc/data/messages/e/else-if-used/good.py +++ b/doc/data/messages/e/else-if-used/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +if input(): + pass +elif len(input()) >= 10: + pass +else: + pass diff --git a/doc/data/messages/e/else-if-used/pylintrc b/doc/data/messages/e/else-if-used/pylintrc new file mode 100644 index 0000000000..5c438919e4 --- /dev/null +++ b/doc/data/messages/e/else-if-used/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.check_elif diff --git a/doc/data/messages/e/empty-comment/bad.py b/doc/data/messages/e/empty-comment/bad.py new file mode 100644 index 0000000000..f9d7f8e57f --- /dev/null +++ b/doc/data/messages/e/empty-comment/bad.py @@ -0,0 +1,5 @@ +# +1:[empty-comment] +# + +# +1:[empty-comment] +x = 0 # diff --git a/doc/data/messages/e/empty-comment/details.rst b/doc/data/messages/e/empty-comment/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/e/empty-comment/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/e/empty-comment/good.py b/doc/data/messages/e/empty-comment/good.py index c40beb573f..f05c63d9d0 100644 --- a/doc/data/messages/e/empty-comment/good.py +++ b/doc/data/messages/e/empty-comment/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +# comment + +x = 0 # comment diff --git a/doc/data/messages/e/empty-comment/pylintrc b/doc/data/messages/e/empty-comment/pylintrc new file mode 100644 index 0000000000..a773b55678 --- /dev/null +++ b/doc/data/messages/e/empty-comment/pylintrc @@ -0,0 +1,2 @@ +[main] +load-plugins=pylint.extensions.empty_comment diff --git a/doc/data/messages/e/eq-without-hash/bad.py b/doc/data/messages/e/eq-without-hash/bad.py new file mode 100644 index 0000000000..42df9e8f10 --- /dev/null +++ b/doc/data/messages/e/eq-without-hash/bad.py @@ -0,0 +1,6 @@ +class Fruit: # [eq-without-hash] + def __init__(self) -> None: + self.name = "apple" + + def __eq__(self, other: object) -> bool: + return isinstance(other, Fruit) and other.name == self.name diff --git a/doc/data/messages/e/eq-without-hash/details.rst b/doc/data/messages/e/eq-without-hash/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/e/eq-without-hash/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/e/eq-without-hash/good.py b/doc/data/messages/e/eq-without-hash/good.py index c40beb573f..bf62450734 100644 --- a/doc/data/messages/e/eq-without-hash/good.py +++ b/doc/data/messages/e/eq-without-hash/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self) -> None: + self.name = "apple" + + def __eq__(self, other: object) -> bool: + return isinstance(other, Fruit) and other.name == self.name + + def __hash__(self) -> int: + return hash(self.name) diff --git a/doc/data/messages/e/eq-without-hash/pylintrc b/doc/data/messages/e/eq-without-hash/pylintrc new file mode 100644 index 0000000000..6e2e015a49 --- /dev/null +++ b/doc/data/messages/e/eq-without-hash/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.eq_without_hash, diff --git a/doc/data/messages/e/eval-used/bad.py b/doc/data/messages/e/eval-used/bad.py new file mode 100644 index 0000000000..db26f17296 --- /dev/null +++ b/doc/data/messages/e/eval-used/bad.py @@ -0,0 +1 @@ +eval("[1, 2, 3]") # [eval-used] diff --git a/doc/data/messages/e/eval-used/details.rst b/doc/data/messages/e/eval-used/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/e/eval-used/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/e/eval-used/good.py b/doc/data/messages/e/eval-used/good.py index c40beb573f..094ecae214 100644 --- a/doc/data/messages/e/eval-used/good.py +++ b/doc/data/messages/e/eval-used/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +from ast import literal_eval + +literal_eval("[1, 2, 3]") diff --git a/doc/data/messages/e/exec-used/bad.py b/doc/data/messages/e/exec-used/bad.py new file mode 100644 index 0000000000..72514e3b72 --- /dev/null +++ b/doc/data/messages/e/exec-used/bad.py @@ -0,0 +1,4 @@ +username = "Ada" +code_to_execute = f"""input('Enter code to be executed please, {username}: ')""" +program = exec(code_to_execute) # [exec-used] +exec(program) # [exec-used] diff --git a/doc/data/messages/e/exec-used/details.rst b/doc/data/messages/e/exec-used/details.rst index ab82045295..246857f32d 100644 --- a/doc/data/messages/e/exec-used/details.rst +++ b/doc/data/messages/e/exec-used/details.rst @@ -1 +1,10 @@ -You can help us make the doc better `by contributing `_ ! +The available methods and variables used in ``exec()`` may introduce a security hole. +You can restrict the use of these variables and methods by passing optional globals +and locals parameters (dictionaries) to the ``exec()`` method. + +However, use of ``exec`` is still insecure. For example, consider the following call +that writes a file to the user's system: + +.. code-block:: python + + exec("""\nwith open("file.txt", "w", encoding="utf-8") as file:\n file.write("# code as nefarious as imaginable")\n""") diff --git a/doc/data/messages/e/exec-used/good.py b/doc/data/messages/e/exec-used/good.py index c40beb573f..ef9b17f3f3 100644 --- a/doc/data/messages/e/exec-used/good.py +++ b/doc/data/messages/e/exec-used/good.py @@ -1 +1,8 @@ -# This is a placeholder for correct code for this message. +def get_user_code(name): + return input(f'Enter code to be executed please, {name}: ') + + +username = "Ada" +allowed_globals = {'__builtins__' : None} +allowed_locals = {'print': print} +exec(get_user_code(username), allowed_globals, allowed_locals) # pylint: disable=exec-used diff --git a/doc/data/messages/e/exec-used/related.rst b/doc/data/messages/e/exec-used/related.rst new file mode 100644 index 0000000000..d840db050e --- /dev/null +++ b/doc/data/messages/e/exec-used/related.rst @@ -0,0 +1 @@ +- `Be careful with exec and eval in Python `_ diff --git a/doc/data/messages/e/expression-not-assigned/bad.py b/doc/data/messages/e/expression-not-assigned/bad.py new file mode 100644 index 0000000000..fa696d41a2 --- /dev/null +++ b/doc/data/messages/e/expression-not-assigned/bad.py @@ -0,0 +1 @@ +str(42) == "42" # [expression-not-assigned] diff --git a/doc/data/messages/e/expression-not-assigned/details.rst b/doc/data/messages/e/expression-not-assigned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/e/expression-not-assigned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/e/expression-not-assigned/good.py b/doc/data/messages/e/expression-not-assigned/good.py index c40beb573f..fda04a837c 100644 --- a/doc/data/messages/e/expression-not-assigned/good.py +++ b/doc/data/messages/e/expression-not-assigned/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +are_equal: bool = str(42) == "42" diff --git a/doc/data/messages/f/f-string-without-interpolation/bad.py b/doc/data/messages/f/f-string-without-interpolation/bad.py new file mode 100644 index 0000000000..a31779832b --- /dev/null +++ b/doc/data/messages/f/f-string-without-interpolation/bad.py @@ -0,0 +1,3 @@ +x = 1 +y = 2 +print(f"x + y = x + y") # [f-string-without-interpolation] diff --git a/doc/data/messages/f/f-string-without-interpolation/details.rst b/doc/data/messages/f/f-string-without-interpolation/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/f/f-string-without-interpolation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/f/f-string-without-interpolation/good.py b/doc/data/messages/f/f-string-without-interpolation/good.py index c40beb573f..9af2f979b9 100644 --- a/doc/data/messages/f/f-string-without-interpolation/good.py +++ b/doc/data/messages/f/f-string-without-interpolation/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +x = 1 +y = 2 +print(f"{x} + {y} = {x + y}") diff --git a/doc/data/messages/f/fatal/details.rst b/doc/data/messages/f/fatal/details.rst index ab82045295..1c43031371 100644 --- a/doc/data/messages/f/fatal/details.rst +++ b/doc/data/messages/f/fatal/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing `_ ! +This is a message linked to an internal problem in pylint. There's nothing to change in your code. diff --git a/doc/data/messages/f/fatal/good.py b/doc/data/messages/f/fatal/good.py deleted file mode 100644 index c40beb573f..0000000000 --- a/doc/data/messages/f/fatal/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/f/format-combined-specification/bad.py b/doc/data/messages/f/format-combined-specification/bad.py new file mode 100644 index 0000000000..6a6a051f21 --- /dev/null +++ b/doc/data/messages/f/format-combined-specification/bad.py @@ -0,0 +1 @@ +print('{} {1}'.format('hello', 'world')) # [format-combined-specification] diff --git a/doc/data/messages/f/format-combined-specification/details.rst b/doc/data/messages/f/format-combined-specification/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/f/format-combined-specification/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/f/format-combined-specification/good.py b/doc/data/messages/f/format-combined-specification/good.py index c40beb573f..542b775076 100644 --- a/doc/data/messages/f/format-combined-specification/good.py +++ b/doc/data/messages/f/format-combined-specification/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +print('{0} {1}'.format('hello', 'world')) +# or +print('{} {}'.format('hello', 'world')) diff --git a/doc/data/messages/f/format-needs-mapping/bad.py b/doc/data/messages/f/format-needs-mapping/bad.py new file mode 100644 index 0000000000..b265ccdb94 --- /dev/null +++ b/doc/data/messages/f/format-needs-mapping/bad.py @@ -0,0 +1 @@ +print("%(x)d %(y)d" % [1, 2]) # [format-needs-mapping] diff --git a/doc/data/messages/f/format-needs-mapping/details.rst b/doc/data/messages/f/format-needs-mapping/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/f/format-needs-mapping/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/f/format-needs-mapping/good.py b/doc/data/messages/f/format-needs-mapping/good.py index c40beb573f..bed0122e33 100644 --- a/doc/data/messages/f/format-needs-mapping/good.py +++ b/doc/data/messages/f/format-needs-mapping/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +print("%(x)d %(y)d" % {"x": 1, "y": 2}) diff --git a/doc/data/messages/f/format-string-without-interpolation/bad.py b/doc/data/messages/f/format-string-without-interpolation/bad.py new file mode 100644 index 0000000000..a5ff6406dd --- /dev/null +++ b/doc/data/messages/f/format-string-without-interpolation/bad.py @@ -0,0 +1 @@ +print("number".format(1)) # [format-string-without-interpolation] diff --git a/doc/data/messages/f/format-string-without-interpolation/details.rst b/doc/data/messages/f/format-string-without-interpolation/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/f/format-string-without-interpolation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/f/format-string-without-interpolation/good.py b/doc/data/messages/f/format-string-without-interpolation/good.py index c40beb573f..097f79662c 100644 --- a/doc/data/messages/f/format-string-without-interpolation/good.py +++ b/doc/data/messages/f/format-string-without-interpolation/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +print("number: {}".format(1)) diff --git a/doc/data/messages/g/global-variable-not-assigned/bad.py b/doc/data/messages/g/global-variable-not-assigned/bad.py new file mode 100644 index 0000000000..d1d907544f --- /dev/null +++ b/doc/data/messages/g/global-variable-not-assigned/bad.py @@ -0,0 +1,6 @@ +TOMATO = "black cherry" + + +def update_tomato(): + global TOMATO # [global-variable-not-assigned] + print(TOMATO) diff --git a/doc/data/messages/g/global-variable-not-assigned/details.rst b/doc/data/messages/g/global-variable-not-assigned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/g/global-variable-not-assigned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/g/global-variable-not-assigned/good.py b/doc/data/messages/g/global-variable-not-assigned/good.py index c40beb573f..0736bb4c7b 100644 --- a/doc/data/messages/g/global-variable-not-assigned/good.py +++ b/doc/data/messages/g/global-variable-not-assigned/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +TOMATO = "black cherry" + + +def update_tomato(): + global TOMATO + TOMATO = "moneymaker" diff --git a/doc/data/messages/g/global-variable-undefined/bad.py b/doc/data/messages/g/global-variable-undefined/bad.py new file mode 100644 index 0000000000..44e4f3a2f8 --- /dev/null +++ b/doc/data/messages/g/global-variable-undefined/bad.py @@ -0,0 +1,3 @@ +def update_tomato(): + global TOMATO # [global-variable-undefined] + TOMATO = "moneymaker" diff --git a/doc/data/messages/g/global-variable-undefined/details.rst b/doc/data/messages/g/global-variable-undefined/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/g/global-variable-undefined/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/g/global-variable-undefined/good.py b/doc/data/messages/g/global-variable-undefined/good.py index c40beb573f..0736bb4c7b 100644 --- a/doc/data/messages/g/global-variable-undefined/good.py +++ b/doc/data/messages/g/global-variable-undefined/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +TOMATO = "black cherry" + + +def update_tomato(): + global TOMATO + TOMATO = "moneymaker" diff --git a/doc/data/messages/i/implicit-str-concat/bad.py b/doc/data/messages/i/implicit-str-concat/bad.py new file mode 100644 index 0000000000..815b70976a --- /dev/null +++ b/doc/data/messages/i/implicit-str-concat/bad.py @@ -0,0 +1,4 @@ +x = ["a" "b"] # [implicit-str-concat] + +with open("hello.txt" "r") as f: # [implicit-str-concat] + print(f.read()) diff --git a/doc/data/messages/i/implicit-str-concat/details.rst b/doc/data/messages/i/implicit-str-concat/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/implicit-str-concat/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/implicit-str-concat/good.py b/doc/data/messages/i/implicit-str-concat/good.py index c40beb573f..ebf50c7471 100644 --- a/doc/data/messages/i/implicit-str-concat/good.py +++ b/doc/data/messages/i/implicit-str-concat/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +x = ["a", "b"] + +with open("hello.txt", "r") as f: + print(f.read()) diff --git a/doc/data/messages/i/import-error/bad.py b/doc/data/messages/i/import-error/bad.py new file mode 100644 index 0000000000..672564c1e8 --- /dev/null +++ b/doc/data/messages/i/import-error/bad.py @@ -0,0 +1 @@ +from patlib import Path # [import-error] diff --git a/doc/data/messages/i/import-error/details.rst b/doc/data/messages/i/import-error/details.rst index ab82045295..aaa3edc118 100644 --- a/doc/data/messages/i/import-error/details.rst +++ b/doc/data/messages/i/import-error/details.rst @@ -1 +1,3 @@ -You can help us make the doc better `by contributing `_ ! +This can happen if you're importing a package that is not installed in your environment, or if you made a typo. + +The solution is to install the package via pip/setup.py/wheel or fix the typo. diff --git a/doc/data/messages/i/import-error/good.py b/doc/data/messages/i/import-error/good.py index c40beb573f..2bb88df4d4 100644 --- a/doc/data/messages/i/import-error/good.py +++ b/doc/data/messages/i/import-error/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +from pathlib import Path diff --git a/doc/data/messages/i/import-self/bad.py b/doc/data/messages/i/import-self/bad.py new file mode 100644 index 0000000000..8783a0ca80 --- /dev/null +++ b/doc/data/messages/i/import-self/bad.py @@ -0,0 +1,5 @@ +from bad import a_function # [import-self] + + +def a_function(): + pass diff --git a/doc/data/messages/i/import-self/details.rst b/doc/data/messages/i/import-self/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/import-self/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/import-self/good.py b/doc/data/messages/i/import-self/good.py index c40beb573f..8eb489d64c 100644 --- a/doc/data/messages/i/import-self/good.py +++ b/doc/data/messages/i/import-self/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def a_function(): + pass diff --git a/doc/data/messages/i/inherit-non-class/bad.py b/doc/data/messages/i/inherit-non-class/bad.py new file mode 100644 index 0000000000..4a7926c0c2 --- /dev/null +++ b/doc/data/messages/i/inherit-non-class/bad.py @@ -0,0 +1,2 @@ +class Fruit(bool): # [inherit-non-class] + pass diff --git a/doc/data/messages/i/inherit-non-class/details.rst b/doc/data/messages/i/inherit-non-class/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/inherit-non-class/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/inherit-non-class/good.py b/doc/data/messages/i/inherit-non-class/good.py index c40beb573f..dc48bd017f 100644 --- a/doc/data/messages/i/inherit-non-class/good.py +++ b/doc/data/messages/i/inherit-non-class/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __bool__(self): + pass diff --git a/doc/data/messages/i/init-is-generator/bad.py b/doc/data/messages/i/init-is-generator/bad.py new file mode 100644 index 0000000000..37c204ab24 --- /dev/null +++ b/doc/data/messages/i/init-is-generator/bad.py @@ -0,0 +1,5 @@ +class Fruit: + def __init__(self, worms): # [init-is-generator] + yield from worms + +apple = Fruit(["Fahad", "Anisha", "Tabatha"]) diff --git a/doc/data/messages/i/init-is-generator/details.rst b/doc/data/messages/i/init-is-generator/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/init-is-generator/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/init-is-generator/good.py b/doc/data/messages/i/init-is-generator/good.py index c40beb573f..483cd46c18 100644 --- a/doc/data/messages/i/init-is-generator/good.py +++ b/doc/data/messages/i/init-is-generator/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self, worms): + self.__worms = worms + + def worms(self): + yield from self.__worms + +apple = Fruit(["Fahad", "Anisha", "Tabatha"]) +for worm in apple.worms(): + pass diff --git a/doc/data/messages/i/invalid-all-object/bad.py b/doc/data/messages/i/invalid-all-object/bad.py new file mode 100644 index 0000000000..74cf738dce --- /dev/null +++ b/doc/data/messages/i/invalid-all-object/bad.py @@ -0,0 +1,11 @@ +__all__ = ( + None, # [invalid-all-object] + Fruit, + Worm, +) + +class Fruit: + pass + +class Worm: + pass diff --git a/doc/data/messages/i/invalid-all-object/details.rst b/doc/data/messages/i/invalid-all-object/details.rst index ab82045295..db3d100aa4 100644 --- a/doc/data/messages/i/invalid-all-object/details.rst +++ b/doc/data/messages/i/invalid-all-object/details.rst @@ -1 +1,2 @@ -You can help us make the doc better `by contributing `_ ! +From `The Python Language Reference – The import statement `_: + "The `public names` defined by a module are determined by checking the module's namespace for a variable named ``__all__``; if defined, it must be a sequence of strings which are names defined or imported by that module." diff --git a/doc/data/messages/i/invalid-all-object/good.py b/doc/data/messages/i/invalid-all-object/good.py index c40beb573f..db5879cf39 100644 --- a/doc/data/messages/i/invalid-all-object/good.py +++ b/doc/data/messages/i/invalid-all-object/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +__all__ = ['Fruit', 'Worm'] + +class Fruit: + pass + +class Worm: + pass diff --git a/doc/data/messages/i/invalid-all-object/related.rst b/doc/data/messages/i/invalid-all-object/related.rst new file mode 100644 index 0000000000..fff337eb9c --- /dev/null +++ b/doc/data/messages/i/invalid-all-object/related.rst @@ -0,0 +1 @@ +- `PEP 8 – Style Guide for Python Code `_ diff --git a/doc/data/messages/i/invalid-bool-returned/bad.py b/doc/data/messages/i/invalid-bool-returned/bad.py new file mode 100644 index 0000000000..8e2df42d94 --- /dev/null +++ b/doc/data/messages/i/invalid-bool-returned/bad.py @@ -0,0 +1,5 @@ +class BadBool: + """__bool__ returns an int""" + + def __bool__(self): # [invalid-bool-returned] + return 1 diff --git a/doc/data/messages/i/invalid-bool-returned/details.rst b/doc/data/messages/i/invalid-bool-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-bool-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-bool-returned/good.py b/doc/data/messages/i/invalid-bool-returned/good.py index c40beb573f..33e00c0e3c 100644 --- a/doc/data/messages/i/invalid-bool-returned/good.py +++ b/doc/data/messages/i/invalid-bool-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodBool: + """__bool__ returns `bool`""" + + def __bool__(self): + return True diff --git a/doc/data/messages/i/invalid-bytes-returned/bad.py b/doc/data/messages/i/invalid-bytes-returned/bad.py new file mode 100644 index 0000000000..5068c85f99 --- /dev/null +++ b/doc/data/messages/i/invalid-bytes-returned/bad.py @@ -0,0 +1,5 @@ +class BadBytes: + """__bytes__ returns """ + + def __bytes__(self): # [invalid-bytes-returned] + return "123" diff --git a/doc/data/messages/i/invalid-bytes-returned/details.rst b/doc/data/messages/i/invalid-bytes-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-bytes-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-bytes-returned/good.py b/doc/data/messages/i/invalid-bytes-returned/good.py index c40beb573f..3bc95489f3 100644 --- a/doc/data/messages/i/invalid-bytes-returned/good.py +++ b/doc/data/messages/i/invalid-bytes-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodBytes: + """__bytes__ returns """ + + def __bytes__(self): + return b"some bytes" diff --git a/doc/data/messages/i/invalid-characters-in-docstring/details.rst b/doc/data/messages/i/invalid-characters-in-docstring/details.rst index ab82045295..9977db1445 100644 --- a/doc/data/messages/i/invalid-characters-in-docstring/details.rst +++ b/doc/data/messages/i/invalid-characters-in-docstring/details.rst @@ -1 +1,2 @@ -You can help us make the doc better `by contributing `_ ! +This is a message linked to an internal problem in enchant. There's nothing to change in your code, +but maybe in pylint's configuration or the way you installed the 'enchant' system library. diff --git a/doc/data/messages/i/invalid-characters-in-docstring/good.py b/doc/data/messages/i/invalid-characters-in-docstring/good.py deleted file mode 100644 index c40beb573f..0000000000 --- a/doc/data/messages/i/invalid-characters-in-docstring/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/i/invalid-class-object/bad.py b/doc/data/messages/i/invalid-class-object/bad.py new file mode 100644 index 0000000000..5c6a6f8df7 --- /dev/null +++ b/doc/data/messages/i/invalid-class-object/bad.py @@ -0,0 +1,5 @@ +class Apple: + pass + + +Apple.__class__ = 1 # [invalid-class-object] diff --git a/doc/data/messages/i/invalid-class-object/details.rst b/doc/data/messages/i/invalid-class-object/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-class-object/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-class-object/good.py b/doc/data/messages/i/invalid-class-object/good.py index c40beb573f..3b50097f1c 100644 --- a/doc/data/messages/i/invalid-class-object/good.py +++ b/doc/data/messages/i/invalid-class-object/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Apple: + pass + + +class RedDelicious: + pass + + +Apple.__class__ = RedDelicious diff --git a/doc/data/messages/i/invalid-envvar-default/bad.py b/doc/data/messages/i/invalid-envvar-default/bad.py new file mode 100644 index 0000000000..8d66765ee7 --- /dev/null +++ b/doc/data/messages/i/invalid-envvar-default/bad.py @@ -0,0 +1,3 @@ +import os + +env = os.getenv('SECRET_KEY', 1) # [invalid-envvar-default] diff --git a/doc/data/messages/i/invalid-envvar-default/details.rst b/doc/data/messages/i/invalid-envvar-default/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-envvar-default/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-envvar-default/good.py b/doc/data/messages/i/invalid-envvar-default/good.py index c40beb573f..67656d04a2 100644 --- a/doc/data/messages/i/invalid-envvar-default/good.py +++ b/doc/data/messages/i/invalid-envvar-default/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +import os + +env = os.getenv('SECRET_KEY', '1') diff --git a/doc/data/messages/i/invalid-envvar-value/bad.py b/doc/data/messages/i/invalid-envvar-value/bad.py new file mode 100644 index 0000000000..56e60fe700 --- /dev/null +++ b/doc/data/messages/i/invalid-envvar-value/bad.py @@ -0,0 +1,3 @@ +import os + +os.getenv(1) # [invalid-envvar-value] diff --git a/doc/data/messages/i/invalid-envvar-value/details.rst b/doc/data/messages/i/invalid-envvar-value/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-envvar-value/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-envvar-value/good.py b/doc/data/messages/i/invalid-envvar-value/good.py index c40beb573f..0b510db39f 100644 --- a/doc/data/messages/i/invalid-envvar-value/good.py +++ b/doc/data/messages/i/invalid-envvar-value/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +import os + +os.getenv('1') diff --git a/doc/data/messages/i/invalid-format-index/bad.py b/doc/data/messages/i/invalid-format-index/bad.py new file mode 100644 index 0000000000..74e6502a16 --- /dev/null +++ b/doc/data/messages/i/invalid-format-index/bad.py @@ -0,0 +1,2 @@ +not_enough_fruits = ["apple"] +print('The second fruit is a {fruits[1]}'.format(fruits=not_enough_fruits)) # [invalid-format-index] diff --git a/doc/data/messages/i/invalid-format-index/details.rst b/doc/data/messages/i/invalid-format-index/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-format-index/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-format-index/good.py b/doc/data/messages/i/invalid-format-index/good.py index c40beb573f..37129745b0 100644 --- a/doc/data/messages/i/invalid-format-index/good.py +++ b/doc/data/messages/i/invalid-format-index/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +enough_fruits = ["apple", "banana"] +print('The second fruit is a {fruits[1]}'.format(fruits=enough_fruits)) diff --git a/doc/data/messages/i/invalid-format-returned/bad.py b/doc/data/messages/i/invalid-format-returned/bad.py new file mode 100644 index 0000000000..21412d91b0 --- /dev/null +++ b/doc/data/messages/i/invalid-format-returned/bad.py @@ -0,0 +1,5 @@ +class BadFormat: + """__format__ returns """ + + def __format__(self, format_spec): # [invalid-format-returned] + return 1 diff --git a/doc/data/messages/i/invalid-format-returned/details.rst b/doc/data/messages/i/invalid-format-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-format-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-format-returned/good.py b/doc/data/messages/i/invalid-format-returned/good.py index c40beb573f..69ab6fc070 100644 --- a/doc/data/messages/i/invalid-format-returned/good.py +++ b/doc/data/messages/i/invalid-format-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodFormat: + """__format__ returns """ + + def __format__(self, format_spec): + return "hello!" diff --git a/doc/data/messages/i/invalid-getnewargs-ex-returned/bad.py b/doc/data/messages/i/invalid-getnewargs-ex-returned/bad.py new file mode 100644 index 0000000000..2f5c5742e7 --- /dev/null +++ b/doc/data/messages/i/invalid-getnewargs-ex-returned/bad.py @@ -0,0 +1,5 @@ +class BadGetNewArgsEx: + """__getnewargs_ex__ returns tuple with incorrect arg length""" + + def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] + return (tuple(1), dict(x="y"), 1) diff --git a/doc/data/messages/i/invalid-getnewargs-ex-returned/details.rst b/doc/data/messages/i/invalid-getnewargs-ex-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-getnewargs-ex-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-getnewargs-ex-returned/good.py b/doc/data/messages/i/invalid-getnewargs-ex-returned/good.py index c40beb573f..b9cbb02889 100644 --- a/doc/data/messages/i/invalid-getnewargs-ex-returned/good.py +++ b/doc/data/messages/i/invalid-getnewargs-ex-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodGetNewArgsEx: + """__getnewargs_ex__ returns """ + + def __getnewargs_ex__(self): + return ((1,), {"2": 2}) diff --git a/doc/data/messages/i/invalid-getnewargs-returned/bad.py b/doc/data/messages/i/invalid-getnewargs-returned/bad.py new file mode 100644 index 0000000000..0864f7deb6 --- /dev/null +++ b/doc/data/messages/i/invalid-getnewargs-returned/bad.py @@ -0,0 +1,5 @@ +class BadGetNewArgs: + """__getnewargs__ returns an integer""" + + def __getnewargs__(self): # [invalid-getnewargs-returned] + return 1 diff --git a/doc/data/messages/i/invalid-getnewargs-returned/details.rst b/doc/data/messages/i/invalid-getnewargs-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-getnewargs-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-getnewargs-returned/good.py b/doc/data/messages/i/invalid-getnewargs-returned/good.py index c40beb573f..bdc547d4d5 100644 --- a/doc/data/messages/i/invalid-getnewargs-returned/good.py +++ b/doc/data/messages/i/invalid-getnewargs-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodGetNewArgs: + """__getnewargs__ returns """ + + def __getnewargs__(self): + return (1, 2) diff --git a/doc/data/messages/i/invalid-hash-returned/bad.py b/doc/data/messages/i/invalid-hash-returned/bad.py new file mode 100644 index 0000000000..ef0a9cb3f3 --- /dev/null +++ b/doc/data/messages/i/invalid-hash-returned/bad.py @@ -0,0 +1,5 @@ +class BadHash: + """__hash__ returns dict""" + + def __hash__(self): # [invalid-hash-returned] + return {} diff --git a/doc/data/messages/i/invalid-hash-returned/details.rst b/doc/data/messages/i/invalid-hash-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-hash-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-hash-returned/good.py b/doc/data/messages/i/invalid-hash-returned/good.py index c40beb573f..c912bf5a49 100644 --- a/doc/data/messages/i/invalid-hash-returned/good.py +++ b/doc/data/messages/i/invalid-hash-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodHash: + """__hash__ returns `int`""" + + def __hash__(self): + return 19 diff --git a/doc/data/messages/i/invalid-index-returned/bad.py b/doc/data/messages/i/invalid-index-returned/bad.py new file mode 100644 index 0000000000..197de0104e --- /dev/null +++ b/doc/data/messages/i/invalid-index-returned/bad.py @@ -0,0 +1,5 @@ +class BadIndex: + """__index__ returns a dict""" + + def __index__(self): # [invalid-index-returned] + return {"19": "19"} diff --git a/doc/data/messages/i/invalid-index-returned/details.rst b/doc/data/messages/i/invalid-index-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-index-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-index-returned/good.py b/doc/data/messages/i/invalid-index-returned/good.py index c40beb573f..3455ac2780 100644 --- a/doc/data/messages/i/invalid-index-returned/good.py +++ b/doc/data/messages/i/invalid-index-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodIndex: + """__index__ returns """ + + def __index__(self): + return 19 diff --git a/doc/data/messages/i/invalid-length-hint-returned/bad.py b/doc/data/messages/i/invalid-length-hint-returned/bad.py new file mode 100644 index 0000000000..9ec400ccce --- /dev/null +++ b/doc/data/messages/i/invalid-length-hint-returned/bad.py @@ -0,0 +1,5 @@ +class BadLengthHint: + """__length_hint__ returns non-int""" + + def __length_hint__(self): # [invalid-length-hint-returned] + return 3.0 diff --git a/doc/data/messages/i/invalid-length-hint-returned/details.rst b/doc/data/messages/i/invalid-length-hint-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-length-hint-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-length-hint-returned/good.py b/doc/data/messages/i/invalid-length-hint-returned/good.py index c40beb573f..ec294183a2 100644 --- a/doc/data/messages/i/invalid-length-hint-returned/good.py +++ b/doc/data/messages/i/invalid-length-hint-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodLengthHint: + """__length_hint__ returns """ + + def __length_hint__(self): + return 10 diff --git a/doc/data/messages/i/invalid-length-returned/bad.py b/doc/data/messages/i/invalid-length-returned/bad.py new file mode 100644 index 0000000000..0dcac176c6 --- /dev/null +++ b/doc/data/messages/i/invalid-length-returned/bad.py @@ -0,0 +1,6 @@ +class FruitBasket: + def __init__(self, fruits): + self.fruits = ["Apple", "Banana", "Orange"] + + def __len__(self): # [invalid-length-returned] + return - len(self.fruits) diff --git a/doc/data/messages/i/invalid-length-returned/details.rst b/doc/data/messages/i/invalid-length-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-length-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-length-returned/good.py b/doc/data/messages/i/invalid-length-returned/good.py index c40beb573f..1af71890fb 100644 --- a/doc/data/messages/i/invalid-length-returned/good.py +++ b/doc/data/messages/i/invalid-length-returned/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +class FruitBasket: + def __init__(self, fruits): + self.fruits = ["Apple", "Banana", "Orange"] + + def __len__(self): + return len(self.fruits) diff --git a/doc/data/messages/i/invalid-metaclass/bad.py b/doc/data/messages/i/invalid-metaclass/bad.py new file mode 100644 index 0000000000..301b4f20ec --- /dev/null +++ b/doc/data/messages/i/invalid-metaclass/bad.py @@ -0,0 +1,2 @@ +class Apple(metaclass=int): # [invalid-metaclass] + pass diff --git a/doc/data/messages/i/invalid-metaclass/details.rst b/doc/data/messages/i/invalid-metaclass/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-metaclass/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-metaclass/good.py b/doc/data/messages/i/invalid-metaclass/good.py index c40beb573f..e8b90fc01c 100644 --- a/doc/data/messages/i/invalid-metaclass/good.py +++ b/doc/data/messages/i/invalid-metaclass/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Plant: + pass + +class Apple(Plant): + pass diff --git a/doc/data/messages/i/invalid-name/details.rst b/doc/data/messages/i/invalid-name/details.rst index 0a612101ef..90160eaba5 100644 --- a/doc/data/messages/i/invalid-name/details.rst +++ b/doc/data/messages/i/invalid-name/details.rst @@ -84,7 +84,7 @@ The following type of names are checked with a predefined pattern: | Name type | Good names | Bad names | +====================+===================================================+============================================================+ | ``typevar`` | ``T``, ``_CallableT``, ``_T_co``, ``AnyStr``, | ``DICT_T``, ``CALLABLE_T``, ``ENUM_T``, ``DeviceType``, | -| | ``DeviceTypeT``, ``IPAddressT`` | ``_StrType`` | +| | ``DeviceTypeT``, ``IPAddressT`` | ``_StrType``, ``TAnyStr`` | +--------------------+---------------------------------------------------+------------------------------------------------------------+ Custom regular expressions diff --git a/doc/data/messages/i/invalid-overridden-method/bad.py b/doc/data/messages/i/invalid-overridden-method/bad.py new file mode 100644 index 0000000000..379f7c0a1b --- /dev/null +++ b/doc/data/messages/i/invalid-overridden-method/bad.py @@ -0,0 +1,7 @@ +class Fruit: + async def bore(self, insect): + insect.eat(self) + +class Apple(Fruit): + def bore(self, insect): # [invalid-overridden-method] + insect.eat(self) diff --git a/doc/data/messages/i/invalid-overridden-method/details.rst b/doc/data/messages/i/invalid-overridden-method/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-overridden-method/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-overridden-method/good.py b/doc/data/messages/i/invalid-overridden-method/good.py index c40beb573f..f5b56308f8 100644 --- a/doc/data/messages/i/invalid-overridden-method/good.py +++ b/doc/data/messages/i/invalid-overridden-method/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +class Fruit: + async def bore(self, insect): + insect.eat(self) + +class Apple(Fruit): + async def bore(self, insect): + insect.eat(self) diff --git a/doc/data/messages/i/invalid-repr-returned/bad.py b/doc/data/messages/i/invalid-repr-returned/bad.py new file mode 100644 index 0000000000..33d22256c6 --- /dev/null +++ b/doc/data/messages/i/invalid-repr-returned/bad.py @@ -0,0 +1,5 @@ +class Repr: + """__repr__ returns """ + + def __repr__(self): # [invalid-repr-returned] + return 1 diff --git a/doc/data/messages/i/invalid-repr-returned/details.rst b/doc/data/messages/i/invalid-repr-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-repr-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-repr-returned/good.py b/doc/data/messages/i/invalid-repr-returned/good.py index c40beb573f..120fbc4d72 100644 --- a/doc/data/messages/i/invalid-repr-returned/good.py +++ b/doc/data/messages/i/invalid-repr-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Repr: + """__repr__ returns """ + + def __repr__(self): + return "apples" diff --git a/doc/data/messages/i/invalid-sequence-index/bad.py b/doc/data/messages/i/invalid-sequence-index/bad.py new file mode 100644 index 0000000000..f153503b95 --- /dev/null +++ b/doc/data/messages/i/invalid-sequence-index/bad.py @@ -0,0 +1,2 @@ +fruits = ['apple', 'banana', 'orange'] +print(fruits['apple']) # [invalid-sequence-index] diff --git a/doc/data/messages/i/invalid-sequence-index/details.rst b/doc/data/messages/i/invalid-sequence-index/details.rst index ab82045295..7ac7b8a47d 100644 --- a/doc/data/messages/i/invalid-sequence-index/details.rst +++ b/doc/data/messages/i/invalid-sequence-index/details.rst @@ -1 +1,2 @@ -You can help us make the doc better `by contributing `_ ! +Be careful with ``[True]`` or ``[False]`` as sequence index, since ``True`` and ``False`` will respectively +be evaluated as ``1`` and ``0`` and will bring the second element of the list and the first without erroring. diff --git a/doc/data/messages/i/invalid-sequence-index/good.py b/doc/data/messages/i/invalid-sequence-index/good.py index c40beb573f..04bdf3ffd1 100644 --- a/doc/data/messages/i/invalid-sequence-index/good.py +++ b/doc/data/messages/i/invalid-sequence-index/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +fruits = ['apple', 'banana', 'orange'] +print(fruits[0]) diff --git a/doc/data/messages/i/invalid-slice-index/bad.py b/doc/data/messages/i/invalid-slice-index/bad.py new file mode 100644 index 0000000000..3a35b4a3f6 --- /dev/null +++ b/doc/data/messages/i/invalid-slice-index/bad.py @@ -0,0 +1,3 @@ +LETTERS = ["a", "b", "c", "d"] + +FIRST_THREE = LETTERS[:"3"] # [invalid-slice-index] diff --git a/doc/data/messages/i/invalid-slice-index/details.rst b/doc/data/messages/i/invalid-slice-index/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-slice-index/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-slice-index/good.py b/doc/data/messages/i/invalid-slice-index/good.py index c40beb573f..e17f31c4d9 100644 --- a/doc/data/messages/i/invalid-slice-index/good.py +++ b/doc/data/messages/i/invalid-slice-index/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +LETTERS = ["a", "b", "c", "d"] + +FIRST_THREE = LETTERS[:3] diff --git a/doc/data/messages/i/invalid-slice-step/bad.py b/doc/data/messages/i/invalid-slice-step/bad.py new file mode 100644 index 0000000000..a860ce14a3 --- /dev/null +++ b/doc/data/messages/i/invalid-slice-step/bad.py @@ -0,0 +1,3 @@ +LETTERS = ["a", "b", "c", "d"] + +LETTERS[::0] # [invalid-slice-step] diff --git a/doc/data/messages/i/invalid-slice-step/good.py b/doc/data/messages/i/invalid-slice-step/good.py new file mode 100644 index 0000000000..c81d80331f --- /dev/null +++ b/doc/data/messages/i/invalid-slice-step/good.py @@ -0,0 +1,3 @@ +LETTERS = ["a", "b", "c", "d"] + +LETTERS[::2] diff --git a/doc/data/messages/i/invalid-slots/bad.py b/doc/data/messages/i/invalid-slots/bad.py new file mode 100644 index 0000000000..d1184c8118 --- /dev/null +++ b/doc/data/messages/i/invalid-slots/bad.py @@ -0,0 +1,2 @@ +class Person: # [invalid-slots] + __slots__ = 42 diff --git a/doc/data/messages/i/invalid-slots/details.rst b/doc/data/messages/i/invalid-slots/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-slots/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-slots/good.py b/doc/data/messages/i/invalid-slots/good.py index c40beb573f..0cb4d1b1e3 100644 --- a/doc/data/messages/i/invalid-slots/good.py +++ b/doc/data/messages/i/invalid-slots/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +class Person: + __slots__ = ("name", "age",) diff --git a/doc/data/messages/i/invalid-star-assignment-target/bad.py b/doc/data/messages/i/invalid-star-assignment-target/bad.py new file mode 100644 index 0000000000..fc69dc7e62 --- /dev/null +++ b/doc/data/messages/i/invalid-star-assignment-target/bad.py @@ -0,0 +1 @@ +*fruit = ['apple', 'banana', 'orange'] # [invalid-star-assignment-target] diff --git a/doc/data/messages/i/invalid-star-assignment-target/details.rst b/doc/data/messages/i/invalid-star-assignment-target/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-star-assignment-target/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-star-assignment-target/good.py b/doc/data/messages/i/invalid-star-assignment-target/good.py index c40beb573f..74cf088e27 100644 --- a/doc/data/messages/i/invalid-star-assignment-target/good.py +++ b/doc/data/messages/i/invalid-star-assignment-target/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +fruit = ['apple', 'banana', 'orange'] diff --git a/doc/data/messages/i/invalid-str-returned/bad.py b/doc/data/messages/i/invalid-str-returned/bad.py new file mode 100644 index 0000000000..6826ce325c --- /dev/null +++ b/doc/data/messages/i/invalid-str-returned/bad.py @@ -0,0 +1,5 @@ +class Str: + """__str__ returns int""" + + def __str__(self): # [invalid-str-returned] + return 1 diff --git a/doc/data/messages/i/invalid-str-returned/details.rst b/doc/data/messages/i/invalid-str-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-str-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-str-returned/good.py b/doc/data/messages/i/invalid-str-returned/good.py index c40beb573f..bf2682b23b 100644 --- a/doc/data/messages/i/invalid-str-returned/good.py +++ b/doc/data/messages/i/invalid-str-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Str: + """__str__ returns """ + + def __str__(self): + return "oranges" diff --git a/doc/data/messages/i/invalid-unary-operand-type/bad.py b/doc/data/messages/i/invalid-unary-operand-type/bad.py new file mode 100644 index 0000000000..77391c3d9e --- /dev/null +++ b/doc/data/messages/i/invalid-unary-operand-type/bad.py @@ -0,0 +1,3 @@ +cherries = 10 +eaten_cherries = int +cherries = - eaten_cherries # [invalid-unary-operand-type] diff --git a/doc/data/messages/i/invalid-unary-operand-type/details.rst b/doc/data/messages/i/invalid-unary-operand-type/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/invalid-unary-operand-type/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/invalid-unary-operand-type/good.py b/doc/data/messages/i/invalid-unary-operand-type/good.py index c40beb573f..a9a47f5a28 100644 --- a/doc/data/messages/i/invalid-unary-operand-type/good.py +++ b/doc/data/messages/i/invalid-unary-operand-type/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +cherries = 10 +eaten_cherries = 2 +cherries -= eaten_cherries diff --git a/doc/data/messages/i/isinstance-second-argument-not-valid-type/bad.py b/doc/data/messages/i/isinstance-second-argument-not-valid-type/bad.py new file mode 100644 index 0000000000..5fb3b83758 --- /dev/null +++ b/doc/data/messages/i/isinstance-second-argument-not-valid-type/bad.py @@ -0,0 +1 @@ +isinstance("apples and oranges", hex) # [isinstance-second-argument-not-valid-type] diff --git a/doc/data/messages/i/isinstance-second-argument-not-valid-type/details.rst b/doc/data/messages/i/isinstance-second-argument-not-valid-type/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/i/isinstance-second-argument-not-valid-type/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/i/isinstance-second-argument-not-valid-type/good.py b/doc/data/messages/i/isinstance-second-argument-not-valid-type/good.py index c40beb573f..c1df5fca82 100644 --- a/doc/data/messages/i/isinstance-second-argument-not-valid-type/good.py +++ b/doc/data/messages/i/isinstance-second-argument-not-valid-type/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +isinstance("apples and oranges", str) diff --git a/doc/data/messages/k/keyword-arg-before-vararg/bad.py b/doc/data/messages/k/keyword-arg-before-vararg/bad.py new file mode 100644 index 0000000000..562724e2b7 --- /dev/null +++ b/doc/data/messages/k/keyword-arg-before-vararg/bad.py @@ -0,0 +1,2 @@ +def func(x=None, *args): # [keyword-arg-before-vararg] + return [x, *args] diff --git a/doc/data/messages/k/keyword-arg-before-vararg/details.rst b/doc/data/messages/k/keyword-arg-before-vararg/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/k/keyword-arg-before-vararg/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/k/keyword-arg-before-vararg/good.py b/doc/data/messages/k/keyword-arg-before-vararg/good.py index c40beb573f..e1f14ba782 100644 --- a/doc/data/messages/k/keyword-arg-before-vararg/good.py +++ b/doc/data/messages/k/keyword-arg-before-vararg/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def func(*args, x=None): + return [*args, x] diff --git a/doc/data/messages/l/line-too-long/bad.py b/doc/data/messages/l/line-too-long/bad.py index 4b82d8cd25..94e9042a3e 100644 --- a/doc/data/messages/l/line-too-long/bad.py +++ b/doc/data/messages/l/line-too-long/bad.py @@ -1 +1,2 @@ -FRUIT = ["apricot", "blackcurrant", "cantaloupe", "dragon fruit", "elderberry", "fig", "grapefruit"] # [line-too-long] +# +1: [line-too-long] +FRUIT = ["apricot", "blackcurrant", "cantaloupe", "dragon fruit", "elderberry", "fig", "grapefruit", ] diff --git a/doc/data/messages/l/line-too-long/details.rst b/doc/data/messages/l/line-too-long/details.rst new file mode 100644 index 0000000000..068a453b48 --- /dev/null +++ b/doc/data/messages/l/line-too-long/details.rst @@ -0,0 +1,3 @@ +If you attempt to disable this message via ``# pylint: disable=line-too-long`` in a module with no code, you may receive a message for ``useless-suppression``. This is a false positive of ``useless-suppression`` we can't easily fix. + +See https://github.com/PyCQA/pylint/issues/3368 for more information. diff --git a/doc/data/messages/l/line-too-long/pylintrc b/doc/data/messages/l/line-too-long/pylintrc new file mode 100644 index 0000000000..81ee5451ec --- /dev/null +++ b/doc/data/messages/l/line-too-long/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +max-line-length=100 diff --git a/doc/data/messages/l/literal-comparison/bad.py b/doc/data/messages/l/literal-comparison/bad.py index d85667b7e4..154d7d6c74 100644 --- a/doc/data/messages/l/literal-comparison/bad.py +++ b/doc/data/messages/l/literal-comparison/bad.py @@ -1,2 +1,2 @@ def is_an_orange(fruit): - return fruit is "orange" # [literal-comparison] + return fruit is "orange" # [literal-comparison] diff --git a/doc/data/messages/l/logging-format-interpolation/details.rst b/doc/data/messages/l/logging-format-interpolation/details.rst index 7344b3f84d..984484a061 100644 --- a/doc/data/messages/l/logging-format-interpolation/details.rst +++ b/doc/data/messages/l/logging-format-interpolation/details.rst @@ -1,2 +1,2 @@ -Another reasonable option is to use f-string. If you want to do that, you need to enable +Another reasonable option is to use f-string. If you want to do that, you need to enable ``logging-format-interpolation`` and disable ``logging-fstring-interpolation``. diff --git a/doc/data/messages/l/logging-format-truncated/bad.py b/doc/data/messages/l/logging-format-truncated/bad.py new file mode 100644 index 0000000000..9f3f162605 --- /dev/null +++ b/doc/data/messages/l/logging-format-truncated/bad.py @@ -0,0 +1,3 @@ +import logging + +logging.warning("Here is a variable: %", my_var) # [logging-format-truncated] diff --git a/doc/data/messages/l/logging-format-truncated/details.rst b/doc/data/messages/l/logging-format-truncated/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/l/logging-format-truncated/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/l/logging-format-truncated/good.py b/doc/data/messages/l/logging-format-truncated/good.py index c40beb573f..5dd3889a6b 100644 --- a/doc/data/messages/l/logging-format-truncated/good.py +++ b/doc/data/messages/l/logging-format-truncated/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +import logging + +logging.warning("Here is a variable: %s", my_var) diff --git a/doc/data/messages/l/logging-fstring-interpolation/details.rst b/doc/data/messages/l/logging-fstring-interpolation/details.rst index d29646061b..c472f1d8ba 100644 --- a/doc/data/messages/l/logging-fstring-interpolation/details.rst +++ b/doc/data/messages/l/logging-fstring-interpolation/details.rst @@ -1,2 +1,2 @@ -This message permits to allow f-string in logging and still be warned of +This message permits to allow f-string in logging and still be warned of ``logging-format-interpolation``. diff --git a/doc/data/messages/l/logging-not-lazy/bad.py b/doc/data/messages/l/logging-not-lazy/bad.py index be6b98cb61..cb6bc3d840 100644 --- a/doc/data/messages/l/logging-not-lazy/bad.py +++ b/doc/data/messages/l/logging-not-lazy/bad.py @@ -3,5 +3,5 @@ try: function() except Exception as e: - logging.error('Error occured: %s' % e) # [logging-not-lazy] + logging.error('Error occurred: %s' % e) # [logging-not-lazy] raise diff --git a/doc/data/messages/l/logging-not-lazy/good.py b/doc/data/messages/l/logging-not-lazy/good.py index ac8503ec99..c61704872f 100644 --- a/doc/data/messages/l/logging-not-lazy/good.py +++ b/doc/data/messages/l/logging-not-lazy/good.py @@ -3,5 +3,5 @@ try: function() except Exception as e: - logging.error('Error occured: %s', e) + logging.error('Error occurred: %s', e) raise diff --git a/doc/data/messages/l/logging-too-few-args/bad.py b/doc/data/messages/l/logging-too-few-args/bad.py index c118c2602e..66d88d0b35 100644 --- a/doc/data/messages/l/logging-too-few-args/bad.py +++ b/doc/data/messages/l/logging-too-few-args/bad.py @@ -3,5 +3,5 @@ try: function() except Exception as e: - logging.error('%s error occured: %s', e) # [logging-too-few-args] + logging.error('%s error occurred: %s', e) # [logging-too-few-args] raise diff --git a/doc/data/messages/l/logging-too-few-args/good.py b/doc/data/messages/l/logging-too-few-args/good.py index 3772c59b20..4b34b58b9e 100644 --- a/doc/data/messages/l/logging-too-few-args/good.py +++ b/doc/data/messages/l/logging-too-few-args/good.py @@ -3,5 +3,5 @@ try: function() except Exception as e: - logging.error('%s error occured: %s', type(e), e) + logging.error('%s error occurred: %s', type(e), e) raise diff --git a/doc/data/messages/l/logging-too-many-args/bad.py b/doc/data/messages/l/logging-too-many-args/bad.py index f94222aa4f..45f485d935 100644 --- a/doc/data/messages/l/logging-too-many-args/bad.py +++ b/doc/data/messages/l/logging-too-many-args/bad.py @@ -3,5 +3,5 @@ try: function() except Exception as e: - logging.error('Error occured: %s', type(e), e) # [logging-too-many-args] + logging.error('Error occurred: %s', type(e), e) # [logging-too-many-args] raise diff --git a/doc/data/messages/l/logging-too-many-args/good.py b/doc/data/messages/l/logging-too-many-args/good.py index 3772c59b20..4b34b58b9e 100644 --- a/doc/data/messages/l/logging-too-many-args/good.py +++ b/doc/data/messages/l/logging-too-many-args/good.py @@ -3,5 +3,5 @@ try: function() except Exception as e: - logging.error('%s error occured: %s', type(e), e) + logging.error('%s error occurred: %s', type(e), e) raise diff --git a/doc/data/messages/l/lost-exception/bad.py b/doc/data/messages/l/lost-exception/bad.py new file mode 100644 index 0000000000..c6e5c02e7d --- /dev/null +++ b/doc/data/messages/l/lost-exception/bad.py @@ -0,0 +1,12 @@ +class FasterThanTheSpeedOfLightError(ZeroDivisionError): + def __init__(self): + super().__init__("You can't go faster than the speed of light !") + + +def calculate_speed(distance: float, time: float) -> float: + try: + return distance / time + except ZeroDivisionError as e: + raise FasterThanTheSpeedOfLightError() from e + finally: + return 299792458 # [lost-exception] diff --git a/doc/data/messages/l/lost-exception/details.rst b/doc/data/messages/l/lost-exception/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/l/lost-exception/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/l/lost-exception/good.py b/doc/data/messages/l/lost-exception/good.py index c40beb573f..a269492621 100644 --- a/doc/data/messages/l/lost-exception/good.py +++ b/doc/data/messages/l/lost-exception/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +class FasterThanTheSpeedOfLightError(ZeroDivisionError): + def __init__(self): + super().__init__("You can't go faster than the speed of light !") + + +def calculate_speed(distance: float, time: float) -> float: + try: + return distance / time + except ZeroDivisionError as e: + raise FasterThanTheSpeedOfLightError() from e diff --git a/doc/data/messages/m/magic-value-comparison/bad.py b/doc/data/messages/m/magic-value-comparison/bad.py new file mode 100644 index 0000000000..536659abe7 --- /dev/null +++ b/doc/data/messages/m/magic-value-comparison/bad.py @@ -0,0 +1,10 @@ +import random + +measurement = random.randint(0, 200) +above_threshold = False +i = 0 +while i < 5: # [magic-value-comparison] + above_threshold = measurement > 100 # [magic-value-comparison] + if above_threshold: + break + measurement = random.randint(0, 200) diff --git a/doc/data/messages/m/magic-value-comparison/good.py b/doc/data/messages/m/magic-value-comparison/good.py new file mode 100644 index 0000000000..4b89609069 --- /dev/null +++ b/doc/data/messages/m/magic-value-comparison/good.py @@ -0,0 +1,15 @@ +import random + +MAX_NUM_OF_ITERATIONS = 5 +THRESHOLD_VAL = 100 +MIN_MEASUREMENT_VAL = 0 +MAX_MEASUREMENT_VAL = 200 + +measurement = random.randint(MIN_MEASUREMENT_VAL, MAX_MEASUREMENT_VAL) +above_threshold = False +i = 0 +while i < MAX_NUM_OF_ITERATIONS: + above_threshold = measurement > THRESHOLD_VAL + if above_threshold: + break + measurement = random.randint(MIN_MEASUREMENT_VAL, MAX_MEASUREMENT_VAL) diff --git a/doc/data/messages/m/magic-value-comparison/pylintrc b/doc/data/messages/m/magic-value-comparison/pylintrc new file mode 100644 index 0000000000..c4980c1350 --- /dev/null +++ b/doc/data/messages/m/magic-value-comparison/pylintrc @@ -0,0 +1,2 @@ +[main] +load-plugins=pylint.extensions.magic_value diff --git a/doc/data/messages/m/method-cache-max-size-none/bad.py b/doc/data/messages/m/method-cache-max-size-none/bad.py index 7e70b688bc..7bef647c52 100644 --- a/doc/data/messages/m/method-cache-max-size-none/bad.py +++ b/doc/data/messages/m/method-cache-max-size-none/bad.py @@ -2,8 +2,11 @@ class Fibonnaci: + def __init__(self): + self.result = [] + @functools.lru_cache(maxsize=None) # [method-cache-max-size-none] def fibonacci(self, n): if n in {0, 1}: - return n - return self.fibonacci(n - 1) + self.fibonacci(n - 2) + self.result.append(n) + self.result.append(self.fibonacci(n - 1) + self.fibonacci(n - 2)) diff --git a/doc/data/messages/m/method-cache-max-size-none/good.py b/doc/data/messages/m/method-cache-max-size-none/good.py index 6f3ca256ae..1ba4136953 100644 --- a/doc/data/messages/m/method-cache-max-size-none/good.py +++ b/doc/data/messages/m/method-cache-max-size-none/good.py @@ -9,5 +9,8 @@ def cached_fibonacci(n): class Fibonnaci: + def __init__(self): + self.result = [] + def fibonacci(self, n): - return cached_fibonacci(n) + self.result.append(cached_fibonacci(n)) diff --git a/doc/data/messages/m/method-check-failed/details.rst b/doc/data/messages/m/method-check-failed/details.rst index ab82045295..1c43031371 100644 --- a/doc/data/messages/m/method-check-failed/details.rst +++ b/doc/data/messages/m/method-check-failed/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing `_ ! +This is a message linked to an internal problem in pylint. There's nothing to change in your code. diff --git a/doc/data/messages/m/method-check-failed/good.py b/doc/data/messages/m/method-check-failed/good.py deleted file mode 100644 index c40beb573f..0000000000 --- a/doc/data/messages/m/method-check-failed/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/m/method-hidden/bad.py b/doc/data/messages/m/method-hidden/bad.py new file mode 100644 index 0000000000..5dc113047b --- /dev/null +++ b/doc/data/messages/m/method-hidden/bad.py @@ -0,0 +1,6 @@ +class Fruit: + def __init__(self, vitamins): + self.vitamins = vitamins + + def vitamins(self): # [method-hidden] + pass diff --git a/doc/data/messages/m/method-hidden/details.rst b/doc/data/messages/m/method-hidden/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/method-hidden/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/method-hidden/good.py b/doc/data/messages/m/method-hidden/good.py index c40beb573f..cfc23c2366 100644 --- a/doc/data/messages/m/method-hidden/good.py +++ b/doc/data/messages/m/method-hidden/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self, vitamins): + self.vitamins = vitamins + + def antioxidants(self): + pass diff --git a/doc/data/messages/m/misplaced-bare-raise/bad.py b/doc/data/messages/m/misplaced-bare-raise/bad.py new file mode 100644 index 0000000000..c7fc0ee995 --- /dev/null +++ b/doc/data/messages/m/misplaced-bare-raise/bad.py @@ -0,0 +1,3 @@ +def validate_positive(x): + if x <= 0: + raise # [misplaced-bare-raise] diff --git a/doc/data/messages/m/misplaced-bare-raise/details.rst b/doc/data/messages/m/misplaced-bare-raise/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/misplaced-bare-raise/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/misplaced-bare-raise/good.py b/doc/data/messages/m/misplaced-bare-raise/good.py index c40beb573f..bc2333c762 100644 --- a/doc/data/messages/m/misplaced-bare-raise/good.py +++ b/doc/data/messages/m/misplaced-bare-raise/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +def validate_positive(x): + if x <= 0: + raise ValueError(f"{x} is not positive") diff --git a/doc/data/messages/m/misplaced-comparison-constant/bad.py b/doc/data/messages/m/misplaced-comparison-constant/bad.py new file mode 100644 index 0000000000..1a5712a324 --- /dev/null +++ b/doc/data/messages/m/misplaced-comparison-constant/bad.py @@ -0,0 +1,8 @@ +def compare_apples(apples=20): + for i in range(10): + if 5 <= i: # [misplaced-comparison-constant] + pass + if 1 == i: # [misplaced-comparison-constant] + pass + if 20 < len(apples): # [misplaced-comparison-constant] + pass diff --git a/doc/data/messages/m/misplaced-comparison-constant/details.rst b/doc/data/messages/m/misplaced-comparison-constant/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/misplaced-comparison-constant/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/misplaced-comparison-constant/good.py b/doc/data/messages/m/misplaced-comparison-constant/good.py index c40beb573f..ba00a7f23a 100644 --- a/doc/data/messages/m/misplaced-comparison-constant/good.py +++ b/doc/data/messages/m/misplaced-comparison-constant/good.py @@ -1 +1,8 @@ -# This is a placeholder for correct code for this message. +def compare_apples(apples=20): + for i in range(10): + if i >= 5: + pass + if i == 1: + pass + if len(apples) > 20: + pass diff --git a/doc/data/messages/m/misplaced-comparison-constant/pylintrc b/doc/data/messages/m/misplaced-comparison-constant/pylintrc new file mode 100644 index 0000000000..aa3b1f8ebc --- /dev/null +++ b/doc/data/messages/m/misplaced-comparison-constant/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.comparison_placement diff --git a/doc/data/messages/m/misplaced-format-function/bad.py b/doc/data/messages/m/misplaced-format-function/bad.py new file mode 100644 index 0000000000..0bd1723696 --- /dev/null +++ b/doc/data/messages/m/misplaced-format-function/bad.py @@ -0,0 +1 @@ +print('Value: {}').format('Car') # [misplaced-format-function] diff --git a/doc/data/messages/m/misplaced-format-function/details.rst b/doc/data/messages/m/misplaced-format-function/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/misplaced-format-function/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/misplaced-format-function/good.py b/doc/data/messages/m/misplaced-format-function/good.py index c40beb573f..809dcf9744 100644 --- a/doc/data/messages/m/misplaced-format-function/good.py +++ b/doc/data/messages/m/misplaced-format-function/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +print('Value: {}'.format('Car')) diff --git a/doc/data/messages/m/misplaced-future/details.rst b/doc/data/messages/m/misplaced-future/details.rst new file mode 100644 index 0000000000..6b1f479b2c --- /dev/null +++ b/doc/data/messages/m/misplaced-future/details.rst @@ -0,0 +1 @@ +A bare raise statement will re-raise the last active exception in the current scope. If the ``raise`` statement is not in an ``except`` or ``finally`` block, a RuntimeError will be raised instead. diff --git a/doc/data/messages/m/missing-format-attribute/bad.py b/doc/data/messages/m/missing-format-attribute/bad.py new file mode 100644 index 0000000000..2144c7d804 --- /dev/null +++ b/doc/data/messages/m/missing-format-attribute/bad.py @@ -0,0 +1 @@ +print("{0.real}".format("1")) # [missing-format-attribute] diff --git a/doc/data/messages/m/missing-format-attribute/details.rst b/doc/data/messages/m/missing-format-attribute/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/missing-format-attribute/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/missing-format-attribute/good.py b/doc/data/messages/m/missing-format-attribute/good.py index c40beb573f..71f89a0bf5 100644 --- a/doc/data/messages/m/missing-format-attribute/good.py +++ b/doc/data/messages/m/missing-format-attribute/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +print("{0.real}".format(1)) diff --git a/doc/data/messages/m/missing-param-doc/pylintrc b/doc/data/messages/m/missing-param-doc/pylintrc new file mode 100644 index 0000000000..4547f98117 --- /dev/null +++ b/doc/data/messages/m/missing-param-doc/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins = pylint.extensions.docparams diff --git a/doc/data/messages/m/missing-raises-doc/pylintrc b/doc/data/messages/m/missing-raises-doc/pylintrc new file mode 100644 index 0000000000..7bdd1242da --- /dev/null +++ b/doc/data/messages/m/missing-raises-doc/pylintrc @@ -0,0 +1,5 @@ +[MAIN] +load-plugins = pylint.extensions.docparams + +[BASIC] +accept-no-raise-doc = no diff --git a/doc/data/messages/m/missing-timeout/bad.py b/doc/data/messages/m/missing-timeout/bad.py new file mode 100644 index 0000000000..52444f28aa --- /dev/null +++ b/doc/data/messages/m/missing-timeout/bad.py @@ -0,0 +1,3 @@ +import requests + +requests.post("http://localhost") # [missing-timeout] diff --git a/doc/data/messages/m/missing-timeout/details.rst b/doc/data/messages/m/missing-timeout/details.rst new file mode 100644 index 0000000000..6926338c26 --- /dev/null +++ b/doc/data/messages/m/missing-timeout/details.rst @@ -0,0 +1,10 @@ +You can add new methods that should have a defined ```timeout`` argument as qualified names +in the ``timeout-methods`` option, for example: + +* ``requests.api.get`` +* ``requests.api.head`` +* ``requests.api.options`` +* ``requests.api.patch`` +* ``requests.api.post`` +* ``requests.api.put`` +* ``requests.api.request`` diff --git a/doc/data/messages/m/missing-timeout/good.py b/doc/data/messages/m/missing-timeout/good.py new file mode 100644 index 0000000000..dbeb51255e --- /dev/null +++ b/doc/data/messages/m/missing-timeout/good.py @@ -0,0 +1,3 @@ +import requests + +requests.post("http://localhost", timeout=10) diff --git a/doc/data/messages/m/missing-type-doc/pylintrc b/doc/data/messages/m/missing-type-doc/pylintrc new file mode 100644 index 0000000000..4547f98117 --- /dev/null +++ b/doc/data/messages/m/missing-type-doc/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins = pylint.extensions.docparams diff --git a/doc/data/messages/m/mixed-format-string/bad.py b/doc/data/messages/m/mixed-format-string/bad.py new file mode 100644 index 0000000000..b0b455e645 --- /dev/null +++ b/doc/data/messages/m/mixed-format-string/bad.py @@ -0,0 +1 @@ +print("x=%(x)d, y=%d" % (0, 1)) # [mixed-format-string] diff --git a/doc/data/messages/m/mixed-format-string/details.rst b/doc/data/messages/m/mixed-format-string/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/mixed-format-string/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/mixed-format-string/good.py b/doc/data/messages/m/mixed-format-string/good.py index c40beb573f..c637e0fb7d 100644 --- a/doc/data/messages/m/mixed-format-string/good.py +++ b/doc/data/messages/m/mixed-format-string/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +print("x=%d, y=%d" % (0, 1)) +print("x=%(x)d, y=%(y)d" % {"x": 0, "y": 1}) diff --git a/doc/data/messages/m/modified-iterating-dict/bad.py b/doc/data/messages/m/modified-iterating-dict/bad.py new file mode 100644 index 0000000000..cd31a62db1 --- /dev/null +++ b/doc/data/messages/m/modified-iterating-dict/bad.py @@ -0,0 +1,6 @@ +fruits = {"apple": 1, "orange": 2, "mango": 3} + +i = 0 +for fruit in fruits: + fruits["apple"] = i # [modified-iterating-dict] + i += 1 diff --git a/doc/data/messages/m/modified-iterating-dict/details.rst b/doc/data/messages/m/modified-iterating-dict/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/modified-iterating-dict/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/modified-iterating-dict/good.py b/doc/data/messages/m/modified-iterating-dict/good.py index c40beb573f..8755a6c458 100644 --- a/doc/data/messages/m/modified-iterating-dict/good.py +++ b/doc/data/messages/m/modified-iterating-dict/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +fruits = {"apple": 1, "orange": 2, "mango": 3} + +i = 0 +for fruit in fruits.copy(): + fruits["apple"] = i + i += 1 diff --git a/doc/data/messages/m/modified-iterating-list/bad.py b/doc/data/messages/m/modified-iterating-list/bad.py new file mode 100644 index 0000000000..57d77150e7 --- /dev/null +++ b/doc/data/messages/m/modified-iterating-list/bad.py @@ -0,0 +1,3 @@ +fruits = ["apple", "orange", "mango"] +for fruit in fruits: + fruits.append("pineapple") # [modified-iterating-list] diff --git a/doc/data/messages/m/modified-iterating-list/details.rst b/doc/data/messages/m/modified-iterating-list/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/modified-iterating-list/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/modified-iterating-list/good.py b/doc/data/messages/m/modified-iterating-list/good.py index c40beb573f..0132f8b64f 100644 --- a/doc/data/messages/m/modified-iterating-list/good.py +++ b/doc/data/messages/m/modified-iterating-list/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +fruits = ["apple", "orange", "mango"] +for fruit in fruits.copy(): + fruits.append("pineapple") diff --git a/doc/data/messages/m/modified-iterating-set/bad.py b/doc/data/messages/m/modified-iterating-set/bad.py new file mode 100644 index 0000000000..bd82a564fa --- /dev/null +++ b/doc/data/messages/m/modified-iterating-set/bad.py @@ -0,0 +1,3 @@ +fruits = {"apple", "orange", "mango"} +for fruit in fruits: + fruits.add(fruit + "yum") # [modified-iterating-set] diff --git a/doc/data/messages/m/modified-iterating-set/details.rst b/doc/data/messages/m/modified-iterating-set/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/modified-iterating-set/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/modified-iterating-set/good.py b/doc/data/messages/m/modified-iterating-set/good.py index c40beb573f..0af8e24265 100644 --- a/doc/data/messages/m/modified-iterating-set/good.py +++ b/doc/data/messages/m/modified-iterating-set/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +fruits = {"apple", "orange", "mango"} +for fruit in fruits.copy(): + fruits.add(fruit + "yum") diff --git a/doc/data/messages/m/multiple-constructor-doc/bad.py b/doc/data/messages/m/multiple-constructor-doc/bad.py new file mode 100644 index 0000000000..44defd19e2 --- /dev/null +++ b/doc/data/messages/m/multiple-constructor-doc/bad.py @@ -0,0 +1,15 @@ +class Point: # [multiple-constructor-doc] + """Represents a point in the xy-coordinate plane. + + :param x: coordinate + :param y: coordinate + """ + + def __init__(self, x, y): + """Represents a point in the xy-coordinate plane. + + :param x: coordinate + :param y: coordinate + """ + self.x = x + self.y = y diff --git a/doc/data/messages/m/multiple-constructor-doc/details.rst b/doc/data/messages/m/multiple-constructor-doc/details.rst index ab82045295..3639ec36be 100644 --- a/doc/data/messages/m/multiple-constructor-doc/details.rst +++ b/doc/data/messages/m/multiple-constructor-doc/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing `_ ! +Both docstrings are acceptable but not both at the same time. diff --git a/doc/data/messages/m/multiple-constructor-doc/good.py b/doc/data/messages/m/multiple-constructor-doc/good.py index c40beb573f..d96d5ce0d7 100644 --- a/doc/data/messages/m/multiple-constructor-doc/good.py +++ b/doc/data/messages/m/multiple-constructor-doc/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Point: + def __init__(self, x, y): + """Represents a point in the xy-coordinate plane. + + :param x: x coordinate + :param y: y coordinate + """ + self.x = x + self.y = y diff --git a/doc/data/messages/m/multiple-constructor-doc/pylintrc b/doc/data/messages/m/multiple-constructor-doc/pylintrc new file mode 100644 index 0000000000..2b6f061c97 --- /dev/null +++ b/doc/data/messages/m/multiple-constructor-doc/pylintrc @@ -0,0 +1,5 @@ +[main] +load-plugins=pylint.extensions.docparams + +[Parameter_documentation] +no-docstring-rgx=^(?!__init__$)_ diff --git a/doc/data/messages/m/multiple-imports/bad.py b/doc/data/messages/m/multiple-imports/bad.py new file mode 100644 index 0000000000..21a990d0bc --- /dev/null +++ b/doc/data/messages/m/multiple-imports/bad.py @@ -0,0 +1 @@ +import os, sys # [multiple-imports] diff --git a/doc/data/messages/m/multiple-imports/details.rst b/doc/data/messages/m/multiple-imports/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/multiple-imports/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/multiple-imports/good.py b/doc/data/messages/m/multiple-imports/good.py index c40beb573f..82cda0b356 100644 --- a/doc/data/messages/m/multiple-imports/good.py +++ b/doc/data/messages/m/multiple-imports/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +import os +import sys diff --git a/doc/data/messages/m/multiple-statements/bad.py b/doc/data/messages/m/multiple-statements/bad.py new file mode 100644 index 0000000000..754eede368 --- /dev/null +++ b/doc/data/messages/m/multiple-statements/bad.py @@ -0,0 +1,5 @@ +fruits = ["apple", "orange", "mango"] + +if "apple" in fruits: pass # [multiple-statements] +else: + print("no apples!") diff --git a/doc/data/messages/m/multiple-statements/details.rst b/doc/data/messages/m/multiple-statements/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/m/multiple-statements/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/m/multiple-statements/good.py b/doc/data/messages/m/multiple-statements/good.py index c40beb573f..9328c83fdc 100644 --- a/doc/data/messages/m/multiple-statements/good.py +++ b/doc/data/messages/m/multiple-statements/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +fruits = ["apple", "orange", "mango"] + +if "apple" in fruits: + pass +else: + print("no apples!") diff --git a/doc/data/messages/n/named-expr-without-context/bad.py b/doc/data/messages/n/named-expr-without-context/bad.py new file mode 100644 index 0000000000..c5d2ffba7d --- /dev/null +++ b/doc/data/messages/n/named-expr-without-context/bad.py @@ -0,0 +1 @@ +(a := 42) # [named-expr-without-context] diff --git a/doc/data/messages/n/named-expr-without-context/good.py b/doc/data/messages/n/named-expr-without-context/good.py new file mode 100644 index 0000000000..50f6b26210 --- /dev/null +++ b/doc/data/messages/n/named-expr-without-context/good.py @@ -0,0 +1,2 @@ +if (a := 42): + print('Success') diff --git a/doc/data/messages/n/nan-comparison/bad.py b/doc/data/messages/n/nan-comparison/bad.py new file mode 100644 index 0000000000..911686520f --- /dev/null +++ b/doc/data/messages/n/nan-comparison/bad.py @@ -0,0 +1,5 @@ +import numpy as np + + +def both_nan(x, y) -> bool: + return x == np.NaN and y == float("nan") # [nan-comparison, nan-comparison] diff --git a/doc/data/messages/n/nan-comparison/details.rst b/doc/data/messages/n/nan-comparison/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/nan-comparison/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/nan-comparison/good.py b/doc/data/messages/n/nan-comparison/good.py index c40beb573f..31f54edf47 100644 --- a/doc/data/messages/n/nan-comparison/good.py +++ b/doc/data/messages/n/nan-comparison/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +import numpy as np + + +def both_nan(x, y) -> bool: + return np.isnan(x) and np.isnan(y) diff --git a/doc/data/messages/n/nested-min-max/bad.py b/doc/data/messages/n/nested-min-max/bad.py new file mode 100644 index 0000000000..b3e13db3a1 --- /dev/null +++ b/doc/data/messages/n/nested-min-max/bad.py @@ -0,0 +1 @@ +print(min(1, min(2, 3))) # [nested-min-max] diff --git a/doc/data/messages/n/nested-min-max/good.py b/doc/data/messages/n/nested-min-max/good.py new file mode 100644 index 0000000000..2d348b2248 --- /dev/null +++ b/doc/data/messages/n/nested-min-max/good.py @@ -0,0 +1 @@ +print(min(1, 2, 3)) diff --git a/doc/data/messages/n/no-classmethod-decorator/bad.py b/doc/data/messages/n/no-classmethod-decorator/bad.py new file mode 100644 index 0000000000..55c4f4d0f8 --- /dev/null +++ b/doc/data/messages/n/no-classmethod-decorator/bad.py @@ -0,0 +1,11 @@ +class Fruit: + COLORS = [] + + def __init__(self, color): + self.color = color + + def pick_colors(cls, *args): + """classmethod to pick fruit colors""" + cls.COLORS = args + + pick_colors = classmethod(pick_colors) # [no-classmethod-decorator] diff --git a/doc/data/messages/n/no-classmethod-decorator/details.rst b/doc/data/messages/n/no-classmethod-decorator/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/no-classmethod-decorator/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/no-classmethod-decorator/good.py b/doc/data/messages/n/no-classmethod-decorator/good.py index c40beb573f..9b70c769d1 100644 --- a/doc/data/messages/n/no-classmethod-decorator/good.py +++ b/doc/data/messages/n/no-classmethod-decorator/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +class Fruit: + COLORS = [] + + def __init__(self, color): + self.color = color + + @classmethod + def pick_colors(cls, *args): + """classmethod to pick fruit colors""" + cls.COLORS = args diff --git a/doc/data/messages/n/no-member/details.rst b/doc/data/messages/n/no-member/details.rst index ff841ec44e..f5a184f64e 100644 --- a/doc/data/messages/n/no-member/details.rst +++ b/doc/data/messages/n/no-member/details.rst @@ -2,8 +2,9 @@ If you are getting the dreaded ``no-member`` error, there is a possibility that either: - pylint found a bug in your code -- You're launching pylint without the dependencies installed in its environment. -- pylint would need to lint a C extension module and is refraining to do so. +- You're launching pylint without the dependencies installed in its environment +- pylint would need to lint a C extension module and is refraining to do so +- pylint does not understand dynamically generated code Linting C extension modules is not supported out of the box, especially since pylint has no way to get an AST object out of the extension module. @@ -27,3 +28,10 @@ build AST objects from all the C extensions that pylint encounters:: Alternatively, since pylint emits a separate error for attributes that cannot be found in C extensions, ``c-extension-no-member``, you can disable this error for your project. + +If something is generated dynamically, pylint won't be able to understand the code +from your library (c-extension or not). You can then specify generated attributes +with the ``generated-members`` option. For example if ``cv2.LINE_AA`` and +``sphinx.generated_member`` create false positives for ``no-member``, you can do:: + + $ pylint --generated-member=cv2.LINE_AA,sphinx.generated_member diff --git a/doc/data/messages/n/no-self-argument/bad.py b/doc/data/messages/n/no-self-argument/bad.py new file mode 100644 index 0000000000..e195060d08 --- /dev/null +++ b/doc/data/messages/n/no-self-argument/bad.py @@ -0,0 +1,3 @@ +class Fruit: + def __init__(this, name): # [no-self-argument] + this.name = name diff --git a/doc/data/messages/n/no-self-argument/details.rst b/doc/data/messages/n/no-self-argument/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/no-self-argument/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/no-self-argument/good.py b/doc/data/messages/n/no-self-argument/good.py index c40beb573f..2e1638a74e 100644 --- a/doc/data/messages/n/no-self-argument/good.py +++ b/doc/data/messages/n/no-self-argument/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self, name): + self.name = name diff --git a/doc/data/messages/n/no-self-use/details.rst b/doc/data/messages/n/no-self-use/details.rst new file mode 100644 index 0000000000..9862ff2de6 --- /dev/null +++ b/doc/data/messages/n/no-self-use/details.rst @@ -0,0 +1,2 @@ +If a function is not using any class attribute it can be a ``@staticmethod``, +or a function outside the class. diff --git a/doc/data/messages/n/no-self-use/good.py b/doc/data/messages/n/no-self-use/good.py index dd401b73e9..cc1ad2bdcf 100644 --- a/doc/data/messages/n/no-self-use/good.py +++ b/doc/data/messages/n/no-self-use/good.py @@ -1,5 +1,3 @@ -"""If a function is not using any class attribute it can be a @staticmethod, or a function outside the class.""" - def developer_greeting(): print("Greetings developer!") diff --git a/doc/data/messages/n/no-staticmethod-decorator/bad.py b/doc/data/messages/n/no-staticmethod-decorator/bad.py new file mode 100644 index 0000000000..181c745e5b --- /dev/null +++ b/doc/data/messages/n/no-staticmethod-decorator/bad.py @@ -0,0 +1,5 @@ +class Worm: + def bore(self): + pass + + bore = staticmethod(bore) # [no-staticmethod-decorator] diff --git a/doc/data/messages/n/no-staticmethod-decorator/details.rst b/doc/data/messages/n/no-staticmethod-decorator/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/no-staticmethod-decorator/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/no-staticmethod-decorator/good.py b/doc/data/messages/n/no-staticmethod-decorator/good.py index c40beb573f..d3b8efaa26 100644 --- a/doc/data/messages/n/no-staticmethod-decorator/good.py +++ b/doc/data/messages/n/no-staticmethod-decorator/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +class Worm: + @staticmethod + def bore(self): + pass diff --git a/doc/data/messages/n/no-value-for-parameter/bad.py b/doc/data/messages/n/no-value-for-parameter/bad.py new file mode 100644 index 0000000000..cf10d9a57c --- /dev/null +++ b/doc/data/messages/n/no-value-for-parameter/bad.py @@ -0,0 +1,4 @@ +def add(x, y): + return x + y + +add(1) # [no-value-for-parameter] diff --git a/doc/data/messages/n/no-value-for-parameter/details.rst b/doc/data/messages/n/no-value-for-parameter/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/no-value-for-parameter/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/no-value-for-parameter/good.py b/doc/data/messages/n/no-value-for-parameter/good.py index c40beb573f..e14c45c21c 100644 --- a/doc/data/messages/n/no-value-for-parameter/good.py +++ b/doc/data/messages/n/no-value-for-parameter/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +def add(x, y): + return x + y + +add(1, 2) diff --git a/doc/data/messages/n/non-ascii-module-import/bad.py b/doc/data/messages/n/non-ascii-module-import/bad.py new file mode 100644 index 0000000000..ce2e811c65 --- /dev/null +++ b/doc/data/messages/n/non-ascii-module-import/bad.py @@ -0,0 +1,3 @@ +from os.path import join as łos # [non-ascii-module-import] + +foo = łos("a", "b") diff --git a/doc/data/messages/n/non-ascii-module-import/details.rst b/doc/data/messages/n/non-ascii-module-import/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/non-ascii-module-import/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/non-ascii-module-import/good.py b/doc/data/messages/n/non-ascii-module-import/good.py index c40beb573f..388a5c78e9 100644 --- a/doc/data/messages/n/non-ascii-module-import/good.py +++ b/doc/data/messages/n/non-ascii-module-import/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +from os.path import join as os_join + +foo = os_join("a", "b") diff --git a/doc/data/messages/n/non-ascii-name/bad.py b/doc/data/messages/n/non-ascii-name/bad.py new file mode 100644 index 0000000000..9545327945 --- /dev/null +++ b/doc/data/messages/n/non-ascii-name/bad.py @@ -0,0 +1 @@ +ápple_count = 4444 # [non-ascii-name] diff --git a/doc/data/messages/n/non-ascii-name/details.rst b/doc/data/messages/n/non-ascii-name/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/non-ascii-name/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/non-ascii-name/good.py b/doc/data/messages/n/non-ascii-name/good.py index c40beb573f..bbddb08d52 100644 --- a/doc/data/messages/n/non-ascii-name/good.py +++ b/doc/data/messages/n/non-ascii-name/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +apple_count = 4444 diff --git a/doc/data/messages/n/non-iterator-returned/bad.py b/doc/data/messages/n/non-iterator-returned/bad.py new file mode 100644 index 0000000000..6e5b99faea --- /dev/null +++ b/doc/data/messages/n/non-iterator-returned/bad.py @@ -0,0 +1,18 @@ +import random + + +class GenericAstrology: + def __init__(self, signs, predictions): + self.signs = signs + self.predictions = predictions + + def __iter__(self): # [non-iterator-returned] + self.index = 0 + self.number_of_prediction = len(self.predictions) + return self + + +SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra"] +PREDICTIONS = ["good things", "bad thing", "existential dread"] +for sign, prediction in GenericAstrology(SIGNS, PREDICTIONS): + print(f"{sign} : {prediction} today") diff --git a/doc/data/messages/n/non-iterator-returned/details.rst b/doc/data/messages/n/non-iterator-returned/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/non-iterator-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/non-iterator-returned/good.py b/doc/data/messages/n/non-iterator-returned/good.py index c40beb573f..3517b3f745 100644 --- a/doc/data/messages/n/non-iterator-returned/good.py +++ b/doc/data/messages/n/non-iterator-returned/good.py @@ -1 +1,25 @@ -# This is a placeholder for correct code for this message. +import random + + +class GenericAstrology: + def __init__(self, signs, predictions): + self.signs = signs + self.predictions = predictions + + def __iter__(self): + self.index = 0 + self.number_of_prediction = len(self.predictions) + return self + + def __next__(self): + if self.index == len(self.signs): + raise StopIteration + self.index += 1 + prediction_index = random.randint(0, self.number_of_prediction - 1) + return self.signs[self.index - 1], self.predictions[prediction_index] + + +SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra"] +PREDICTIONS = ["good things", "bad thing", "existential dread"] +for sign, prediction in GenericAstrology(SIGNS, PREDICTIONS): + print(f"{sign} : {prediction} today") diff --git a/doc/data/messages/n/non-parent-init-called/bad.py b/doc/data/messages/n/non-parent-init-called/bad.py new file mode 100644 index 0000000000..75254db824 --- /dev/null +++ b/doc/data/messages/n/non-parent-init-called/bad.py @@ -0,0 +1,15 @@ +class Animal: + def __init__(self): + self.is_multicellular = True + + +class Vertebrate(Animal): + def __init__(self): + super().__init__() + self.has_vertebrae = True + + +class Cat(Vertebrate): + def __init__(self): + Animal.__init__(self) # [non-parent-init-called] + self.is_adorable = True diff --git a/doc/data/messages/n/non-parent-init-called/details.rst b/doc/data/messages/n/non-parent-init-called/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/non-parent-init-called/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/non-parent-init-called/good.py b/doc/data/messages/n/non-parent-init-called/good.py index c40beb573f..bf6abca100 100644 --- a/doc/data/messages/n/non-parent-init-called/good.py +++ b/doc/data/messages/n/non-parent-init-called/good.py @@ -1 +1,15 @@ -# This is a placeholder for correct code for this message. +class Animal: + def __init__(self): + self.is_multicellular = True + + +class Vertebrate(Animal): + def __init__(self): + super().__init__() + self.has_vertebrae = True + + +class Cat(Vertebrate): + def __init__(self): + super().__init__() + self.is_adorable = True diff --git a/doc/data/messages/n/non-str-assignment-to-dunder-name/bad.py b/doc/data/messages/n/non-str-assignment-to-dunder-name/bad.py new file mode 100644 index 0000000000..59f13a13c6 --- /dev/null +++ b/doc/data/messages/n/non-str-assignment-to-dunder-name/bad.py @@ -0,0 +1,5 @@ +class Fruit: + pass + + +Fruit.__name__ = 1 # [non-str-assignment-to-dunder-name] diff --git a/doc/data/messages/n/non-str-assignment-to-dunder-name/details.rst b/doc/data/messages/n/non-str-assignment-to-dunder-name/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/non-str-assignment-to-dunder-name/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/non-str-assignment-to-dunder-name/good.py b/doc/data/messages/n/non-str-assignment-to-dunder-name/good.py index c40beb573f..ff55f18001 100644 --- a/doc/data/messages/n/non-str-assignment-to-dunder-name/good.py +++ b/doc/data/messages/n/non-str-assignment-to-dunder-name/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Fruit: + pass + + +Fruit.__name__ = "FRUIT" diff --git a/doc/data/messages/n/nonexistent-operator/bad.py b/doc/data/messages/n/nonexistent-operator/bad.py new file mode 100644 index 0000000000..ab284fa3b1 --- /dev/null +++ b/doc/data/messages/n/nonexistent-operator/bad.py @@ -0,0 +1,5 @@ +i = 0 + +while i <= 10: + print(i) + ++i # [nonexistent-operator] diff --git a/doc/data/messages/n/nonexistent-operator/details.rst b/doc/data/messages/n/nonexistent-operator/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/nonexistent-operator/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/nonexistent-operator/good.py b/doc/data/messages/n/nonexistent-operator/good.py index c40beb573f..e4bbadea20 100644 --- a/doc/data/messages/n/nonexistent-operator/good.py +++ b/doc/data/messages/n/nonexistent-operator/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +i = 0 + +while i <= 10: + print(i) + i += 1 diff --git a/doc/data/messages/n/nonlocal-and-global/bad.py b/doc/data/messages/n/nonlocal-and-global/bad.py new file mode 100644 index 0000000000..44dd5a3092 --- /dev/null +++ b/doc/data/messages/n/nonlocal-and-global/bad.py @@ -0,0 +1,11 @@ +NUMBER = 42 + + +def update_number(number): # [nonlocal-and-global] + global NUMBER + nonlocal NUMBER + NUMBER = number + print(f"New global number is: {NUMBER}") + + +update_number(24) diff --git a/doc/data/messages/n/nonlocal-and-global/details.rst b/doc/data/messages/n/nonlocal-and-global/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/nonlocal-and-global/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/nonlocal-and-global/good.py b/doc/data/messages/n/nonlocal-and-global/good.py index c40beb573f..f094601ed6 100644 --- a/doc/data/messages/n/nonlocal-and-global/good.py +++ b/doc/data/messages/n/nonlocal-and-global/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +NUMBER = 42 + + +def update_number(number): + global NUMBER + NUMBER = number + print(f"New global number is: {NUMBER}") + + +update_number(24) diff --git a/doc/data/messages/n/nonlocal-without-binding/bad.py b/doc/data/messages/n/nonlocal-without-binding/bad.py new file mode 100644 index 0000000000..6a166e09fc --- /dev/null +++ b/doc/data/messages/n/nonlocal-without-binding/bad.py @@ -0,0 +1,3 @@ +class Fruit: + def get_color(self): + nonlocal colors # [nonlocal-without-binding] diff --git a/doc/data/messages/n/nonlocal-without-binding/details.rst b/doc/data/messages/n/nonlocal-without-binding/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/nonlocal-without-binding/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/nonlocal-without-binding/good.py b/doc/data/messages/n/nonlocal-without-binding/good.py index c40beb573f..cce884ac8d 100644 --- a/doc/data/messages/n/nonlocal-without-binding/good.py +++ b/doc/data/messages/n/nonlocal-without-binding/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Fruit: + colors = ["red", "green"] + + def get_color(self): + nonlocal colors diff --git a/doc/data/messages/n/not-a-mapping/bad.py b/doc/data/messages/n/not-a-mapping/bad.py new file mode 100644 index 0000000000..79ca9215eb --- /dev/null +++ b/doc/data/messages/n/not-a-mapping/bad.py @@ -0,0 +1,5 @@ +def print_colors(**colors): + print(colors) + + +print_colors(**list("red", "black")) # [not-a-mapping] diff --git a/doc/data/messages/n/not-a-mapping/details.rst b/doc/data/messages/n/not-a-mapping/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/not-a-mapping/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/not-a-mapping/good.py b/doc/data/messages/n/not-a-mapping/good.py index c40beb573f..3de53ac5db 100644 --- a/doc/data/messages/n/not-a-mapping/good.py +++ b/doc/data/messages/n/not-a-mapping/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +def print_colors(**colors): + print(colors) + + +print_colors(**dict(red=1, black=2)) diff --git a/doc/data/messages/n/not-async-context-manager/bad.py b/doc/data/messages/n/not-async-context-manager/bad.py new file mode 100644 index 0000000000..e20402316b --- /dev/null +++ b/doc/data/messages/n/not-async-context-manager/bad.py @@ -0,0 +1,11 @@ +class ContextManager: + def __enter__(self): + pass + + def __exit__(self, *exc): + pass + + +async def foo(): + async with ContextManager(): # [not-async-context-manager] + pass diff --git a/doc/data/messages/n/not-async-context-manager/details.rst b/doc/data/messages/n/not-async-context-manager/details.rst index ab82045295..636366942f 100644 --- a/doc/data/messages/n/not-async-context-manager/details.rst +++ b/doc/data/messages/n/not-async-context-manager/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing `_ ! +Async context manager doesn't implement ``__aenter__`` and ``__aexit__``. It can't be emitted when using Python < 3.5. diff --git a/doc/data/messages/n/not-async-context-manager/good.py b/doc/data/messages/n/not-async-context-manager/good.py index c40beb573f..8d527e85fc 100644 --- a/doc/data/messages/n/not-async-context-manager/good.py +++ b/doc/data/messages/n/not-async-context-manager/good.py @@ -1 +1,11 @@ -# This is a placeholder for correct code for this message. +class AsyncContextManager: + def __aenter__(self): + pass + + def __aexit__(self, *exc): + pass + + +async def foo(): + async with AsyncContextManager(): + pass diff --git a/doc/data/messages/n/not-context-manager/bad.py b/doc/data/messages/n/not-context-manager/bad.py new file mode 100644 index 0000000000..35107dbac2 --- /dev/null +++ b/doc/data/messages/n/not-context-manager/bad.py @@ -0,0 +1,7 @@ +class MyContextManager: + def __enter__(self): + pass + + +with MyContextManager() as c: # [not-context-manager] + pass diff --git a/doc/data/messages/n/not-context-manager/details.rst b/doc/data/messages/n/not-context-manager/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/not-context-manager/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/not-context-manager/good.py b/doc/data/messages/n/not-context-manager/good.py index c40beb573f..c20745742d 100644 --- a/doc/data/messages/n/not-context-manager/good.py +++ b/doc/data/messages/n/not-context-manager/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +class MyContextManager: + def __enter__(self): + pass + + def __exit__(self, *exc): + pass + + +with MyContextManager() as c: + pass diff --git a/doc/data/messages/n/notimplemented-raised/bad.py b/doc/data/messages/n/notimplemented-raised/bad.py new file mode 100644 index 0000000000..4dcf2effb2 --- /dev/null +++ b/doc/data/messages/n/notimplemented-raised/bad.py @@ -0,0 +1,3 @@ +class Worm: + def bore(self): + raise NotImplemented # [notimplemented-raised] diff --git a/doc/data/messages/n/notimplemented-raised/details.rst b/doc/data/messages/n/notimplemented-raised/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/n/notimplemented-raised/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/n/notimplemented-raised/good.py b/doc/data/messages/n/notimplemented-raised/good.py index c40beb573f..4d38caf302 100644 --- a/doc/data/messages/n/notimplemented-raised/good.py +++ b/doc/data/messages/n/notimplemented-raised/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +class Worm: + def bore(self): + raise NotImplementedError diff --git a/doc/data/messages/o/overlapping-except/bad.py b/doc/data/messages/o/overlapping-except/bad.py new file mode 100644 index 0000000000..eaf1e9d7f7 --- /dev/null +++ b/doc/data/messages/o/overlapping-except/bad.py @@ -0,0 +1,5 @@ +def divide_x_by_y(x: float, y: float): + try: + print(x / y) + except (ArithmeticError, FloatingPointError) as e: # [overlapping-except] + print(f"There was an issue: {e}") diff --git a/doc/data/messages/o/overlapping-except/details.rst b/doc/data/messages/o/overlapping-except/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/o/overlapping-except/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/o/overlapping-except/good.py b/doc/data/messages/o/overlapping-except/good.py index c40beb573f..41a727545c 100644 --- a/doc/data/messages/o/overlapping-except/good.py +++ b/doc/data/messages/o/overlapping-except/good.py @@ -1 +1,16 @@ -# This is a placeholder for correct code for this message. +def divide_x_by_y(x: float, y: float): + try: + print(x / y) + except FloatingPointError as e: + print(f"There was a FloatingPointError: {e}") + except ArithmeticError as e: + # FloatingPointError were already caught at this point + print(f"There was an OverflowError or a ZeroDivisionError: {e}") + +# Or: + +def divide_x_by_y(x: float, y: float): + try: + print(x / y) + except ArithmeticError as e: + print(f"There was an OverflowError, a ZeroDivisionError or a FloatingPointError: {e}") diff --git a/doc/data/messages/o/overlapping-except/pylintrc b/doc/data/messages/o/overlapping-except/pylintrc new file mode 100644 index 0000000000..0f64b39adf --- /dev/null +++ b/doc/data/messages/o/overlapping-except/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.overlapping_exceptions, diff --git a/doc/data/messages/o/overlapping-except/related.rst b/doc/data/messages/o/overlapping-except/related.rst new file mode 100644 index 0000000000..c806f76dfc --- /dev/null +++ b/doc/data/messages/o/overlapping-except/related.rst @@ -0,0 +1 @@ +- `Exception hierarchy `_ diff --git a/doc/data/messages/o/overridden-final-method/pylintrc b/doc/data/messages/o/overridden-final-method/pylintrc index 85fc502b37..a711c25d12 100644 --- a/doc/data/messages/o/overridden-final-method/pylintrc +++ b/doc/data/messages/o/overridden-final-method/pylintrc @@ -1,2 +1,2 @@ -[testoptions] -min_pyver=3.8 +[MAIN] +py-version=3.8 diff --git a/doc/data/messages/p/parse-error/details.rst b/doc/data/messages/p/parse-error/details.rst index ab82045295..1c43031371 100644 --- a/doc/data/messages/p/parse-error/details.rst +++ b/doc/data/messages/p/parse-error/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing `_ ! +This is a message linked to an internal problem in pylint. There's nothing to change in your code. diff --git a/doc/data/messages/p/pointless-exception-statement/bad.py b/doc/data/messages/p/pointless-exception-statement/bad.py new file mode 100644 index 0000000000..3dcd1dc002 --- /dev/null +++ b/doc/data/messages/p/pointless-exception-statement/bad.py @@ -0,0 +1 @@ +Exception("This exception is a statement.") # [pointless-exception-statement] diff --git a/doc/data/messages/p/pointless-exception-statement/good.py b/doc/data/messages/p/pointless-exception-statement/good.py new file mode 100644 index 0000000000..718388c873 --- /dev/null +++ b/doc/data/messages/p/pointless-exception-statement/good.py @@ -0,0 +1 @@ +raise Exception("This will raise.") diff --git a/doc/data/messages/p/positional-only-arguments-expected/bad.py b/doc/data/messages/p/positional-only-arguments-expected/bad.py new file mode 100644 index 0000000000..30720555aa --- /dev/null +++ b/doc/data/messages/p/positional-only-arguments-expected/bad.py @@ -0,0 +1,5 @@ +def cube(n, /): + """Takes in a number n, returns the cube of n""" + return n**3 + +cube(n=2) # [positional-only-arguments-expected] diff --git a/doc/data/messages/p/positional-only-arguments-expected/good.py b/doc/data/messages/p/positional-only-arguments-expected/good.py new file mode 100644 index 0000000000..ca734e546d --- /dev/null +++ b/doc/data/messages/p/positional-only-arguments-expected/good.py @@ -0,0 +1,5 @@ +def cube(n, /): + """Takes in a number n, returns the cube of n""" + return n**3 + +cube(2) diff --git a/doc/data/messages/p/positional-only-arguments-expected/related.rst b/doc/data/messages/p/positional-only-arguments-expected/related.rst new file mode 100644 index 0000000000..63145e6f3a --- /dev/null +++ b/doc/data/messages/p/positional-only-arguments-expected/related.rst @@ -0,0 +1 @@ +- `PEP 570 `_ diff --git a/doc/data/messages/p/possibly-unused-variable/bad.py b/doc/data/messages/p/possibly-unused-variable/bad.py new file mode 100644 index 0000000000..b64aee88be --- /dev/null +++ b/doc/data/messages/p/possibly-unused-variable/bad.py @@ -0,0 +1,4 @@ +def choose_fruits(fruits): + print(fruits) + color = "red" # [possibly-unused-variable] + return locals() diff --git a/doc/data/messages/p/possibly-unused-variable/details.rst b/doc/data/messages/p/possibly-unused-variable/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/p/possibly-unused-variable/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/p/possibly-unused-variable/good.py b/doc/data/messages/p/possibly-unused-variable/good.py index c40beb573f..0951183286 100644 --- a/doc/data/messages/p/possibly-unused-variable/good.py +++ b/doc/data/messages/p/possibly-unused-variable/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +def choose_fruits(fruits): + current_locals = locals() + print(fruits) + color = "red" + print(color) + return current_locals diff --git a/doc/data/messages/p/preferred-module/bad.py b/doc/data/messages/p/preferred-module/bad.py new file mode 100644 index 0000000000..a047ff36d9 --- /dev/null +++ b/doc/data/messages/p/preferred-module/bad.py @@ -0,0 +1 @@ +import urllib # [preferred-module] diff --git a/doc/data/messages/p/preferred-module/details.rst b/doc/data/messages/p/preferred-module/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/p/preferred-module/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/p/preferred-module/good.py b/doc/data/messages/p/preferred-module/good.py index c40beb573f..20b15530dd 100644 --- a/doc/data/messages/p/preferred-module/good.py +++ b/doc/data/messages/p/preferred-module/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +import requests diff --git a/doc/data/messages/p/preferred-module/pylintrc b/doc/data/messages/p/preferred-module/pylintrc new file mode 100644 index 0000000000..00ee499307 --- /dev/null +++ b/doc/data/messages/p/preferred-module/pylintrc @@ -0,0 +1,2 @@ +[IMPORTS] +preferred-modules=urllib:requests, diff --git a/doc/data/messages/p/property-with-parameters/bad.py b/doc/data/messages/p/property-with-parameters/bad.py new file mode 100644 index 0000000000..d80ca14c93 --- /dev/null +++ b/doc/data/messages/p/property-with-parameters/bad.py @@ -0,0 +1,4 @@ +class Worm: + @property + def bore(self, depth): # [property-with-parameters] + pass diff --git a/doc/data/messages/p/property-with-parameters/details.rst b/doc/data/messages/p/property-with-parameters/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/p/property-with-parameters/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/p/property-with-parameters/good.py b/doc/data/messages/p/property-with-parameters/good.py index c40beb573f..af6a4fb56a 100644 --- a/doc/data/messages/p/property-with-parameters/good.py +++ b/doc/data/messages/p/property-with-parameters/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Worm: + @property + def bore(self): + """Property accessed with '.bore'.""" + pass + + def bore_with_depth(depth): + """Function called with .bore_with_depth(depth).""" + pass diff --git a/doc/data/messages/p/protected-access/bad.py b/doc/data/messages/p/protected-access/bad.py new file mode 100644 index 0000000000..150138f05d --- /dev/null +++ b/doc/data/messages/p/protected-access/bad.py @@ -0,0 +1,7 @@ +class Worm: + def __swallow(self): + pass + + +jim = Worm() +jim.__swallow() # [protected-access] diff --git a/doc/data/messages/p/protected-access/details.rst b/doc/data/messages/p/protected-access/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/p/protected-access/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/p/protected-access/good.py b/doc/data/messages/p/protected-access/good.py index c40beb573f..dddfdd194e 100644 --- a/doc/data/messages/p/protected-access/good.py +++ b/doc/data/messages/p/protected-access/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +class Worm: + def __swallow(self): + pass + + def eat(self): + return self.__swallow() + + +jim = Worm() +jim.eat() diff --git a/doc/data/messages/r/raise-missing-from/related.rst b/doc/data/messages/r/raise-missing-from/related.rst index 1bcca1b7b7..9bf33339cb 100644 --- a/doc/data/messages/r/raise-missing-from/related.rst +++ b/doc/data/messages/r/raise-missing-from/related.rst @@ -1 +1 @@ -- `PEP 3132 `_ +- `PEP 3134 `_ diff --git a/doc/data/messages/r/raising-bad-type/bad.py b/doc/data/messages/r/raising-bad-type/bad.py new file mode 100644 index 0000000000..2614764aa3 --- /dev/null +++ b/doc/data/messages/r/raising-bad-type/bad.py @@ -0,0 +1,10 @@ +class FasterThanTheSpeedOfLightError(ZeroDivisionError): + def __init__(self): + super().__init__("You can't go faster than the speed of light !") + + +def calculate_speed(distance: float, time: float) -> float: + try: + return distance / time + except ZeroDivisionError as e: + raise None # [raising-bad-type] diff --git a/doc/data/messages/r/raising-bad-type/details.rst b/doc/data/messages/r/raising-bad-type/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/raising-bad-type/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/raising-bad-type/good.py b/doc/data/messages/r/raising-bad-type/good.py index c40beb573f..a269492621 100644 --- a/doc/data/messages/r/raising-bad-type/good.py +++ b/doc/data/messages/r/raising-bad-type/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +class FasterThanTheSpeedOfLightError(ZeroDivisionError): + def __init__(self): + super().__init__("You can't go faster than the speed of light !") + + +def calculate_speed(distance: float, time: float) -> float: + try: + return distance / time + except ZeroDivisionError as e: + raise FasterThanTheSpeedOfLightError() from e diff --git a/doc/data/messages/r/raising-format-tuple/bad.py b/doc/data/messages/r/raising-format-tuple/bad.py new file mode 100644 index 0000000000..50519bc4c3 --- /dev/null +++ b/doc/data/messages/r/raising-format-tuple/bad.py @@ -0,0 +1 @@ +raise RuntimeError("This looks wrong %s %s", ("a", "b")) # [raising-format-tuple] diff --git a/doc/data/messages/r/raising-format-tuple/details.rst b/doc/data/messages/r/raising-format-tuple/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/raising-format-tuple/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/raising-format-tuple/good.py b/doc/data/messages/r/raising-format-tuple/good.py index c40beb573f..d368710d14 100644 --- a/doc/data/messages/r/raising-format-tuple/good.py +++ b/doc/data/messages/r/raising-format-tuple/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +raise RuntimeError("This looks wrong %s %s" % ("a", "b")) diff --git a/doc/data/messages/r/raising-non-exception/bad.py b/doc/data/messages/r/raising-non-exception/bad.py new file mode 100644 index 0000000000..fba3b87434 --- /dev/null +++ b/doc/data/messages/r/raising-non-exception/bad.py @@ -0,0 +1 @@ +raise str # [raising-non-exception] diff --git a/doc/data/messages/r/raising-non-exception/details.rst b/doc/data/messages/r/raising-non-exception/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/raising-non-exception/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/raising-non-exception/good.py b/doc/data/messages/r/raising-non-exception/good.py index c40beb573f..fe382e91a3 100644 --- a/doc/data/messages/r/raising-non-exception/good.py +++ b/doc/data/messages/r/raising-non-exception/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +raise Exception("Goodbye world !") diff --git a/doc/data/messages/r/redefined-argument-from-local/bad.py b/doc/data/messages/r/redefined-argument-from-local/bad.py new file mode 100644 index 0000000000..24d441219d --- /dev/null +++ b/doc/data/messages/r/redefined-argument-from-local/bad.py @@ -0,0 +1,3 @@ +def show(host_id=10.11): + for host_id, host in [[12.13, 'Venus'], [14.15, 'Mars']]: # [redefined-argument-from-local] + print(host_id, host) diff --git a/doc/data/messages/r/redefined-argument-from-local/details.rst b/doc/data/messages/r/redefined-argument-from-local/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/redefined-argument-from-local/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/redefined-argument-from-local/good.py b/doc/data/messages/r/redefined-argument-from-local/good.py index c40beb573f..330f94f454 100644 --- a/doc/data/messages/r/redefined-argument-from-local/good.py +++ b/doc/data/messages/r/redefined-argument-from-local/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +def show(host_id=10.11): + for inner_host_id, host in [[12.13, 'Venus'], [14.15, 'Mars']]: + print(host_id, inner_host_id, host) diff --git a/doc/data/messages/r/redefined-builtin/bad.py b/doc/data/messages/r/redefined-builtin/bad.py new file mode 100644 index 0000000000..da9bda8354 --- /dev/null +++ b/doc/data/messages/r/redefined-builtin/bad.py @@ -0,0 +1,2 @@ +def map(): # [redefined-builtin] + pass diff --git a/doc/data/messages/r/redefined-builtin/details.rst b/doc/data/messages/r/redefined-builtin/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/redefined-builtin/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/redefined-builtin/good.py b/doc/data/messages/r/redefined-builtin/good.py index c40beb573f..ab733ed41b 100644 --- a/doc/data/messages/r/redefined-builtin/good.py +++ b/doc/data/messages/r/redefined-builtin/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def map_iterable(): + pass diff --git a/doc/data/messages/r/redefined-outer-name/bad.py b/doc/data/messages/r/redefined-outer-name/bad.py new file mode 100644 index 0000000000..3d03c9cd5d --- /dev/null +++ b/doc/data/messages/r/redefined-outer-name/bad.py @@ -0,0 +1,6 @@ +count = 10 + + +def count_it(count): # [redefined-outer-name] + for i in range(count): + print(i) diff --git a/doc/data/messages/r/redefined-outer-name/details.rst b/doc/data/messages/r/redefined-outer-name/details.rst index ab82045295..475e6a3448 100644 --- a/doc/data/messages/r/redefined-outer-name/details.rst +++ b/doc/data/messages/r/redefined-outer-name/details.rst @@ -1 +1,23 @@ -You can help us make the doc better `by contributing `_ ! +A common issue is that this message is triggered when using `pytest` `fixtures `_: + +.. code-block:: python + + import pytest + + @pytest.fixture + def setup(): + ... + + + def test_something(setup): # [redefined-outer-name] + ... + +One solution to this problem is to explicitly name the fixture: + +.. code-block:: python + + @pytest.fixture(name="setup") + def setup_fixture(): + ... + +Alternatively `pylint` plugins like `pylint-pytest `_ can be used. diff --git a/doc/data/messages/r/redefined-outer-name/good.py b/doc/data/messages/r/redefined-outer-name/good.py index c40beb573f..1350598389 100644 --- a/doc/data/messages/r/redefined-outer-name/good.py +++ b/doc/data/messages/r/redefined-outer-name/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +count = 10 + + +def count_it(limit): + for i in range(limit): + print(i) diff --git a/doc/data/messages/r/redefined-variable-type/bad.py b/doc/data/messages/r/redefined-variable-type/bad.py new file mode 100644 index 0000000000..02b93948fb --- /dev/null +++ b/doc/data/messages/r/redefined-variable-type/bad.py @@ -0,0 +1,2 @@ +x = 1 +x = "2" # [redefined-variable-type] diff --git a/doc/data/messages/r/redefined-variable-type/details.rst b/doc/data/messages/r/redefined-variable-type/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/redefined-variable-type/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/redefined-variable-type/good.py b/doc/data/messages/r/redefined-variable-type/good.py index c40beb573f..c3afc3f9db 100644 --- a/doc/data/messages/r/redefined-variable-type/good.py +++ b/doc/data/messages/r/redefined-variable-type/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +x = 1 +x = 2 diff --git a/doc/data/messages/r/redefined-variable-type/pylintrc b/doc/data/messages/r/redefined-variable-type/pylintrc new file mode 100644 index 0000000000..48cfb90bee --- /dev/null +++ b/doc/data/messages/r/redefined-variable-type/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.redefined_variable_type, diff --git a/doc/data/messages/r/redundant-keyword-arg/bad.py b/doc/data/messages/r/redundant-keyword-arg/bad.py new file mode 100644 index 0000000000..8e2f9b6ec4 --- /dev/null +++ b/doc/data/messages/r/redundant-keyword-arg/bad.py @@ -0,0 +1,5 @@ +def square(x): + return x * x + + +square(5, x=4) # [redundant-keyword-arg] diff --git a/doc/data/messages/r/redundant-keyword-arg/details.rst b/doc/data/messages/r/redundant-keyword-arg/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/redundant-keyword-arg/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/redundant-keyword-arg/good.py b/doc/data/messages/r/redundant-keyword-arg/good.py index c40beb573f..32352b77bc 100644 --- a/doc/data/messages/r/redundant-keyword-arg/good.py +++ b/doc/data/messages/r/redundant-keyword-arg/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +def square(x): + return x * x + + +square(x=4) +# or +square(5) diff --git a/doc/data/messages/r/redundant-returns-doc/bad.py b/doc/data/messages/r/redundant-returns-doc/bad.py new file mode 100644 index 0000000000..5d018db4c9 --- /dev/null +++ b/doc/data/messages/r/redundant-returns-doc/bad.py @@ -0,0 +1,9 @@ +def print_fruits(fruits): # [redundant-returns-doc] + """Print list of fruits + + Returns + ------- + str + """ + print(fruits) + return None diff --git a/doc/data/messages/r/redundant-returns-doc/details.rst b/doc/data/messages/r/redundant-returns-doc/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/redundant-returns-doc/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/redundant-returns-doc/good.py b/doc/data/messages/r/redundant-returns-doc/good.py index c40beb573f..7f3eeebbbd 100644 --- a/doc/data/messages/r/redundant-returns-doc/good.py +++ b/doc/data/messages/r/redundant-returns-doc/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +def print_fruits(fruits): + """Print list of fruits + + Returns + ------- + str + """ + print(fruits) + return ",".join(fruits) diff --git a/doc/data/messages/r/redundant-returns-doc/pylintrc b/doc/data/messages/r/redundant-returns-doc/pylintrc new file mode 100644 index 0000000000..4547f98117 --- /dev/null +++ b/doc/data/messages/r/redundant-returns-doc/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins = pylint.extensions.docparams diff --git a/doc/data/messages/r/redundant-typehint-argument/bad.py b/doc/data/messages/r/redundant-typehint-argument/bad.py new file mode 100644 index 0000000000..1717593244 --- /dev/null +++ b/doc/data/messages/r/redundant-typehint-argument/bad.py @@ -0,0 +1,3 @@ +from typing import Union + +sweet_count: Union[int, str, int] = 42 # [redundant-typehint-argument] diff --git a/doc/data/messages/r/redundant-typehint-argument/good.py b/doc/data/messages/r/redundant-typehint-argument/good.py new file mode 100644 index 0000000000..ce5e3c06b7 --- /dev/null +++ b/doc/data/messages/r/redundant-typehint-argument/good.py @@ -0,0 +1,3 @@ +from typing import Union + +sweet_count: Union[str, int] = 42 diff --git a/doc/data/messages/r/redundant-typehint-argument/pylintrc b/doc/data/messages/r/redundant-typehint-argument/pylintrc new file mode 100644 index 0000000000..75114c1482 --- /dev/null +++ b/doc/data/messages/r/redundant-typehint-argument/pylintrc @@ -0,0 +1,5 @@ +[main] +load-plugins=pylint.extensions.typing + +[testoptions] +min_pyver=3.7 diff --git a/doc/data/messages/r/redundant-u-string-prefix/bad.py b/doc/data/messages/r/redundant-u-string-prefix/bad.py new file mode 100644 index 0000000000..bae9738a5d --- /dev/null +++ b/doc/data/messages/r/redundant-u-string-prefix/bad.py @@ -0,0 +1,2 @@ +def print_fruit(): + print(u"Apple") # [redundant-u-string-prefix] diff --git a/doc/data/messages/r/redundant-u-string-prefix/details.rst b/doc/data/messages/r/redundant-u-string-prefix/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/redundant-u-string-prefix/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/redundant-u-string-prefix/good.py b/doc/data/messages/r/redundant-u-string-prefix/good.py index c40beb573f..64b2d500d6 100644 --- a/doc/data/messages/r/redundant-u-string-prefix/good.py +++ b/doc/data/messages/r/redundant-u-string-prefix/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def print_fruit(): + print("Apple") diff --git a/doc/data/messages/r/redundant-yields-doc/bad.py b/doc/data/messages/r/redundant-yields-doc/bad.py new file mode 100644 index 0000000000..c2d1e68758 --- /dev/null +++ b/doc/data/messages/r/redundant-yields-doc/bad.py @@ -0,0 +1,9 @@ +def give_fruits(fruits): # [redundant-yields-doc] + """Something about fruits + + Yields + ------- + list + fruits + """ + return fruits diff --git a/doc/data/messages/r/redundant-yields-doc/details.rst b/doc/data/messages/r/redundant-yields-doc/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/redundant-yields-doc/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/redundant-yields-doc/good.py b/doc/data/messages/r/redundant-yields-doc/good.py index c40beb573f..1055a0c60f 100644 --- a/doc/data/messages/r/redundant-yields-doc/good.py +++ b/doc/data/messages/r/redundant-yields-doc/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +def give_fruits(fruits): + """Something about fruits + + Yields + ------- + str + fruit + """ + for fruit in fruits: + yield fruit diff --git a/doc/data/messages/r/redundant-yields-doc/pylintrc b/doc/data/messages/r/redundant-yields-doc/pylintrc new file mode 100644 index 0000000000..4547f98117 --- /dev/null +++ b/doc/data/messages/r/redundant-yields-doc/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins = pylint.extensions.docparams diff --git a/doc/data/messages/r/repeated-keyword/bad.py b/doc/data/messages/r/repeated-keyword/bad.py new file mode 100644 index 0000000000..5fd2d9488d --- /dev/null +++ b/doc/data/messages/r/repeated-keyword/bad.py @@ -0,0 +1,6 @@ +def func(a, b, c): + return a, b, c + + +func(1, 2, c=3, **{"c": 4}) # [repeated-keyword] +func(1, 2, **{"c": 3}, **{"c": 4}) # [repeated-keyword] diff --git a/doc/data/messages/r/repeated-keyword/details.rst b/doc/data/messages/r/repeated-keyword/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/r/repeated-keyword/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/r/repeated-keyword/good.py b/doc/data/messages/r/repeated-keyword/good.py index c40beb573f..f7bba62bab 100644 --- a/doc/data/messages/r/repeated-keyword/good.py +++ b/doc/data/messages/r/repeated-keyword/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +def func(a, b, c): + return a, b, c + + +func(1, 2, c=3) diff --git a/doc/data/messages/s/self-cls-assignment/bad.py b/doc/data/messages/s/self-cls-assignment/bad.py new file mode 100644 index 0000000000..64541405fe --- /dev/null +++ b/doc/data/messages/s/self-cls-assignment/bad.py @@ -0,0 +1,9 @@ +class Fruit: + @classmethod + def list_fruits(cls): + cls = 'apple' # [self-cls-assignment] + + def print_color(self, *colors): + self = "red" # [self-cls-assignment] + color = colors[1] + print(color) diff --git a/doc/data/messages/s/self-cls-assignment/details.rst b/doc/data/messages/s/self-cls-assignment/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/self-cls-assignment/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/self-cls-assignment/good.py b/doc/data/messages/s/self-cls-assignment/good.py index c40beb573f..ae8b172fa9 100644 --- a/doc/data/messages/s/self-cls-assignment/good.py +++ b/doc/data/messages/s/self-cls-assignment/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Fruit: + @classmethod + def list_fruits(cls): + fruit = 'apple' + print(fruit) + + def print_color(self, *colors): + color = colors[1] + print(color) diff --git a/doc/data/messages/s/shadowed-import/bad.py b/doc/data/messages/s/shadowed-import/bad.py new file mode 100644 index 0000000000..847ec962c2 --- /dev/null +++ b/doc/data/messages/s/shadowed-import/bad.py @@ -0,0 +1,3 @@ +from pathlib import Path + +import FastAPI.Path as Path # [shadowed-import] diff --git a/doc/data/messages/s/shadowed-import/good.py b/doc/data/messages/s/shadowed-import/good.py new file mode 100644 index 0000000000..9f19861d31 --- /dev/null +++ b/doc/data/messages/s/shadowed-import/good.py @@ -0,0 +1,3 @@ +from pathlib import Path + +import FastAPI.Path as FastApiPath diff --git a/doc/data/messages/s/shallow-copy-environ/bad.py b/doc/data/messages/s/shallow-copy-environ/bad.py new file mode 100644 index 0000000000..be6c0203bc --- /dev/null +++ b/doc/data/messages/s/shallow-copy-environ/bad.py @@ -0,0 +1,4 @@ +import copy +import os + +copied_env = copy.copy(os.environ) # [shallow-copy-environ] diff --git a/doc/data/messages/s/shallow-copy-environ/details.rst b/doc/data/messages/s/shallow-copy-environ/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/shallow-copy-environ/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/shallow-copy-environ/good.py b/doc/data/messages/s/shallow-copy-environ/good.py index c40beb573f..814ac9a84a 100644 --- a/doc/data/messages/s/shallow-copy-environ/good.py +++ b/doc/data/messages/s/shallow-copy-environ/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +import os + +copied_env = os.environ.copy() diff --git a/doc/data/messages/s/signature-differs/bad.py b/doc/data/messages/s/signature-differs/bad.py new file mode 100644 index 0000000000..9a46a6fd10 --- /dev/null +++ b/doc/data/messages/s/signature-differs/bad.py @@ -0,0 +1,9 @@ +class Animal: + def run(self, distance=0): + print(f"Ran {distance} km!") + + +class Dog(Animal): + def run(self, distance): # [signature-differs] + super(Animal, self).run(distance) + print("Fetched that stick, wuff !") diff --git a/doc/data/messages/s/signature-differs/details.rst b/doc/data/messages/s/signature-differs/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/signature-differs/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/signature-differs/good.py b/doc/data/messages/s/signature-differs/good.py index c40beb573f..6510a41744 100644 --- a/doc/data/messages/s/signature-differs/good.py +++ b/doc/data/messages/s/signature-differs/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Animal: + def run(self, distance=0): + print(f"Ran {distance} km!") + + +class Dog(Animal): + def run(self, distance=0): + super(Animal, self).run(distance) + print("Fetched that stick, wuff !") diff --git a/doc/data/messages/s/simplifiable-condition/bad.py b/doc/data/messages/s/simplifiable-condition/bad.py new file mode 100644 index 0000000000..e3ffe5de94 --- /dev/null +++ b/doc/data/messages/s/simplifiable-condition/bad.py @@ -0,0 +1,2 @@ +def has_apples(apples) -> bool: + return bool(apples or False) # [simplifiable-condition] diff --git a/doc/data/messages/s/simplifiable-condition/details.rst b/doc/data/messages/s/simplifiable-condition/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/simplifiable-condition/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/simplifiable-condition/good.py b/doc/data/messages/s/simplifiable-condition/good.py index c40beb573f..400a2788ce 100644 --- a/doc/data/messages/s/simplifiable-condition/good.py +++ b/doc/data/messages/s/simplifiable-condition/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def has_apples(apples) -> bool: + return bool(apples) diff --git a/doc/data/messages/s/simplify-boolean-expression/bad.py b/doc/data/messages/s/simplify-boolean-expression/bad.py new file mode 100644 index 0000000000..06806d350e --- /dev/null +++ b/doc/data/messages/s/simplify-boolean-expression/bad.py @@ -0,0 +1,2 @@ +def has_oranges(oranges, apples=None) -> bool: + return apples and False or oranges # [simplify-boolean-expression] diff --git a/doc/data/messages/s/simplify-boolean-expression/details.rst b/doc/data/messages/s/simplify-boolean-expression/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/simplify-boolean-expression/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/simplify-boolean-expression/good.py b/doc/data/messages/s/simplify-boolean-expression/good.py index c40beb573f..ca1c8a26f8 100644 --- a/doc/data/messages/s/simplify-boolean-expression/good.py +++ b/doc/data/messages/s/simplify-boolean-expression/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def has_oranges(oranges, apples=None) -> bool: + return oranges diff --git a/doc/data/messages/s/single-string-used-for-slots/bad.py b/doc/data/messages/s/single-string-used-for-slots/bad.py new file mode 100644 index 0000000000..9331dd9a4d --- /dev/null +++ b/doc/data/messages/s/single-string-used-for-slots/bad.py @@ -0,0 +1,5 @@ +class Fruit: # [single-string-used-for-slots] + __slots__ = "name" + + def __init__(self, name): + self.name = name diff --git a/doc/data/messages/s/single-string-used-for-slots/details.rst b/doc/data/messages/s/single-string-used-for-slots/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/single-string-used-for-slots/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/single-string-used-for-slots/good.py b/doc/data/messages/s/single-string-used-for-slots/good.py index c40beb573f..9f0ea3f1fe 100644 --- a/doc/data/messages/s/single-string-used-for-slots/good.py +++ b/doc/data/messages/s/single-string-used-for-slots/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Fruit: + __slots__ = ("name",) + + def __init__(self, name): + self.name = name diff --git a/doc/data/messages/s/singledispatch-method/bad.py b/doc/data/messages/s/singledispatch-method/bad.py new file mode 100644 index 0000000000..49e545b92d --- /dev/null +++ b/doc/data/messages/s/singledispatch-method/bad.py @@ -0,0 +1,19 @@ +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/doc/data/messages/s/singledispatch-method/details.rst b/doc/data/messages/s/singledispatch-method/details.rst new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/data/messages/s/singledispatch-method/good.py b/doc/data/messages/s/singledispatch-method/good.py new file mode 100644 index 0000000000..f38047cd13 --- /dev/null +++ b/doc/data/messages/s/singledispatch-method/good.py @@ -0,0 +1,19 @@ +from functools import singledispatch + + +class Board: + @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]}" diff --git a/doc/data/messages/s/singledispatchmethod-function/bad.py b/doc/data/messages/s/singledispatchmethod-function/bad.py new file mode 100644 index 0000000000..d2255f8659 --- /dev/null +++ b/doc/data/messages/s/singledispatchmethod-function/bad.py @@ -0,0 +1,19 @@ +from functools import singledispatchmethod + + +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/doc/data/messages/s/singledispatchmethod-function/details.rst b/doc/data/messages/s/singledispatchmethod-function/details.rst new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/data/messages/s/singledispatchmethod-function/good.py b/doc/data/messages/s/singledispatchmethod-function/good.py new file mode 100644 index 0000000000..1bc3570b51 --- /dev/null +++ b/doc/data/messages/s/singledispatchmethod-function/good.py @@ -0,0 +1,18 @@ +from functools import singledispatchmethod + + +class Board: + @singledispatchmethod + def convert_position(cls, position): + pass + + @singledispatchmethod + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @singledispatchmethod + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/star-needs-assignment-target/bad.py b/doc/data/messages/s/star-needs-assignment-target/bad.py new file mode 100644 index 0000000000..f09102dcfe --- /dev/null +++ b/doc/data/messages/s/star-needs-assignment-target/bad.py @@ -0,0 +1 @@ +stars = *["Sirius", "Arcturus", "Vega"] # [star-needs-assignment-target] diff --git a/doc/data/messages/s/star-needs-assignment-target/details.rst b/doc/data/messages/s/star-needs-assignment-target/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/star-needs-assignment-target/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/star-needs-assignment-target/good.py b/doc/data/messages/s/star-needs-assignment-target/good.py index c40beb573f..867ce1162c 100644 --- a/doc/data/messages/s/star-needs-assignment-target/good.py +++ b/doc/data/messages/s/star-needs-assignment-target/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +sirius, *arcturus_and_vega = ["Sirius", "Arcturus", "Vega"] diff --git a/doc/data/messages/s/stop-iteration-return/bad.py b/doc/data/messages/s/stop-iteration-return/bad.py new file mode 100644 index 0000000000..d4cd2b35aa --- /dev/null +++ b/doc/data/messages/s/stop-iteration-return/bad.py @@ -0,0 +1,20 @@ +def fruit_generator(): + for fruit in ["apple", "banana"]: + yield fruit + raise StopIteration # [stop-iteration-return] + + +def two_fruits_generator(fruits): + for fruit in fruits: + yield fruit, next(fruits) # [stop-iteration-return] + + +def two_good_fruits_generator(fruits): + for fruit in fruits: + if not fruit.is_tasty(): + continue + while True: + next_fruit = next(fruits) # [stop-iteration-return] + if next_fruit.is_tasty(): + yield fruit, next_fruit + break diff --git a/doc/data/messages/s/stop-iteration-return/details.rst b/doc/data/messages/s/stop-iteration-return/details.rst index ab82045295..c03f8e1636 100644 --- a/doc/data/messages/s/stop-iteration-return/details.rst +++ b/doc/data/messages/s/stop-iteration-return/details.rst @@ -1 +1,2 @@ -You can help us make the doc better `by contributing `_ ! +It's possible to give a default value to ``next`` or catch the ``StopIteration``, +or return directly. A ``StopIteration`` cannot be propagated from a generator. diff --git a/doc/data/messages/s/stop-iteration-return/good.py b/doc/data/messages/s/stop-iteration-return/good.py index c40beb573f..eec33d7e42 100644 --- a/doc/data/messages/s/stop-iteration-return/good.py +++ b/doc/data/messages/s/stop-iteration-return/good.py @@ -1 +1,32 @@ -# This is a placeholder for correct code for this message. +def fruit_generator(): + """The example is simple enough you don't need an explicit return.""" + for fruit in ["apple", "banana"]: + yield fruit + + +def two_fruits_generator(fruits): + """Catching the StopIteration.""" + for fruit in fruits: + try: + yield fruit, next(fruits) + except StopIteration: + print("Sorry there is only one fruit left.") + yield fruit, None + + +def two_good_fruits_generator(fruits): + """A return can be used to end the iterator early, but not a StopIteration.""" + for fruit in fruits: + if not fruit.is_tasty(): + continue + while True: + next_fruit = next(fruits, None) + if next_fruit is None: + print("Sorry there is only one fruit left.") + yield fruit, None + # We reached the end of the 'fruits' generator but raising a + # StopIteration instead of returning would create a RuntimeError + return + if next_fruit.is_tasty(): + yield fruit, next_fruit + break diff --git a/doc/data/messages/s/stop-iteration-return/related.rst b/doc/data/messages/s/stop-iteration-return/related.rst new file mode 100644 index 0000000000..60758c8f7b --- /dev/null +++ b/doc/data/messages/s/stop-iteration-return/related.rst @@ -0,0 +1 @@ +- `PEP 479 `_ diff --git a/doc/data/messages/s/subclassed-final-class/pylintrc b/doc/data/messages/s/subclassed-final-class/pylintrc index 85fc502b37..a711c25d12 100644 --- a/doc/data/messages/s/subclassed-final-class/pylintrc +++ b/doc/data/messages/s/subclassed-final-class/pylintrc @@ -1,2 +1,2 @@ -[testoptions] -min_pyver=3.8 +[MAIN] +py-version=3.8 diff --git a/doc/data/messages/s/subprocess-run-check/bad.py b/doc/data/messages/s/subprocess-run-check/bad.py new file mode 100644 index 0000000000..05e682811e --- /dev/null +++ b/doc/data/messages/s/subprocess-run-check/bad.py @@ -0,0 +1,3 @@ +import subprocess + +proc = subprocess.run(["ls"]) # [subprocess-run-check] diff --git a/doc/data/messages/s/subprocess-run-check/details.rst b/doc/data/messages/s/subprocess-run-check/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/subprocess-run-check/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/subprocess-run-check/good.py b/doc/data/messages/s/subprocess-run-check/good.py index c40beb573f..1de760ae2c 100644 --- a/doc/data/messages/s/subprocess-run-check/good.py +++ b/doc/data/messages/s/subprocess-run-check/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +import subprocess + +proc = subprocess.run(["ls"], check=False) diff --git a/doc/data/messages/s/subprocess-run-check/related.rst b/doc/data/messages/s/subprocess-run-check/related.rst new file mode 100644 index 0000000000..0b66d8a08f --- /dev/null +++ b/doc/data/messages/s/subprocess-run-check/related.rst @@ -0,0 +1 @@ +- `subprocess.run documentation `_ diff --git a/doc/data/messages/s/super-init-not-called/bad.py b/doc/data/messages/s/super-init-not-called/bad.py new file mode 100644 index 0000000000..b0e0c4c85b --- /dev/null +++ b/doc/data/messages/s/super-init-not-called/bad.py @@ -0,0 +1,9 @@ +class Fruit: + def __init__(self, name="fruit"): + self.name = name + print("Creating a {self.name}") + + +class Apple(Fruit): + def __init__(self): # [super-init-not-called] + print("Creating an apple") diff --git a/doc/data/messages/s/super-init-not-called/details.rst b/doc/data/messages/s/super-init-not-called/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/super-init-not-called/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/super-init-not-called/good.py b/doc/data/messages/s/super-init-not-called/good.py index c40beb573f..7f8a17706e 100644 --- a/doc/data/messages/s/super-init-not-called/good.py +++ b/doc/data/messages/s/super-init-not-called/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self, name="fruit"): + self.name = name + print("Creating a {self.name}") + + +class Apple(Fruit): + def __init__(self): + super().__init__("apple") diff --git a/doc/data/messages/s/superfluous-parens/bad.py b/doc/data/messages/s/superfluous-parens/bad.py new file mode 100644 index 0000000000..aeb0af0758 --- /dev/null +++ b/doc/data/messages/s/superfluous-parens/bad.py @@ -0,0 +1,9 @@ +x = input() +y = input() +if (x == y): # [superfluous-parens] + pass + +i = 0 +exclude = [] +if (i - 0) in exclude: # [superfluous-parens] + pass diff --git a/doc/data/messages/s/superfluous-parens/details.rst b/doc/data/messages/s/superfluous-parens/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/s/superfluous-parens/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/s/superfluous-parens/good.py b/doc/data/messages/s/superfluous-parens/good.py index c40beb573f..7d20f5e1d3 100644 --- a/doc/data/messages/s/superfluous-parens/good.py +++ b/doc/data/messages/s/superfluous-parens/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +x = input() +y = input() +if x == y: + pass + +i = 0 +exclude = [] +if i - 0 in exclude: + pass diff --git a/doc/data/messages/s/syntax-error/bad.py b/doc/data/messages/s/syntax-error/bad.py new file mode 100644 index 0000000000..6a34478e15 --- /dev/null +++ b/doc/data/messages/s/syntax-error/bad.py @@ -0,0 +1,5 @@ +fruit_stock = { + 'apple': 42, + 'orange': 21 # [syntax-error] + 'banana': 12 +} diff --git a/doc/data/messages/s/syntax-error/good.py b/doc/data/messages/s/syntax-error/good.py new file mode 100644 index 0000000000..eccab87464 --- /dev/null +++ b/doc/data/messages/s/syntax-error/good.py @@ -0,0 +1,5 @@ +fruit_stock = { + 'apple': 42, + 'orange': 21, + 'banana': 12 +} diff --git a/doc/data/messages/t/too-few-public-methods/bad.py b/doc/data/messages/t/too-few-public-methods/bad.py new file mode 100644 index 0000000000..bc8748f5db --- /dev/null +++ b/doc/data/messages/t/too-few-public-methods/bad.py @@ -0,0 +1,7 @@ +class Worm: # [too-few-public-methods] + def __init__(self, name: str, fruit_of_residence: Fruit): + self.name = name + self.fruit_of_residence = fruit_of_residence + + def bore(self): + print(f"{self.name} is boring into {self.fruit_of_residence}") diff --git a/doc/data/messages/t/too-few-public-methods/details.rst b/doc/data/messages/t/too-few-public-methods/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-few-public-methods/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-few-public-methods/good.py b/doc/data/messages/t/too-few-public-methods/good.py index c40beb573f..ca4b5b6fb3 100644 --- a/doc/data/messages/t/too-few-public-methods/good.py +++ b/doc/data/messages/t/too-few-public-methods/good.py @@ -1 +1,28 @@ -# This is a placeholder for correct code for this message. +import dataclasses + + +class Worm: + def __init__(self, name: str, fruit_of_residence: Fruit): + self.name = name + self.fruit_of_residence = fruit_of_residence + + def bore(self): + print(f"{self.name} is boring into {self.fruit_of_residence}") + + def wiggle(self): + print(f"{self.name} wiggle around wormily.") + +# or + +@dataclasses.dataclass +class Worm: + name:str + fruit_of_residence: Fruit + +def bore(worm: Worm): + print(f"{worm.name} is boring into {worm.fruit_of_residence}") + +# or + +def bore(fruit: Fruit, worm_name: str): + print(f"{worm_name} is boring into {fruit}") diff --git a/doc/data/messages/t/too-many-ancestors/bad.py b/doc/data/messages/t/too-many-ancestors/bad.py new file mode 100644 index 0000000000..dfe70b83e7 --- /dev/null +++ b/doc/data/messages/t/too-many-ancestors/bad.py @@ -0,0 +1,24 @@ +class Animal: ... +class BeakyAnimal(Animal): ... +class FurryAnimal(Animal): ... +class Swimmer(Animal): ... +class EggLayer(Animal): ... +class VenomousAnimal(Animal): ... +class ProtectedSpecie(Animal): ... +class BeaverTailedAnimal(Animal): ... +class Vertebrate(Animal): ... + + +# max of 7 by default, can be configured +# each edge of a diamond inheritance counts +class Playtypus( # [too-many-ancestors] + BeakyAnimal, + FurryAnimal, + Swimmer, + EggLayer, + VenomousAnimal, + ProtectedSpecie, + BeaverTailedAnimal, + Vertebrate, +): + pass diff --git a/doc/data/messages/t/too-many-ancestors/details.rst b/doc/data/messages/t/too-many-ancestors/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-ancestors/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-ancestors/good.py b/doc/data/messages/t/too-many-ancestors/good.py index c40beb573f..5853b25c92 100644 --- a/doc/data/messages/t/too-many-ancestors/good.py +++ b/doc/data/messages/t/too-many-ancestors/good.py @@ -1 +1,33 @@ -# This is a placeholder for correct code for this message. +class Animal: + beaver_tailed: bool + can_swim: bool + has_beak: bool + has_fur: bool + has_vertebrae: bool + lays_egg: bool + protected_specie: bool + venomous: bool + + +class Invertebrate(Animal): + has_vertebrae = False + + +class Vertebrate(Animal): + has_vertebrae = True + + +class Mammal(Vertebrate): + has_beak = False + has_fur = True + lays_egg = False + venomous = False + + +class Playtypus(Mammal): + beaver_tailed = True + can_swim = True + has_beak = True + lays_egg = False + protected_specie = True + venomous = True diff --git a/doc/data/messages/t/too-many-boolean-expressions/bad.py b/doc/data/messages/t/too-many-boolean-expressions/bad.py new file mode 100644 index 0000000000..5f3ae4194f --- /dev/null +++ b/doc/data/messages/t/too-many-boolean-expressions/bad.py @@ -0,0 +1,5 @@ +def can_be_divided_by_two_and_are_not_zero(x, y, z): + # Maximum number of boolean expressions in an if statement (by default 5) + # +1: [too-many-boolean-expressions] + if (x and y and z) and (x % 2 == 0 and y % 2 == 0 and z % 2 == 0): + pass diff --git a/doc/data/messages/t/too-many-boolean-expressions/details.rst b/doc/data/messages/t/too-many-boolean-expressions/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-boolean-expressions/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-boolean-expressions/good.py b/doc/data/messages/t/too-many-boolean-expressions/good.py index c40beb573f..1da0254600 100644 --- a/doc/data/messages/t/too-many-boolean-expressions/good.py +++ b/doc/data/messages/t/too-many-boolean-expressions/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +def can_be_divided_by_two_and_are_not_zero(x, y, z): + if all(i and i%2==0 for i in [x, y, z]): + pass diff --git a/doc/data/messages/t/too-many-branches/bad.py b/doc/data/messages/t/too-many-branches/bad.py new file mode 100644 index 0000000000..d3fe8f4616 --- /dev/null +++ b/doc/data/messages/t/too-many-branches/bad.py @@ -0,0 +1,23 @@ +def num_to_word(x): # [too-many-branches] + if x == 0: + return "zero" + elif x == 1: + return "one" + elif x == 2: + return "two" + elif x == 3: + return "three" + elif x == 4: + return "four" + elif x == 5: + return "five" + elif x == 6: + return "six" + elif x == 7: + return "seven" + elif x == 8: + return "eight" + elif x == 9: + return "nine" + else: + return None diff --git a/doc/data/messages/t/too-many-branches/details.rst b/doc/data/messages/t/too-many-branches/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-branches/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-branches/good.py b/doc/data/messages/t/too-many-branches/good.py index c40beb573f..785c386352 100644 --- a/doc/data/messages/t/too-many-branches/good.py +++ b/doc/data/messages/t/too-many-branches/good.py @@ -1 +1,13 @@ -# This is a placeholder for correct code for this message. +def num_to_word(x): + return { + 0: "zero", + 1: "one", + 2: "two", + 3: "three", + 4: "for", + 5: "fie", + 6: "six", + 7: "seven", + 8: "eight", + 9: "nine", + }.get(x) diff --git a/doc/data/messages/t/too-many-branches/pylintrc b/doc/data/messages/t/too-many-branches/pylintrc new file mode 100644 index 0000000000..08831a29b0 --- /dev/null +++ b/doc/data/messages/t/too-many-branches/pylintrc @@ -0,0 +1,2 @@ +[main] +max-branches=10 diff --git a/doc/data/messages/t/too-many-format-args/bad.py b/doc/data/messages/t/too-many-format-args/bad.py index 0c98c084d1..548f410420 100644 --- a/doc/data/messages/t/too-many-format-args/bad.py +++ b/doc/data/messages/t/too-many-format-args/bad.py @@ -1 +1,2 @@ -print("Today is {0}, so tomorrow will be {1}".format("Monday", "Tuesday", "Wednesday")) # [too-many-format-args] +# +1: [too-many-format-args] +print("Today is {0}, so tomorrow will be {1}".format("Monday", "Tuesday", "Wednesday")) diff --git a/doc/data/messages/t/too-many-function-args/bad.py b/doc/data/messages/t/too-many-function-args/bad.py new file mode 100644 index 0000000000..97eedb9446 --- /dev/null +++ b/doc/data/messages/t/too-many-function-args/bad.py @@ -0,0 +1,6 @@ +class Fruit: + def __init__(self, color): + self.color = color + + +apple = Fruit("red", "apple", [1, 2, 3]) # [too-many-function-args] diff --git a/doc/data/messages/t/too-many-function-args/details.rst b/doc/data/messages/t/too-many-function-args/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-function-args/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-function-args/good.py b/doc/data/messages/t/too-many-function-args/good.py index c40beb573f..338b8e1e8e 100644 --- a/doc/data/messages/t/too-many-function-args/good.py +++ b/doc/data/messages/t/too-many-function-args/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self, color, name): + self.color = color + self.name = name + + +apple = Fruit("red", "apple") diff --git a/doc/data/messages/t/too-many-instance-attributes/bad.py b/doc/data/messages/t/too-many-instance-attributes/bad.py new file mode 100644 index 0000000000..0892a08799 --- /dev/null +++ b/doc/data/messages/t/too-many-instance-attributes/bad.py @@ -0,0 +1,13 @@ +class Fruit: # [too-many-instance-attributes] + def __init__(self): + # max of 7 attributes by default, can be configured + self.worm_name = "Jimmy" + self.worm_type = "Codling Moths" + self.worm_color = "light brown" + self.fruit_name = "Little Apple" + self.fruit_color = "Bright red" + self.fruit_vitamins = ["A", "B1"] + self.fruit_antioxidants = None + self.secondary_worm_name = "Kim" + self.secondary_worm_type = "Apple maggot" + self.secondary_worm_color = "Whitish" diff --git a/doc/data/messages/t/too-many-instance-attributes/details.rst b/doc/data/messages/t/too-many-instance-attributes/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-instance-attributes/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-instance-attributes/good.py b/doc/data/messages/t/too-many-instance-attributes/good.py index c40beb573f..08630707a6 100644 --- a/doc/data/messages/t/too-many-instance-attributes/good.py +++ b/doc/data/messages/t/too-many-instance-attributes/good.py @@ -1 +1,20 @@ -# This is a placeholder for correct code for this message. +import dataclasses + + +@dataclasses.dataclass +class Worm: + name: str + type: str + color: str + + +class Fruit: + def __init__(self): + self.name = "Little Apple" + self.color = "Bright red" + self.vitamins = ["A", "B1"] + self.antioxidants = None + self.worms = [ + Worm(name="Jimmy", type="Codling Moths", color="light brown"), + Worm(name="Kim", type="Apple maggot", color="Whitish"), + ] diff --git a/doc/data/messages/t/too-many-nested-blocks/bad.py b/doc/data/messages/t/too-many-nested-blocks/bad.py new file mode 100644 index 0000000000..8a2c79ee13 --- /dev/null +++ b/doc/data/messages/t/too-many-nested-blocks/bad.py @@ -0,0 +1,10 @@ +def correct_fruits(fruits): + if len(fruits) > 1: # [too-many-nested-blocks] + if "apple" in fruits: + if "orange" in fruits: + count = fruits["orange"] + if count % 2: + if "kiwi" in fruits: + if count == 2: + return True + return False diff --git a/doc/data/messages/t/too-many-nested-blocks/details.rst b/doc/data/messages/t/too-many-nested-blocks/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-nested-blocks/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-nested-blocks/good.py b/doc/data/messages/t/too-many-nested-blocks/good.py index c40beb573f..1b14770ae3 100644 --- a/doc/data/messages/t/too-many-nested-blocks/good.py +++ b/doc/data/messages/t/too-many-nested-blocks/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +def correct_fruits(fruits): + if len(fruits) > 1 and "apple" in fruits and "orange" in fruits: + count = fruits["orange"] + if count % 2 and "kiwi" in fruits and count == 2: + return True + return False diff --git a/doc/data/messages/t/too-many-return-statements/bad.py b/doc/data/messages/t/too-many-return-statements/bad.py new file mode 100644 index 0000000000..e421d29a7e --- /dev/null +++ b/doc/data/messages/t/too-many-return-statements/bad.py @@ -0,0 +1,16 @@ +def to_string(x): # [too-many-return-statements] + # max of 6 by default, can be configured + if x == 1: + return 'This is one.' + if x == 2: + return 'This is two.' + if x == 3: + return 'This is three.' + if x == 4: + return 'This is four.' + if x == 5: + return 'This is five.' + if x == 6: + return 'This is six.' + if x == 7: + return 'This is seven.' diff --git a/doc/data/messages/t/too-many-return-statements/details.rst b/doc/data/messages/t/too-many-return-statements/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-return-statements/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-return-statements/good.py b/doc/data/messages/t/too-many-return-statements/good.py index c40beb573f..0c4032f53e 100644 --- a/doc/data/messages/t/too-many-return-statements/good.py +++ b/doc/data/messages/t/too-many-return-statements/good.py @@ -1 +1,13 @@ -# This is a placeholder for correct code for this message. +NUMBERS_TO_STRINGS = { + 1: 'one', + 2: 'two', + 3: 'three', + 4: 'four', + 5: 'five', + 6: 'six', + 7: 'seven' +} + + +def to_string(x): + return f'This is {NUMBERS_TO_STRINGS.get(x)}.' diff --git a/doc/data/messages/t/too-many-star-expressions/bad.py b/doc/data/messages/t/too-many-star-expressions/bad.py new file mode 100644 index 0000000000..e8443c82ee --- /dev/null +++ b/doc/data/messages/t/too-many-star-expressions/bad.py @@ -0,0 +1 @@ +*stars, *constellations = ["Sirius", "Arcturus", "Vega"] # [too-many-star-expressions] diff --git a/doc/data/messages/t/too-many-star-expressions/details.rst b/doc/data/messages/t/too-many-star-expressions/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-star-expressions/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-star-expressions/good.py b/doc/data/messages/t/too-many-star-expressions/good.py index c40beb573f..73f0970251 100644 --- a/doc/data/messages/t/too-many-star-expressions/good.py +++ b/doc/data/messages/t/too-many-star-expressions/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +*sirius_and_arcturus, vega = ["Sirius", "Arcturus", "Vega"] diff --git a/doc/data/messages/t/too-many-try-statements/bad.py b/doc/data/messages/t/too-many-try-statements/bad.py new file mode 100644 index 0000000000..4e816ad39c --- /dev/null +++ b/doc/data/messages/t/too-many-try-statements/bad.py @@ -0,0 +1,10 @@ +FRUITS = {"apple": 1, "orange": 10} + + +def pick_fruit(name): + try: # [too-many-try-statements] + count = FRUITS[name] + count += 1 + print(f"Got fruit count {count}") + except KeyError: + return diff --git a/doc/data/messages/t/too-many-try-statements/details.rst b/doc/data/messages/t/too-many-try-statements/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/too-many-try-statements/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/too-many-try-statements/good.py b/doc/data/messages/t/too-many-try-statements/good.py index c40beb573f..faea966a15 100644 --- a/doc/data/messages/t/too-many-try-statements/good.py +++ b/doc/data/messages/t/too-many-try-statements/good.py @@ -1 +1,11 @@ -# This is a placeholder for correct code for this message. +FRUITS = {"apple": 1, "orange": 10} + + +def pick_fruit(name): + try: + count = FRUITS[name] + except KeyError: + return + + count += 1 + print(f"Got fruit count {count}") diff --git a/doc/data/messages/t/too-many-try-statements/pylintrc b/doc/data/messages/t/too-many-try-statements/pylintrc new file mode 100644 index 0000000000..438a80b6d0 --- /dev/null +++ b/doc/data/messages/t/too-many-try-statements/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.broad_try_clause, diff --git a/doc/data/messages/t/trailing-newlines/bad.py b/doc/data/messages/t/trailing-newlines/bad.py new file mode 100644 index 0000000000..91f8090d30 --- /dev/null +++ b/doc/data/messages/t/trailing-newlines/bad.py @@ -0,0 +1,3 @@ +print("apple") +# The file ends with 2 lines that are empty # +1: [trailing-newlines] + diff --git a/doc/data/messages/t/trailing-newlines/details.rst b/doc/data/messages/t/trailing-newlines/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/trailing-newlines/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/trailing-newlines/good.py b/doc/data/messages/t/trailing-newlines/good.py index c40beb573f..57725797f8 100644 --- a/doc/data/messages/t/trailing-newlines/good.py +++ b/doc/data/messages/t/trailing-newlines/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +print("apple") diff --git a/doc/data/messages/t/trailing-whitespace/bad.py b/doc/data/messages/t/trailing-whitespace/bad.py new file mode 100644 index 0000000000..1b2cbf41cb --- /dev/null +++ b/doc/data/messages/t/trailing-whitespace/bad.py @@ -0,0 +1,2 @@ +print("Hello") # [trailing-whitespace] +# ^^^ trailing whitespaces diff --git a/doc/data/messages/t/trailing-whitespace/details.rst b/doc/data/messages/t/trailing-whitespace/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/t/trailing-whitespace/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/t/trailing-whitespace/good.py b/doc/data/messages/t/trailing-whitespace/good.py index c40beb573f..2f9a147db1 100644 --- a/doc/data/messages/t/trailing-whitespace/good.py +++ b/doc/data/messages/t/trailing-whitespace/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +print("Hello") diff --git a/doc/data/messages/u/unbalanced-dict-unpacking/bad.py b/doc/data/messages/u/unbalanced-dict-unpacking/bad.py new file mode 100644 index 0000000000..9162ccc45e --- /dev/null +++ b/doc/data/messages/u/unbalanced-dict-unpacking/bad.py @@ -0,0 +1,4 @@ +FRUITS = {"apple": 2, "orange": 3, "mellon": 10} + +for fruit, price in FRUITS.values(): # [unbalanced-dict-unpacking] + print(fruit) diff --git a/doc/data/messages/u/unbalanced-dict-unpacking/good.py b/doc/data/messages/u/unbalanced-dict-unpacking/good.py new file mode 100644 index 0000000000..450e034896 --- /dev/null +++ b/doc/data/messages/u/unbalanced-dict-unpacking/good.py @@ -0,0 +1,4 @@ +FRUITS = {"apple": 2, "orange": 3, "mellon": 10} + +for fruit, price in FRUITS.items(): + print(fruit) diff --git a/doc/data/messages/u/undefined-loop-variable/bad.py b/doc/data/messages/u/undefined-loop-variable/bad.py new file mode 100644 index 0000000000..88b0c11fb8 --- /dev/null +++ b/doc/data/messages/u/undefined-loop-variable/bad.py @@ -0,0 +1,5 @@ +def find_even_number(numbers): + for x in numbers: + if x % 2 == 0: + break + return x # [undefined-loop-variable] diff --git a/doc/data/messages/u/undefined-loop-variable/details.rst b/doc/data/messages/u/undefined-loop-variable/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/undefined-loop-variable/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/undefined-loop-variable/good.py b/doc/data/messages/u/undefined-loop-variable/good.py index c40beb573f..eaea1723d8 100644 --- a/doc/data/messages/u/undefined-loop-variable/good.py +++ b/doc/data/messages/u/undefined-loop-variable/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +def find_even_number(numbers): + for x in numbers: + if x % 2: + return x + return None diff --git a/doc/data/messages/u/unexpected-keyword-arg/bad.py b/doc/data/messages/u/unexpected-keyword-arg/bad.py new file mode 100644 index 0000000000..40ef297b1d --- /dev/null +++ b/doc/data/messages/u/unexpected-keyword-arg/bad.py @@ -0,0 +1,5 @@ +def print_coordinates(x=0, y=0): + print(f"{x=}, {y=}") + + +print_coordinates(x=1, y=2, z=3) # [unexpected-keyword-arg] diff --git a/doc/data/messages/u/unexpected-keyword-arg/details.rst b/doc/data/messages/u/unexpected-keyword-arg/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unexpected-keyword-arg/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unexpected-keyword-arg/good.py b/doc/data/messages/u/unexpected-keyword-arg/good.py index c40beb573f..076b3f0bdd 100644 --- a/doc/data/messages/u/unexpected-keyword-arg/good.py +++ b/doc/data/messages/u/unexpected-keyword-arg/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +def print_coordinates(x=0, y=0): + print(f"{x=}, {y=}") + + +print_coordinates(x=1, y=2) diff --git a/doc/data/messages/u/unexpected-special-method-signature/bad.py b/doc/data/messages/u/unexpected-special-method-signature/bad.py new file mode 100644 index 0000000000..4324d39d00 --- /dev/null +++ b/doc/data/messages/u/unexpected-special-method-signature/bad.py @@ -0,0 +1,6 @@ +class ContextManager: + def __enter__(self, context): # [unexpected-special-method-signature] + pass + + def __exit__(self, type): # [unexpected-special-method-signature] + pass diff --git a/doc/data/messages/u/unexpected-special-method-signature/details.rst b/doc/data/messages/u/unexpected-special-method-signature/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unexpected-special-method-signature/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unexpected-special-method-signature/good.py b/doc/data/messages/u/unexpected-special-method-signature/good.py index c40beb573f..89ef3b663e 100644 --- a/doc/data/messages/u/unexpected-special-method-signature/good.py +++ b/doc/data/messages/u/unexpected-special-method-signature/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +class ContextManager: + def __enter__(self): + pass + + def __exit__(self, type, value, traceback): + pass diff --git a/doc/data/messages/u/unhashable-dict-key/details.rst b/doc/data/messages/u/unhashable-dict-key/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unhashable-dict-key/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unhashable-dict-key/good.py b/doc/data/messages/u/unhashable-dict-key/good.py deleted file mode 100644 index c40beb573f..0000000000 --- a/doc/data/messages/u/unhashable-dict-key/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/u/unhashable-member/bad.py b/doc/data/messages/u/unhashable-member/bad.py new file mode 100644 index 0000000000..30c47dad38 --- /dev/null +++ b/doc/data/messages/u/unhashable-member/bad.py @@ -0,0 +1,2 @@ +# Print the number of apples: +print({"apple": 42}[["apple"]]) # [unhashable-member] diff --git a/doc/data/messages/u/unhashable-member/good.py b/doc/data/messages/u/unhashable-member/good.py new file mode 100644 index 0000000000..1678a60789 --- /dev/null +++ b/doc/data/messages/u/unhashable-member/good.py @@ -0,0 +1,2 @@ +# Print the number of apples: +print({"apple": 42}["apple"]) diff --git a/doc/data/messages/u/unnecessary-dict-index-lookup/bad.py b/doc/data/messages/u/unnecessary-dict-index-lookup/bad.py new file mode 100644 index 0000000000..0471758619 --- /dev/null +++ b/doc/data/messages/u/unnecessary-dict-index-lookup/bad.py @@ -0,0 +1,4 @@ +FRUITS = {"apple": 1, "orange": 10, "berry": 22} + +for fruit_name, fruit_count in FRUITS.items(): + print(FRUITS[fruit_name]) # [unnecessary-dict-index-lookup] diff --git a/doc/data/messages/u/unnecessary-dict-index-lookup/details.rst b/doc/data/messages/u/unnecessary-dict-index-lookup/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unnecessary-dict-index-lookup/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unnecessary-dict-index-lookup/good.py b/doc/data/messages/u/unnecessary-dict-index-lookup/good.py index c40beb573f..c2ee5ed575 100644 --- a/doc/data/messages/u/unnecessary-dict-index-lookup/good.py +++ b/doc/data/messages/u/unnecessary-dict-index-lookup/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +FRUITS = {"apple": 1, "orange": 10, "berry": 22} + +for fruit_name, fruit_count in FRUITS.items(): + print(fruit_count) diff --git a/doc/data/messages/u/unnecessary-lambda/bad.py b/doc/data/messages/u/unnecessary-lambda/bad.py new file mode 100644 index 0000000000..ddf84f7fa8 --- /dev/null +++ b/doc/data/messages/u/unnecessary-lambda/bad.py @@ -0,0 +1,5 @@ +function = lambda x: print(x) # [unnecessary-lambda] + +function("Hello world !") + +df.apply(lambda x: str(x)) # [unnecessary-lambda] diff --git a/doc/data/messages/u/unnecessary-lambda/details.rst b/doc/data/messages/u/unnecessary-lambda/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unnecessary-lambda/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unnecessary-lambda/good.py b/doc/data/messages/u/unnecessary-lambda/good.py index c40beb573f..9f0678f02f 100644 --- a/doc/data/messages/u/unnecessary-lambda/good.py +++ b/doc/data/messages/u/unnecessary-lambda/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +print("Hello world !") + +df.apply(str) diff --git a/doc/data/messages/u/unneeded-not/bad.py b/doc/data/messages/u/unneeded-not/bad.py new file mode 100644 index 0000000000..1a9ac50b1d --- /dev/null +++ b/doc/data/messages/u/unneeded-not/bad.py @@ -0,0 +1,2 @@ +if not not input(): # [unneeded-not] + pass diff --git a/doc/data/messages/u/unneeded-not/details.rst b/doc/data/messages/u/unneeded-not/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unneeded-not/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unneeded-not/good.py b/doc/data/messages/u/unneeded-not/good.py index c40beb573f..ac129106b0 100644 --- a/doc/data/messages/u/unneeded-not/good.py +++ b/doc/data/messages/u/unneeded-not/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +if input(): + pass diff --git a/doc/data/messages/u/unrecognized-inline-option/bad.py b/doc/data/messages/u/unrecognized-inline-option/bad.py new file mode 100644 index 0000000000..ff49aa9204 --- /dev/null +++ b/doc/data/messages/u/unrecognized-inline-option/bad.py @@ -0,0 +1,2 @@ +# +1: [unrecognized-inline-option] +# pylint:applesoranges=1 diff --git a/doc/data/messages/u/unrecognized-inline-option/details.rst b/doc/data/messages/u/unrecognized-inline-option/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unrecognized-inline-option/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unrecognized-inline-option/good.py b/doc/data/messages/u/unrecognized-inline-option/good.py index c40beb573f..2fdb3780af 100644 --- a/doc/data/messages/u/unrecognized-inline-option/good.py +++ b/doc/data/messages/u/unrecognized-inline-option/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +# pylint: enable=too-many-public-methods diff --git a/doc/data/messages/u/unsubscriptable-object/bad.py b/doc/data/messages/u/unsubscriptable-object/bad.py new file mode 100644 index 0000000000..8b168a0af8 --- /dev/null +++ b/doc/data/messages/u/unsubscriptable-object/bad.py @@ -0,0 +1,5 @@ +class Fruit: + pass + + +Fruit()[1] # [unsubscriptable-object] diff --git a/doc/data/messages/u/unsubscriptable-object/details.rst b/doc/data/messages/u/unsubscriptable-object/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unsubscriptable-object/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unsubscriptable-object/good.py b/doc/data/messages/u/unsubscriptable-object/good.py index c40beb573f..56e91444e9 100644 --- a/doc/data/messages/u/unsubscriptable-object/good.py +++ b/doc/data/messages/u/unsubscriptable-object/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self): + self.colors = ["red", "orange", "yellow"] + + def __getitem__(self, idx): + return self.colors[idx] + + +Fruit()[1] diff --git a/doc/data/messages/u/unsupported-assignment-operation/bad.py b/doc/data/messages/u/unsupported-assignment-operation/bad.py new file mode 100644 index 0000000000..26ee1a9931 --- /dev/null +++ b/doc/data/messages/u/unsupported-assignment-operation/bad.py @@ -0,0 +1,6 @@ +def pick_fruits(fruits): + for fruit in fruits: + print(fruit) + + +pick_fruits(["apple"])[0] = "orange" # [unsupported-assignment-operation] diff --git a/doc/data/messages/u/unsupported-assignment-operation/details.rst b/doc/data/messages/u/unsupported-assignment-operation/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unsupported-assignment-operation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unsupported-assignment-operation/good.py b/doc/data/messages/u/unsupported-assignment-operation/good.py index c40beb573f..13fe34c053 100644 --- a/doc/data/messages/u/unsupported-assignment-operation/good.py +++ b/doc/data/messages/u/unsupported-assignment-operation/good.py @@ -1 +1,8 @@ -# This is a placeholder for correct code for this message. +def pick_fruits(fruits): + for fruit in fruits: + print(fruit) + + return [] + + +pick_fruits(["apple"])[0] = "orange" diff --git a/doc/data/messages/u/unsupported-delete-operation/bad.py b/doc/data/messages/u/unsupported-delete-operation/bad.py new file mode 100644 index 0000000000..a7870e3a81 --- /dev/null +++ b/doc/data/messages/u/unsupported-delete-operation/bad.py @@ -0,0 +1,3 @@ +FRUITS = ("apple", "orange", "berry") + +del FRUITS[0] # [unsupported-delete-operation] diff --git a/doc/data/messages/u/unsupported-delete-operation/details.rst b/doc/data/messages/u/unsupported-delete-operation/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unsupported-delete-operation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unsupported-delete-operation/good.py b/doc/data/messages/u/unsupported-delete-operation/good.py index c40beb573f..8143c4feee 100644 --- a/doc/data/messages/u/unsupported-delete-operation/good.py +++ b/doc/data/messages/u/unsupported-delete-operation/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +FRUITS = ["apple", "orange", "berry"] + +del FRUITS[0] diff --git a/doc/data/messages/u/unsupported-membership-test/bad.py b/doc/data/messages/u/unsupported-membership-test/bad.py new file mode 100644 index 0000000000..37502ecd33 --- /dev/null +++ b/doc/data/messages/u/unsupported-membership-test/bad.py @@ -0,0 +1,5 @@ +class Fruit: + pass + + +apple = "apple" in Fruit() # [unsupported-membership-test] diff --git a/doc/data/messages/u/unsupported-membership-test/details.rst b/doc/data/messages/u/unsupported-membership-test/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unsupported-membership-test/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unsupported-membership-test/good.py b/doc/data/messages/u/unsupported-membership-test/good.py index c40beb573f..96b96d4d56 100644 --- a/doc/data/messages/u/unsupported-membership-test/good.py +++ b/doc/data/messages/u/unsupported-membership-test/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +class Fruit: + FRUITS = ["apple", "orange"] + def __contains__(self, name): + return name in self.FRUITS + + +apple = "apple" in Fruit() diff --git a/doc/data/messages/u/unused-argument/bad.py b/doc/data/messages/u/unused-argument/bad.py new file mode 100644 index 0000000000..c605fb76de --- /dev/null +++ b/doc/data/messages/u/unused-argument/bad.py @@ -0,0 +1,2 @@ +def print_point(x, y): # [unused-argument] + print(f"Point is located at {x},{x}") diff --git a/doc/data/messages/u/unused-argument/details.rst b/doc/data/messages/u/unused-argument/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unused-argument/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unused-argument/good.py b/doc/data/messages/u/unused-argument/good.py index c40beb573f..a093d7d2a6 100644 --- a/doc/data/messages/u/unused-argument/good.py +++ b/doc/data/messages/u/unused-argument/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def print_point(x, y): + print(f"Point is located at {x},{y}") diff --git a/doc/data/messages/u/unused-format-string-argument/bad.py b/doc/data/messages/u/unused-format-string-argument/bad.py new file mode 100644 index 0000000000..49eda97f1f --- /dev/null +++ b/doc/data/messages/u/unused-format-string-argument/bad.py @@ -0,0 +1 @@ +print("{x} {y}".format(x=1, y=2, z=3)) # [unused-format-string-argument] diff --git a/doc/data/messages/u/unused-format-string-argument/details.rst b/doc/data/messages/u/unused-format-string-argument/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unused-format-string-argument/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unused-format-string-argument/good.py b/doc/data/messages/u/unused-format-string-argument/good.py index c40beb573f..dc54549437 100644 --- a/doc/data/messages/u/unused-format-string-argument/good.py +++ b/doc/data/messages/u/unused-format-string-argument/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +print("{x} {y} {z}".format(x=1, y=2, z=3)) +# or +print("{x} {y}".format(x=1, y=2)) diff --git a/doc/data/messages/u/unused-private-member/bad.py b/doc/data/messages/u/unused-private-member/bad.py new file mode 100644 index 0000000000..b56bcaad31 --- /dev/null +++ b/doc/data/messages/u/unused-private-member/bad.py @@ -0,0 +1,5 @@ +class Fruit: + FRUITS = {"apple": "red", "orange": "orange"} + + def __print_color(self): # [unused-private-member] + pass diff --git a/doc/data/messages/u/unused-private-member/details.rst b/doc/data/messages/u/unused-private-member/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unused-private-member/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unused-private-member/good.py b/doc/data/messages/u/unused-private-member/good.py index c40beb573f..02df36c442 100644 --- a/doc/data/messages/u/unused-private-member/good.py +++ b/doc/data/messages/u/unused-private-member/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Fruit: + FRUITS = {"apple": "red", "orange": "orange"} + + def __print_color(self, name, color): + print(f"{name}: {color}") + + def print(self): + for fruit, color in self.FRUITS.items(): + self.__print_color(fruit, color) diff --git a/doc/data/messages/u/unused-variable/bad.py b/doc/data/messages/u/unused-variable/bad.py new file mode 100644 index 0000000000..1de7d2b073 --- /dev/null +++ b/doc/data/messages/u/unused-variable/bad.py @@ -0,0 +1,4 @@ +def print_fruits(): + fruit1 = "orange" + fruit2 = "apple" # [unused-variable] + print(fruit1) diff --git a/doc/data/messages/u/unused-variable/details.rst b/doc/data/messages/u/unused-variable/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/unused-variable/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/unused-variable/good.py b/doc/data/messages/u/unused-variable/good.py index c40beb573f..69b7663900 100644 --- a/doc/data/messages/u/unused-variable/good.py +++ b/doc/data/messages/u/unused-variable/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +def print_fruits(): + fruit1 = "orange" + fruit2 = "apple" + print(fruit1, fruit2) diff --git a/doc/data/messages/u/use-a-generator/details.rst b/doc/data/messages/u/use-a-generator/details.rst index 3abe2d7482..a2cd578e4b 100644 --- a/doc/data/messages/u/use-a-generator/details.rst +++ b/doc/data/messages/u/use-a-generator/details.rst @@ -1,3 +1,3 @@ -By using a generator you can cut the execution tree and exit directly at the first element that is ``False`` for ``all`` or ``True`` for ``any`` instead of -calculating all the elements. Except in the worst possible case where you still need to evaluate everything (all values +By using a generator you can cut the execution tree and exit directly at the first element that is ``False`` for ``all`` or ``True`` for ``any`` instead of +calculating all the elements. Except in the worst possible case where you still need to evaluate everything (all values are True for ``all`` or all values are false for ``any``) performance will be better. diff --git a/doc/data/messages/u/use-dict-literal/bad.py b/doc/data/messages/u/use-dict-literal/bad.py index 6c3056b6fb..2d90a91e8f 100644 --- a/doc/data/messages/u/use-dict-literal/bad.py +++ b/doc/data/messages/u/use-dict-literal/bad.py @@ -1 +1,3 @@ empty_dict = dict() # [use-dict-literal] +new_dict = dict(foo="bar") # [use-dict-literal] +new_dict = dict(**another_dict) # [use-dict-literal] diff --git a/doc/data/messages/u/use-dict-literal/details.rst b/doc/data/messages/u/use-dict-literal/details.rst new file mode 100644 index 0000000000..8511485cd9 --- /dev/null +++ b/doc/data/messages/u/use-dict-literal/details.rst @@ -0,0 +1,4 @@ +https://gist.github.com/hofrob/ad143aaa84c096f42489c2520a3875f9 + +This example script shows an 18% increase in performance when using a literal over the +constructor in python version 3.10.6. diff --git a/doc/data/messages/u/use-dict-literal/good.py b/doc/data/messages/u/use-dict-literal/good.py index 5f7d64debd..237d2c881d 100644 --- a/doc/data/messages/u/use-dict-literal/good.py +++ b/doc/data/messages/u/use-dict-literal/good.py @@ -1 +1,7 @@ empty_dict = {} + +# create using a literal dict +new_dict = {"foo": "bar"} + +# shallow copy a dict +new_dict = {**another_dict} diff --git a/doc/data/messages/u/use-implicit-booleaness-not-comparison/bad.py b/doc/data/messages/u/use-implicit-booleaness-not-comparison/bad.py new file mode 100644 index 0000000000..78411ec2aa --- /dev/null +++ b/doc/data/messages/u/use-implicit-booleaness-not-comparison/bad.py @@ -0,0 +1,4 @@ +z = [] + +if z != []: # [use-implicit-booleaness-not-comparison] + print("z is not an empty sequence") diff --git a/doc/data/messages/u/use-implicit-booleaness-not-comparison/details.rst b/doc/data/messages/u/use-implicit-booleaness-not-comparison/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/use-implicit-booleaness-not-comparison/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/use-implicit-booleaness-not-comparison/good.py b/doc/data/messages/u/use-implicit-booleaness-not-comparison/good.py index c40beb573f..6801d91eb2 100644 --- a/doc/data/messages/u/use-implicit-booleaness-not-comparison/good.py +++ b/doc/data/messages/u/use-implicit-booleaness-not-comparison/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +z = [] + +if z: + print("z is not an empty sequence") diff --git a/doc/data/messages/u/use-implicit-booleaness-not-len/bad.py b/doc/data/messages/u/use-implicit-booleaness-not-len/bad.py new file mode 100644 index 0000000000..722282a593 --- /dev/null +++ b/doc/data/messages/u/use-implicit-booleaness-not-len/bad.py @@ -0,0 +1,4 @@ +fruits = ["orange", "apple"] + +if len(fruits): # [use-implicit-booleaness-not-len] + print(fruits) diff --git a/doc/data/messages/u/use-implicit-booleaness-not-len/details.rst b/doc/data/messages/u/use-implicit-booleaness-not-len/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/use-implicit-booleaness-not-len/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/use-implicit-booleaness-not-len/good.py b/doc/data/messages/u/use-implicit-booleaness-not-len/good.py index c40beb573f..0e84921487 100644 --- a/doc/data/messages/u/use-implicit-booleaness-not-len/good.py +++ b/doc/data/messages/u/use-implicit-booleaness-not-len/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +fruits = ["orange", "apple"] + +if fruits: + print(fruits) diff --git a/doc/data/messages/u/use-set-for-membership/pylintrc b/doc/data/messages/u/use-set-for-membership/pylintrc new file mode 100644 index 0000000000..d7d2a30016 --- /dev/null +++ b/doc/data/messages/u/use-set-for-membership/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.set_membership diff --git a/doc/data/messages/u/used-before-assignment/bad.py b/doc/data/messages/u/used-before-assignment/bad.py new file mode 100644 index 0000000000..6918a07de1 --- /dev/null +++ b/doc/data/messages/u/used-before-assignment/bad.py @@ -0,0 +1,2 @@ +print(hello) # [used-before-assignment] +hello = "Hello World !" diff --git a/doc/data/messages/u/used-before-assignment/details.rst b/doc/data/messages/u/used-before-assignment/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/used-before-assignment/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/used-before-assignment/good.py b/doc/data/messages/u/used-before-assignment/good.py index c40beb573f..f5d4d58b47 100644 --- a/doc/data/messages/u/used-before-assignment/good.py +++ b/doc/data/messages/u/used-before-assignment/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +hello = "Hello World !" +print(hello) diff --git a/doc/data/messages/u/used-prior-global-declaration/bad.py b/doc/data/messages/u/used-prior-global-declaration/bad.py new file mode 100644 index 0000000000..804a1f34af --- /dev/null +++ b/doc/data/messages/u/used-prior-global-declaration/bad.py @@ -0,0 +1,7 @@ +TOMATO = "black cherry" + + +def update_tomato(): + print(TOMATO) # [used-prior-global-declaration] + global TOMATO + TOMATO = "cherry tomato" diff --git a/doc/data/messages/u/used-prior-global-declaration/details.rst b/doc/data/messages/u/used-prior-global-declaration/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/used-prior-global-declaration/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/used-prior-global-declaration/good.py b/doc/data/messages/u/used-prior-global-declaration/good.py index c40beb573f..0736bb4c7b 100644 --- a/doc/data/messages/u/used-prior-global-declaration/good.py +++ b/doc/data/messages/u/used-prior-global-declaration/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +TOMATO = "black cherry" + + +def update_tomato(): + global TOMATO + TOMATO = "moneymaker" diff --git a/doc/data/messages/u/useless-else-on-loop/bad.py b/doc/data/messages/u/useless-else-on-loop/bad.py new file mode 100644 index 0000000000..dcd713f6d0 --- /dev/null +++ b/doc/data/messages/u/useless-else-on-loop/bad.py @@ -0,0 +1,6 @@ +def find_even_number(numbers): + for x in numbers: + if x % 2 == 0: + return x + else: # [useless-else-on-loop] + print("Did not find an even number") diff --git a/doc/data/messages/u/useless-else-on-loop/details.rst b/doc/data/messages/u/useless-else-on-loop/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/useless-else-on-loop/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/useless-else-on-loop/good.py b/doc/data/messages/u/useless-else-on-loop/good.py index c40beb573f..a0bf9cd461 100644 --- a/doc/data/messages/u/useless-else-on-loop/good.py +++ b/doc/data/messages/u/useless-else-on-loop/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +def find_even_number(numbers): + for x in numbers: + if x % 2 == 0: + return x + print("Did not find an even number") diff --git a/doc/data/messages/u/useless-import-alias/details.rst b/doc/data/messages/u/useless-import-alias/details.rst index 5b804e1e9c..f1349efe8c 100644 --- a/doc/data/messages/u/useless-import-alias/details.rst +++ b/doc/data/messages/u/useless-import-alias/details.rst @@ -1,8 +1,8 @@ Known issue ----------- -If you prefer to use "from-as" to explicitly reexport in API (`from fruit import orange as orange`) -instead of using `__all__` this message will be a false positive. +If you prefer to use "from-as" to explicitly reexport in API (``from fruit import orange as orange``) +instead of using ``__all__`` this message will be a false positive. -If that's the case use `pylint: disable=useless-import-alias` before your imports in your API files. -`False positive 'useless-import-alias' error for mypy-compatible explicit re-exports #6006 `_ +Use ``--allow-reexport-from-package`` to allow explicit reexports by alias +in package ``__init__`` files. diff --git a/doc/data/messages/u/useless-import-alias/related.rst b/doc/data/messages/u/useless-import-alias/related.rst index 4693aeeb88..04e084d9cb 100644 --- a/doc/data/messages/u/useless-import-alias/related.rst +++ b/doc/data/messages/u/useless-import-alias/related.rst @@ -1,3 +1,4 @@ +- :ref:`--allow-reexport-from-package` - `PEP 8, Import Guideline `_ - :ref:`Pylint block-disable ` - `mypy --no-implicit-reexport `_ diff --git a/doc/data/messages/u/useless-param-doc/bad.py b/doc/data/messages/u/useless-param-doc/bad.py new file mode 100644 index 0000000000..f6c289d361 --- /dev/null +++ b/doc/data/messages/u/useless-param-doc/bad.py @@ -0,0 +1,7 @@ +def say_hello(_new: str) -> str: # [useless-param-doc] + """say hello! + + :param _new: + :return: comment + """ + return "hello" diff --git a/doc/data/messages/u/useless-param-doc/details.rst b/doc/data/messages/u/useless-param-doc/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/useless-param-doc/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/useless-param-doc/good.py b/doc/data/messages/u/useless-param-doc/good.py index c40beb573f..58d8fa0f18 100644 --- a/doc/data/messages/u/useless-param-doc/good.py +++ b/doc/data/messages/u/useless-param-doc/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +def say_hello(_new: str) -> str: + """say hello! + + :return: comment + """ + return "hello" diff --git a/doc/data/messages/u/useless-param-doc/pylintrc b/doc/data/messages/u/useless-param-doc/pylintrc new file mode 100644 index 0000000000..2b125598a8 --- /dev/null +++ b/doc/data/messages/u/useless-param-doc/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.docparams, diff --git a/doc/data/messages/u/useless-parent-delegation/bad.py b/doc/data/messages/u/useless-parent-delegation/bad.py new file mode 100644 index 0000000000..9010c9ea9b --- /dev/null +++ b/doc/data/messages/u/useless-parent-delegation/bad.py @@ -0,0 +1,10 @@ +class Animal: + + def eat(self, food): + print(f"Eating {food}") + + +class Human(Animal): + + def eat(self, food): # [useless-parent-delegation] + super(Human, self).eat(food) diff --git a/doc/data/messages/u/useless-parent-delegation/good.py b/doc/data/messages/u/useless-parent-delegation/good.py new file mode 100644 index 0000000000..d463b45641 --- /dev/null +++ b/doc/data/messages/u/useless-parent-delegation/good.py @@ -0,0 +1,8 @@ +class Animal: + + def eat(self, food): + print(f"Eating {food}") + + +class Human(Animal): + """There is no need to override 'eat' it has the same signature as the implementation in Animal.""" diff --git a/doc/data/messages/u/useless-parent-delegation/related.rst b/doc/data/messages/u/useless-parent-delegation/related.rst new file mode 100644 index 0000000000..1f356c13a4 --- /dev/null +++ b/doc/data/messages/u/useless-parent-delegation/related.rst @@ -0,0 +1 @@ +- `Stackoverflow explanation for 'useless-super-delegation' `_ diff --git a/doc/data/messages/u/useless-super-delegation/details.rst b/doc/data/messages/u/useless-super-delegation/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/useless-super-delegation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/useless-super-delegation/good.py b/doc/data/messages/u/useless-super-delegation/good.py deleted file mode 100644 index c40beb573f..0000000000 --- a/doc/data/messages/u/useless-super-delegation/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/u/useless-type-doc/bad.py b/doc/data/messages/u/useless-type-doc/bad.py new file mode 100644 index 0000000000..dd6393f570 --- /dev/null +++ b/doc/data/messages/u/useless-type-doc/bad.py @@ -0,0 +1,8 @@ +def print_fruit(fruit, _): # [useless-type-doc] + """docstring ... + + Args: + fruit (str): A fruit. + _ (float): Another argument. + """ + print(fruit) diff --git a/doc/data/messages/u/useless-type-doc/details.rst b/doc/data/messages/u/useless-type-doc/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/useless-type-doc/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/useless-type-doc/good.py b/doc/data/messages/u/useless-type-doc/good.py index c40beb573f..f3ee96ad61 100644 --- a/doc/data/messages/u/useless-type-doc/good.py +++ b/doc/data/messages/u/useless-type-doc/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +def print_fruit(fruit): + """docstring ... + + Args: + fruit (str): A fruit. + """ + print(fruit) diff --git a/doc/data/messages/u/useless-type-doc/pylintrc b/doc/data/messages/u/useless-type-doc/pylintrc new file mode 100644 index 0000000000..2b125598a8 --- /dev/null +++ b/doc/data/messages/u/useless-type-doc/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.docparams, diff --git a/doc/data/messages/u/useless-with-lock/bad.py b/doc/data/messages/u/useless-with-lock/bad.py new file mode 100644 index 0000000000..cb7f0d80ad --- /dev/null +++ b/doc/data/messages/u/useless-with-lock/bad.py @@ -0,0 +1,6 @@ +import threading + +with threading.Lock(): # [useless-with-lock] + print("Make your bed.") +with threading.Lock(): # [useless-with-lock] + print("Sleep in it") diff --git a/doc/data/messages/u/useless-with-lock/details.rst b/doc/data/messages/u/useless-with-lock/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/u/useless-with-lock/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/u/useless-with-lock/good.py b/doc/data/messages/u/useless-with-lock/good.py index c40beb573f..86b104990b 100644 --- a/doc/data/messages/u/useless-with-lock/good.py +++ b/doc/data/messages/u/useless-with-lock/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +import threading + +lock = threading.Lock() +with lock: + print("Make your bed.") +with lock: + print("Sleep in it.") diff --git a/doc/data/messages/u/using-final-decorator-in-unsupported-version/bad.py b/doc/data/messages/u/using-final-decorator-in-unsupported-version/bad.py index 59ec6ebc32..1cfcad6c44 100644 --- a/doc/data/messages/u/using-final-decorator-in-unsupported-version/bad.py +++ b/doc/data/messages/u/using-final-decorator-in-unsupported-version/bad.py @@ -1,8 +1,8 @@ -from typing import final - - -@final # [using-final-decorator-in-unsupported-version] -class Playtypus(Animal): - @final # [using-final-decorator-in-unsupported-version] - def lay_egg(self): - ... +from typing import final + + +@final # [using-final-decorator-in-unsupported-version] +class Playtypus(Animal): + @final # [using-final-decorator-in-unsupported-version] + def lay_egg(self): + ... diff --git a/doc/data/messages/u/using-final-decorator-in-unsupported-version/good.py b/doc/data/messages/u/using-final-decorator-in-unsupported-version/good.py index 9fe62f8f7c..d2f76b4ce2 100644 --- a/doc/data/messages/u/using-final-decorator-in-unsupported-version/good.py +++ b/doc/data/messages/u/using-final-decorator-in-unsupported-version/good.py @@ -1,3 +1,3 @@ -class Playtypus(Animal): - def lay_egg(self): - ... +class Playtypus(Animal): + def lay_egg(self): + ... diff --git a/doc/data/messages/u/using-final-decorator-in-unsupported-version/pylintrc b/doc/data/messages/u/using-final-decorator-in-unsupported-version/pylintrc new file mode 100644 index 0000000000..77eb3be645 --- /dev/null +++ b/doc/data/messages/u/using-final-decorator-in-unsupported-version/pylintrc @@ -0,0 +1,2 @@ +[main] +py-version=3.7 diff --git a/doc/data/messages/w/wrong-exception-operation/bad.py b/doc/data/messages/w/wrong-exception-operation/bad.py new file mode 100644 index 0000000000..20fcc2aab4 --- /dev/null +++ b/doc/data/messages/w/wrong-exception-operation/bad.py @@ -0,0 +1,4 @@ +try: + 1/0 +except (ValueError + TypeError): # [wrong-exception-operation] + pass diff --git a/doc/data/messages/w/wrong-exception-operation/details.rst b/doc/data/messages/w/wrong-exception-operation/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/w/wrong-exception-operation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/w/wrong-exception-operation/good.py b/doc/data/messages/w/wrong-exception-operation/good.py index c40beb573f..4171dbb600 100644 --- a/doc/data/messages/w/wrong-exception-operation/good.py +++ b/doc/data/messages/w/wrong-exception-operation/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +try: + 1/0 +except (ValueError, TypeError): + pass diff --git a/doc/data/messages/w/wrong-spelling-in-comment/bad.py b/doc/data/messages/w/wrong-spelling-in-comment/bad.py new file mode 100644 index 0000000000..becaf40e55 --- /dev/null +++ b/doc/data/messages/w/wrong-spelling-in-comment/bad.py @@ -0,0 +1 @@ +# There's a mistkae in this string # [wrong-spelling-in-comment] diff --git a/doc/data/messages/w/wrong-spelling-in-comment/details.rst b/doc/data/messages/w/wrong-spelling-in-comment/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/w/wrong-spelling-in-comment/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/w/wrong-spelling-in-comment/good.py b/doc/data/messages/w/wrong-spelling-in-comment/good.py index c40beb573f..84af67f5a6 100644 --- a/doc/data/messages/w/wrong-spelling-in-comment/good.py +++ b/doc/data/messages/w/wrong-spelling-in-comment/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +# There's no mistake in this string diff --git a/doc/data/messages/w/wrong-spelling-in-comment/pylintrc b/doc/data/messages/w/wrong-spelling-in-comment/pylintrc new file mode 100644 index 0000000000..dd11bf811a --- /dev/null +++ b/doc/data/messages/w/wrong-spelling-in-comment/pylintrc @@ -0,0 +1,3 @@ +[main] +# This might not run in your env if you don't have the en_US dict installed. +spelling-dict=en_US diff --git a/doc/data/messages/w/wrong-spelling-in-docstring/bad.py b/doc/data/messages/w/wrong-spelling-in-docstring/bad.py new file mode 100644 index 0000000000..b5c9149ae8 --- /dev/null +++ b/doc/data/messages/w/wrong-spelling-in-docstring/bad.py @@ -0,0 +1 @@ +"""There's a mistkae in this string""" # [wrong-spelling-in-docstring] diff --git a/doc/data/messages/w/wrong-spelling-in-docstring/details.rst b/doc/data/messages/w/wrong-spelling-in-docstring/details.rst deleted file mode 100644 index ab82045295..0000000000 --- a/doc/data/messages/w/wrong-spelling-in-docstring/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing `_ ! diff --git a/doc/data/messages/w/wrong-spelling-in-docstring/good.py b/doc/data/messages/w/wrong-spelling-in-docstring/good.py index c40beb573f..10d19e91ed 100644 --- a/doc/data/messages/w/wrong-spelling-in-docstring/good.py +++ b/doc/data/messages/w/wrong-spelling-in-docstring/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +"""There's no mistake in this string""" diff --git a/doc/data/messages/w/wrong-spelling-in-docstring/pylintrc b/doc/data/messages/w/wrong-spelling-in-docstring/pylintrc new file mode 100644 index 0000000000..dd11bf811a --- /dev/null +++ b/doc/data/messages/w/wrong-spelling-in-docstring/pylintrc @@ -0,0 +1,3 @@ +[main] +# This might not run in your env if you don't have the en_US dict installed. +spelling-dict=en_US diff --git a/doc/development_guide/api/index.rst b/doc/development_guide/api/index.rst index 373c498667..00e6e1a9f2 100644 --- a/doc/development_guide/api/index.rst +++ b/doc/development_guide/api/index.rst @@ -7,10 +7,9 @@ Python program thanks to their APIs: .. sourcecode:: python - from pylint import run_pylint, run_epylint, run_pyreverse, run_symilar + from pylint import run_pylint, run_pyreverse, run_symilar run_pylint("--disable=C", "myfile.py") - run_epylint(...) run_pyreverse(...) run_symilar(...) diff --git a/doc/development_guide/api/pylint.rst b/doc/development_guide/api/pylint.rst index 9ba371ea71..000006ac82 100644 --- a/doc/development_guide/api/pylint.rst +++ b/doc/development_guide/api/pylint.rst @@ -40,7 +40,7 @@ reporter initialized with a custom stream: pylint_output = StringIO() # Custom open stream reporter = TextReporter(pylint_output) - Run(["test_file.py"], reporter=reporter, do_exit=False) + Run(["test_file.py"], reporter=reporter, exit=False) print(pylint_output.getvalue()) # Retrieve and print the text report The reporter can accept any stream object as as parameter. In this example, @@ -53,7 +53,7 @@ the stream outputs to a file: with open("report.out", "w") as f: reporter = TextReporter(f) - Run(["test_file.py"], reporter=reporter, do_exit=False) + Run(["test_file.py"], reporter=reporter, exit=False) This would be useful to capture pylint output in an open stream which can be passed onto another program. diff --git a/doc/development_guide/contributor_guide/contribute.rst b/doc/development_guide/contributor_guide/contribute.rst index 9dcfd8d949..0aaf2f8ed8 100644 --- a/doc/development_guide/contributor_guide/contribute.rst +++ b/doc/development_guide/contributor_guide/contribute.rst @@ -1,20 +1,75 @@ -.. -*- coding: utf-8 -*- - ============== Contributing ============== .. _repository: +Finding something to do +----------------------- + +Want to contribute to pylint? There's a lot of things you can do. +Here's a list of links you can check depending on what you want to do: + +- `Asking a question on discord`_, or `on github`_ +- `Opening an issue`_ +- `Making the documentation better`_ +- `Making the error message better`_ +- `Reproducing bugs and confirming that issues are valid`_ +- `Investigating or debugging complicated issues`_ +- `Designing or specifying a solution`_ +- `Giving your opinion on ongoing discussion`_ +- `Fixing bugs and crashes`_ +- `Fixing false positives`_ +- `Creating new features or fixing false negatives`_ +- `Reviewing pull requests`_ + +.. _`Asking a question on discord`: https://discord.com/invite/qYxpadCgkx +.. _`on github`: https://github.com/PyCQA/pylint/issues/new/choose +.. _`Opening an issue`: https://github.com/PyCQA/pylint/issues/new?assignees=&labels=Needs+triage+%3Ainbox_tray%3A&template=BUG-REPORT.yml +.. _`Making the documentation better`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22Documentation+%3Agreen_book%3A%22 +.. _`Making the error message better`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+project%3Apycqa%2Fpylint%2F4+ +.. _`Reproducing bugs and confirming that issues are valid`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs+reproduction+%3Amag%3A%22%2C%22Cannot+reproduce+%F0%9F%A4%B7%22 +.. _`Investigating or debugging complicated issues`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs+investigation+%F0%9F%94%AC%22 +.. _`Designing or specifying a solution`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs+design+proposal+%3Alock%3A%22%2C%22Needs+specification+%3Aclosed_lock_with_key%3A%22 +.. _`Giving your opinion on ongoing discussion`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs+decision+%3Alock%3A%22 +.. _`Fixing bugs and crashes`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22Bug+%3Abeetle%3A%22%2C%22Crash+%F0%9F%92%A5%22 +.. _`Fixing false positives`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22False+Positive+%F0%9F%A6%9F%22 +.. _`Creating new features or fixing false negatives`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22False+Negative+%F0%9F%A6%8B%22%2C%22Enhancement+%E2%9C%A8%22 +.. _`Reviewing pull requests`: https://github.com/PyCQA/pylint/pulls?q=is%3Aopen+is%3Apr+label%3A%22Needs+review+%F0%9F%94%8D%22 + + +If you are a pylint maintainer there's also: + +- `Triaging issues`_ +- `Labeling issues that do not have an actionable label yet`_ +- `Preparing the next patch release`_ +- `Checking stale pull requests status`_ + +.. _`Triaging issues`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs+triage+%3Ainbox_tray%3A%22 +.. _`Labeling issues that do not have an actionable label yet`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+-label%3A%22Needs+astroid+Brain+%F0%9F%A7%A0%22+-label%3A%22Needs+astroid+update%22+-label%3A%22Needs+backport%22+-label%3A%22Needs+decision+%3Alock%3A%22+-label%3A%22Needs+investigation+%F0%9F%94%AC%22+-label%3A%22Needs+PR%22+-label%3A%22Needs+reproduction+%3Amag%3A%22+-label%3A%22Needs+review+%F0%9F%94%8D%22+-label%3A%22Needs+triage+%3Ainbox_tray%3A%22+-label%3A%22Waiting+on+author%22+-label%3A%22Work+in+progress%22+-label%3AMaintenance+sort%3Aupdated-desc+-label%3A%22Needs+specification+%3Aclosed_lock_with_key%3A%22+-label%3A%22Needs+design+proposal+%3Alock%3A%22 +.. _`Preparing the next patch release`: https://github.com/PyCQA/pylint/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs+backport%22 +.. _`Checking stale pull requests status`: https://github.com/PyCQA/pylint/pulls?q=is%3Aopen+is%3Apr+label%3A%22Work+in+progress%22%2C%22Needs+astroid+update%22%2C%22Waiting+on+author%22 + + +Creating a pull request +----------------------- + Got a change for Pylint? Below are a few steps you should take to make sure your patch gets accepted: -- We recommend using Python 3.8 or higher for development of Pylint as it gives - you access to the latest ``ast`` parser. +- You must use at least Python 3.8 for development of Pylint as it gives + you access to the latest ``ast`` parser and some pre-commit hooks do not + support python 3.7. + - Install the dev dependencies, see :ref:`contributor_install`. + - Use our test suite and write new tests, see :ref:`contributor_testing`. -- Add an entry to the change log describing the change in `doc/whatsnew/2/2.15/index.rst` - (or ``doc/whatsnew/2/2.14/full.rst`` if the change needs backporting in 2.14). + +.. keep this in sync with the description of PULL_REQUEST_TEMPLATE.md! + +- Create a news fragment with `towncrier create .` which will be + included in the changelog. `` can be one of: breaking, user_action, feature, + new_check, removed_check, extension, false_positive, false_negative, bugfix, other, internal. If necessary you can write details or offer examples on how the new change is supposed to work. - Document your change, if it is a non-trivial one. diff --git a/doc/development_guide/contributor_guide/release.md b/doc/development_guide/contributor_guide/release.md index 24bcc3749c..a076838fd8 100644 --- a/doc/development_guide/contributor_guide/release.md +++ b/doc/development_guide/contributor_guide/release.md @@ -10,10 +10,6 @@ the maintenance branch. If so, release a last patch release first. See - Write the `Summary -- Release highlights` in `doc/whatsnew` and upgrade the release date. -- Remove the empty changelog for the last unreleased patch version `X.Y-1.Z'`. (For - example: `v2.3.5`) -- Check the result of `git diff vX.Y-1.Z' doc/whatsnew`. (For example: - `git diff v2.3.4 doc/whatsnew`) - Install the release dependencies: `pip3 install -r requirements_test.txt` - Bump the version and release by using `tbump X.Y.0 --no-push --no-tag`. (For example: `tbump 2.4.0 --no-push --no-tag`) @@ -47,17 +43,26 @@ branch - Create a `maintenance/X.Y.x` (For example: `maintenance/2.4.x` from the `v2.4.0` tag.) - Close the current milestone and create the new ones (For example: close `2.4.0`, create `2.4.1` and `2.6.0`) - -## Backporting a fix from `main` to the maintenance branch - -Whenever a commit on `main` should be released in a patch release on the current -maintenance branch we cherry-pick the commit from `main`. - -- During the merge request on `main`, make sure that the changelog is for the patch - version `X.Y-1.Z'`. (For example: `v2.3.5`) -- After the PR is merged on `main` cherry-pick the commits on the `maintenance/X.Y.x` - branch (For example: from `maintenance/2.4.x` cherry-pick a commit from `main`) -- Remove the "need backport" label from cherry-picked issues +- Hide and deactivate all the patch releases for the previous minor release on + [readthedoc](https://readthedocs.org/projects/pylint/versions/), except the last one. + (For example: hide `v2.4.0`, `v2.4.1`, `v2.4.2` and keep only `v2.4.3`) + +## Back-porting a fix from `main` to the maintenance branch + +Whenever a PR on `main` should be released in a patch release on the current maintenance +branch: + +- Label the PR with `backport maintenance/X.Y-1.x`. (For example + `backport maintenance/2.3.x`) +- Squash the PR before merging (alternatively rebase if there's a single commit) +- (If the automated cherry-pick has conflicts) + - Add a `Needs backport` label and do it manually. + - You might alternatively also: + - Cherry-pick the changes that create the conflict if it's not a new feature before + doing the original PR cherry-pick manually. + - Decide to wait for the next minor to release the PR + - In any case upgrade the milestones in the original PR and newly cherry-picked PR + to match reality. - Release a patch version ## Releasing a patch version @@ -65,16 +70,16 @@ maintenance branch we cherry-pick the commit from `main`. We release patch versions when a crash or a bug is fixed on the main branch and has been cherry-picked on the maintenance branch. -- Check the result of `git diff vX.Y-1.Z-1 doc/whatsnew`. (For example: - `git diff v2.3.4 doc/whatsnew`) - Install the release dependencies: `pip3 install -r requirements_test.txt` - Bump the version and release by using `tbump X.Y-1.Z --no-push`. (For example: `tbump 2.3.5 --no-push`) - Check the result visually with `git show`. -- Open a merge request to run the CI tests for this branch +- Open a merge request of `release-X.Y-1.Z'` in `maintenance/X.Y.x` (For example: + `release-2.3.5-branch` in `maintenance/2.3.x`) to run the CI tests for this branch. - Create and push the tag. - Release the version on GitHub with the same name as the tag and copy and paste the - appropriate changelog in the description. This triggers the PyPI release. + changelog from the ReadtheDoc generated documentation from the pull request pipeline + in the description. This triggers the PyPI release. - Merge the `maintenance/X.Y.x` branch on the main branch. The main branch should have the changelog for `X.Y-1.Z+1` (For example `v2.3.6`). This merge is required so `pre-commit autoupdate` works for pylint. diff --git a/doc/development_guide/contributor_guide/tests/launching_test.rst b/doc/development_guide/contributor_guide/tests/launching_test.rst index b982bbbeb2..02114f01f6 100644 --- a/doc/development_guide/contributor_guide/tests/launching_test.rst +++ b/doc/development_guide/contributor_guide/tests/launching_test.rst @@ -57,16 +57,25 @@ Primer tests Pylint also uses what we refer to as ``primer`` tests. These are tests that are run automatically in our Continuous Integration and check whether any changes in Pylint lead to crashes or fatal errors -on the ``stdlib`` and a selection of external repositories. +on the ``stdlib``, and also assess a pull request's impact on the linting of a selection of external +repositories by posting the diff against ``pylint``'s current output as a comment. -To run the ``primer`` tests you can add either ``--primer-stdlib`` or ``--primer-external`` to the -pytest_ command. If you want to only run the ``primer`` you can add either of their marks, for example:: +To run the primer test for the ``stdlib``, which only checks for crashes and fatal errors, you can add +``--primer-stdlib`` to the pytest_ command. For example:: pytest -m primer_stdlib --primer-stdlib -The external ``primer`` can be run with:: +To produce the output generated on Continuous Integration for the linting of external repositories, +run these commands:: - pytest -m primer_external_batch_one --primer-external # Runs batch one + python tests/primer/__main__.py prepare --clone + python tests/primer/__main__.py run --type=pr + +To fully simulate the process on Continuous Integration, you should then checkout ``main``, and +then run these commands:: + + python tests/primer/__main__.py run --type=main + python tests/primer/__main__.py compare The list of repositories is created on the basis of three criteria: 1) projects need to use a diverse range of language features, 2) projects need to be well maintained and 3) projects should not have a codebase diff --git a/doc/development_guide/contributor_guide/tests/writing_test.rst b/doc/development_guide/contributor_guide/tests/writing_test.rst index 2d9844b16a..c616d172b9 100644 --- a/doc/development_guide/contributor_guide/tests/writing_test.rst +++ b/doc/development_guide/contributor_guide/tests/writing_test.rst @@ -34,13 +34,13 @@ the unittests. Functional tests ---------------- -These are residing under ``/pylint/test/functional`` and they are formed of multiple +These are located under ``/pylint/test/functional`` and they are formed of multiple components. First, each Python file is considered to be a test case and it -should be accompanied by a .txt file, having the same name, with the messages +should be accompanied by a ``.txt`` file, having the same name. The ``.txt`` file contains the ``pylint`` messages that are supposed to be emitted by the given test file. -In the Python file, each line for which Pylint is supposed to emit a message -has to be annotated with a comment in the form ``# [message_symbol]``, as in:: +In your ``.py`` test file, each line for which Pylint is supposed to emit a message +has to be annotated with a comment following this pattern ``# [message_symbol]``, as in:: a, b, c = 1 # [unbalanced-tuple-unpacking] @@ -48,14 +48,15 @@ If multiple messages are expected on the same line, then this syntax can be used a, b, c = 1.test # [unbalanced-tuple-unpacking, no-member] -You can also use ``# +n: [`` with n an integer if the above syntax would make the line too long or other reasons:: +You can also use ``# +n: [`` where ``n`` is an integer to deal with special cases, e.g., where the above regular syntax makes the line too long:: - # +1: [empty-comment] - # + A = 5 + # +1: [singleton-comparison] + B = A == None # The test will look for the `singleton-comparison` message in this line -If you need special control over Pylint's configuration, you can also create a .rc file, which -can have sections of Pylint's configuration. -The .rc file can also contain a section ``[testoptions]`` to pass options for the functional +If you need special control over Pylint's configuration, you can also create a ``.rc`` file, which +can set sections of Pylint's configuration. +The ``.rc`` file can also contain a section ``[testoptions]`` to pass options for the functional test runner. The following options are currently supported: - "min_pyver": Minimal python version required to run the test diff --git a/doc/development_guide/how_tos/custom_checkers.rst b/doc/development_guide/how_tos/custom_checkers.rst index eff8cf5439..3cbf81c830 100644 --- a/doc/development_guide/how_tos/custom_checkers.rst +++ b/doc/development_guide/how_tos/custom_checkers.rst @@ -76,30 +76,7 @@ So far we have defined the following required components of our checker: * A message dictionary. Each checker is being used for finding problems in your code, the problems being displayed to the user through **messages**. The message dictionary should specify what messages the checker is - going to emit. It has the following format:: - - msgs = { - "message-id": ( - "displayed-message", "message-symbol", "message-help" - ) - } - - - * The ``message-id`` should be a 4-digit number, - prefixed with a **message category**. - There are multiple message categories, - these being ``C``, ``W``, ``E``, ``F``, ``R``, - standing for ``Convention``, ``Warning``, ``Error``, ``Fatal`` and ``Refactoring``. - The 4 digits should not conflict with existing checkers - and the first 2 digits should consistent across the checker. - - * The ``displayed-message`` is used for displaying the message to the user, - once it is emitted. - - * The ``message-symbol`` is an alias of the message id - and it can be used wherever the message id can be used. - - * The ``message-help`` is used when calling ``pylint --help-msg``. + going to emit. See `Defining a Message`_ for the details about defining a new message. We have also defined an optional component of the checker. The options list defines any user configurable options. @@ -113,7 +90,12 @@ It has the following format:: * The ``option-symbol`` is a unique name for the option. This is used on the command line and in config files. The hyphen is replaced by an underscore when used in the checker, - similarly to how you would use ``argparse.Namespace``. + similarly to how you would use ``argparse.Namespace``: + + .. code-block:: python + + if not self.linter.config.ignore_ints: + ... Next we'll track when we enter and leave a function. @@ -146,7 +128,7 @@ which is called with an ``.astroid.nodes.Return`` node. .. TODO We can shorten/remove this bit once astroid has API docs. We'll need to be able to figure out what attributes an -``.astroid.nodes.Return` node has available. +``.astroid.nodes.Return`` node has available. We can use ``astroid.extract_node`` for this:: >>> node = astroid.extract_node("return 5") @@ -236,10 +218,82 @@ Now we can debug our checker! .. Note:: ``my_plugin`` refers to a module called ``my_plugin.py``. - This module can be made available to pylint by putting this - module's parent directory in your ``PYTHONPATH`` - environment variable or by adding the ``my_plugin.py`` - file to the ``pylint/checkers`` directory if running from source. + The preferred way of making this plugin available to pylint is + by installing it as a package. This can be done either from a packaging index like + ``PyPI`` or by installing it from a local source such as with ``pip install``. + + Alternatively, the plugin module can be made available to pylint by + putting this module's parent directory in your ``PYTHONPATH`` + environment variable. + + If your pylint config has an ``init-hook`` that modifies + ``sys.path`` to include the module's parent directory, this + will also work, but only if either: + + * the ``init-hook`` and the ``load-plugins`` list are both + defined in a configuration file, or... + * the ``init-hook`` is passed as a command-line argument and + the ``load-plugins`` list is in the configuration file + + So, you cannot load a custom plugin by modifying ``sys.path`` if you + supply the ``init-hook`` in a configuration file, but pass the module name + in via ``--load-plugins`` on the command line. + This is because pylint loads plugins specified on command + line before loading any configuration from other sources. + +Defining a Message +------------------ + +Pylint message is defined using the following format:: + + msgs = { + "E0401": ( # message id + "Unable to import %s", # template of displayed message + "import-error", # message symbol + "Used when pylint has been unable to import a module.", # Message description + { # Additional parameters: + # message control support for the old names of the messages: + "old_names": [("F0401", "old-import-error")] + "minversion": (3, 5), # No check under this version + "maxversion": (3, 7), # No check above this version + }, + ), + +The message is then formatted using the ``args`` parameter from ``add_message`` i.e. in +``self.add_message("import-error", args=module_we_cant_import, node=importnode)``, the value in ``module_we_cant_import`` say ``patglib`` will be interpolled and the final result will be: +``Unable to import patglib`` + + +* The ``message-id`` should be a 4-digit number, + prefixed with a **message category**. + There are multiple message categories, + these being ``C``, ``W``, ``E``, ``F``, ``R``, + standing for ``Convention``, ``Warning``, ``Error``, ``Fatal`` and ``Refactoring``. + The 4 digits should not conflict with existing checkers + and the first 2 digits should consistent across the checker (except shared messages). + +* The ``displayed-message`` is used for displaying the message to the user, + once it is emitted. + +* The ``message-symbol`` is an alias of the message id + and it can be used wherever the message id can be used. + +* The ``message-help`` is used when calling ``pylint --help-msg``. + +Optionally message can contain optional extra options: + +* The ``old_names`` option permits to change the message id or symbol of a message without breaking the message control used on the old messages by users. The option is specified as a list + of tuples (``message-id``, ``old-message-symbol``) e.g. ``{"old_names": [("F0401", "old-import-error")]}``. + The symbol / msgid association must be unique so if you're changing the message id the symbol also need to change and you can generally use the ``old-`` prefix for that. + +* The ``minversion`` or ``maxversion`` options specify minimum or maximum version of python + relevant for this message. The option value is specified as tuple with major version number + as first number and minor version number as second number e.g. ``{"minversion": (3, 5)}`` + +* The ``shared`` option enables sharing message between multiple checkers. As mentioned + previously, normally the message cannot be shared between multiple checkers. + To allow having message shared between multiple checkers, the ``shared`` option must + be set to ``True``. Parallelize a Checker --------------------- diff --git a/doc/development_guide/how_tos/plugins.rst b/doc/development_guide/how_tos/plugins.rst index bc2c0f14cb..3940f24812 100644 --- a/doc/development_guide/how_tos/plugins.rst +++ b/doc/development_guide/how_tos/plugins.rst @@ -1,5 +1,7 @@ .. -*- coding: utf-8 -*- +.. _plugins: + How To Write a Pylint Plugin ============================ diff --git a/doc/exts/pylint_extensions.py b/doc/exts/pylint_extensions.py index 8dec34a7d6..406d2d39d4 100755 --- a/doc/exts/pylint_extensions.py +++ b/doc/exts/pylint_extensions.py @@ -6,22 +6,42 @@ """Script used to generate the extensions file before building the actual documentation.""" +from __future__ import annotations + import os import re import sys import warnings -from typing import Optional +from typing import Any import sphinx from sphinx.application import Sphinx +from pylint.checkers import BaseChecker from pylint.constants import MAIN_CHECKER_NAME from pylint.lint import PyLinter +from pylint.typing import MessageDefinitionTuple, OptionDict, ReportsCallable from pylint.utils import get_rst_title +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + + +class _CheckerInfo(TypedDict): + """Represents data about a checker.""" + + checker: BaseChecker + options: list[tuple[str, OptionDict, Any]] + msgs: dict[str, MessageDefinitionTuple] + reports: list[tuple[str, str, ReportsCallable]] + doc: str + module: str + # pylint: disable-next=unused-argument -def builder_inited(app: Optional[Sphinx]) -> None: +def builder_inited(app: Sphinx | None) -> None: """Output full documentation in ReST format for all extension modules.""" # PACKAGE/docs/exts/pylint_extensions.py --> PACKAGE/ base_path = os.path.dirname( @@ -30,7 +50,7 @@ def builder_inited(app: Optional[Sphinx]) -> None: # PACKAGE/ --> PACKAGE/pylint/extensions ext_path = os.path.join(base_path, "pylint", "extensions") modules = [] - doc_files = {} + doc_files: dict[str, str] = {} for filename in os.listdir(ext_path): name, ext = os.path.splitext(filename) if name[0] == "_": @@ -79,15 +99,26 @@ def builder_inited(app: Optional[Sphinx]) -> None: checker, information = checker_information j = -1 checker = information["checker"] - del information["checker"] if i == max_len - 1: # Remove the \n\n at the end of the file j = -3 - print(checker.get_full_documentation(**information)[:j], file=stream) - - -def get_plugins_info(linter, doc_files): - by_checker = {} + print( + checker.get_full_documentation( + msgs=information["msgs"], + options=information["options"], + reports=information["reports"], + doc=information["doc"], + module=information["module"], + show_options=False, + )[:j], + file=stream, + ) + + +def get_plugins_info( + linter: PyLinter, doc_files: dict[str, str] +) -> dict[BaseChecker, _CheckerInfo]: + by_checker: dict[BaseChecker, _CheckerInfo] = {} for checker in linter.get_checkers(): if checker.name == MAIN_CHECKER_NAME: continue @@ -113,18 +144,18 @@ def get_plugins_info(linter, doc_files): except KeyError: with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) - by_checker[checker] = { - "checker": checker, - "options": list(checker.options_and_values()), - "msgs": dict(checker.msgs), - "reports": list(checker.reports), - "doc": doc, - "module": module, - } + by_checker[checker] = _CheckerInfo( + checker=checker, + options=list(checker.options_and_values()), + msgs=dict(checker.msgs), + reports=list(checker.reports), + doc=doc, + module=module, + ) return by_checker -def setup(app): +def setup(app: Sphinx) -> dict[str, str]: app.connect("builder-inited", builder_inited) return {"version": sphinx.__display_version__} diff --git a/doc/exts/pylint_features.py b/doc/exts/pylint_features.py index e2eb57d745..fcf4f01c8d 100755 --- a/doc/exts/pylint_features.py +++ b/doc/exts/pylint_features.py @@ -8,8 +8,9 @@ documentation. """ +from __future__ import annotations + import os -from typing import Optional import sphinx from sphinx.application import Sphinx @@ -19,7 +20,7 @@ # pylint: disable-next=unused-argument -def builder_inited(app: Optional[Sphinx]) -> None: +def builder_inited(app: Sphinx | None) -> None: # PACKAGE/docs/exts/pylint_extensions.py --> PACKAGE/ base_path = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -36,10 +37,10 @@ def builder_inited(app: Optional[Sphinx]) -> None: """ ) - print_full_documentation(linter, stream) + print_full_documentation(linter, stream, False) -def setup(app): +def setup(app: Sphinx) -> dict[str, str]: app.connect("builder-inited", builder_inited) return {"version": sphinx.__display_version__} diff --git a/doc/exts/pylint_messages.py b/doc/exts/pylint_messages.py index 0e5f138722..074e693873 100644 --- a/doc/exts/pylint_messages.py +++ b/doc/exts/pylint_messages.py @@ -7,7 +7,7 @@ import os from collections import defaultdict from inspect import getmodule -from itertools import chain +from itertools import chain, groupby from pathlib import Path from typing import DefaultDict, Dict, List, NamedTuple, Optional, Tuple @@ -43,6 +43,8 @@ class MessageData(NamedTuple): related_links: str checker_module_name: str checker_module_path: str + shared: bool = False + default_enabled: bool = True MessagesDict = Dict[str, List[MessageData]] @@ -78,12 +80,24 @@ def _get_message_data(data_path: Path) -> Tuple[str, str, str, str]: related = _get_titled_rst( title="Related links", text=_get_rst_as_str(related_rst_path) ) - assert (not bad_code and not related) or ( - "placeholder" not in good_code and "help us make the doc better" not in details - ), "Please remove placeholders if you completed the documentation" + _check_placeholders(bad_code, details, good_py_path, related) return good_code, bad_code, details, related +def _check_placeholders( + bad_code: str, details: str, good_py_path: Path, related: str +) -> None: + if bad_code or related: + placeholder_details = "help us make the doc better" in details + with open(good_py_path) as f: + placeholder_good = "placeholder" in f.read() + assert_msg = ( + f"Please remove placeholders in '{good_py_path.parent}' " + f"as you started completing the documentation" + ) + assert not placeholder_good and not placeholder_details, assert_msg + + def _get_titled_rst(title: str, text: str) -> str: """Return rst code with a title if there is anything in the section.""" return f"**{title}:**\n\n{text}" if text else "" @@ -105,12 +119,10 @@ def _get_python_code_as_rst(code_path: Path) -> str: """ if not code_path.exists(): return "" - with open(code_path, encoding="utf-8") as f: - file_content = f.readlines() return f"""\ -.. code-block:: python - -{"".join(" " + i for i in file_content)}""" +.. literalinclude:: /{code_path.relative_to(Path.cwd())} + :language: python +""" def _create_placeholders( @@ -159,6 +171,7 @@ def _get_all_messages( ((checker, msg) for msg in checker.messages) for checker in linter.get_checkers() ) + for checker, message in checker_message_mapping: good_code, bad_code, details, related = _get_message_data( _get_message_data_path(message) @@ -181,15 +194,22 @@ def _get_all_messages( related, checker_module.__name__, checker_module.__file__, + message.shared, + message.default_enabled, ) msg_type = MSG_TYPES_DOC[message.msgid[0]] messages_dict[msg_type].append(message_data) if message.old_names: for old_name in message.old_names: category = MSG_TYPES_DOC[old_name[0][0]] - old_messages[category][(old_name[1], old_name[0])].append( - (message.symbol, msg_type) - ) + # We check if the message is already in old_messages so + # we don't duplicate shared messages. + if (message.symbol, msg_type) not in old_messages[category][ + (old_name[1], old_name[0]) + ]: + old_messages[category][(old_name[1], old_name[0])].append( + (message.symbol, msg_type) + ) return messages_dict, old_messages @@ -224,19 +244,26 @@ def _write_message_page(messages_dict: MessagesDict) -> None: if not category_dir.exists(): category_dir.mkdir(parents=True, exist_ok=True) for message in messages: + if message.shared: + continue if not _message_needs_update(message, category): continue _write_single_message_page(category_dir, message) + for _, shared_messages in groupby( + sorted( + (message for message in messages if message.shared), key=lambda m: m.id + ), + key=lambda m: m.id, + ): + shared_messages_list = list(shared_messages) + if len(shared_messages_list) > 1: + _write_single_shared_message_page(category_dir, shared_messages_list) + else: + _write_single_message_page(category_dir, shared_messages_list[0]) -def _write_single_message_page(category_dir: Path, message: MessageData) -> None: - checker_module_rel_path = os.path.relpath( - message.checker_module_path, PYLINT_BASE_PATH - ) - messages_file = os.path.join(category_dir, f"{message.name}.rst") - with open(messages_file, "w", encoding="utf-8") as stream: - stream.write( - f""".. _{message.name}: +def _generate_single_message_body(message: MessageData) -> str: + body = f""".. _{message.name}: {get_rst_title(f"{message.name} / {message.id}", "=")} **Message emitted:** @@ -246,25 +273,56 @@ def _write_single_message_page(category_dir: Path, message: MessageData) -> None **Description:** *{message.definition.description}* +""" + if not message.default_enabled: + body += f""" +.. caution:: + This message is disabled by default. To enable it, add ``{message.name}`` to the ``enable`` option. -{message.good_code} +""" + + body += f""" {message.bad_code} +{message.good_code} {message.details} {message.related_links} """ - ) - if message.checker_module_name.startswith("pylint.extensions."): - stream.write( - f""" + if message.checker_module_name.startswith("pylint.extensions."): + body += f""" .. note:: - This message is emitted by the optional :ref:`'{message.checker}'<{message.checker_module_name}>` checker which requires the ``{message.checker_module_name}`` - plugin to be loaded. + This message is emitted by the optional :ref:`'{message.checker}'<{message.checker_module_name}>` + checker which requires the ``{message.checker_module_name}`` plugin to be loaded. """ - ) - checker_url = ( - f"https://github.com/PyCQA/pylint/blob/main/{checker_module_rel_path}" + return body + + +def _generate_checker_url(message: MessageData) -> str: + checker_module_rel_path = os.path.relpath( + message.checker_module_path, PYLINT_BASE_PATH + ) + return f"https://github.com/PyCQA/pylint/blob/main/{checker_module_rel_path}" + + +def _write_single_shared_message_page( + category_dir: Path, messages: List[MessageData] +) -> None: + message = messages[0] + with open(category_dir / f"{message.name}.rst", "w", encoding="utf-8") as stream: + stream.write(_generate_single_message_body(message)) + checker_urls = ", ".join( + [ + f"`{message.checker} <{_generate_checker_url(message)}>`__" + for message in messages + ] ) + stream.write(f"Created by the {checker_urls} checkers.") + + +def _write_single_message_page(category_dir: Path, message: MessageData) -> None: + with open(category_dir / f"{message.name}.rst", "w", encoding="utf-8") as stream: + stream.write(_generate_single_message_body(message)) + checker_url = _generate_checker_url(message) stream.write(f"Created by the `{message.checker} <{checker_url}>`__ checker.") @@ -299,7 +357,11 @@ def _write_messages_list_page( "refactor", "information", ): - messages = sorted(messages_dict[category], key=lambda item: item.name) + # We need to remove all duplicated shared messages + messages = sorted( + {msg.id: msg for msg in messages_dict[category]}.values(), + key=lambda item: item.name, + ) old_messages = sorted(old_messages_dict[category], key=lambda item: item[0]) messages_string = "".join( f" {category}/{message.name}\n" for message in messages diff --git a/doc/exts/pylint_options.py b/doc/exts/pylint_options.py index 73126a6a05..f402125a73 100644 --- a/doc/exts/pylint_options.py +++ b/doc/exts/pylint_options.py @@ -66,7 +66,9 @@ def _get_all_options(linter: PyLinter) -> OptionsDataDict: def _create_checker_section( checker: str, options: list[OptionsData], linter: PyLinter ) -> str: - checker_string = get_rst_title(f"``{checker.capitalize()}`` Checker", "^") + checker_string = f".. _{checker}-options:\n\n" + checker_string += get_rst_title(f"``{checker.capitalize()}`` **Checker**", "-") + toml_doc = tomlkit.document() pylint_tool_table = tomlkit.table(is_super_table=True) toml_doc.add(tomlkit.key(["tool", "pylint"]), pylint_tool_table) @@ -75,11 +77,11 @@ def _create_checker_section( for option in sorted(options, key=lambda x: x.name): checker_string += get_rst_title(f"--{option.name}", '"') - checker_string += f"\nDescription:\n *{option.optdict.get('help')}*\n\n" + checker_string += f"*{option.optdict.get('help')}*\n\n" if option.optdict.get("default") == "": - checker_string += 'Default:\n ``""``\n\n\n' + checker_string += '**Default:** ``""``\n\n\n' else: - checker_string += f"Default:\n ``{option.optdict.get('default')}``\n\n\n" + checker_string += f"**Default:** ``{option.optdict.get('default')}``\n\n\n" # Start adding the option to the toml example if option.optdict.get("hide_from_config_file"): @@ -118,7 +120,7 @@ def _create_checker_section(
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -138,13 +140,13 @@ def _write_options_page(options: OptionsDataDict, linter: PyLinter) -> None: ".. This file is auto-generated. Make any changes to the associated\n" ".. docs extension in 'doc/exts/pylint_options.py'.\n\n" ".. _all-options:", - get_rst_title("Standard Checkers:", "^"), + get_rst_title("Standard Checkers", "^"), ] found_extensions = False for checker, checker_options in options.items(): if not found_extensions and checker_options[0].extension: - sections.append(get_rst_title("Extensions:", "^")) + sections.append(get_rst_title("Extensions", "^")) found_extensions = True sections.append(_create_checker_section(checker, checker_options, linter)) diff --git a/doc/faq.rst b/doc/faq.rst index 01ac3e5351..f4c5be31ef 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -23,13 +23,12 @@ See :ref:`upgrading pylint in the installation guide `. How do I find the name corresponding to a specific command line option? ----------------------------------------------------------------------- -You can generate a sample configuration file with ``--generate-toml-config``. -Every option present on the command line before this will be included in -the toml file +See :ref:`the configuration documentation `. -For example:: +What is the format of the configuration file? +--------------------------------------------- - pylint --disable=bare-except,invalid-name --class-rgx='[A-Z][a-z]+' --generate-toml-config +The configuration file can be an ``ini`` or ``toml`` file. See the :ref:`exhaustive list of possible options `. How to disable a particular message? ------------------------------------ @@ -58,10 +57,6 @@ doing so arguments usage won't be checked. Another solution is to use one of the names defined in the "dummy-variables" configuration variable for unused argument ("_" and "dummy" by default). -What is the format of the configuration file? ---------------------------------------------- - -The configuration file can be an ``ini`` or ``toml`` file. See the :ref:`exhaustive list of possible options `. Why are there a bunch of messages disabled by default? ------------------------------------------------------ @@ -75,7 +70,7 @@ You can see the plugin you need to explicitly :ref:`load in the technical refere Which messages should I disable to avoid duplicates if I use other popular linters ? ------------------------------------------------------------------------------------ -pycodestyle_: unneeded-not, line-too-long, unnecessary-semicolon, trailing-whitespace, missing-final-newline, bad-indentation, multiple-statements, bare-except, wrong-import-position +pycodestyle_: bad-indentation, bare-except, line-too-long, missing-final-newline, multiple-statements, singleton-comparison, trailing-whitespace, unnecessary-semicolon, unneeded-not, wrong-import-position pyflakes_: undefined-variable, unused-import, unused-variable diff --git a/doc/requirements.txt b/doc/requirements.txt index 326cc2fc2f..d2da8f4dcd 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,6 @@ -Sphinx==4.5.0 +Sphinx==5.3.0 sphinx-reredirects<1 -myst-parser~=0.17 -furo==2022.4.7 +myst-parser~=0.18 +towncrier~=22.12 +furo==2022.12.7 -e . diff --git a/doc/test_messages_documentation.py b/doc/test_messages_documentation.py index 3a20a15c4b..663fa3aab6 100644 --- a/doc/test_messages_documentation.py +++ b/doc/test_messages_documentation.py @@ -22,7 +22,7 @@ def total(self): import pytest -from pylint import checkers, config +from pylint import checkers from pylint.config.config_initialization import _config_initialization from pylint.lint import PyLinter from pylint.message.message import Message @@ -81,18 +81,19 @@ def __init__(self, test_file: Tuple[str, Path]) -> None: # Check if this message has a custom configuration file (e.g. for enabling optional checkers). # If not, use the default configuration. config_file: Optional[Path] - if (test_file[1].parent / "pylintrc").exists(): - config_file = test_file[1].parent / "pylintrc" - else: - config_file = next(config.find_default_config_files(), None) - + msgid, full_path = test_file + pylintrc = full_path.parent / "pylintrc" + config_file = pylintrc if pylintrc.exists() else None + print(f"Config file used: {config_file}") + args = [ + str(full_path), + "--disable=all", + f"--enable=F,{msgid},astroid-error,syntax-error", + ] + print(f"Command used:\npylint {' '.join(args)}") _config_initialization( self._linter, - args_list=[ - str(test_file[1]), - "--disable=all", - f"--enable={test_file[0]},astroid-error,fatal,syntax-error", - ], + args_list=args, reporter=_test_reporter, config_file=config_file, ) @@ -118,6 +119,8 @@ def get_expected_messages(stream: TextIO) -> MessageCounter: line = match.group("line") if line is None: lineno = i + 1 + elif line.startswith("+") or line.startswith("-"): + lineno = i + 1 + int(line) else: lineno = int(line) @@ -142,17 +145,34 @@ def _get_actual(self) -> MessageCounter: def _runTest(self) -> None: """Run the test and assert message differences.""" - self._linter.check([str(self._test_file[1])]) + self._linter.check([str(self._test_file[1]), "--rcfile="]) expected_messages = self._get_expected() actual_messages = self._get_actual() if self.is_good_test_file(): - msg = "There should be no warning raised for 'good.py'" - assert actual_messages.total() == 0, msg # type: ignore[attr-defined] + assert actual_messages.total() == 0, self.assert_message_good( + actual_messages + ) if self.is_bad_test_file(): msg = "There should be at least one warning raised for 'bad.py'" - assert actual_messages.total() > 0, msg # type: ignore[attr-defined] + assert actual_messages.total() > 0, msg assert expected_messages == actual_messages + def assert_message_good(self, actual_messages: MessageCounter) -> str: + if not actual_messages: + return "" + messages = "\n- ".join(f"{v} (l. {i})" for i, v in actual_messages) + msg = f"""There should be no warning raised for 'good.py' but these messages were raised: +- {messages} + +See: + +""" + with open(self._test_file[1]) as f: + lines = [line[:-1] for line in f.readlines()] + for line_index, value in actual_messages: + lines[line_index - 1] += f" # <-- /!\\ unexpected '{value}' /!\\" + return msg + "\n".join(lines) + @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES) @pytest.mark.filterwarnings("ignore::DeprecationWarning") diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 173b255cdc..fce4722341 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -4,55 +4,27 @@ Tutorial ======== -:Author: Robert Kirkpatrick - - -Intro ------ - -Beginner to coding standards? Pylint can be your guide to reveal what's really -going on behind the scenes and help you to become a more aware programmer. - -Sharing code is a rewarding endeavor. Putting your code ``out there`` can be -either an act of philanthropy, ``coming of age``, or a basic extension of belief -in open source. Whatever the motivation, your good intentions may not have the -desired outcome if people find your code hard to use or understand. The Python -community has formalized some recommended programming styles to help everyone -write code in a common, agreed-upon style that makes the most sense for shared -code. This style is captured in `PEP 8`_, the "Style Guide for Python Code". -Pylint can be a quick and easy way of -seeing if your code has captured the essence of `PEP 8`_ and is therefore -``friendly`` to other potential users. - -Perhaps you're not ready to share your code but you'd like to learn a bit more -about writing better code and don't know where to start. Pylint can tell you -where you may have run astray and point you in the direction to figure out what -you have done and how to do better. - This tutorial is all about approaching coding standards with little or no knowledge of in-depth programming or the code standards themselves. It's the equivalent of skipping the manual and jumping right in. -My command line prompt for these examples is: +The command line prompt for these examples is: .. sourcecode:: console - robertk01 Desktop$ + tutor Desktop$ .. _PEP 8: https://peps.python.org/pep-0008/ Getting Started --------------- -Running Pylint with no arguments will invoke the help dialogue and give you an -idea of the arguments available to you. Do that now, i.e.: +Running Pylint with the ``--help`` arguments will give you an idea of the arguments +available. Do that now, i.e.: .. sourcecode:: console - robertk01 Desktop$ pylint - ... - a bunch of stuff - ... + pylint --help A couple of the options that we'll focus on here are: :: @@ -66,17 +38,12 @@ A couple of the options that we'll focus on here are: :: --reports= --output-format= -If you need more detail, you can also ask for an even longer help message, -like so: :: +If you need more detail, you can also ask for an even longer help message: :: - robertk01 Desktop$ pylint --long-help - ... - Even more stuff - ... + pylint --long-help -Pay attention to the last bit of this longer help output. This gives you a -hint of what -Pylint is going to ``pick on``: :: +Pay attention to the last bit of this longer help output. This gives you a +hint of what Pylint is going to ``pick on``: :: Output: Using the default text output, the message format is : @@ -90,155 +57,148 @@ Pylint is going to ``pick on``: :: further processing. When Pylint is first run on a fresh piece of code, a common complaint is that it -is too ``noisy``. The current default configuration is set to enforce all possible -warnings. We'll use some of the options I noted above to make it suit your -preferences a bit better (and thus make it emit messages only when needed). - +is too ``noisy``. The default configuration enforce a lot of warnings. +We'll use some of the options we noted above to make it suit your +preferences a bit better. Your First Pylint'ing --------------------- -We'll use a basic Python script as fodder for our tutorial. -The starting code we will use is called simplecaesar.py and is here in its -entirety: +We'll use a basic Python script with ``black`` already applied on it, +as fodder for our tutorial. The starting code we will use is called +``simplecaesar.py`` and is here in its entirety: .. sourcecode:: python - #!/usr/bin/env python3 - - import string; + #!/usr/bin/env python3 - shift = 3 - choice = input("would you like to encode or decode?") - word = input("Please enter text") - letters = string.ascii_letters + string.punctuation + string.digits - encoded = '' - if choice == "encode": - for letter in word: - if letter == ' ': - encoded = encoded + ' ' - else: - x = letters.index(letter) + shift - encoded = encoded + letters[x] - if choice == "decode": - for letter in word: - if letter == ' ': - encoded = encoded + ' ' - else: - x = letters.index(letter) - shift - encoded = encoded + letters[x] + import string - print(encoded) + shift = 3 + choice = input("would you like to encode or decode?") + word = input("Please enter text") + letters = string.ascii_letters + string.punctuation + string.digits + encoded = "" + if choice == "encode": + for letter in word: + if letter == " ": + encoded = encoded + " " + else: + x = letters.index(letter) + shift + encoded = encoded + letters[x] + if choice == "decode": + for letter in word: + if letter == " ": + encoded = encoded + " " + else: + x = letters.index(letter) - shift + encoded = encoded + letters[x] + print(encoded) -Let's get started. -If we run this: +Let's get started. If we run this: .. sourcecode:: console - robertk01 Desktop$ pylint simplecaesar.py - ************* Module simplecaesar - simplecaesar.py:3:0: W0301: Unnecessary semicolon (unnecessary-semicolon) - simplecaesar.py:1:0: C0114: Missing module docstring (missing-module-docstring) - simplecaesar.py:5:0: C0103: Constant name "shift" doesn't conform to UPPER_CASE naming style (invalid-name) - simplecaesar.py:9:0: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) - simplecaesar.py:13:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + tutor Desktop$ pylint simplecaesar.py + ************* Module simplecaesar + simplecaesar.py:1:0: C0114: Missing module docstring (missing-module-docstring) + simplecaesar.py:5:0: C0103: Constant name "shift" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:8:0: C0103: Constant name "letters" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:9:0: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:13:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:15:12: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:16:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:20:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:22:12: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:23:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) - ----------------------------------- - Your code has been rated at 7.37/10 + ----------------------------------- + Your code has been rated at 4.74/10 -Previous experience taught me that the default output for the messages -needed a bit more info. We can see the second line is: :: +We can see the second line is: :: "simplecaesar.py:1:0: C0114: Missing module docstring (missing-module-docstring)" -This basically means that line 1 violates a convention ``C0114``. It's telling me I really should have a docstring. -I agree, but what if I didn't fully understand what rule I violated. Knowing only that I violated a convention -isn't much help if I'm a newbie. Another piece of information there is the -message symbol between parens, ``missing-module-docstring`` here. +This basically means that line 1 at column 0 violates the convention ``C0114``. +Another piece of information is the message symbol between parens, +``missing-module-docstring``. -If I want to read up a bit more about that, I can go back to the +If we want to read up a bit more about that, we can go back to the command line and try this: .. sourcecode:: console - robertk01 Desktop$ pylint --help-msg=missing-module-docstring + tutor Desktop$ pylint --help-msg=missing-module-docstring :missing-module-docstring (C0114): *Missing module docstring* Used when a module has no docstring.Empty modules do not require a docstring. This message belongs to the basic checker. - -Yeah, ok. That one was a bit of a no-brainer, but I have run into error messages -that left me with no clue about what went wrong, simply because I was unfamiliar -with the underlying mechanism of code theory. One error that puzzled my newbie -mind was: :: - - :too-many-instance-attributes (R0902): *Too many instance attributes (%s/%s)* - -I get it now thanks to Pylint pointing it out to me. If you don't get that one, -pour a fresh cup of coffee and look into it - let your programmer mind grow! - +That one was a bit of a no-brainer, but we can also run into error messages +where we are unfamiliar with the underlying code theory. The Next Step ------------- Now that we got some configuration stuff out of the way, let's see what we can -do with the remaining warnings. - -If we add a docstring to describe what the code is meant to do that will help. -There are 5 ``invalid-name`` messages that we will get to later. Lastly, I -put an unnecessary semicolon at the end of the import line so I'll -fix that too. To sum up, I'll add a docstring to line 2, and remove the ``;`` -from line 3. - -Here is the updated code: +do with the remaining warnings. If we add a docstring to describe what the code +is meant to do that will help. There are ``invalid-name`` messages that we will +get to later. Here is the updated code: .. sourcecode:: python - #!/usr/bin/env python3 - """This script prompts a user to enter a message to encode or decode - using a classic Caesar shift substitution (3 letter shift)""" - - import string - - shift = 3 - choice = input("would you like to encode or decode?") - word = input("Please enter text") - letters = string.ascii_letters + string.punctuation + string.digits - encoded = '' - if choice == "encode": - for letter in word: - if letter == ' ': - encoded = encoded + ' ' - else: - x = letters.index(letter) + shift - encoded = encoded + letters[x] - if choice == "decode": - for letter in word: - if letter == ' ': - encoded = encoded + ' ' - else: - x = letters.index(letter) - shift - encoded = encoded + letters[x] - - print(encoded) + #!/usr/bin/env python3 + + """This script prompts a user to enter a message to encode or decode + using a classic Caesar shift substitution (3 letter shift)""" + + import string + + shift = 3 + choice = input("would you like to encode or decode?") + word = input("Please enter text") + letters = string.ascii_letters + string.punctuation + string.digits + encoded = "" + if choice == "encode": + for letter in word: + if letter == " ": + encoded = encoded + " " + else: + x = letters.index(letter) + shift + encoded = encoded + letters[x] + if choice == "decode": + for letter in word: + if letter == " ": + encoded = encoded + " " + else: + x = letters.index(letter) - shift + encoded = encoded + letters[x] + + print(encoded) Here is what happens when we run it: .. sourcecode:: console - robertk01 Desktop$ pylint simplecaesar.py - ************* Module simplecaesar - simplecaesar.py:7:0: C0103: Constant name "shift" doesn't conform to UPPER_CASE naming style (invalid-name) - simplecaesar.py:11:0: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) - simplecaesar.py:15:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + tutor Desktop$ pylint simplecaesar.py + ************* Module simplecaesar + simplecaesar.py:8:0: C0103: Constant name "shift" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:11:0: C0103: Constant name "letters" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:12:0: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:16:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:18:12: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:19:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:23:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:25:12: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:26:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) - ------------------------------------------------------------------ - Your code has been rated at 8.42/10 (previous run: 7.37/10, +1.05) + ------------------------------------------------------------------ + Your code has been rated at 5.26/10 (previous run: 4.74/10, +0.53) -Nice! Pylint told us how much our code rating has improved since our last run, and we're down to just the ``invalid-name`` messages. +Nice! Pylint told us how much our code rating has improved since our last run, +and we're down to just the ``invalid-name`` messages. There are fairly well defined conventions around naming things like instance variables, functions, classes, etc. The conventions focus on the use of @@ -246,11 +206,11 @@ UPPERCASE and lowercase as well as the characters that separate multiple words in the name. This lends itself well to checking via a regular expression, thus the **should match (([A-Z\_][A-Z1-9\_]*)|(__.*__))$**. -In this case Pylint is telling me that those variables appear to be constants +In this case Pylint is telling us that those variables appear to be constants and should be all UPPERCASE. This is an in-house convention that has lived with Pylint since its inception. You too can create your own in-house naming conventions but for the purpose of this tutorial, we want to stick to the `PEP 8`_ -standard. In this case, the variables I declared should follow the convention +standard. In this case, the variables we declared should follow the convention of all lowercase. The appropriate rule would be something like: "should match [a-z\_][a-z0-9\_]{2,30}$". Notice the lowercase letters in the regular expression (a-z versus A-Z). @@ -260,14 +220,15 @@ will now be quite quiet: .. sourcecode:: console - robertk01 Desktop$ pylint --const-rgx='[a-z_][a-z0-9_]{2,30}$' simplecaesar.py + tutor Desktop$ pylint simplecaesar.py --const-rgx='[a-z\_][a-z0-9\_]{2,30}$' + ************* Module simplecaesar + simplecaesar.py:18:12: C0103: Constant name "x" doesn't conform to '[a-z\\_][a-z0-9\\_]{2,30}$' pattern (invalid-name) + simplecaesar.py:25:12: C0103: Constant name "x" doesn't conform to '[a-z\\_][a-z0-9\\_]{2,30}$' pattern (invalid-name) - ------------------------------------------------------------------- - Your code has been rated at 10.00/10 (previous run: 8.42/10, +1.58) + ------------------------------------------------------------------ + Your code has been rated at 8.95/10 (previous run: 5.26/10, +3.68) - -Regular expressions can be quite a beast so take my word on this particular -example but go ahead and `read up`_ on them if you want. +You can `read up`_ on regular expressions or use `a website to help you`_. .. tip:: It would really be a pain to specify that regex on the command line all the time, particularly if we're using many other options. @@ -276,6 +237,5 @@ example but go ahead and `read up`_ on them if you want. quickly sharing them with others. Invoking ``pylint --generate-toml-config`` will create a sample ``.toml`` section with all the options set and explained in comments. This can then be added to your ``pyproject.toml`` file or any other ``.toml`` file pointed to with the ``--rcfile`` option. -That's it for the basic intro. More tutorials will follow. - .. _`read up`: https://docs.python.org/library/re.html +.. _`a website to help you`: https://regex101.com/ diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index 86026dd975..793b3889be 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -13,13 +13,17 @@ Pylint provides the following optional plugins: - :ref:`pylint.extensions.comparetozero` - :ref:`pylint.extensions.comparison_placement` - :ref:`pylint.extensions.confusing_elif` +- :ref:`pylint.extensions.consider_refactoring_into_while_condition` - :ref:`pylint.extensions.consider_ternary_expression` +- :ref:`pylint.extensions.dict_init_mutate` - :ref:`pylint.extensions.docparams` - :ref:`pylint.extensions.docstyle` +- :ref:`pylint.extensions.dunder` - :ref:`pylint.extensions.empty_comment` - :ref:`pylint.extensions.emptystring` - :ref:`pylint.extensions.eq_without_hash` - :ref:`pylint.extensions.for_any_all` +- :ref:`pylint.extensions.magic_value` - :ref:`pylint.extensions.mccabe` - :ref:`pylint.extensions.no_self_use` - :ref:`pylint.extensions.overlapping_exceptions` @@ -42,12 +46,7 @@ Broad Try Clause checker This checker is provided by ``pylint.extensions.broad_try_clause``. Verbatim name of the checker is ``broad_try_clause``. -Broad Try Clause checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:max-try-statements: - Maximum number of statements allowed in a try clause - - Default: ``1`` +See also :ref:`broad_try_clause checker's options' documentation ` Broad Try Clause checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -69,12 +68,7 @@ Checkers that can improve code consistency. As such they don't necessarily provide a performance benefit and are often times opinionated. -Code Style checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^^ -:max-line-length-suggestions: - Max line length for which to sill emit suggestions. Used to prevent optional - suggestions which would get split by a code formatter (e.g., black). Will - default to the setting for ``max-line-length``. +See also :ref:`code_style checker's options' documentation ` Code Style checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -89,6 +83,9 @@ Code Style checker Messages Emitted when an if assignment is directly followed by an if statement and both can be combined by using an assignment expression ``:=``. Requires Python 3.8 and ``py-version >= 3.8``. +:consider-using-augmented-assign (R6104): *Use '%s' to do an augmented assign directly* + Emitted when an assignment is referring to the object that it is assigning + to. This can be changed to be an augmented assign. Disabled by default! .. _pylint.extensions.emptystring: @@ -101,7 +98,7 @@ Verbatim name of the checker is ``compare-to-empty-string``. Compare-To-Empty-String checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:compare-to-empty-string (C1901): *Avoid comparisons to empty string* +:compare-to-empty-string (C1901): *"%s" can be simplified to "%s" as an empty string is falsey* Used when Pylint detects comparison to an empty string constant. @@ -115,7 +112,7 @@ Verbatim name of the checker is ``compare-to-zero``. Compare-To-Zero checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:compare-to-zero (C2001): *Avoid comparisons to zero* +:compare-to-zero (C2001): *"%s" can be simplified to "%s" as 0 is falsey* Used when Pylint detects comparison to a 0 constant. @@ -167,6 +164,22 @@ Consider-Using-Any-Or-All checker Messages any or all. +.. _pylint.extensions.consider_refactoring_into_while_condition: + +Consider Refactoring Into While checker +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This checker is provided by ``pylint.extensions.consider_refactoring_into_while_condition``. +Verbatim name of the checker is ``consider_refactoring_into_while``. + +Consider Refactoring Into While checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:consider-refactoring-into-while-condition (R3501): *Consider using 'while %s' instead of 'while %s:' an 'if', and a 'break'* + Emitted when `while True:` loop is used and the first statement is a break + condition. The ``if / break`` construct can be removed if the check is + inverted and moved to the ``while`` statement. + + .. _pylint.extensions.consider_ternary_expression: Consider Ternary Expression checker @@ -202,12 +215,7 @@ you can use the ``bad-functions`` option:: $ pylint a.py --load-plugins=pylint.extensions.bad_builtin --bad-functions=apply,reduce ... -Deprecated Builtins checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:bad-functions: - List of builtins function names that should not be used, separated by a comma - - Default: ``map,filter`` +See also :ref:`deprecated_builtins checker's options' documentation ` Deprecated Builtins checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -265,12 +273,7 @@ higher than a preestablished value, which can be controlled through the $ pylint a.py --load-plugins=pylint.extensions.mccabe --max-complexity=50 $ -Design checker Options -^^^^^^^^^^^^^^^^^^^^^^ -:max-complexity: - McCabe complexity cyclomatic threshold - - Default: ``10`` +See also :ref:`design checker's options' documentation ` Design checker Messages ^^^^^^^^^^^^^^^^^^^^^^^ @@ -279,6 +282,21 @@ Design checker Messages Cyclomatic +.. _pylint.extensions.dict_init_mutate: + +Dict-Init-Mutate checker +~~~~~~~~~~~~~~~~~~~~~~~~ + +This checker is provided by ``pylint.extensions.dict_init_mutate``. +Verbatim name of the checker is ``dict-init-mutate``. + +Dict-Init-Mutate checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:dict-init-mutate (C3401): *Declare all known key/values when initializing the dictionary.* + Dictionaries can be initialized with a single statement using dictionary + literal syntax. + + .. _pylint.extensions.docstyle: Docstyle checker @@ -295,6 +313,23 @@ Docstyle checker Messages Used when a blank line is found at the beginning of a docstring. +.. _pylint.extensions.dunder: + +Dunder checker +~~~~~~~~~~~~~~ + +This checker is provided by ``pylint.extensions.dunder``. +Verbatim name of the checker is ``dunder``. + +See also :ref:`dunder checker's options' documentation ` + +Dunder checker Messages +^^^^^^^^^^^^^^^^^^^^^^^ +:bad-dunder-name (W3201): *Bad or misspelled dunder method name %s.* + Used when a dunder method is misspelled or defined with a name not within the + predefined list of dunder names. + + .. _pylint.extensions.check_elif: Else If Used checker @@ -310,6 +345,20 @@ Else If Used checker Messages does not contain statements that would be unrelated to it. +.. _pylint.extensions.empty_comment: + +Empty-Comment checker +~~~~~~~~~~~~~~~~~~~~~ + +This checker is provided by ``pylint.extensions.empty_comment``. +Verbatim name of the checker is ``empty-comment``. + +Empty-Comment checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:empty-comment (R2044): *Line with empty comment* + Used when a # symbol appears on a line not followed by an actual comment + + .. _pylint.extensions.eq_without_hash: Eq-Without-Hash checker @@ -341,6 +390,23 @@ Import-Private-Name checker Messages underscores should be considered private. +.. _pylint.extensions.magic_value: + +Magic-Value checker +~~~~~~~~~~~~~~~~~~~ + +This checker is provided by ``pylint.extensions.magic_value``. +Verbatim name of the checker is ``magic-value``. + +See also :ref:`magic-value checker's options' documentation ` + +Magic-Value checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:magic-value-comparison (R2004): *Consider using a named constant or an enum instead of '%s'.* + Using named constants instead of magic values helps improve readability and + maintainability of your code, try to avoid them in comparisons. + + .. _pylint.extensions.redefined_variable_type: Multiple Types checker @@ -550,33 +616,7 @@ docstring defining the interface, e.g. a superclass method, after "see":: Naming inconsistencies in existing parameter and their type documentations are still detected. -Parameter Documentation checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:accept-no-param-doc: - Whether to accept totally missing parameter documentation in the docstring of - a function that has parameters. - - Default: ``yes`` -:accept-no-raise-doc: - Whether to accept totally missing raises documentation in the docstring of a - function that raises an exception. - - Default: ``yes`` -:accept-no-return-doc: - Whether to accept totally missing return documentation in the docstring of a - function that returns a statement. - - Default: ``yes`` -:accept-no-yields-doc: - Whether to accept totally missing yields documentation in the docstring of a - generator. - - Default: ``yes`` -:default-docstring-type: - If the docstring type cannot be guessed the specified docstring type will be - used. - - Default: ``default`` +See also :ref:`parameter_documentation checker's options' documentation ` Parameter Documentation checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -626,20 +666,6 @@ Redefined-Loop-Name checker Messages Used when a loop variable is overwritten in the loop body. -.. _pylint.extensions.empty_comment: - -Refactoring checker -~~~~~~~~~~~~~~~~~~~ - -This checker is provided by ``pylint.extensions.empty_comment``. -Verbatim name of the checker is ``refactoring``. - -Refactoring checker Messages -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:empty-comment (R2044): *Line with empty comment* - Used when a # symbol appears on a line not followed by an actual comment - - .. _pylint.extensions.set_membership: Set Membership checker @@ -667,17 +693,7 @@ Typing checker Documentation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Find issue specifically related to type annotations. -Typing checker Options -^^^^^^^^^^^^^^^^^^^^^^ -:runtime-typing: - Set to ``no`` if the app / library does **NOT** need to support runtime - introspection of type annotations. If you use type annotations - **exclusively** for type checking of an application, you're probably fine. - For libraries, evaluate if some users what to access the type hints at - runtime first, e.g., through ``typing.get_type_hints``. Applies to Python - versions 3.7 - 3.9 - - Default: ``yes`` +See also :ref:`typing checker's options' documentation ` Typing checker Messages ^^^^^^^^^^^^^^^^^^^^^^^ @@ -698,6 +714,9 @@ Typing checker Messages :consider-alternative-union-syntax (R6003): *Consider using alternative Union syntax instead of '%s'%s* Emitted when 'typing.Union' or 'typing.Optional' is used instead of the alternative Union syntax 'int | None'. +:redundant-typehint-argument (R6006): *Type `%s` is used more than once in union type annotation. Remove redundant typehints.* + Duplicated type arguments will be skipped by `mypy` tool, therefore should be + removed to avoid confusion. .. _pylint.extensions.while_used: @@ -712,3 +731,4 @@ While Used checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^ :while-used (W0149): *Used `while` loop* Unbounded `while` loops can often be rewritten as bounded `for` loops. + Exceptions can be made for cases such as event loops, listeners, etc. diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index 908f3f003c..1ebf5ca532 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -4,139 +4,6 @@ Pylint features .. This file is auto-generated. Make any changes to the associated .. docs extension in 'doc/exts/pylint_features.py'. -Pylint global options and switches ----------------------------------- - -Pylint provides global options and switches. - -General options -~~~~~~~~~~~~~~~ -:ignore: - Files or directories to be skipped. They should be base names, not paths. - - Default: ``CVS`` -:ignore-patterns: - Files or directories matching the regex patterns are skipped. The regex - matches against base names, not paths. The default value ignores Emacs file - locks - - Default: ``^\.#`` -:ignore-paths: - Add files or directories matching the regex patterns to the ignore-list. The - regex matches against paths and can be in Posix or Windows format. -:persistent: - Pickle collected data for later comparisons. - - Default: ``yes`` -:load-plugins: - List of plugins (as comma separated values of python module names) to load, - usually to register additional checkers. -:fail-under: - Specify a score threshold to be exceeded before program exits with error. - - Default: ``10`` -:fail-on: - Return non-zero exit code if any of these messages/categories are detected, - even if score is above --fail-under value. Syntax same as enable. Messages - specified are enabled, while categories only check already-enabled messages. -:jobs: - Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the - number of processors available to use, and will cap the count on Windows to - avoid hangs. - - Default: ``1`` -:unsafe-load-any-extension: - Allow loading of arbitrary C extensions. Extensions are imported into the - active Python interpreter and may run arbitrary code. -:limit-inference-results: - Control the amount of potential inferred values when inferring a single - object. This can help the performance when dealing with large functions or - complex, nested conditions. - - Default: ``100`` -:extension-pkg-allow-list: - A comma-separated list of package or module names from where C extensions may - be loaded. Extensions are loading into the active Python interpreter and may - run arbitrary code. -:extension-pkg-whitelist: - A comma-separated list of package or module names from where C extensions may - be loaded. Extensions are loading into the active Python interpreter and may - run arbitrary code. (This is an alternative name to extension-pkg-allow-list - for backward compatibility.) -:suggestion-mode: - When enabled, pylint would attempt to guess common misconfiguration and emit - user-friendly hints instead of false-positive error messages. - - Default: ``yes`` -:exit-zero: - Always return a 0 (non-error) status code, even if lint errors are found. - This is primarily useful in continuous integration scripts. -:from-stdin: - Interpret the stdin as a python script, whose filename needs to be passed as - the module_or_package argument. -:recursive: - Discover python modules and packages in the file system subtree. -:py-version: - Minimum Python version to use for version dependent checks. Will default to - the version used to run pylint. -: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. -:analyse-fallback-blocks: - Analyse import fallback blocks. This can be used to support both Python 2 and - 3 compatible code, which means that the block might have code that exists - only in one or another interpreter, leading to false positives when analysed. - -Messages control options -~~~~~~~~~~~~~~~~~~~~~~~~ -:confidence: - Only show warnings with the listed confidence levels. Leave empty to show - all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, - UNDEFINED. - - Default: ``HIGH,CONTROL_FLOW,INFERENCE,INFERENCE_FAILURE,UNDEFINED`` -:enable: - Enable the message, report, category or checker with the given id(s). You can - either give multiple identifier separated by comma (,) or put this option - multiple time (only on the command line, not in the configuration file where - it should appear only once). See also the "--disable" option for examples. -:disable: - Disable the message, report, category or checker with the given id(s). You - can either give multiple identifiers separated by comma (,) or put this - option multiple times (only on the command line, not in the configuration - file where it should appear only once). You can also use "--disable=all" to - disable everything first and then re-enable specific checks. For example, if - you want to run only the similarities checker, you can use "--disable=all - --enable=similarities". If you want to run only the classes checker, but have - no Warning level messages displayed, use "--disable=all --enable=classes - --disable=W". - -Reports options -~~~~~~~~~~~~~~~ -:output-format: - Set the output format. Available formats are text, parseable, colorized, json - and msvs (visual studio). You can also give a reporter class, e.g. - mypackage.mymodule.MyReporterClass. -:reports: - Tells whether to display a full report or only the messages. -:evaluation: - Python expression which should return a score less than or equal to 10. You - have access to the variables 'fatal', 'error', 'warning', 'refactor', - 'convention', and 'info' which contain the number of messages in each - category, as well as 'statement' which is the total number of statements - analyzed. This score is used by the global evaluation report (RP0004). - - Default: ``max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))`` -:score: - Activate the evaluation score. - - Default: ``yes`` -:msg-template: - Template used to display messages. This is a python new-style format string - used to format the message information. See doc for all details. - Pylint checkers' options and switches ------------------------------------- @@ -169,131 +36,7 @@ Basic checker Verbatim name of the checker is ``basic``. -Basic checker Options -^^^^^^^^^^^^^^^^^^^^^ -:no-docstring-rgx: - Regular expression which should only match function or class names that do - not require a docstring. - - Default: ``^_`` -:docstring-min-length: - Minimum line length for functions/classes that require docstrings, shorter - ones are exempt. - - Default: ``-1`` -:good-names: - Good variable names which should always be accepted, separated by a comma. - - Default: ``i,j,k,ex,Run,_`` -:good-names-rgxs: - Good variable names regexes, separated by a comma. If names match any regex, - they will always be accepted -:bad-names: - Bad variable names which should always be refused, separated by a comma. - - Default: ``foo,bar,baz,toto,tutu,tata`` -:bad-names-rgxs: - Bad variable names regexes, separated by a comma. If names match any regex, - they will always be refused -:name-group: - Colon-delimited sets of names that determine each other's naming style when - the name regexes allow several styles. -:include-naming-hint: - Include a hint for the correct naming format with invalid-name. -:property-classes: - List of decorators that produce properties, such as abc.abstractproperty. Add - to this list to register other decorators that produce valid properties. - These decorators are taken in consideration only for invalid-name. - - Default: ``abc.abstractproperty`` -:argument-naming-style: - Naming style matching correct argument names. - - Default: ``snake_case`` -:argument-rgx: - Regular expression matching correct argument names. Overrides argument- - naming-style. If left empty, argument names will be checked with the set - naming style. -:attr-naming-style: - Naming style matching correct attribute names. - - Default: ``snake_case`` -:attr-rgx: - Regular expression matching correct attribute names. Overrides attr-naming- - style. If left empty, attribute names will be checked with the set naming - style. -:class-naming-style: - Naming style matching correct class names. - - Default: ``PascalCase`` -:class-rgx: - Regular expression matching correct class names. Overrides class-naming- - style. If left empty, class names will be checked with the set naming style. -:class-attribute-naming-style: - Naming style matching correct class attribute names. - - Default: ``any`` -:class-attribute-rgx: - Regular expression matching correct class attribute names. Overrides class- - attribute-naming-style. If left empty, class attribute names will be checked - with the set naming style. -:class-const-naming-style: - Naming style matching correct class constant names. - - Default: ``UPPER_CASE`` -:class-const-rgx: - Regular expression matching correct class constant names. Overrides class- - const-naming-style. If left empty, class constant names will be checked with - the set naming style. -:const-naming-style: - Naming style matching correct constant names. - - Default: ``UPPER_CASE`` -:const-rgx: - Regular expression matching correct constant names. Overrides const-naming- - style. If left empty, constant names will be checked with the set naming - style. -:function-naming-style: - Naming style matching correct function names. - - Default: ``snake_case`` -:function-rgx: - Regular expression matching correct function names. Overrides function- - naming-style. If left empty, function names will be checked with the set - naming style. -:inlinevar-naming-style: - Naming style matching correct inline iteration names. - - Default: ``any`` -:inlinevar-rgx: - Regular expression matching correct inline iteration names. Overrides - inlinevar-naming-style. If left empty, inline iteration names will be checked - with the set naming style. -:method-naming-style: - Naming style matching correct method names. - - Default: ``snake_case`` -:method-rgx: - Regular expression matching correct method names. Overrides method-naming- - style. If left empty, method names will be checked with the set naming style. -:module-naming-style: - Naming style matching correct module names. - - Default: ``snake_case`` -:module-rgx: - Regular expression matching correct module names. Overrides module-naming- - style. If left empty, module names will be checked with the set naming style. -:typevar-rgx: - Regular expression matching correct type variable names. If left empty, type - variable names will be checked with the set naming style. -:variable-naming-style: - Naming style matching correct variable names. - - Default: ``snake_case`` -:variable-rgx: - Regular expression matching correct variable names. Overrides variable- - naming-style. If left empty, variable names will be checked with the set - naming style. +See also :ref:`basic checker's options' documentation ` Basic checker Messages ^^^^^^^^^^^^^^^^^^^^^^ @@ -303,7 +46,7 @@ Basic checker Messages Used when a function / class / method is redefined. :continue-in-finally (E0116): *'continue' not supported inside 'finally' clause* Emitted when the `continue` keyword is found inside a finally clause, which - is a SyntaxError. This message can't be emitted when using Python >= 3.8. + is a SyntaxError. :abstract-class-instantiated (E0110): *Abstract class %r with abstract methods instantiated* Used when an abstract class with `abc.ABCMeta` as metaclass has abstract methods and is instantiated. @@ -352,7 +95,7 @@ Basic checker Messages Used when a break or a return statement is found inside the finally clause of a try...finally block: the exceptions raised in the try clause will be silently swallowed instead of being re-raised. -:assert-on-tuple (W0199): *Assert called on a 2-item-tuple. Did you mean 'assert x,y'?* +:assert-on-tuple (W0199): *Assert called on a populated tuple. Did you mean 'assert x,y'?* A call of assert on a tuple will always evaluate to true if the tuple is not empty, and will always evaluate to false if it is. :assert-on-string-literal (W0129): *Assert statement has a string literal as its first argument. The assert will %s fail.* @@ -365,8 +108,8 @@ Basic checker Messages was made, which might suggest that some parenthesis were omitted, resulting in potential unwanted behaviour. :nan-comparison (W0177): *Comparison %s should be %s* - Used when an expression is compared to NaNvalues like numpy.NaN and - float('nan') + Used when an expression is compared to NaN values like numpy.NaN and + float('nan'). :dangerous-default-value (W0102): *Dangerous default value %s as argument* Used when a mutable value as list or dictionary is detected in a default value for an argument. @@ -378,6 +121,9 @@ Basic checker Messages Loops should only have an else clause if they can exit early with a break statement, otherwise the statements under else should be on the same scope as the loop itself. +:pointless-exception-statement (W0133): *Exception statement has no effect* + Used when an exception is created without being assigned, raised or returned + for subsequent use elsewhere. :expression-not-assigned (W0106): *Expression "%s" is assigned to nothing* Used when an expression that is not a function call is assigned to nothing. Probably something else was intended. @@ -392,6 +138,9 @@ Basic checker Messages argument list as the lambda itself; such lambda expressions are in all but a few cases replaceable with the function being called in the body of the lambda. +:named-expr-without-context (W0131): *Named expression used without context* + Emitted if named expression is used to do a regular assignment outside a + context like if, for, while, or a comprehension. :redeclared-assigned-name (W0128): *Redeclared variable %r in assignment* Emitted when we detect that a variable was redeclared in the same assignment. :pointless-statement (W0104): *Statement seems to have no effect* @@ -411,8 +160,10 @@ Basic checker Messages using `ast.literal_eval` for safely evaluating strings containing Python expressions from untrusted sources. :exec-used (W0122): *Use of exec* - Used when you use the "exec" statement (function for Python 3), to discourage - its usage. That doesn't mean you cannot use it ! + Raised when the 'exec' statement is used. It's dangerous to use this function + for a user input, and it's also slower than actual code in general. This + doesn't mean you should never use it, but you should consider alternatives + first and restrict the functions available. :using-constant-test (W0125): *Using a conditional statement with a constant value* Emitted when a conditional statement (If or ternary if) uses a constant value for its test. This might not be what the user intended to do. @@ -423,7 +174,7 @@ Basic checker Messages When two literals are compared with each other the result is a constant. Using the constant directly is both easier to read and more performant. Initializing 'True' and 'False' this way is not required since Python 2.3. -:literal-comparison (R0123): *Comparison to literal* +:literal-comparison (R0123): *In '%s', use '%s' when comparing constant literals not '%s' ('%s')* Used when comparing an object to a literal, which is usually what you do not want to do, since you can compare to a different literal than what was expected altogether. @@ -441,12 +192,14 @@ Basic checker Messages Used when a module, function, class or method has an empty docstring (it would be too easy ;). :missing-class-docstring (C0115): *Missing class docstring* - Used when a class has no docstring.Even an empty class must have a docstring. + Used when a class has no docstring. Even an empty class must have a + docstring. :missing-function-docstring (C0116): *Missing function or method docstring* - Used when a function or method has no docstring.Some special methods like + Used when a function or method has no docstring. Some special methods like __init__ do not require a docstring. :missing-module-docstring (C0114): *Missing module docstring* - Used when a module has no docstring.Empty modules do not require a docstring. + Used when a module has no docstring. Empty modules do not require a + docstring. :typevar-name-incorrect-variance (C0105): *Type variable name does not reflect variance%s* Emitted when a TypeVar name doesn't reflect its type variance. According to PEP8, it is recommended to add suffixes '_co' and '_contra' to the variables @@ -474,27 +227,7 @@ Classes checker Verbatim name of the checker is ``classes``. -Classes checker Options -^^^^^^^^^^^^^^^^^^^^^^^ -:defining-attr-methods: - List of method names used to declare (i.e. assign) instance attributes. - - Default: ``__init__,__new__,setUp,__post_init__`` -:valid-classmethod-first-arg: - List of valid names for the first argument in a class method. - - Default: ``cls`` -:valid-metaclass-classmethod-first-arg: - List of valid names for the first argument in a metaclass class method. - - Default: ``cls`` -:exclude-protected: - List of member names, which should be excluded from the protected access - warning. - - Default: ``_asdict,_fields,_replace,_source,_make`` -:check-protected-access-in-special-methods: - Warn about protected attribute access inside special methods +See also :ref:`classes checker's options' documentation ` Classes checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -514,18 +247,18 @@ Classes checker Messages Used when a class has an inconsistent method resolution order. :inherit-non-class (E0239): *Inheriting %r, which is not a class.* Used when a class inherits from something which is not a class. -:invalid-class-object (E0243): *Invalid __class__ object* - Used when an invalid object is assigned to a __class__ property. Only a class - is permitted. :invalid-slots (E0238): *Invalid __slots__ object* Used when an invalid __slots__ is found in class. Only a string, an iterable or a sequence is permitted. +:invalid-class-object (E0243): *Invalid assignment to '__class__'. Should be a class definition but got a '%s'* + Used when an invalid object is assigned to a __class__ property. Only a class + is permitted. :invalid-slots-object (E0236): *Invalid object %r in __slots__, must contain only non empty strings* Used when an invalid (non-string) object occurs in __slots__. -:no-method-argument (E0211): *Method has no argument* +:no-method-argument (E0211): *Method %r has no argument* Used when a method which should have the bound instance as first argument has no argument defined. -:no-self-argument (E0213): *Method should have "self" as first argument* +:no-self-argument (E0213): *Method %r should have "self" as first argument* Used when a method has an attribute different the "self" as first argument. This is considered as an error since this is a so common convention that you shouldn't break it! @@ -578,7 +311,7 @@ Classes checker Messages Used when an instance attribute is defined outside the __init__ method. :subclassed-final-class (W0240): *Class %r is a subclass of a class decorated with typing.final: %r* Used when a class decorated with typing.final has been subclassed. -:abstract-method (W0223): *Method %r is abstract in class %r but is not overridden* +:abstract-method (W0223): *Method %r is abstract in class %r but is not overridden in child class %r* Used when an abstract method (i.e. raise NotImplementedError) is not overridden in concrete class. :overridden-final-method (W0239): *Method %r overrides a method decorated with typing.final which is defined in class %r* @@ -600,9 +333,10 @@ Classes checker Messages call and does not work as expected. :unused-private-member (W0238): *Unused private member `%s.%s`* Emitted when a private member of a class is defined but not used. -:useless-super-delegation (W0235): *Useless super delegation in method %r* +:useless-parent-delegation (W0246): *Useless parent or super() delegation in method %r* Used whenever we can detect that an overridden method is useless, relying on - super() delegation to do the same thing as another method from the MRO. + parent or super() delegation to do the same thing as another method from the + MRO. :non-parent-init-called (W0233): *__init__ method from a non direct base class %r is called* Used when an __init__ method is called on a class which is not in the direct ancestors for the analysed class. @@ -644,54 +378,7 @@ Design checker Verbatim name of the checker is ``design``. -Design checker Options -^^^^^^^^^^^^^^^^^^^^^^ -:max-args: - Maximum number of arguments for function / method. - - Default: ``5`` -:max-locals: - Maximum number of locals for function / method body. - - Default: ``15`` -:max-returns: - Maximum number of return / yield for function / method body. - - Default: ``6`` -:max-branches: - Maximum number of branch for function / method body. - - Default: ``12`` -:max-statements: - Maximum number of statements in function / method body. - - Default: ``50`` -:max-parents: - Maximum number of parents for a class (see R0901). - - Default: ``7`` -:ignored-parents: - List of qualified class names to ignore when counting class parents (see - R0901) -:max-attributes: - Maximum number of attributes for a class (see R0902). - - Default: ``7`` -:min-public-methods: - Minimum number of public methods for a class (see R0903). - - Default: ``2`` -:max-public-methods: - Maximum number of public methods for a class (see R0904). - - Default: ``20`` -:max-bool-expr: - Maximum number of boolean expressions in an if statement (see R0916). - - Default: ``5`` -:exclude-too-few-public-methods: - List of regular expressions of class ancestor names to ignore when counting - public methods (see R0903) +See also :ref:`design checker's options' documentation ` Design checker Messages ^^^^^^^^^^^^^^^^^^^^^^^ @@ -728,12 +415,7 @@ Exceptions checker Verbatim name of the checker is ``exceptions``. -Exceptions checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^^ -:overgeneral-exceptions: - Exceptions that will emit a warning when caught. - - Default: ``BaseException,Exception`` +See also :ref:`exceptions checker's options' documentation ` Exceptions checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -744,9 +426,9 @@ Exceptions checker Messages :catching-non-exception (E0712): *Catching an exception which doesn't inherit from Exception: %s* Used when a class which doesn't inherit from Exception is used as an exception in an except clause. -:bad-exception-context (E0703): *Exception context set to something which is not an exception, nor None* - Used when using the syntax "raise ... from ...", where the exception context - is not an exception, nor None. +:bad-exception-cause (E0705): *Exception cause set to something which is not an exception, nor None* + Used when using the syntax "raise ... from ...", where the exception cause is + not an exception, nor None. :notimplemented-raised (E0711): *NotImplemented raised - should raise NotImplementedError* Used when NotImplemented is raised instead of NotImplementedError :raising-bad-type (E0702): *Raising %s while only classes or instances are allowed* @@ -764,9 +446,10 @@ Exceptions checker Messages :duplicate-except (W0705): *Catching previously caught exception type %s* Used when an except catches a type that was already caught by a previous handler. -:broad-except (W0703): *Catching too general exception %s* - Used when an except catches a too general exception, possibly burying - unrelated errors. +:broad-exception-caught (W0718): *Catching too general exception %s* + If you use a naked ``except Exception:`` clause, you might end up catching + exceptions other than the ones you expect to catch. This can hide bugs or + make it harder to debug programs when unrelated errors are hidden. :raise-missing-from (W0707): *Consider explicitly re-raising using %s'%s from %s'* Python's exception chaining shows the traceback of the current exception, but also of the original exception. When you raise a new exception after another @@ -785,7 +468,17 @@ Exceptions checker Messages valid for the exception in question. Usually emitted when having binary operations between exceptions in except handlers. :bare-except (W0702): *No exception type(s) specified* - Used when an except clause doesn't specify exceptions type to catch. + A bare ``except:`` clause will catch ``SystemExit`` and ``KeyboardInterrupt`` + exceptions, making it harder to interrupt a program with ``Control-C``, and + can disguise other problems. If you want to catch all exceptions that signal + program errors, use ``except Exception:`` (bare except is equivalent to + ``except BaseException:``). +:broad-exception-raised (W0719): *Raising too general exception: %s* + Raising exceptions that are too generic force you to catch exceptions + generically too. It will force you to use a naked ``except Exception:`` + clause. You might then end up catching exceptions other than the ones you + expect to catch. This can hide bugs or make it harder to debug programs when + unrelated errors are hidden. :try-except-raise (W0706): *The except handler raises immediately* Used when an except handler uses raise as its first or only operator. This is useless because it raises back the exception immediately. Remove the raise @@ -797,37 +490,7 @@ Format checker Verbatim name of the checker is ``format``. -Format checker Options -^^^^^^^^^^^^^^^^^^^^^^ -:max-line-length: - Maximum number of characters on a single line. - - Default: ``100`` -:ignore-long-lines: - Regexp for a line that is allowed to be longer than the limit. - - Default: ``^\s*(# )??$`` -:single-line-if-stmt: - Allow the body of an if to be on the same line as the test if there is no - else. -:single-line-class-stmt: - Allow the body of a class to be on the same line as the declaration if body - contains single statement. -:max-module-lines: - Maximum number of lines in a module. - - Default: ``1000`` -:indent-string: - String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 - tab). - - Default: ``' '`` -:indent-after-paren: - Number of spaces of indent required inside a hanging or continued line. - - Default: ``4`` -:expected-line-ending-format: - Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +See also :ref:`format checker's options' documentation ` Format checker Messages ^^^^^^^^^^^^^^^^^^^^^^^ @@ -862,34 +525,7 @@ Imports checker Verbatim name of the checker is ``imports``. -Imports checker Options -^^^^^^^^^^^^^^^^^^^^^^^ -:deprecated-modules: - Deprecated modules which should not be used, separated by a comma. -:preferred-modules: - Couples of modules and preferred modules, separated by a comma. -:import-graph: - Output a graph (.gv or any supported image format) of all (i.e. internal and - external) dependencies to the given file (report RP0402 must not be - disabled). -:ext-import-graph: - Output a graph (.gv or any supported image format) of external dependencies - to the given file (report RP0402 must not be disabled). -:int-import-graph: - Output a graph (.gv or any supported image format) of internal dependencies - to the given file (report RP0402 must not be disabled). -:known-standard-library: - Force import order to recognize a module as part of the standard - compatibility libraries. -:known-third-party: - Force import order to recognize a module as part of a third party library. - - Default: ``enchant`` -:allow-any-import-level: - List of modules that can be imported at any level, not just the top level - one. -:allow-wildcard-with-all: - Allow wildcard imports from modules that define __all__. +See also :ref:`imports checker's options' documentation ` Imports checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -898,14 +534,16 @@ Imports checker Messages package. :import-error (E0401): *Unable to import %s* Used when pylint has been unable to import a module. -:deprecated-module (W0402): *Deprecated module %r* +:deprecated-module (W4901): *Deprecated module %r* A module marked as deprecated is imported. :import-self (W0406): *Module import itself* Used when a module is importing itself. :preferred-module (W0407): *Prefer importing %r instead of %r* Used when a module imported has a preferred replacement module. :reimported (W0404): *Reimport %r (imported line %s)* - Used when a module is reimported multiple times. + Used when a module is imported more than once. +:shadowed-import (W0416): *Shadowed %r (imported line %s)* + Used when a module is aliased with a name that shadows another import. :wildcard-import (W0401): *Wildcard import %s* Used when `from module import *` is detected. :misplaced-future (W0410): *__future__ import is not the first non docstring statement* @@ -915,21 +553,21 @@ Imports checker Messages Used when a cyclic import between two or more modules is detected. :consider-using-from-import (R0402): *Use 'from %s import %s' instead* Emitted when a submodule of a package is imported and aliased with the same - name. E.g., instead of ``import concurrent.futures as futures`` use ``from - concurrent import futures`` + name, e.g., instead of ``import concurrent.futures as futures`` use ``from + concurrent import futures``. :wrong-import-order (C0411): *%s should be placed before %s* Used when PEP8 import order is not respected (standard imports first, then - third-party libraries, then local imports) + third-party libraries, then local imports). :wrong-import-position (C0413): *Import "%s" should be placed at the top of the module* - Used when code and imports are mixed + Used when code and imports are mixed. :useless-import-alias (C0414): *Import alias does not rename original package* - Used when an import alias is same as original package.e.g using import numpy - as numpy instead of import numpy as np + Used when an import alias is same as original package, e.g., using import + numpy as numpy instead of import numpy as np. :import-outside-toplevel (C0415): *Import outside toplevel (%s)* Used when an import statement is used anywhere other than the module toplevel. Move this import to the top of the file. :ungrouped-imports (C0412): *Imports from package %s are not grouped* - Used when imports are not grouped by packages + Used when imports are not grouped by packages. :multiple-imports (C0410): *Multiple imports on one line (%s)* Used when import statement importing multiple modules is detected. @@ -959,18 +597,7 @@ Logging checker Verbatim name of the checker is ``logging``. -Logging checker Options -^^^^^^^^^^^^^^^^^^^^^^^ -:logging-modules: - Logging modules to check that the string format arguments are in logging - function parameter format. - - Default: ``logging`` -:logging-format-style: - The type of string formatting that logging methods do. `old` means using % - formatting, `new` is for `{}` formatting. - - Default: ``old`` +See also :ref:`logging checker's options' documentation ` Logging checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1007,6 +634,26 @@ Logging checker Messages format-interpolation is disabled then you can use str.format. +Method Args checker +~~~~~~~~~~~~~~~~~~~ + +Verbatim name of the checker is ``method_args``. + +See also :ref:`method_args checker's options' documentation ` + +Method Args checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:positional-only-arguments-expected (E3102): *`%s()` got some positional-only arguments passed as keyword arguments: %s* + Emitted when positional-only arguments have been passed as keyword arguments. + Remove the keywords for the affected arguments in the function call. This + message can't be emitted when using Python < 3.8. +:missing-timeout (W3101): *Missing timeout argument for method '%s' can cause your program to hang indefinitely* + Used when a method needs a 'timeout' parameter in order to avoid waiting for + a long time. If no timeout is specified explicitly the default value is used. + For example for 'requests' the program will never time out (i.e. hang + indefinitely). + + Metrics checker ~~~~~~~~~~~~~~~ @@ -1022,14 +669,7 @@ Miscellaneous checker Verbatim name of the checker is ``miscellaneous``. -Miscellaneous checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:notes: - List of note tags to take in consideration, separated by a comma. - - Default: ``FIXME,XXX,TODO`` -:notes-rgx: - Regular expression of note tags to take in consideration. +See also :ref:`miscellaneous checker's options' documentation ` Miscellaneous checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1058,6 +698,17 @@ Modified Iteration checker Messages use a copy of the list. +Nested Min Max checker +~~~~~~~~~~~~~~~~~~~~~~ + +Verbatim name of the checker is ``nested_min_max``. + +Nested Min Max checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:nested-min-max (W3301): *Do not use nested call of '%s'; it's possible to do '%s' instead* + Nested calls ``min(1, min(2, 3))`` can be rewritten as ``min(1, 2, 3)``. + + Newstyle checker ~~~~~~~~~~~~~~~~ @@ -1103,19 +754,7 @@ Refactoring checker Verbatim name of the checker is ``refactoring``. -Refactoring checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:max-nested-blocks: - Maximum number of nested blocks for function / method body - - Default: ``5`` -:never-returning-functions: - Complete name of functions that never returns. When checking for - inconsistent-return-statements if a never returning function is called then - it will be considered as an explicit return statement and no message will be - printed. - - Default: ``sys.exit,argparse.parse_error`` +See also :ref:`refactoring checker's options' documentation ` Refactoring checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1125,18 +764,25 @@ Refactoring checker Messages Emitted when a boolean condition can be simplified to a constant value. :simplify-boolean-expression (R1709): *Boolean expression may be simplified to %s* Emitted when redundant pre-python 2.5 ternary syntax is used. -:consider-using-in (R1714): *Consider merging these comparisons with "in" to %r* - To check if a variable is equal to one of many values,combine the values into - a tuple and check if the variable is contained "in" it instead of checking - for equality against each of the values.This is faster and less verbose. +:consider-using-in (R1714): *Consider merging these comparisons with 'in' by using '%s %sin (%s)'. Use a set instead if elements are hashable.* + To check if a variable is equal to one of many values, combine the values + into a set or tuple and check if the variable is contained "in" it instead of + checking for equality against each of the values. This is faster and less + verbose. :consider-merging-isinstance (R1701): *Consider merging these isinstance calls to isinstance(%s, (%s))* Used when multiple consecutive isinstance calls can be merged into one. +:use-dict-literal (R1735): *Consider using '%s' instead of a call to 'dict'.* + Emitted when using dict() to create a dictionary instead of a literal '{ ... + }'. The literal is faster as it avoids an additional function call. :consider-using-max-builtin (R1731): *Consider using '%s' instead of unnecessary if block* Using the max builtin instead of a conditional improves readability and conciseness. :consider-using-min-builtin (R1730): *Consider using '%s' instead of unnecessary if block* Using the min builtin instead of a conditional improves readability and conciseness. +:consider-using-sys-exit (R1722): *Consider using 'sys.exit' instead* + Contrary to 'exit()' or 'quit()', 'sys.exit' does not rely on the site module + being available (as the 'sys' module is always available). :consider-using-with (R1732): *Consider using 'with' for resource-allocating operations* Emitted if a resource-allocating assignment or call may be replaced by a 'with' block. By using 'with' the release of the allocated resources is @@ -1150,15 +796,15 @@ Refactoring checker Messages :consider-using-dict-comprehension (R1717): *Consider using a dictionary comprehension* Emitted when we detect the creation of a dictionary using the dict() callable and a transient list. Although there is nothing syntactically wrong with this - code, it is hard to read and can be simplified to a dict comprehension.Also + code, it is hard to read and can be simplified to a dict comprehension. Also it is faster since you don't need to create another transient list :consider-using-generator (R1728): *Consider using a generator instead '%s(%s)'* If your container can be large using a generator will bring better performance. :consider-using-set-comprehension (R1718): *Consider using a set comprehension* Although there is nothing syntactically wrong with this code, it is hard to - read and can be simplified to a set comprehension.Also it is faster since you - don't need to create another transient list + read and can be simplified to a set comprehension. Also it is faster since + you don't need to create another transient list :consider-using-get (R1715): *Consider using dict.get for getting values from a dict if a key is present or a default if not* Using the builtin dict.get for getting a value from a dictionary if a key is present or a default if not, is simpler and considered more idiomatic, @@ -1166,16 +812,11 @@ Refactoring checker Messages :consider-using-join (R1713): *Consider using str.join(sequence) for concatenating strings from an iterable* Using str.join(sequence) is faster, uses less memory and increases readability compared to for-loop iteration. -:consider-using-sys-exit (R1722): *Consider using sys.exit()* - Instead of using exit() or quit(), consider using the sys.exit(). :consider-using-ternary (R1706): *Consider using ternary (%s)* Used when one of known pre-python 2.5 ternary syntax is used. :consider-swap-variables (R1712): *Consider using tuple unpacking for swapping variables* You do not have to use a temporary variable in order to swap variables. Using "tuple unpacking" to directly swap variables makes the intention more clear. -:use-dict-literal (R1735): *Consider using {} instead of dict()* - Emitted when using dict() to create an empty dictionary instead of the - literal {}. The literal is faster as it avoids an additional function call. :trailing-comma-tuple (R1707): *Disallow trailing comma tuple* In Python, a tuple is actually created by the comma symbol, not by the parentheses. Unfortunately, one can actually create a tuple by misplacing a @@ -1196,7 +837,7 @@ Refactoring checker Messages operations, such as for iteration, with statement assignment and exception handler assignment. :chained-comparison (R1716): *Simplify chained comparison between the operands* - This message is emitted when pylint encounters boolean operation like"a < b + This message is emitted when pylint encounters boolean operation like "a < b and b < c", suggesting instead to refactor it to "a < b < c" :simplifiable-if-expression (R1719): *The if expression can be replaced with %s* Used when an if expression can be replaced with 'bool(test)' or simply 'test' @@ -1239,9 +880,9 @@ Refactoring checker Messages Emitted when a single "return" or "return None" statement is found at the end of function or method definition. This statement can safely be removed because Python will implicitly return None -:use-implicit-booleaness-not-comparison (C1803): *'%s' can be simplified to '%s' as an empty sequence is falsey* +:use-implicit-booleaness-not-comparison (C1803): *'%s' can be simplified to '%s' as an empty %s is falsey* Used when Pylint detects that collection literal comparison is being used to - check for emptiness; Use implicit booleaness insteadof a collection classes; + check for emptiness; Use implicit booleaness instead of a collection classes; empty collections are considered as false :unneeded-not (C0113): *Consider changing "%s" to "%s"* Used when a boolean expression contains an unneeded negation. @@ -1280,28 +921,7 @@ Similarities checker Verbatim name of the checker is ``similarities``. -Similarities checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:min-similarity-lines: - Minimum lines number of a similarity. - - Default: ``4`` -:ignore-comments: - Comments are removed from the similarity computation - - Default: ``yes`` -:ignore-docstrings: - Docstrings are removed from the similarity computation - - Default: ``yes`` -:ignore-imports: - Imports are removed from the similarity computation - - Default: ``yes`` -:ignore-signatures: - Signatures are removed from the similarity computation - - Default: ``yes`` +See also :ref:`similarities checker's options' documentation ` Similarities checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1320,27 +940,7 @@ Spelling checker Verbatim name of the checker is ``spelling``. -Spelling checker Options -^^^^^^^^^^^^^^^^^^^^^^^^ -:spelling-dict: - Spelling dictionary name. Available dictionaries: none. To make it work, - install the 'python-enchant' package. -:spelling-ignore-words: - List of comma separated words that should not be checked. -:spelling-private-dict-file: - A path to a file that contains the private dictionary; one word per line. -:spelling-store-unknown-words: - Tells whether to store unknown words to the private dictionary (see the - --spelling-private-dict-file option) instead of raising a message. -:max-spelling-suggestions: - Limits count of emitted suggestions for spelling mistakes. - - Default: ``4`` -:spelling-ignore-comment-directives: - List of comma separated words that should be considered directives if they - appear at the beginning of a comment and should not be checked. - - Default: ``fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:`` +See also :ref:`spelling checker's options' documentation ` Spelling checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1362,6 +962,12 @@ Stdlib checker Messages :invalid-envvar-value (E1507): *%s does not support %s type argument* Env manipulation functions support only string type arguments. See https://docs.python.org/3/library/os.html#os.getenv. +:singledispatch-method (E1519): *singledispatch decorator should not be used with methods, use singledispatchmethod instead.* + singledispatch should decorate functions and not class/instance methods. Use + singledispatchmethod for those cases. +:singledispatchmethod-function (E1520): *singledispatchmethod decorator should not be used with functions, use singledispatch instead.* + singledispatchmethod should decorate class/instance methods and not + functions. Use singledispatch for those cases. :bad-open-mode (W1501): *"%s" is not a valid mode for open.* Python supports: r, w, a[, x] modes with b, +, and U (only with r) options. See https://docs.python.org/3/library/functions.html#open @@ -1374,7 +980,12 @@ Stdlib checker Messages linked to the function and therefore never garbage collected. Unless your instance will never need to be garbage collected (singleton) it is recommended to refactor code to avoid this pattern or add a maxsize to the - cache.The default value for maxsize is 128. + cache. The default value for maxsize is 128. +:subprocess-run-check (W1510): *'subprocess.run' used without explicitly defining the value for 'check'.* + The ``check`` keyword is set to False by default. It means the process + launched by ``subprocess.run`` can exit with a non-zero exit code and fail + silently. It's better to set it explicitly to make clear what the error- + handling behavior is. :forgotten-debug-statement (W1515): *Leaving functions creating breakpoints in production code is not recommended* Calls to breakpoint(), sys.breakpointhook() and pdb.set_trace() should be removed from code that is not actively being debugged. @@ -1391,13 +1002,13 @@ Stdlib checker Messages they represent matches midnight UTC. This behaviour was fixed in Python 3.5. See https://bugs.python.org/issue13936 for reference. This message can't be emitted when using Python >= 3.5. -:deprecated-argument (W1511): *Using deprecated argument %s of method %s()* +:deprecated-argument (W4903): *Using deprecated argument %s of method %s()* The argument is marked as deprecated and will be removed in the future. -:deprecated-class (W1512): *Using deprecated class %s of module %s* +:deprecated-class (W4904): *Using deprecated class %s of module %s* The class is marked as deprecated and will be removed in the future. -:deprecated-decorator (W1513): *Using deprecated decorator %s()* +:deprecated-decorator (W4905): *Using deprecated decorator %s()* The decorator is marked as deprecated and will be removed in the future. -:deprecated-method (W1505): *Using deprecated method %s()* +:deprecated-method (W4902): *Using deprecated method %s()* The method is marked as deprecated and will be removed in the future. :unspecified-encoding (W1514): *Using open without explicitly specifying an encoding* It is better to specify an encoding when opening documents. Using the system @@ -1407,15 +1018,11 @@ Stdlib checker Messages The preexec_fn parameter is not safe to use in the presence of threads in your application. The child process could deadlock before exec is called. If you must use it, keep it trivial! Minimize the number of libraries you call - into.https://docs.python.org/3/library/subprocess.html#popen-constructor -:subprocess-run-check (W1510): *Using subprocess.run without explicitly set `check` is not recommended.* - The check parameter should always be used with explicitly set `check` keyword - to make clear what the error-handling behavior - is.https://docs.python.org/3/library/subprocess.html#subprocess.run + into. See https://docs.python.org/3/library/subprocess.html#popen-constructor :bad-thread-instantiation (W1506): *threading.Thread needs the target function* The warning is emitted when a threading.Thread class is instantiated without - the target function being passed. By default, the first parameter is the - group param, not the target param. + the target function being passed as a kwarg or as a second argument. By + default, the first parameter is the group param, not the target param. String checker @@ -1423,14 +1030,7 @@ String checker Verbatim name of the checker is ``string``. -String checker Options -^^^^^^^^^^^^^^^^^^^^^^ -:check-str-concat-over-line-jumps: - This flag controls whether the implicit-str-concat should generate a warning - on implicit string concatenation in sequences defined over several lines. -:check-quote-consistency: - This flag controls whether inconsistent-quotes generates a warning when the - character used as a quote delimiter is used inconsistently within a module. +See also :ref:`string checker's options' documentation ` String checker Messages ^^^^^^^^^^^^^^^^^^^^^^^ @@ -1529,68 +1129,7 @@ Typecheck checker Verbatim name of the checker is ``typecheck``. -Typecheck checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^ -:ignore-on-opaque-inference: - This flag controls whether pylint should warn about no-member and similar - checks whenever an opaque object is returned when inferring. The inference - can return multiple potential results while evaluating a Python object, but - some branches might not be evaluated, which results in partial inference. In - that case, it might be useful to still emit no-member and other checks for - the rest of the inferred objects. - - Default: ``yes`` -:mixin-class-rgx: - Regex pattern to define which classes are considered mixins. - - Default: ``.*[Mm]ixin`` -:ignore-mixin-members: - Tells whether missing members accessed in mixin class should be ignored. A - class is considered mixin if its name matches the mixin-class-rgx option. - - Default: ``yes`` -:ignored-checks-for-mixins: - List of symbolic message names to ignore for Mixin members. - - Default: ``no-member,not-async-context-manager,not-context-manager,attribute-defined-outside-init`` -:ignore-none: - Tells whether to warn about missing members when the owner of the attribute - is inferred to be None. - - Default: ``yes`` -:ignored-classes: - List of class names for which member attributes should not be checked (useful - for classes with dynamically set attributes). This supports the use of - qualified names. - - Default: ``optparse.Values,thread._local,_thread._local,argparse.Namespace`` -:generated-members: - List of members which are set dynamically and missed by pylint inference - system, and so shouldn't trigger E1101 when accessed. Python regular - expressions are accepted. -:contextmanager-decorators: - List of decorators that produce context managers, such as - contextlib.contextmanager. Add to this list to register other decorators that - produce valid context managers. - - Default: ``contextlib.contextmanager`` -:missing-member-hint-distance: - The minimum edit distance a name should have in order to be considered a - similar match for a missing member name. - - Default: ``1`` -:missing-member-max-choices: - The total number of similar names that should be taken in consideration when - showing a hint for a missing member. - - Default: ``1`` -:missing-member-hint: - Show a hint with possible names when a member name was not found. The aspect - of finding the hint is based on edit distance. - - Default: ``yes`` -:signature-mutators: - List of decorators that change the signature of a decorated function. +See also :ref:`typecheck checker's options' documentation ` Typecheck checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1610,6 +1149,9 @@ Typecheck checker Messages Used when a variable is accessed for a nonexistent member. :not-callable (E1102): *%s is not callable* Used when an object being called has been inferred to a non callable object. +:unhashable-member (E1143): *'%s' is unhashable and can't be used as a %s in a %s* + Emitted when a dict key or set member is not hashable (i.e. doesn't define + __hash__ method). :await-outside-async (E1142): *'await' should be used within an async function* Emitted when await is used outside an async function. :redundant-keyword-arg (E1124): *Argument %r passed by position and keyword in %s call* @@ -1625,9 +1167,6 @@ Typecheck checker Messages :not-context-manager (E1129): *Context manager '%s' doesn't implement __enter__ and __exit__.* Used when an instance in a with statement doesn't implement the context manager protocol(__enter__/__exit__). -:unhashable-dict-key (E1140): *Dict key is unhashable* - Emitted when a dict key is not hashable (i.e. doesn't define __hash__ - method). :repeated-keyword (E1132): *Got multiple values for keyword argument %r in function call* Emitted when a function call got multiple values for a keyword. :invalid-metaclass (E1139): *Invalid metaclass %r used* @@ -1647,6 +1186,9 @@ Typecheck checker Messages :invalid-slice-index (E1127): *Slice index is not an int, None, or instance with __index__* Used when a slice index is not an integer, None, or an object with an __index__ method. +:invalid-slice-step (E1144): *Slice step cannot be 0* + Used when a slice step is 0 and the object doesn't implement a custom + __getitem__ method. :too-many-function-args (E1121): *Too many positional arguments for %s call* Used when a function call passes too many positional arguments. :unexpected-keyword-arg (E1123): *Unexpected keyword argument %r in %s call* @@ -1765,39 +1307,7 @@ Variables checker Verbatim name of the checker is ``variables``. -Variables checker Options -^^^^^^^^^^^^^^^^^^^^^^^^^ -:init-import: - Tells whether we should check for unused import in __init__ files. -:dummy-variables-rgx: - A regular expression matching the name of dummy variables (i.e. expected to - not be used). - - Default: ``_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_`` -:additional-builtins: - List of additional names supposed to be defined in builtins. Remember that - you should avoid defining new builtins when possible. -:callbacks: - List of strings which can identify a callback function by name. A callback - name must start or end with one of those strings. - - Default: ``cb_,_cb`` -:redefining-builtins-modules: - List of qualified module names which can have objects that can redefine - builtins. - - Default: ``six.moves,past.builtins,future.builtins,builtins,io`` -:ignored-argument-names: - Argument names that match this expression will be ignored. Default to name - with leading underscore. - - Default: ``_.*|^ignored_|^unused_`` -:allow-global-unused-variables: - Tells whether unused global variables should be treated as a violation. - - Default: ``yes`` -:allowed-redefined-builtins: - List of names allowed to shadow builtins +See also :ref:`variables checker's options' documentation ` Variables checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1830,7 +1340,9 @@ Variables checker Messages variable is not defined in the module scope. :self-cls-assignment (W0642): *Invalid assignment to %s in method* Invalid assignment to self or cls in instance or class method respectively. -:unbalanced-tuple-unpacking (W0632): *Possible unbalanced tuple unpacking with sequence%s: left side has %d label(s), right side has %d value(s)* +:unbalanced-dict-unpacking (W0644): *Possible unbalanced dict unpacking with %s: left side has %d label%s, right side has %d value%s* + Used when there is an unbalanced dict unpacking in assignment or for loop +:unbalanced-tuple-unpacking (W0632): *Possible unbalanced tuple unpacking with sequence %s: left side has %d label%s, right side has %d value%s* Used when there is an unbalanced tuple unpacking in assignment :possibly-unused-variable (W0641): *Possibly unused variable %r* Used when a variable is defined but might not be used. The possibility comes diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index 50c8ffd298..b83a2ca083 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -5,280 +5,208 @@ .. _all-options: -Standard Checkers: -^^^^^^^^^^^^^^^^^^ +Standard Checkers +^^^^^^^^^^^^^^^^^ + +.. _main-options: -``Main`` Checker -^^^^^^^^^^^^^^^^ +``Main`` **Checker** +-------------------- --analyse-fallback-blocks """"""""""""""""""""""""" +*Analyse import fallback blocks. This can be used to support both Python 2 and 3 compatible code, which means that the block might have code that exists only in one or another interpreter, leading to false positives when analysed.* + +**Default:** ``False`` + -Description: - *Analyse import fallback blocks. This can be used to support both Python 2 and 3 compatible code, which means that the block might have code that exists only in one or another interpreter, leading to false positives when analysed.* +--clear-cache-post-run +"""""""""""""""""""""" +*Clear in-memory caches upon conclusion of linting. Useful if running pylint in a server-like mode.* -Default: - ``False`` +**Default:** ``False`` --confidence """""""""""" +*Only show warnings with the listed confidence levels. Leave empty to show all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED.* -Description: - *Only show warnings with the listed confidence levels. Leave empty to show all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED.* - -Default: - ``['HIGH', 'CONTROL_FLOW', 'INFERENCE', 'INFERENCE_FAILURE', 'UNDEFINED']`` +**Default:** ``['HIGH', 'CONTROL_FLOW', 'INFERENCE', 'INFERENCE_FAILURE', 'UNDEFINED']`` --disable """"""""" +*Disable the message, report, category or checker with the given id(s). You can either give multiple identifiers separated by comma (,) or put this option multiple times (only on the command line, not in the configuration file where it should appear only once). You can also use "--disable=all" to disable everything first and then re-enable specific checks. For example, if you want to run only the similarities checker, you can use "--disable=all --enable=similarities". If you want to run only the classes checker, but have no Warning level messages displayed, use "--disable=all --enable=classes --disable=W".* -Description: - *Disable the message, report, category or checker with the given id(s). You can either give multiple identifiers separated by comma (,) or put this option multiple times (only on the command line, not in the configuration file where it should appear only once). You can also use "--disable=all" to disable everything first and then re-enable specific checks. For example, if you want to run only the similarities checker, you can use "--disable=all --enable=similarities". If you want to run only the classes checker, but have no Warning level messages displayed, use "--disable=all --enable=classes --disable=W".* - -Default: - ``()`` +**Default:** ``()`` --enable """""""" +*Enable the message, report, category or checker with the given id(s). You can either give multiple identifier separated by comma (,) or put this option multiple time (only on the command line, not in the configuration file where it should appear only once). See also the "--disable" option for examples.* -Description: - *Enable the message, report, category or checker with the given id(s). You can either give multiple identifier separated by comma (,) or put this option multiple time (only on the command line, not in the configuration file where it should appear only once). See also the "--disable" option for examples.* - -Default: - ``()`` +**Default:** ``()`` --evaluation """""""""""" +*Python expression which should return a score less than or equal to 10. You have access to the variables 'fatal', 'error', 'warning', 'refactor', 'convention', and 'info' which contain the number of messages in each category, as well as 'statement' which is the total number of statements analyzed. This score is used by the global evaluation report (RP0004).* -Description: - *Python expression which should return a score less than or equal to 10. You have access to the variables 'fatal', 'error', 'warning', 'refactor', 'convention', and 'info' which contain the number of messages in each category, as well as 'statement' which is the total number of statements analyzed. This score is used by the global evaluation report (RP0004).* - -Default: - ``max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))`` +**Default:** ``max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))`` --exit-zero """"""""""" +*Always return a 0 (non-error) status code, even if lint errors are found. This is primarily useful in continuous integration scripts.* -Description: - *Always return a 0 (non-error) status code, even if lint errors are found. This is primarily useful in continuous integration scripts.* - -Default: - ``False`` +**Default:** ``False`` --extension-pkg-allow-list """""""""""""""""""""""""" +*A comma-separated list of package or module names from where C extensions may be loaded. Extensions are loading into the active Python interpreter and may run arbitrary code.* -Description: - *A comma-separated list of package or module names from where C extensions may be loaded. Extensions are loading into the active Python interpreter and may run arbitrary code.* - -Default: - ``[]`` +**Default:** ``[]`` --extension-pkg-whitelist """"""""""""""""""""""""" +*A comma-separated list of package or module names from where C extensions may be loaded. Extensions are loading into the active Python interpreter and may run arbitrary code. (This is an alternative name to extension-pkg-allow-list for backward compatibility.)* -Description: - *A comma-separated list of package or module names from where C extensions may be loaded. Extensions are loading into the active Python interpreter and may run arbitrary code. (This is an alternative name to extension-pkg-allow-list for backward compatibility.)* - -Default: - ``[]`` +**Default:** ``[]`` --fail-on """"""""" +*Return non-zero exit code if any of these messages/categories are detected, even if score is above --fail-under value. Syntax same as enable. Messages specified are enabled, while categories only check already-enabled messages.* -Description: - *Return non-zero exit code if any of these messages/categories are detected, even if score is above --fail-under value. Syntax same as enable. Messages specified are enabled, while categories only check already-enabled messages.* - -Default: - ``""`` +**Default:** ``""`` --fail-under """""""""""" +*Specify a score threshold under which the program will exit with error.* -Description: - *Specify a score threshold to be exceeded before program exits with error.* - -Default: - ``10`` +**Default:** ``10`` --from-stdin """""""""""" +*Interpret the stdin as a python script, whose filename needs to be passed as the module_or_package argument.* -Description: - *Interpret the stdin as a python script, whose filename needs to be passed as the module_or_package argument.* - -Default: - ``False`` +**Default:** ``False`` --ignore """""""" +*Files or directories to be skipped. They should be base names, not paths.* -Description: - *Files or directories to be skipped. They should be base names, not paths.* - -Default: - ``('CVS',)`` +**Default:** ``('CVS',)`` --ignore-paths """""""""""""" +*Add files or directories matching the regular expressions patterns to the ignore-list. The regex matches against paths and can be in Posix or Windows format. Because '\\' represents the directory delimiter on Windows systems, it can't be used as an escape character.* -Description: - *Add files or directories matching the regex patterns to the ignore-list. The regex matches against paths and can be in Posix or Windows format.* - -Default: - ``[]`` +**Default:** ``[]`` --ignore-patterns """"""""""""""""" +*Files or directories matching the regular expression patterns are skipped. The regex matches against base names, not paths. The default value ignores Emacs file locks* -Description: - *Files or directories matching the regex patterns are skipped. The regex matches against base names, not paths. The default value ignores Emacs file locks* - -Default: - ``(re.compile('^\\.#'),)`` +**Default:** ``(re.compile('^\\.#'),)`` --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.* -Description: - *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.* - -Default: - ``()`` +**Default:** ``()`` --jobs """""" +*Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the number of processors available to use, and will cap the count on Windows to avoid hangs.* -Description: - *Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the number of processors available to use, and will cap the count on Windows to avoid hangs.* - -Default: - ``1`` +**Default:** ``1`` --limit-inference-results """"""""""""""""""""""""" +*Control the amount of potential inferred values when inferring a single object. This can help the performance when dealing with large functions or complex, nested conditions.* -Description: - *Control the amount of potential inferred values when inferring a single object. This can help the performance when dealing with large functions or complex, nested conditions.* - -Default: - ``100`` +**Default:** ``100`` --load-plugins """""""""""""" +*List of plugins (as comma separated values of python module names) to load, usually to register additional checkers.* -Description: - *List of plugins (as comma separated values of python module names) to load, usually to register additional checkers.* - -Default: - ``()`` +**Default:** ``()`` --msg-template """""""""""""" +*Template used to display messages. This is a python new-style format string used to format the message information. See doc for all details.* -Description: - *Template used to display messages. This is a python new-style format string used to format the message information. See doc for all details.* - -Default: - ``""`` +**Default:** ``""`` --output-format """"""""""""""" +*Set the output format. Available formats are text, parseable, colorized, json and msvs (visual studio). You can also give a reporter class, e.g. mypackage.mymodule.MyReporterClass.* -Description: - *Set the output format. Available formats are text, parseable, colorized, json and msvs (visual studio). You can also give a reporter class, e.g. mypackage.mymodule.MyReporterClass.* - -Default: - ``text`` +**Default:** ``text`` --persistent """""""""""" +*Pickle collected data for later comparisons.* -Description: - *Pickle collected data for later comparisons.* - -Default: - ``True`` +**Default:** ``True`` --py-version """""""""""" +*Minimum Python version to use for version dependent checks. Will default to the version used to run pylint.* -Description: - *Minimum Python version to use for version dependent checks. Will default to the version used to run pylint.* - -Default: - ``(3, 10)`` +**Default:** ``(3, 10)`` --recursive """"""""""" +*Discover python modules and packages in the file system subtree.* -Description: - *Discover python modules and packages in the file system subtree.* - -Default: - ``False`` +**Default:** ``False`` --reports """"""""" +*Tells whether to display a full report or only the messages.* -Description: - *Tells whether to display a full report or only the messages.* - -Default: - ``False`` +**Default:** ``False`` --score """"""" +*Activate the evaluation score.* -Description: - *Activate the evaluation score.* - -Default: - ``True`` +**Default:** ``True`` --suggestion-mode """"""""""""""""" +*When enabled, pylint would attempt to guess common misconfiguration and emit user-friendly hints instead of false-positive error messages.* -Description: - *When enabled, pylint would attempt to guess common misconfiguration and emit user-friendly hints instead of false-positive error messages.* - -Default: - ``True`` +**Default:** ``True`` --unsafe-load-any-extension """"""""""""""""""""""""""" +*Allow loading of arbitrary C extensions. Extensions are imported into the active Python interpreter and may run arbitrary code.* -Description: - *Allow loading of arbitrary C extensions. Extensions are imported into the active Python interpreter and may run arbitrary code.* - -Default: - ``False`` +**Default:** ``False`` @@ -287,18 +215,20 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml [tool.pylint.main] analyse-fallback-blocks = false + clear-cache-post-run = false + confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] - # disable = + disable = ["consider-using-augmented-assign"] - # enable = + enable = [] evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))" @@ -353,326 +283,232 @@ Default:
-``Basic`` Checker -^^^^^^^^^^^^^^^^^ +.. _basic-options: + +``Basic`` **Checker** +--------------------- --argument-naming-style """"""""""""""""""""""" +*Naming style matching correct argument names.* -Description: - *Naming style matching correct argument names.* - -Default: - ``snake_case`` +**Default:** ``snake_case`` --argument-rgx """""""""""""" +*Regular expression matching correct argument names. Overrides argument-naming-style. If left empty, argument names will be checked with the set naming style.* -Description: - *Regular expression matching correct argument names. Overrides argument-naming-style. If left empty, argument names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --attr-naming-style """"""""""""""""""" +*Naming style matching correct attribute names.* -Description: - *Naming style matching correct attribute names.* - -Default: - ``snake_case`` +**Default:** ``snake_case`` --attr-rgx """""""""" +*Regular expression matching correct attribute names. Overrides attr-naming-style. If left empty, attribute names will be checked with the set naming style.* -Description: - *Regular expression matching correct attribute names. Overrides attr-naming-style. If left empty, attribute names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --bad-names """"""""""" +*Bad variable names which should always be refused, separated by a comma.* -Description: - *Bad variable names which should always be refused, separated by a comma.* - -Default: - ``('foo', 'bar', 'baz', 'toto', 'tutu', 'tata')`` +**Default:** ``('foo', 'bar', 'baz', 'toto', 'tutu', 'tata')`` --bad-names-rgxs """""""""""""""" +*Bad variable names regexes, separated by a comma. If names match any regex, they will always be refused* -Description: - *Bad variable names regexes, separated by a comma. If names match any regex, they will always be refused* - -Default: - ``""`` +**Default:** ``""`` --class-attribute-naming-style """""""""""""""""""""""""""""" +*Naming style matching correct class attribute names.* -Description: - *Naming style matching correct class attribute names.* - -Default: - ``any`` +**Default:** ``any`` --class-attribute-rgx """"""""""""""""""""" +*Regular expression matching correct class attribute names. Overrides class-attribute-naming-style. If left empty, class attribute names will be checked with the set naming style.* -Description: - *Regular expression matching correct class attribute names. Overrides class-attribute-naming-style. If left empty, class attribute names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --class-const-naming-style """""""""""""""""""""""""" +*Naming style matching correct class constant names.* -Description: - *Naming style matching correct class constant names.* - -Default: - ``UPPER_CASE`` +**Default:** ``UPPER_CASE`` --class-const-rgx """"""""""""""""" +*Regular expression matching correct class constant names. Overrides class-const-naming-style. If left empty, class constant names will be checked with the set naming style.* -Description: - *Regular expression matching correct class constant names. Overrides class-const-naming-style. If left empty, class constant names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --class-naming-style """""""""""""""""""" +*Naming style matching correct class names.* -Description: - *Naming style matching correct class names.* - -Default: - ``PascalCase`` +**Default:** ``PascalCase`` --class-rgx """"""""""" +*Regular expression matching correct class names. Overrides class-naming-style. If left empty, class names will be checked with the set naming style.* -Description: - *Regular expression matching correct class names. Overrides class-naming-style. If left empty, class names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --const-naming-style """""""""""""""""""" +*Naming style matching correct constant names.* -Description: - *Naming style matching correct constant names.* - -Default: - ``UPPER_CASE`` +**Default:** ``UPPER_CASE`` --const-rgx """"""""""" +*Regular expression matching correct constant names. Overrides const-naming-style. If left empty, constant names will be checked with the set naming style.* -Description: - *Regular expression matching correct constant names. Overrides const-naming-style. If left empty, constant names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --docstring-min-length """""""""""""""""""""" +*Minimum line length for functions/classes that require docstrings, shorter ones are exempt.* -Description: - *Minimum line length for functions/classes that require docstrings, shorter ones are exempt.* - -Default: - ``-1`` +**Default:** ``-1`` --function-naming-style """"""""""""""""""""""" +*Naming style matching correct function names.* -Description: - *Naming style matching correct function names.* - -Default: - ``snake_case`` +**Default:** ``snake_case`` --function-rgx """""""""""""" +*Regular expression matching correct function names. Overrides function-naming-style. If left empty, function names will be checked with the set naming style.* -Description: - *Regular expression matching correct function names. Overrides function-naming-style. If left empty, function names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --good-names """""""""""" +*Good variable names which should always be accepted, separated by a comma.* -Description: - *Good variable names which should always be accepted, separated by a comma.* - -Default: - ``('i', 'j', 'k', 'ex', 'Run', '_')`` +**Default:** ``('i', 'j', 'k', 'ex', 'Run', '_')`` --good-names-rgxs """"""""""""""""" +*Good variable names regexes, separated by a comma. If names match any regex, they will always be accepted* -Description: - *Good variable names regexes, separated by a comma. If names match any regex, they will always be accepted* - -Default: - ``""`` +**Default:** ``""`` --include-naming-hint """"""""""""""""""""" +*Include a hint for the correct naming format with invalid-name.* -Description: - *Include a hint for the correct naming format with invalid-name.* - -Default: - ``False`` +**Default:** ``False`` --inlinevar-naming-style """""""""""""""""""""""" +*Naming style matching correct inline iteration names.* -Description: - *Naming style matching correct inline iteration names.* - -Default: - ``any`` +**Default:** ``any`` --inlinevar-rgx """"""""""""""" +*Regular expression matching correct inline iteration names. Overrides inlinevar-naming-style. If left empty, inline iteration names will be checked with the set naming style.* -Description: - *Regular expression matching correct inline iteration names. Overrides inlinevar-naming-style. If left empty, inline iteration names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --method-naming-style """"""""""""""""""""" +*Naming style matching correct method names.* -Description: - *Naming style matching correct method names.* - -Default: - ``snake_case`` +**Default:** ``snake_case`` --method-rgx """""""""""" +*Regular expression matching correct method names. Overrides method-naming-style. If left empty, method names will be checked with the set naming style.* -Description: - *Regular expression matching correct method names. Overrides method-naming-style. If left empty, method names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --module-naming-style """"""""""""""""""""" +*Naming style matching correct module names.* -Description: - *Naming style matching correct module names.* - -Default: - ``snake_case`` +**Default:** ``snake_case`` --module-rgx """""""""""" +*Regular expression matching correct module names. Overrides module-naming-style. If left empty, module names will be checked with the set naming style.* -Description: - *Regular expression matching correct module names. Overrides module-naming-style. If left empty, module names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --name-group """""""""""" +*Colon-delimited sets of names that determine each other's naming style when the name regexes allow several styles.* -Description: - *Colon-delimited sets of names that determine each other's naming style when the name regexes allow several styles.* - -Default: - ``()`` +**Default:** ``()`` --no-docstring-rgx """""""""""""""""" +*Regular expression which should only match function or class names that do not require a docstring.* -Description: - *Regular expression which should only match function or class names that do not require a docstring.* - -Default: - ``re.compile('^_')`` +**Default:** ``re.compile('^_')`` --property-classes """""""""""""""""" +*List of decorators that produce properties, such as abc.abstractproperty. Add to this list to register other decorators that produce valid properties. These decorators are taken in consideration only for invalid-name.* -Description: - *List of decorators that produce properties, such as abc.abstractproperty. Add to this list to register other decorators that produce valid properties. These decorators are taken in consideration only for invalid-name.* - -Default: - ``('abc.abstractproperty',)`` +**Default:** ``('abc.abstractproperty',)`` --typevar-rgx """"""""""""" +*Regular expression matching correct type variable names. If left empty, type variable names will be checked with the set naming style.* -Description: - *Regular expression matching correct type variable names. If left empty, type variable names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` --variable-naming-style """"""""""""""""""""""" +*Naming style matching correct variable names.* -Description: - *Naming style matching correct variable names.* - -Default: - ``snake_case`` +**Default:** ``snake_case`` --variable-rgx """""""""""""" +*Regular expression matching correct variable names. Overrides variable-naming-style. If left empty, variable names will be checked with the set naming style.* -Description: - *Regular expression matching correct variable names. Overrides variable-naming-style. If left empty, variable names will be checked with the set naming style.* - -Default: - ``None`` +**Default:** ``None`` @@ -681,7 +517,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -757,56 +593,43 @@ Default:
-``Classes`` Checker -^^^^^^^^^^^^^^^^^^^ +.. _classes-options: + +``Classes`` **Checker** +----------------------- --check-protected-access-in-special-methods """"""""""""""""""""""""""""""""""""""""""" +*Warn about protected attribute access inside special methods* -Description: - *Warn about protected attribute access inside special methods* - -Default: - ``False`` +**Default:** ``False`` --defining-attr-methods """"""""""""""""""""""" +*List of method names used to declare (i.e. assign) instance attributes.* -Description: - *List of method names used to declare (i.e. assign) instance attributes.* - -Default: - ``('__init__', '__new__', 'setUp', '__post_init__')`` +**Default:** ``('__init__', '__new__', 'setUp', '__post_init__')`` --exclude-protected """"""""""""""""""" +*List of member names, which should be excluded from the protected access warning.* -Description: - *List of member names, which should be excluded from the protected access warning.* - -Default: - ``('_asdict', '_fields', '_replace', '_source', '_make')`` +**Default:** ``('_asdict', '_fields', '_replace', '_source', '_make')`` --valid-classmethod-first-arg """"""""""""""""""""""""""""" +*List of valid names for the first argument in a class method.* -Description: - *List of valid names for the first argument in a class method.* - -Default: - ``('cls',)`` +**Default:** ``('cls',)`` --valid-metaclass-classmethod-first-arg """"""""""""""""""""""""""""""""""""""" +*List of valid names for the first argument in a metaclass class method.* -Description: - *List of valid names for the first argument in a metaclass class method.* - -Default: - ``('cls',)`` +**Default:** ``('mcs',)`` @@ -815,7 +638,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -828,7 +651,7 @@ Default: valid-classmethod-first-arg = ["cls"] - valid-metaclass-classmethod-first-arg = ["cls"] + valid-metaclass-classmethod-first-arg = ["mcs"] @@ -837,136 +660,99 @@ Default:
-``Design`` Checker -^^^^^^^^^^^^^^^^^^ +.. _design-options: + +``Design`` **Checker** +---------------------- --exclude-too-few-public-methods """""""""""""""""""""""""""""""" +*List of regular expressions of class ancestor names to ignore when counting public methods (see R0903)* -Description: - *List of regular expressions of class ancestor names to ignore when counting public methods (see R0903)* - -Default: - ``[]`` +**Default:** ``[]`` --ignored-parents """"""""""""""""" +*List of qualified class names to ignore when counting class parents (see R0901)* -Description: - *List of qualified class names to ignore when counting class parents (see R0901)* - -Default: - ``()`` +**Default:** ``()`` --max-args """""""""" +*Maximum number of arguments for function / method.* -Description: - *Maximum number of arguments for function / method.* - -Default: - ``5`` +**Default:** ``5`` --max-attributes """""""""""""""" +*Maximum number of attributes for a class (see R0902).* -Description: - *Maximum number of attributes for a class (see R0902).* - -Default: - ``7`` +**Default:** ``7`` --max-bool-expr """"""""""""""" +*Maximum number of boolean expressions in an if statement (see R0916).* -Description: - *Maximum number of boolean expressions in an if statement (see R0916).* - -Default: - ``5`` +**Default:** ``5`` --max-branches """""""""""""" +*Maximum number of branch for function / method body.* -Description: - *Maximum number of branch for function / method body.* - -Default: - ``12`` +**Default:** ``12`` --max-complexity """""""""""""""" +*McCabe complexity cyclomatic threshold* -Description: - *McCabe complexity cyclomatic threshold* - -Default: - ``10`` +**Default:** ``10`` --max-locals """""""""""" +*Maximum number of locals for function / method body.* -Description: - *Maximum number of locals for function / method body.* - -Default: - ``15`` +**Default:** ``15`` --max-parents """"""""""""" +*Maximum number of parents for a class (see R0901).* -Description: - *Maximum number of parents for a class (see R0901).* - -Default: - ``7`` +**Default:** ``7`` --max-public-methods """""""""""""""""""" +*Maximum number of public methods for a class (see R0904).* -Description: - *Maximum number of public methods for a class (see R0904).* - -Default: - ``20`` +**Default:** ``20`` --max-returns """"""""""""" +*Maximum number of return / yield for function / method body.* -Description: - *Maximum number of return / yield for function / method body.* - -Default: - ``6`` +**Default:** ``6`` --max-statements """""""""""""""" +*Maximum number of statements in function / method body.* -Description: - *Maximum number of statements in function / method body.* - -Default: - ``50`` +**Default:** ``50`` --min-public-methods """""""""""""""""""" +*Minimum number of public methods for a class (see R0903).* -Description: - *Minimum number of public methods for a class (see R0903).* - -Default: - ``2`` +**Default:** ``2`` @@ -975,7 +761,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1013,16 +799,15 @@ Default:
-``Exceptions`` Checker -^^^^^^^^^^^^^^^^^^^^^^ +.. _exceptions-options: + +``Exceptions`` **Checker** +-------------------------- --overgeneral-exceptions """""""""""""""""""""""" +*Exceptions that will emit a warning when caught.* -Description: - *Exceptions that will emit a warning when caught.* - -Default: - ``('BaseException', 'Exception')`` +**Default:** ``('builtins.BaseException', 'builtins.Exception')`` @@ -1031,12 +816,12 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml [tool.pylint.exceptions] - overgeneral-exceptions = ["BaseException", "Exception"] + overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] @@ -1045,86 +830,64 @@ Default:
-``Format`` Checker -^^^^^^^^^^^^^^^^^^ +.. _format-options: + +``Format`` **Checker** +---------------------- --expected-line-ending-format """"""""""""""""""""""""""""" +*Expected format of line ending, e.g. empty (any line ending), LF or CRLF.* -Description: - *Expected format of line ending, e.g. empty (any line ending), LF or CRLF.* - -Default: - ``""`` +**Default:** ``""`` --ignore-long-lines """"""""""""""""""" +*Regexp for a line that is allowed to be longer than the limit.* -Description: - *Regexp for a line that is allowed to be longer than the limit.* - -Default: - ``^\s*(# )??$`` +**Default:** ``^\s*(# )??$`` --indent-after-paren """""""""""""""""""" +*Number of spaces of indent required inside a hanging or continued line.* -Description: - *Number of spaces of indent required inside a hanging or continued line.* - -Default: - ``4`` +**Default:** ``4`` --indent-string """"""""""""""" +*String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 tab).* -Description: - *String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 tab).* - -Default: - `` `` +**Default:** `` `` --max-line-length """"""""""""""""" +*Maximum number of characters on a single line.* -Description: - *Maximum number of characters on a single line.* - -Default: - ``100`` +**Default:** ``100`` --max-module-lines """""""""""""""""" +*Maximum number of lines in a module.* -Description: - *Maximum number of lines in a module.* - -Default: - ``1000`` +**Default:** ``1000`` --single-line-class-stmt """""""""""""""""""""""" +*Allow the body of a class to be on the same line as the declaration if body contains single statement.* -Description: - *Allow the body of a class to be on the same line as the declaration if body contains single statement.* - -Default: - ``False`` +**Default:** ``False`` --single-line-if-stmt """"""""""""""""""""" +*Allow the body of an if to be on the same line as the test if there is no else.* -Description: - *Allow the body of an if to be on the same line as the test if there is no else.* - -Default: - ``False`` +**Default:** ``False`` @@ -1133,7 +896,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1161,96 +924,78 @@ Default:
-``Imports`` Checker -^^^^^^^^^^^^^^^^^^^ +.. _imports-options: + +``Imports`` **Checker** +----------------------- --allow-any-import-level """""""""""""""""""""""" +*List of modules that can be imported at any level, not just the top level one.* -Description: - *List of modules that can be imported at any level, not just the top level one.* +**Default:** ``()`` -Default: - ``()`` + +--allow-reexport-from-package +""""""""""""""""""""""""""""" +*Allow explicit reexports by alias from a package __init__.* + +**Default:** ``False`` --allow-wildcard-with-all """"""""""""""""""""""""" +*Allow wildcard imports from modules that define __all__.* -Description: - *Allow wildcard imports from modules that define __all__.* - -Default: - ``False`` +**Default:** ``False`` --deprecated-modules """""""""""""""""""" +*Deprecated modules which should not be used, separated by a comma.* -Description: - *Deprecated modules which should not be used, separated by a comma.* - -Default: - ``()`` +**Default:** ``()`` --ext-import-graph """""""""""""""""" +*Output a graph (.gv or any supported image format) of external dependencies to the given file (report RP0402 must not be disabled).* -Description: - *Output a graph (.gv or any supported image format) of external dependencies to the given file (report RP0402 must not be disabled).* - -Default: - ``""`` +**Default:** ``""`` --import-graph """""""""""""" +*Output a graph (.gv or any supported image format) of all (i.e. internal and external) dependencies to the given file (report RP0402 must not be disabled).* -Description: - *Output a graph (.gv or any supported image format) of all (i.e. internal and external) dependencies to the given file (report RP0402 must not be disabled).* - -Default: - ``""`` +**Default:** ``""`` --int-import-graph """""""""""""""""" +*Output a graph (.gv or any supported image format) of internal dependencies to the given file (report RP0402 must not be disabled).* -Description: - *Output a graph (.gv or any supported image format) of internal dependencies to the given file (report RP0402 must not be disabled).* - -Default: - ``""`` +**Default:** ``""`` --known-standard-library """""""""""""""""""""""" +*Force import order to recognize a module as part of the standard compatibility libraries.* -Description: - *Force import order to recognize a module as part of the standard compatibility libraries.* - -Default: - ``()`` +**Default:** ``()`` --known-third-party """"""""""""""""""" +*Force import order to recognize a module as part of a third party library.* -Description: - *Force import order to recognize a module as part of a third party library.* - -Default: - ``('enchant',)`` +**Default:** ``('enchant',)`` --preferred-modules """"""""""""""""""" +*Couples of modules and preferred modules, separated by a comma.* -Description: - *Couples of modules and preferred modules, separated by a comma.* - -Default: - ``()`` +**Default:** ``()`` @@ -1259,13 +1004,15 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml [tool.pylint.imports] allow-any-import-level = [] + allow-reexport-from-package = false + allow-wildcard-with-all = false deprecated-modules = [] @@ -1289,26 +1036,22 @@ Default:
-``Logging`` Checker -^^^^^^^^^^^^^^^^^^^ +.. _logging-options: + +``Logging`` **Checker** +----------------------- --logging-format-style """""""""""""""""""""" +*The type of string formatting that logging methods do. `old` means using % formatting, `new` is for `{}` formatting.* -Description: - *The type of string formatting that logging methods do. `old` means using % formatting, `new` is for `{}` formatting.* - -Default: - ``old`` +**Default:** ``old`` --logging-modules """"""""""""""""" +*Logging modules to check that the string format arguments are in logging function parameter format.* -Description: - *Logging modules to check that the string format arguments are in logging function parameter format.* - -Default: - ``('logging',)`` +**Default:** ``('logging',)`` @@ -1317,7 +1060,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1333,26 +1076,53 @@ Default:
-``Miscellaneous`` Checker -^^^^^^^^^^^^^^^^^^^^^^^^^ +.. _method_args-options: + +``Method_args`` **Checker** +--------------------------- +--timeout-methods +""""""""""""""""" +*List of qualified names (i.e., library.method) which require a timeout parameter e.g. 'requests.api.get,requests.api.post'* + +**Default:** ``('requests.api.delete', 'requests.api.get', 'requests.api.head', 'requests.api.options', 'requests.api.patch', 'requests.api.post', 'requests.api.put', 'requests.api.request')`` + + + +.. raw:: html + +
+ Example configuration section + +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. + +.. code-block:: toml + + [tool.pylint.method_args] + timeout-methods = ["requests.api.delete", "requests.api.get", "requests.api.head", "requests.api.options", "requests.api.patch", "requests.api.post", "requests.api.put", "requests.api.request"] + + + +.. raw:: html + +
+ + +.. _miscellaneous-options: + +``Miscellaneous`` **Checker** +----------------------------- --notes """"""" +*List of note tags to take in consideration, separated by a comma.* -Description: - *List of note tags to take in consideration, separated by a comma.* - -Default: - ``('FIXME', 'XXX', 'TODO')`` +**Default:** ``('FIXME', 'XXX', 'TODO')`` --notes-rgx """"""""""" +*Regular expression of note tags to take in consideration.* -Description: - *Regular expression of note tags to take in consideration.* - -Default: - ``""`` +**Default:** ``""`` @@ -1361,7 +1131,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1377,26 +1147,22 @@ Default:
-``Refactoring`` Checker -^^^^^^^^^^^^^^^^^^^^^^^ +.. _refactoring-options: + +``Refactoring`` **Checker** +--------------------------- --max-nested-blocks """"""""""""""""""" +*Maximum number of nested blocks for function / method body* -Description: - *Maximum number of nested blocks for function / method body* - -Default: - ``5`` +**Default:** ``5`` --never-returning-functions """"""""""""""""""""""""""" +*Complete name of functions that never returns. When checking for inconsistent-return-statements if a never returning function is called then it will be considered as an explicit return statement and no message will be printed.* -Description: - *Complete name of functions that never returns. When checking for inconsistent-return-statements if a never returning function is called then it will be considered as an explicit return statement and no message will be printed.* - -Default: - ``('sys.exit', 'argparse.parse_error')`` +**Default:** ``('sys.exit', 'argparse.parse_error')`` @@ -1405,7 +1171,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1421,56 +1187,43 @@ Default:
-``Similarities`` Checker -^^^^^^^^^^^^^^^^^^^^^^^^ +.. _similarities-options: + +``Similarities`` **Checker** +---------------------------- --ignore-comments """"""""""""""""" +*Comments are removed from the similarity computation* -Description: - *Comments are removed from the similarity computation* - -Default: - ``True`` +**Default:** ``True`` --ignore-docstrings """"""""""""""""""" +*Docstrings are removed from the similarity computation* -Description: - *Docstrings are removed from the similarity computation* - -Default: - ``True`` +**Default:** ``True`` --ignore-imports """""""""""""""" +*Imports are removed from the similarity computation* -Description: - *Imports are removed from the similarity computation* - -Default: - ``True`` +**Default:** ``True`` --ignore-signatures """"""""""""""""""" +*Signatures are removed from the similarity computation* -Description: - *Signatures are removed from the similarity computation* - -Default: - ``True`` +**Default:** ``True`` --min-similarity-lines """""""""""""""""""""" +*Minimum lines number of a similarity.* -Description: - *Minimum lines number of a similarity.* - -Default: - ``4`` +**Default:** ``4`` @@ -1479,7 +1232,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1501,66 +1254,50 @@ Default:
-``Spelling`` Checker -^^^^^^^^^^^^^^^^^^^^ +.. _spelling-options: + +``Spelling`` **Checker** +------------------------ --max-spelling-suggestions """""""""""""""""""""""""" +*Limits count of emitted suggestions for spelling mistakes.* -Description: - *Limits count of emitted suggestions for spelling mistakes.* - -Default: - ``4`` +**Default:** ``4`` --spelling-dict """"""""""""""" +*Spelling dictionary name. Available dictionaries: en (aspell), en_AU (aspell), en_CA (aspell), en_GB (aspell), en_US (aspell).* -Description: - *Spelling dictionary name. Available dictionaries: none. To make it work, install the 'python-enchant' package.* - -Default: - ``""`` +**Default:** ``""`` --spelling-ignore-comment-directives """""""""""""""""""""""""""""""""""" +*List of comma separated words that should be considered directives if they appear at the beginning of a comment and should not be checked.* -Description: - *List of comma separated words that should be considered directives if they appear at the beginning of a comment and should not be checked.* - -Default: - ``fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:`` +**Default:** ``fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:`` --spelling-ignore-words """"""""""""""""""""""" +*List of comma separated words that should not be checked.* -Description: - *List of comma separated words that should not be checked.* - -Default: - ``""`` +**Default:** ``""`` --spelling-private-dict-file """""""""""""""""""""""""""" +*A path to a file that contains the private dictionary; one word per line.* -Description: - *A path to a file that contains the private dictionary; one word per line.* - -Default: - ``""`` +**Default:** ``""`` --spelling-store-unknown-words """""""""""""""""""""""""""""" +*Tells whether to store unknown words to the private dictionary (see the --spelling-private-dict-file option) instead of raising a message.* -Description: - *Tells whether to store unknown words to the private dictionary (see the --spelling-private-dict-file option) instead of raising a message.* - -Default: - ``n`` +**Default:** ``n`` @@ -1569,7 +1306,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1593,26 +1330,22 @@ Default:
-``String`` Checker -^^^^^^^^^^^^^^^^^^ +.. _string-options: + +``String`` **Checker** +---------------------- --check-quote-consistency """"""""""""""""""""""""" +*This flag controls whether inconsistent-quotes generates a warning when the character used as a quote delimiter is used inconsistently within a module.* -Description: - *This flag controls whether inconsistent-quotes generates a warning when the character used as a quote delimiter is used inconsistently within a module.* - -Default: - ``False`` +**Default:** ``False`` --check-str-concat-over-line-jumps """""""""""""""""""""""""""""""""" +*This flag controls whether the implicit-str-concat should generate a warning on implicit string concatenation in sequences defined over several lines.* -Description: - *This flag controls whether the implicit-str-concat should generate a warning on implicit string concatenation in sequences defined over several lines.* - -Default: - ``False`` +**Default:** ``False`` @@ -1621,7 +1354,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1637,126 +1370,92 @@ Default:
-``Typecheck`` Checker -^^^^^^^^^^^^^^^^^^^^^ +.. _typecheck-options: + +``Typecheck`` **Checker** +------------------------- --contextmanager-decorators """"""""""""""""""""""""""" +*List of decorators that produce context managers, such as contextlib.contextmanager. Add to this list to register other decorators that produce valid context managers.* -Description: - *List of decorators that produce context managers, such as contextlib.contextmanager. Add to this list to register other decorators that produce valid context managers.* - -Default: - ``['contextlib.contextmanager']`` +**Default:** ``['contextlib.contextmanager']`` --generated-members """"""""""""""""""" +*List of members which are set dynamically and missed by pylint inference system, and so shouldn't trigger E1101 when accessed. Python regular expressions are accepted.* -Description: - *List of members which are set dynamically and missed by pylint inference system, and so shouldn't trigger E1101 when accessed. Python regular expressions are accepted.* - -Default: - ``()`` +**Default:** ``()`` --ignore-mixin-members """""""""""""""""""""" +*Tells whether missing members accessed in mixin class should be ignored. A class is considered mixin if its name matches the mixin-class-rgx option.* -Description: - *Tells whether missing members accessed in mixin class should be ignored. A class is considered mixin if its name matches the mixin-class-rgx option.* - -Default: - ``True`` +**Default:** ``True`` --ignore-none """"""""""""" +*Tells whether to warn about missing members when the owner of the attribute is inferred to be None.* -Description: - *Tells whether to warn about missing members when the owner of the attribute is inferred to be None.* - -Default: - ``True`` +**Default:** ``True`` --ignore-on-opaque-inference """""""""""""""""""""""""""" +*This flag controls whether pylint should warn about no-member and similar checks whenever an opaque object is returned when inferring. The inference can return multiple potential results while evaluating a Python object, but some branches might not be evaluated, which results in partial inference. In that case, it might be useful to still emit no-member and other checks for the rest of the inferred objects.* -Description: - *This flag controls whether pylint should warn about no-member and similar checks whenever an opaque object is returned when inferring. The inference can return multiple potential results while evaluating a Python object, but some branches might not be evaluated, which results in partial inference. In that case, it might be useful to still emit no-member and other checks for the rest of the inferred objects.* - -Default: - ``True`` +**Default:** ``True`` --ignored-checks-for-mixins """"""""""""""""""""""""""" +*List of symbolic message names to ignore for Mixin members.* -Description: - *List of symbolic message names to ignore for Mixin members.* - -Default: - ``['no-member', 'not-async-context-manager', 'not-context-manager', 'attribute-defined-outside-init']`` +**Default:** ``['no-member', 'not-async-context-manager', 'not-context-manager', 'attribute-defined-outside-init']`` --ignored-classes """"""""""""""""" +*List of class names for which member attributes should not be checked (useful for classes with dynamically set attributes). This supports the use of qualified names.* -Description: - *List of class names for which member attributes should not be checked (useful for classes with dynamically set attributes). This supports the use of qualified names.* - -Default: - ``('optparse.Values', 'thread._local', '_thread._local', 'argparse.Namespace')`` +**Default:** ``('optparse.Values', 'thread._local', '_thread._local', 'argparse.Namespace')`` --missing-member-hint """"""""""""""""""""" +*Show a hint with possible names when a member name was not found. The aspect of finding the hint is based on edit distance.* -Description: - *Show a hint with possible names when a member name was not found. The aspect of finding the hint is based on edit distance.* - -Default: - ``True`` +**Default:** ``True`` --missing-member-hint-distance """""""""""""""""""""""""""""" +*The minimum edit distance a name should have in order to be considered a similar match for a missing member name.* -Description: - *The minimum edit distance a name should have in order to be considered a similar match for a missing member name.* - -Default: - ``1`` +**Default:** ``1`` --missing-member-max-choices """""""""""""""""""""""""""" +*The total number of similar names that should be taken in consideration when showing a hint for a missing member.* -Description: - *The total number of similar names that should be taken in consideration when showing a hint for a missing member.* - -Default: - ``1`` +**Default:** ``1`` --mixin-class-rgx """"""""""""""""" +*Regex pattern to define which classes are considered mixins.* -Description: - *Regex pattern to define which classes are considered mixins.* - -Default: - ``.*[Mm]ixin`` +**Default:** ``.*[Mm]ixin`` --signature-mutators """""""""""""""""""" +*List of decorators that change the signature of a decorated function.* -Description: - *List of decorators that change the signature of a decorated function.* - -Default: - ``[]`` +**Default:** ``[]`` @@ -1765,7 +1464,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1801,86 +1500,64 @@ Default:
-``Variables`` Checker -^^^^^^^^^^^^^^^^^^^^^ +.. _variables-options: + +``Variables`` **Checker** +------------------------- --additional-builtins """"""""""""""""""""" +*List of additional names supposed to be defined in builtins. Remember that you should avoid defining new builtins when possible.* -Description: - *List of additional names supposed to be defined in builtins. Remember that you should avoid defining new builtins when possible.* - -Default: - ``()`` +**Default:** ``()`` --allow-global-unused-variables """"""""""""""""""""""""""""""" +*Tells whether unused global variables should be treated as a violation.* -Description: - *Tells whether unused global variables should be treated as a violation.* - -Default: - ``True`` +**Default:** ``True`` --allowed-redefined-builtins """""""""""""""""""""""""""" +*List of names allowed to shadow builtins* -Description: - *List of names allowed to shadow builtins* - -Default: - ``()`` +**Default:** ``()`` --callbacks """"""""""" +*List of strings which can identify a callback function by name. A callback name must start or end with one of those strings.* -Description: - *List of strings which can identify a callback function by name. A callback name must start or end with one of those strings.* - -Default: - ``('cb_', '_cb')`` +**Default:** ``('cb_', '_cb')`` --dummy-variables-rgx """"""""""""""""""""" +*A regular expression matching the name of dummy variables (i.e. expected to not be used).* -Description: - *A regular expression matching the name of dummy variables (i.e. expected to not be used).* - -Default: - ``_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_`` +**Default:** ``_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_`` --ignored-argument-names """""""""""""""""""""""" +*Argument names that match this expression will be ignored.* -Description: - *Argument names that match this expression will be ignored. Default to name with leading underscore.* - -Default: - ``re.compile('_.*|^ignored_|^unused_')`` +**Default:** ``re.compile('_.*|^ignored_|^unused_')`` --init-import """"""""""""" +*Tells whether we should check for unused import in __init__ files.* -Description: - *Tells whether we should check for unused import in __init__ files.* - -Default: - ``False`` +**Default:** ``False`` --redefining-builtins-modules """"""""""""""""""""""""""""" +*List of qualified module names which can have objects that can redefine builtins.* -Description: - *List of qualified module names which can have objects that can redefine builtins.* - -Default: - ``('six.moves', 'past.builtins', 'future.builtins', 'builtins', 'io')`` +**Default:** ``('six.moves', 'past.builtins', 'future.builtins', 'builtins', 'io')`` @@ -1889,7 +1566,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1917,20 +1594,19 @@ Default:
-Extensions: -^^^^^^^^^^^ +Extensions +^^^^^^^^^^ + +.. _broad_try_clause-options: -``Broad_try_clause`` Checker -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``Broad_try_clause`` **Checker** +-------------------------------- --max-try-statements """""""""""""""""""" +*Maximum number of statements allowed in a try clause* -Description: - *Maximum number of statements allowed in a try clause* - -Default: - ``1`` +**Default:** ``1`` @@ -1939,7 +1615,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1953,16 +1629,15 @@ Default:
-``Code_style`` Checker -^^^^^^^^^^^^^^^^^^^^^^ +.. _code_style-options: + +``Code_style`` **Checker** +-------------------------- --max-line-length-suggestions """"""""""""""""""""""""""""" +*Max line length for which to sill emit suggestions. Used to prevent optional suggestions which would get split by a code formatter (e.g., black). Will default to the setting for ``max-line-length``.* -Description: - *Max line length for which to sill emit suggestions. Used to prevent optional suggestions which would get split by a code formatter (e.g., black). Will default to the setting for ``max-line-length``.* - -Default: - ``0`` +**Default:** ``0`` @@ -1971,7 +1646,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -1985,16 +1660,15 @@ Default:
-``Deprecated_builtins`` Checker -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. _deprecated_builtins-options: + +``Deprecated_builtins`` **Checker** +----------------------------------- --bad-functions """"""""""""""" +*List of builtins function names that should not be used, separated by a comma* -Description: - *List of builtins function names that should not be used, separated by a comma* - -Default: - ``['map', 'filter']`` +**Default:** ``['map', 'filter']`` @@ -2003,7 +1677,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -2017,56 +1691,105 @@ Default:
-``Parameter_documentation`` Checker -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. _dunder-options: + +``Dunder`` **Checker** +---------------------- +--good-dunder-names +""""""""""""""""""" +*Good dunder names which should always be accepted.* + +**Default:** ``[]`` + + + +.. raw:: html + +
+ Example configuration section + +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. + +.. code-block:: toml + + [tool.pylint.dunder] + good-dunder-names = [] + + + +.. raw:: html + +
+ + +.. _magic-value-options: + +``Magic-value`` **Checker** +--------------------------- +--valid-magic-values +"""""""""""""""""""" +*List of valid magic values that `magic-value-compare` will not detect. Supports integers, floats, negative numbers, for empty string enter ``''``, for backslash values just use one backslash e.g \n.* + +**Default:** ``(0, -1, 1, '', '__main__')`` + + + +.. raw:: html + +
+ Example configuration section + +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. + +.. code-block:: toml + + [tool.pylint.magic-value] + valid-magic-values = [0, -1, 1, "", "__main__"] + + + +.. raw:: html + +
+ + +.. _parameter_documentation-options: + +``Parameter_documentation`` **Checker** +--------------------------------------- --accept-no-param-doc """"""""""""""""""""" +*Whether to accept totally missing parameter documentation in the docstring of a function that has parameters.* -Description: - *Whether to accept totally missing parameter documentation in the docstring of a function that has parameters.* - -Default: - ``True`` +**Default:** ``True`` --accept-no-raise-doc """"""""""""""""""""" +*Whether to accept totally missing raises documentation in the docstring of a function that raises an exception.* -Description: - *Whether to accept totally missing raises documentation in the docstring of a function that raises an exception.* - -Default: - ``True`` +**Default:** ``True`` --accept-no-return-doc """""""""""""""""""""" +*Whether to accept totally missing return documentation in the docstring of a function that returns a statement.* -Description: - *Whether to accept totally missing return documentation in the docstring of a function that returns a statement.* - -Default: - ``True`` +**Default:** ``True`` --accept-no-yields-doc """""""""""""""""""""" +*Whether to accept totally missing yields documentation in the docstring of a generator.* -Description: - *Whether to accept totally missing yields documentation in the docstring of a generator.* - -Default: - ``True`` +**Default:** ``True`` --default-docstring-type """""""""""""""""""""""" +*If the docstring type cannot be guessed the specified docstring type will be used.* -Description: - *If the docstring type cannot be guessed the specified docstring type will be used.* - -Default: - ``default`` +**Default:** ``default`` @@ -2075,7 +1798,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml @@ -2097,16 +1820,15 @@ Default:
-``Typing`` Checker -^^^^^^^^^^^^^^^^^^ +.. _typing-options: + +``Typing`` **Checker** +---------------------- --runtime-typing """""""""""""""" +*Set to ``no`` if the app / library does **NOT** need to support runtime introspection of type annotations. If you use type annotations **exclusively** for type checking of an application, you're probably fine. For libraries, evaluate if some users want to access the type hints at runtime first, e.g., through ``typing.get_type_hints``. Applies to Python versions 3.7 - 3.9* -Description: - *Set to ``no`` if the app / library does **NOT** need to support runtime introspection of type annotations. If you use type annotations **exclusively** for type checking of an application, you're probably fine. For libraries, evaluate if some users what to access the type hints at runtime first, e.g., through ``typing.get_type_hints``. Applies to Python versions 3.7 - 3.9* - -Default: - ``True`` +**Default:** ``True`` @@ -2115,7 +1837,7 @@ Default:
Example configuration section -**Note:** Only ``pylint.tool`` is required, the section title is not. These are the default values. +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. .. code-block:: toml diff --git a/doc/user_guide/configuration/index.rst b/doc/user_guide/configuration/index.rst index d0d8b695b7..ffe8c51a38 100644 --- a/doc/user_guide/configuration/index.rst +++ b/doc/user_guide/configuration/index.rst @@ -7,6 +7,24 @@ Configuration Pylint is highly configurable. There are a lot of options to follow the needs of various projects and a lot of checks to activate if they suit your style. +You can generate a sample configuration file with ``--generate-toml-config`` +or ``--generate-rcfile``. Every option present on the command line before this +will be included in the file. + +For example:: + + pylint --disable=bare-except,invalid-name --class-rgx='[A-Z][a-z]+' --generate-toml-config + +In practice, it is often better to create a minimal configuration file which only contains +configuration overrides. For all other options, Pylint will use its default values. + +.. note:: + + The internals that create the configuration files fall back to the default values if + no other value was given. This means that some values depend on the interpreter that + was used to generate the file. Most notably ``py-version`` which defaults to the + current interpreter. + .. toctree:: :maxdepth: 2 :titlesonly: diff --git a/doc/user_guide/installation/command_line_installation.rst b/doc/user_guide/installation/command_line_installation.rst index e881667ae0..6d3aaac1ea 100644 --- a/doc/user_guide/installation/command_line_installation.rst +++ b/doc/user_guide/installation/command_line_installation.rst @@ -10,17 +10,19 @@ works with your interpreter. We recommend ``pip``: pip install pylint -Use the newest Python interpreter if you can. +Or if you want to also check spelling with ``enchant`` (you might need to +`install the enchant C library `_): -It's possible to analyse code written for older interpreters by using the ``py-version`` -option and setting it to the old interpreter. For example you can check that there are -no ``f-strings`` in Python 3.5 code using Python 3.8 with an up-to-date pylint even if -Python 3.5 is past end of life (EOL). +.. code-block:: sh + + pip install pylint[spelling] + +The newest pylint supports all Python interpreters that are not past end of life. -We do not guarantee that ``py-version`` will work for all EOL python interpreters indefinitely, -(for anything before python 3.5, it probably won't). If a newer version does not work for you, -the best available pylint might be an old version that works with your old interpreter but -without the bug fixes and features of later versions. +We recommend to use the latest interpreter because we rely on the ``ast`` builtin +module that gets better with each new Python interpreter. For example a Python +3.6 interpreter can't analyse 3.8 syntax (amongst others, because of the new walrus operator) while a 3.8 +interpreter can also deal with Python 3.6. See :ref:`using pylint with multiple interpreters ` for more details. .. note:: You can also use ``conda`` or your system package manager on debian based OS. diff --git a/doc/user_guide/installation/ide_integration/flymake-emacs.rst b/doc/user_guide/installation/ide_integration/flymake-emacs.rst index 44b9b3262b..79310ff59d 100644 --- a/doc/user_guide/installation/ide_integration/flymake-emacs.rst +++ b/doc/user_guide/installation/ide_integration/flymake-emacs.rst @@ -4,12 +4,10 @@ Using Pylint through Flymake in Emacs ===================================== .. warning:: - If you're reading this doc and are actually using flymake please - open a support question at https://github.com/PyCQA/pylint/issues/new/choose - and tell us, we don't have any maintainers for emacs and are thinking about - dropping the support. - -.. TODO 3.0, do we still need to support flymake ? + The Emacs package now has its own repository and is looking for a maintainer. + If you're reading this doc and are interested in maintaining this package or + are actually using flymake please open an issue at + https://github.com/emacsorphanage/pylint/issues/new/choose To enable Flymake for Python, insert the following into your .emacs: diff --git a/doc/user_guide/installation/ide_integration/index.rst b/doc/user_guide/installation/ide_integration/index.rst index c1bf5eb4df..c359c8ee15 100644 --- a/doc/user_guide/installation/ide_integration/index.rst +++ b/doc/user_guide/installation/ide_integration/index.rst @@ -18,6 +18,7 @@ Below you can find tutorials for some of the most common ones. - PyDev_ - pyscripter_ in the `Tool -> Tools` menu. - Spyder_ in the `View -> Panes -> Static code analysis` +- `Sublime Text`_ - :ref:`TextMate ` - Vim_ - `Visual Studio Code`_ in the `Preferences -> Settings` menu @@ -36,6 +37,7 @@ Below you can find tutorials for some of the most common ones. .. _pydev: https://www.pydev.org/manual_adv_pylint.html .. _pyscripter: https://github.com/pyscripter/pyscripter .. _spyder: https://docs.spyder-ide.org/current/panes/pylint.html +.. _Sublime Text: https://packagecontrol.io/packages/SublimeLinter-pylint .. _Vim: https://www.vim.org/scripts/script.php?script_id=891 .. _Visual Studio: https://docs.microsoft.com/visualstudio/python/code-pylint .. _Visual Studio Code: https://code.visualstudio.com/docs/python/linting#_pylint diff --git a/doc/user_guide/installation/index.rst b/doc/user_guide/installation/index.rst index 806f748892..c7ec0a8d3c 100644 --- a/doc/user_guide/installation/index.rst +++ b/doc/user_guide/installation/index.rst @@ -3,9 +3,10 @@ Installation Pylint can be installed: -- As a command line tool -- Integrated in your editor/ide -- As a pre-commit hook +- :ref:`As a command line tool ` +- :ref:`Integrated in your editor/ide ` +- :ref:`As a pre-commit hook ` +- :ref:`For multiple python interpreters in your continuous integration ` .. toctree:: :maxdepth: 2 @@ -15,5 +16,6 @@ Pylint can be installed: command_line_installation.rst ide_integration/index pre-commit-integration.rst + with-multiple-interpreters.rst badge upgrading_pylint.rst diff --git a/doc/user_guide/installation/with-multiple-interpreters.rst b/doc/user_guide/installation/with-multiple-interpreters.rst new file mode 100644 index 0000000000..b61d298823 --- /dev/null +++ b/doc/user_guide/installation/with-multiple-interpreters.rst @@ -0,0 +1,15 @@ +.. _continuous-integration: + +Installation with multiple interpreters +======================================= + +It's possible to analyse code written for older or multiple interpreters by using +the ``py-version`` option and setting it to the oldest supported interpreter of your code. For example you can check +that there are no ``f-strings`` in Python 3.5 code using Python 3.8 with an up-to-date +pylint even if Python 3.5 is past end of life (EOL) and the version of pylint you use is not +compatible with it. + +We do not guarantee that ``py-version`` will work for all EOL Python interpreters indefinitely, +(for anything before Python 3.5, it probably won't). If a newer version does not work for you, +the best available pylint might be an old version that works with your old interpreter but +without the bug fixes and features of later versions. diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index b5ae469def..6c4405b429 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -56,7 +56,7 @@ All messages in the error category: error/await-outside-async error/bad-configuration-section error/bad-except-order - error/bad-exception-context + error/bad-exception-cause error/bad-format-character error/bad-plugin-value error/bad-reversed-sequence @@ -102,6 +102,7 @@ All messages in the error category: error/invalid-repr-returned error/invalid-sequence-index error/invalid-slice-index + error/invalid-slice-step error/invalid-slots error/invalid-slots-object error/invalid-star-assignment-target @@ -136,6 +137,7 @@ All messages in the error category: error/not-context-manager error/not-in-loop error/notimplemented-raised + error/positional-only-arguments-expected error/potential-index-error error/raising-bad-type error/raising-non-exception @@ -145,6 +147,8 @@ All messages in the error category: error/return-arg-in-generator error/return-in-init error/return-outside-function + error/singledispatch-method + error/singledispatchmethod-function error/star-needs-assignment-target error/syntax-error error/too-few-format-args @@ -156,7 +160,7 @@ All messages in the error category: error/undefined-variable error/unexpected-keyword-arg error/unexpected-special-method-signature - error/unhashable-dict-key + error/unhashable-member error/unpacking-non-sequence error/unrecognized-inline-option error/unrecognized-option @@ -177,10 +181,12 @@ All renamed messages in the error category: :titlesonly: error/bad-context-manager + error/bad-exception-context error/bad-option-value error/maybe-no-member error/old-non-iterator-returned-2 error/old-unbalanced-tuple-unpacking + error/unhashable-dict-key .. _warning-category: @@ -203,6 +209,7 @@ All messages in the warning category: warning/assert-on-tuple warning/attribute-defined-outside-init warning/bad-builtin + warning/bad-dunder-name warning/bad-format-string warning/bad-format-string-key warning/bad-indentation @@ -212,7 +219,8 @@ All messages in the warning category: warning/bare-except warning/binary-op-exception warning/boolean-datetime - warning/broad-except + warning/broad-exception-caught + warning/broad-exception-raised warning/cell-var-from-loop warning/comparison-with-callable warning/confusing-with-statement @@ -265,17 +273,21 @@ All messages in the warning category: warning/missing-raises-doc warning/missing-return-doc warning/missing-return-type-doc + warning/missing-timeout warning/missing-type-doc warning/missing-yield-doc warning/missing-yield-type-doc warning/modified-iterating-list warning/multiple-constructor-doc + warning/named-expr-without-context warning/nan-comparison + warning/nested-min-max warning/non-ascii-file-name warning/non-parent-init-called warning/non-str-assignment-to-dunder-name warning/overlapping-except warning/overridden-final-method + warning/pointless-exception-statement warning/pointless-statement warning/pointless-string-statement warning/possibly-unused-variable @@ -295,6 +307,7 @@ All messages in the warning category: warning/reimported warning/self-assigning-variable warning/self-cls-assignment + warning/shadowed-import warning/shallow-copy-environ warning/signature-differs warning/subclassed-final-class @@ -304,6 +317,7 @@ All messages in the warning category: warning/super-without-brackets warning/too-many-try-statements warning/try-except-raise + warning/unbalanced-dict-unpacking warning/unbalanced-tuple-unpacking warning/undefined-loop-variable warning/unknown-option-value @@ -322,7 +336,7 @@ All messages in the warning category: warning/unused-wildcard-import warning/useless-else-on-loop warning/useless-param-doc - warning/useless-super-delegation + warning/useless-parent-delegation warning/useless-type-doc warning/useless-with-lock warning/using-constant-test @@ -338,10 +352,16 @@ All renamed messages in the warning category: :maxdepth: 1 :titlesonly: + warning/broad-except warning/cache-max-size-none warning/implicit-str-concat-in-sequence warning/lru-cache-decorating-method warning/old-assignment-from-none + warning/old-deprecated-argument + warning/old-deprecated-class + warning/old-deprecated-decorator + warning/old-deprecated-method + warning/old-deprecated-module warning/old-empty-docstring warning/old-missing-param-doc warning/old-missing-returns-doc @@ -350,6 +370,7 @@ All renamed messages in the warning category: warning/old-non-iterator-returned-1 warning/old-unidiomatic-typecheck warning/old-unpacking-non-sequence + warning/useless-super-delegation .. _convention-category: @@ -374,6 +395,7 @@ All messages in the convention category: convention/consider-using-dict-items convention/consider-using-enumerate convention/consider-using-f-string + convention/dict-init-mutate convention/disallowed-name convention/docstring-first-line-empty convention/empty-docstring @@ -448,9 +470,11 @@ All messages in the refactor category: refactor/confusing-consecutive-elif refactor/consider-alternative-union-syntax refactor/consider-merging-isinstance + refactor/consider-refactoring-into-while-condition refactor/consider-swap-variables refactor/consider-using-alias refactor/consider-using-assignment-expr + refactor/consider-using-augmented-assign refactor/consider-using-dict-comprehension refactor/consider-using-from-import refactor/consider-using-generator @@ -471,6 +495,7 @@ All messages in the refactor category: refactor/empty-comment refactor/inconsistent-return-statements refactor/literal-comparison + refactor/magic-value-comparison refactor/no-classmethod-decorator refactor/no-else-break refactor/no-else-continue @@ -481,6 +506,7 @@ All messages in the refactor category: refactor/property-with-parameters refactor/redefined-argument-from-local refactor/redefined-variable-type + refactor/redundant-typehint-argument refactor/simplifiable-condition refactor/simplifiable-if-expression refactor/simplifiable-if-statement diff --git a/doc/user_guide/usage/output.rst b/doc/user_guide/usage/output.rst index c98eb8476b..50a7fa76e4 100644 --- a/doc/user_guide/usage/output.rst +++ b/doc/user_guide/usage/output.rst @@ -131,7 +131,7 @@ Following the analysis message, Pylint can display a set of reports, each one focusing on a particular aspect of the project, such as number of messages by categories, modules dependencies. These features can be enabled through the ``--reports=y`` option, or its shorthand -version ``-rn``. +version ``-ry``. For instance, the metrics report displays summaries gathered from the current run. diff --git a/doc/user_guide/usage/run.rst b/doc/user_guide/usage/run.rst index 9a15d04a38..84e1a8e2f7 100644 --- a/doc/user_guide/usage/run.rst +++ b/doc/user_guide/usage/run.rst @@ -9,22 +9,24 @@ Pylint is meant to be called from the command line. The usage is :: pylint [options] modules_or_packages -By default the ``pylint`` command only accepts a list of python modules and packages. Using a -directory which is not a package results in an error:: +By default the ``pylint`` command only accepts a list of python modules and packages. +On versions below 2.15, specifying a directory that is not an explicit package +(with ``__init__.py``) results in an error:: pylint mydir ************* Module mydir mydir/__init__.py:1:0: F0010: error while code parsing: Unable to load file mydir/__init__.py: [Errno 2] No such file or directory: 'mydir/__init__.py' (parse-error) -When ``--recursive=y`` option is used, modules and packages are also accepted as parameters:: +Thus, on versions before 2.15, or when dealing with certain edge cases that have not yet been solved, +using the ``--recursive=y`` option allows for linting a namespace package:: pylint --recursive=y mydir mymodule mypackage This option makes ``pylint`` attempt to discover all modules (files ending with ``.py`` extension) -and all packages (all directories containing a ``__init__.py`` file). +and all explicit packages (all directories containing a ``__init__.py`` file). -Pylint **will not import** this package or module, though uses Python internals +Pylint **will not import** this package or module, but it does use Python internals to locate them and as such is subject to the same rules and configuration. You should pay attention to your ``PYTHONPATH``, since it is a common error to analyze an installed version of a module instead of the development version. diff --git a/doc/whatsnew/1/1.0.rst b/doc/whatsnew/1/1.0.rst index 52ffe8644c..a3dc48b588 100644 --- a/doc/whatsnew/1/1.0.rst +++ b/doc/whatsnew/1/1.0.rst @@ -82,7 +82,9 @@ Release date: 2013-08-06 raise Exception, args * Support for PEP 3102 and new missing-kwoa (E1125) message for missing - mandatory keyword argument (logilab.org's #107788) + mandatory keyword argument + + Closes Logilab #107788 * Fix spelling of max-branchs option, now max-branches @@ -92,19 +94,30 @@ Release date: 2013-08-06 * Follow astng renaming to astroid -* bitbucket #37: check for unbalanced unpacking in assignments +* Check for unbalanced unpacking in assignments + + Closes BitBucket #37 + +* Fix incomplete-protocol false positive for read-only containers like tuple + + Closes BitBucket #25 + +* Fix False positive E1003 on Python 3 for argument-less super() + + Closes BitBucket #16 + +* Put back documentation in source distribution + + Closes BitBucket #6 -* bitbucket #25: fix incomplete-protocol false positive for read-only - containers like tuple +* epylint shouldn't hang anymore when there is a large output on pylint'stderr -* bitbucket #16: fix False positive E1003 on Python 3 for argument-less super() + Closes BitBucket #15 -* bitbucket #6: put back documentation in source distribution +* Fix epylint w/ python3 -* bitbucket #15: epylint shouldn't hang anymore when there is a large - output on pylint'stderr + Closes BitBucket #7 -* bitbucket #7: fix epylint w/ python3 +* Remove string module from the default list of deprecated modules -* bitbucket #3: remove string module from the default list of deprecated - modules + Closes BitBucket #3 diff --git a/doc/whatsnew/1/1.1.rst b/doc/whatsnew/1/1.1.rst index 1103efc653..ece4c5447e 100644 --- a/doc/whatsnew/1/1.1.rst +++ b/doc/whatsnew/1/1.1.rst @@ -9,7 +9,9 @@ Release date: 2013-12-22 emitted as a regular warn(). * Avoid false used-before-assignment for except handler defined - identifier used on the same line (#111). + identifier used on the same line. + + Closes #111 * Combine 'no-space-after-operator', 'no-space-after-comma' and 'no-space-before-operator' into a new warning 'bad-whitespace'. @@ -26,14 +28,17 @@ Release date: 2013-12-22 * Add 'bad-context-manager' error, checking that '__exit__' special method accepts the right number of arguments. -* Run pylint as a python module 'python -m pylint' (anatoly techtonik). +* Run pylint as a python module 'python -m pylint' (Anatoly Techtonik). * Check for non-exception classes inside an except clause. * epylint support options to give to pylint after the file to analyze and - have basic input validation (bitbucket #53 and #54), patches provided by + have basic input validation, patches provided by felipeochoa and Brian Lane. + Closes BitBucket #53 + Closes BitBucket #54 + * Added a new warning, 'non-iterator-returned', for non-iterators returned by '__iter__'. @@ -42,21 +47,29 @@ Release date: 2013-12-22 (unbalanced-tuple-unpacking). * useless-else-on-loop not emitted if there is a break in the - else clause of inner loop (#117). + else clause of inner loop. + + Closes #117 -* don't mark ``input`` as a bad function when using python3 (#110). +* don't mark ``input`` as a bad function when using python3. + + Closes #110 * badly-implemented-container caused several problems in its current implementation. Deactivate it until we have something - better. See #112 for instance. + better. + + Refs #112 * Use attribute regexp for properties in python3, as in python2 -* Create the PYLINTHOME directory when needed, it might fail and lead to +* Create the ``PYLINTHOME`` directory when needed, it might fail and lead to spurious warnings on import of pylint.config. * Fix setup.py so that pylint properly install on Windows when using python3 * Various documentation fixes and enhancements -* Fix issue #55 (false-positive trailing-whitespace on Windows) +* Fix a false-positive trailing-whitespace on Windows + + Closes #55 diff --git a/doc/whatsnew/1/1.2.rst b/doc/whatsnew/1/1.2.rst index 3b0a9a96a5..55ef46f0e6 100644 --- a/doc/whatsnew/1/1.2.rst +++ b/doc/whatsnew/1/1.2.rst @@ -11,18 +11,25 @@ Release date: 2014-04-30 lines. * Emit [assignment-from-none] when the function contains bare returns. - Fixes BitBucket issue #191. + + Closes BitBucket #191 * Added a new warning for closing over variables that are - defined in loops. Fixes Bitbucket issue #176. + defined in loops. + + Closes BitBucket #176 * Do not warn about \u escapes in string literals when Unicode literals - are used for Python 2.*. Fixes BitBucket issue #151. + are used for Python 2.*. + + Closes BitBucket #151 * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. -* Fix explicit checking of python script (1.2 regression, #219) +* Fix explicit checking of python script (1.2 regression) + + Closes #219 * Restore --init-hook, renamed accidentally into --init-hooks in 1.2.0 @@ -37,27 +44,34 @@ What's New in Pylint 1.2.0? Release date: 2014-04-18 * Pass the current python paths to pylint process when invoked via - epylint. Fixes BitBucket issue #133. + epylint. + + Closes BitBucket #133. * Add -i / --include-ids and -s / --symbols back as completely ignored - options. Fixes BitBucket issue #180. + options. + + Closes BitBucket #180. + +* Extend the number of cases in which logging calls are detected. -* Extend the number of cases in which logging calls are detected. Fixes - bitbucket issue #182. + Closes BitBucket #182. * Improve pragma handling to not detect pylint:* strings in non-comments. - Fixes BitBucket issue #79. + + Closes BitBucket #79. * Do not crash with UnknownMessage if an unknown message ID/name appears in disable or enable in the configuration. Patch by Cole Robinson. - Fixes bitbucket issue #170. -* Add new warning 'eval-used', checking that the builtin function ``eval`` - was used. + Closes BitBucket #170 + +* Add new warning 'eval-used', checking that the builtin function ``eval`` was used. * Make it possible to show a naming hint for invalid name by setting - include-naming-hint. Also make the naming hints configurable. Fixes - BitBucket issue #138. + include-naming-hint. Also make the naming hints configurable. + + Closes BitBucket #138 * Added support for enforcing multiple, but consistent name styles for different name types inside a single module; based on a patch written @@ -78,18 +92,23 @@ Release date: 2014-04-18 Closes #166 * Python 2.5 support restored: fixed small issues preventing pylint to run - on python 2.5. Bitbucket issues #50 and #62. + on python 2.5. + + Closes BitBucket #50 + Closes BitBucket #62 -* bitbucket #128: pylint doesn't crash when looking - for used-before-assignment in context manager - assignments. +* pylint doesn't crash when looking for used-before-assignment in context manager assignments. + + Closes BitBucket #128 * Add new warning, 'bad-reversed-sequence', for checking that the reversed() builtin receive a sequence (implements ``__getitem__`` and ``__len__``, without being a dict or a dict subclass) or an instance which implements ``__reversed__``. -* Mark ``file`` as a bad function when using python2 (closes #8). +* Mark ``file`` as a bad function when using python2 + + Closes #8 * Add new warning 'bad-exception-context', checking that ``raise ... from ...`` uses a proper exception context @@ -99,7 +118,9 @@ Release date: 2014-04-18 for 'nonlocal' uses. * Emit 'undefined-all-variable' if a package's __all__ - variable contains a missing submodule (closes #126). + variable contains a missing submodule. + + Closes #126 * Add a new warning 'abstract-class-instantiated' for checking that abstract classes created with ``abc`` module and diff --git a/doc/whatsnew/1/1.8/full.rst b/doc/whatsnew/1/1.8/full.rst index 4452aae2dc..9e79cd7067 100644 --- a/doc/whatsnew/1/1.8/full.rst +++ b/doc/whatsnew/1/1.8/full.rst @@ -95,7 +95,7 @@ Release date: 2017-12-15 When unknown reporter class will be selected as Pylint reporter, meaningful error message would be raised instead of bare ``ImportError`` - or ``AttribueError`` related to module or reporter class being not found. + or ``AttributeError`` related to module or reporter class being not found. Closes #1388 diff --git a/doc/whatsnew/2/2.12/full.rst b/doc/whatsnew/2/2.12/full.rst index 7421835db2..923dd9a25e 100644 --- a/doc/whatsnew/2/2.12/full.rst +++ b/doc/whatsnew/2/2.12/full.rst @@ -199,12 +199,12 @@ Release date: 2021-11-24 Closes #4774 -* ``mising-param-doc`` now correctly parses asterisks for variable length and +* ``missing-param-doc`` now correctly parses asterisks for variable length and keyword parameters Closes #3733 -* ``mising-param-doc`` now correctly handles Numpy parameter documentation without +* ``missing-param-doc`` now correctly handles Numpy parameter documentation without explicit typing Closes #5222 diff --git a/doc/whatsnew/2/2.12/summary.rst b/doc/whatsnew/2/2.12/summary.rst index 60099c6cda..cab7784a61 100644 --- a/doc/whatsnew/2/2.12/summary.rst +++ b/doc/whatsnew/2/2.12/summary.rst @@ -187,12 +187,12 @@ Other Changes Closes #5194 -* ``mising-param-doc`` now correctly parses asterisks for variable length and +* ``missing-param-doc`` now correctly parses asterisks for variable length and keyword parameters Closes #3733 -* ``mising-param-doc`` now correctly handles Numpy parameter documentation without +* ``missing-param-doc`` now correctly handles Numpy parameter documentation without explicit typing Closes #5222 diff --git a/doc/whatsnew/2/2.13/full.rst b/doc/whatsnew/2/2.13/full.rst index 5fd71c4664..f723731dc4 100644 --- a/doc/whatsnew/2/2.13/full.rst +++ b/doc/whatsnew/2/2.13/full.rst @@ -160,7 +160,7 @@ Release date: 2022-03-31 Closes #6027 -* Fix crash for ``unneccessary-ellipsis`` checker when an ellipsis is used inside of a container or a lambda expression. +* Fix crash for ``unnecessary-ellipsis`` checker when an ellipsis is used inside of a container or a lambda expression. Closes #6036 Closes #6037 @@ -265,7 +265,7 @@ Release date: 2022-03-24 Closes #5840 -* Updated the position of messages for class and function defintions to no longer cover +* Updated the position of messages for class and function definitions to no longer cover the complete definition. Only the ``def`` or ``class`` + the name of the class/function are covered. diff --git a/doc/whatsnew/2/2.13/summary.rst b/doc/whatsnew/2/2.13/summary.rst index 8305285911..73d377a762 100644 --- a/doc/whatsnew/2/2.13/summary.rst +++ b/doc/whatsnew/2/2.13/summary.rst @@ -168,7 +168,7 @@ Other Changes Closes #352 -* Updated the position of messages for class and function defintions to no longer cover +* Updated the position of messages for class and function definitions to no longer cover the complete definition. Only the ``def`` or ``class`` + the name of the class/function are covered. diff --git a/doc/whatsnew/2/2.14/full.rst b/doc/whatsnew/2/2.14/full.rst index db24814364..2853a9f665 100644 --- a/doc/whatsnew/2/2.14/full.rst +++ b/doc/whatsnew/2/2.14/full.rst @@ -84,7 +84,6 @@ What's New in Pylint 2.14.2? ---------------------------- Release date: 2022-06-15 - * Fixed a false positive for ``unused-variable`` when a function returns an ``argparse.Namespace`` object. @@ -119,7 +118,6 @@ Release date: 2022-06-15 Closes #6811 - What's New in Pylint 2.14.1? ---------------------------- Release date: 2022-06-06 diff --git a/doc/whatsnew/2/2.14/summary.rst b/doc/whatsnew/2/2.14/summary.rst index 54f10a4fe9..9ef0f04d95 100644 --- a/doc/whatsnew/2/2.14/summary.rst +++ b/doc/whatsnew/2/2.14/summary.rst @@ -1,5 +1,5 @@ :Release: 2.14 -:Date: TBA +:Date: 2022-06-01 Summary -- Release highlights ============================= diff --git a/doc/whatsnew/2/2.15/index.rst b/doc/whatsnew/2/2.15/index.rst index 519232b22e..aab05caef8 100644 --- a/doc/whatsnew/2/2.15/index.rst +++ b/doc/whatsnew/2/2.15/index.rst @@ -6,39 +6,567 @@ :maxdepth: 2 :Release: 2.15 -:Date: TBA +:Date: 2022-08-26 Summary -- Release highlights ============================= +In pylint 2.15.0, we added a new check ``missing-timeout`` to warn of +default timeout values that could cause a program to be hanging indefinitely. -New checkers -============ +We improved ``pylint``'s handling of namespace packages. More packages should +be linted without resorting to using the ``--recursive=y`` option. +We still welcome any community effort to help review, integrate, and add good/bad examples to the doc for +`_. This should be doable without any ``pylint`` or ``astroid`` +knowledge, so this is the perfect entrypoint if you want to contribute to ``pylint`` or open source without +any experience with our code! -Removed checkers -================ +Internally, we changed the way we generate the release notes, thanks to DudeNr33. +There will be no more conflict resolution to do in the changelog, and every contributor rejoice. +Marc Byrne became a maintainer, welcome to the team ! -Extensions -========== +.. towncrier release notes start +What's new in Pylint 2.15.10? +----------------------------- +Release date: 2023-01-09 -False positives fixed -===================== +False Positives Fixed +--------------------- -False negatives fixed -===================== +- Fix ``use-sequence-for-iteration`` when unpacking a set with ``*``. + Closes #5788 (`#5788 `_) -Other bug fixes -=============== +- Fix false positive ``assigning-non-slot`` when a class attribute is + re-assigned. + + Closes #6001 (`#6001 `_) + +- Fixes ``used-before-assignment`` false positive when the walrus operator + is used in a ternary operator. + + Closes #7779 (`#7779 `_) + +- Prevent ``used-before-assignment`` when imports guarded by ``if + TYPE_CHECKING`` + are guarded again when used. + + Closes #7979 (`#7979 `_) + + + +Other Bug Fixes +--------------- + +- Using custom braces in ``msg-template`` will now work properly. + + Closes #5636 (`#5636 `_) + + +What's new in Pylint 2.15.9? +---------------------------- +Release date: 2022-12-17 + + +False Positives Fixed +--------------------- + +- Fix false-positive for ``used-before-assignment`` in pattern matching + with a guard. + + Closes #5327 (`#5327 `_) + + + +Other Bug Fixes +--------------- + +- Pylint will no longer deadlock if a parallel job is killed but fail + immediately instead. + + Closes #3899 (`#3899 `_) + +- When pylint exit due to bad arguments being provided the exit code will now + be the expected ``32``. + + Refs #7931 (`#7931 `_) + +- Fixes a ``ModuleNotFound`` exception when running pylint on a Django project + with the ``pylint_django`` plugin enabled. + + Closes #7938 (`#7938 `_) + + +What's new in Pylint 2.15.8? +---------------------------- +Release date: 2022-12-05 + + +False Positives Fixed +--------------------- + +- Document a known false positive for ``useless-suppression`` when disabling + ``line-too-long`` in a module with only comments and no code. + + Closes #3368 (`#3368 `_) + +- Fix ``logging-fstring-interpolation`` false positive raised when logging and + f-string with ``%s`` formatting. + + Closes #4984 (`#4984 `_) + +- Fixes false positive ``abstract-method`` on Protocol classes. + + Closes #7209 (`#7209 `_) + +- Fix ``missing-param-doc`` false positive when function parameter has an + escaped underscore. + + Closes #7827 (`#7827 `_) + +- ``multiple-statements`` no longer triggers for function stubs using inlined + ``...``. + + Closes #7860 (`#7860 `_) + + +What's new in Pylint 2.15.7? +---------------------------- +Release date: 2022-11-27 + + +False Positives Fixed +--------------------- + +- Fix ``deprecated-method`` false positive when alias for method is similar to + name of deprecated method. + + Closes #5886 (`#5886 `_) + +- Fix a false positive for ``used-before-assignment`` for imports guarded by + ``typing.TYPE_CHECKING`` later used in variable annotations. + + Closes #7609 (`#7609 `_) + + + +Other Bug Fixes +--------------- + +- Pylint will now filter duplicates given to it before linting. The output + should + be the same whether a file is given/discovered multiple times or not. + + Closes #6242, #4053 (`#6242 `_) + +- Fixes a crash in ``stop-iteration-return`` when the ``next`` builtin is + called without arguments. + + Closes #7828 (`#7828 `_) + + +What's new in Pylint 2.15.6? +---------------------------- +Release date: 2022-11-19 + + +False Positives Fixed +--------------------- + +- Fix false positive for ``unhashable-member`` when subclassing ``dict`` and + using the subclass as a dictionary key. + + Closes #7501 (`#7501 `_) + +- ``unnecessary-list-index-lookup`` will not be wrongly emitted if + ``enumerate`` is called with ``start``. + + Closes #7682 (`#7682 `_) + +- Don't warn about ``stop-iteration-return`` when using ``next()`` over + ``itertools.cycle``. + + Closes #7765 (`#7765 `_) + + + +Other Bug Fixes +--------------- + +- Messages sent to reporter are now copied so a reporter cannot modify the + message sent to other reporters. + + Closes #7214 (`#7214 `_) + +- Fixes edge case of custom method named ``next`` raised an astroid error. + + Closes #7610 (`#7610 `_) + +- Fix crash that happened when parsing files with unexpected encoding starting + with 'utf' like ``utf13``. + + Closes #7661 (`#7661 `_) + +- Fix a crash when a child class with an ``__init__`` method inherits from a + parent class with an ``__init__`` class attribute. + + Closes #7742 (`#7742 `_) + + +What's new in Pylint 2.15.5? +---------------------------- +Release date: 2022-10-21 + + +False Positives Fixed +--------------------- + +- Fix a false positive for ``simplify-boolean-expression`` when multiple values + are inferred for a constant. + + Closes #7626 (`#7626 `_) + + + +Other Bug Fixes +--------------- + +- Remove ``__index__`` dunder method call from ``unnecessary-dunder-call`` + check. + + Closes #6795 (`#6795 `_) + +- Fixed a multi-processing crash that prevents using any more than 1 thread on + MacOS. + + The returned module objects and errors that were cached by the linter plugin + loader + cannot be reliably pickled. This means that ``dill`` would throw an error + when + attempting to serialise the linter object for multi-processing use. + + Closes #7635. (`#7635 `_) + + + +Other Changes +------------- + +- Add a keyword-only ``compare_constants`` argument to ``safe_infer``. + + Refs #7626 (`#7626 `_) + +- Sort ``--generated-rcfile`` output. + + Refs #7655 (`#7655 `_) + + +What's new in Pylint 2.15.4? +---------------------------- +Release date: 2022-10-10 + + +False Positives Fixed +--------------------- + +- Fix the message for ``unnecessary-dunder-call`` for ``__aiter__`` and + ``__anext__``. Also + only emit the warning when ``py-version`` >= 3.10. + + Closes #7529 (`#7529 `_) + + + +Other Bug Fixes +--------------- + +- Fix bug in detecting ``unused-variable`` when iterating on variable. + + Closes #3044 (`#3044 `_) + +- Fixed handling of ``--`` as separator between positional arguments and flags. + This was not actually fixed in 2.14.5. + + Closes #7003, Refs #7096 (`#7003 + `_) + +- Report ``no-self-argument`` rather than ``no-method-argument`` for methods + with variadic arguments. + + Closes #7507 (`#7507 `_) + +- Fixed an issue where ``syntax-error`` couldn't be raised on files with + invalid encodings. + + Closes #7522 (`#7522 `_) + +- Fix false positive for ``redefined-outer-name`` when aliasing ``typing`` + e.g. as ``t`` and guarding imports under ``t.TYPE_CHECKING``. + + Closes #7524 (`#7524 `_) + +- Fixed a crash of the ``modified_iterating`` checker when iterating on a set + defined as a class attribute. + + Closes #7528 (`#7528 `_) + +- Fix bug in scanning of names inside arguments to ``typing.Literal``. + See https://peps.python.org/pep-0586/#literals-enums-and-forward-references + for details. + + Refs #3299 (`#3299 `_) + + +Other Changes +------------- + +- Add method name to the error messages of ``no-method-argument`` and + ``no-self-argument``. + + Closes #7507 (`#7507 `_) + + +What's new in Pylint 2.15.3? +---------------------------- +Release date: 2022-09-19 + + +- Fixed a crash in the ``unhashable-member`` checker when using a ``lambda`` as a dict key. + + Closes #7453 (`#7453 `_) +- Fix a crash in the ``modified-iterating-dict`` checker involving instance attributes. + + Closes #7461 (`#7461 `_) +- ``invalid-class-object`` does not crash anymore when ``__class__`` is assigned alongside another variable. + + Closes #7467 (`#7467 `_) +- Fix false positive for ``global-variable-not-assigned`` when a global variable is re-assigned via an ``ImportFrom`` node. + + Closes #4809 (`#4809 `_) +- Fix false positive for ``undefined-loop-variable`` in ``for-else`` loops that use a function + having a return type annotation of ``NoReturn`` or ``Never``. + + Closes #7311 (`#7311 `_) +- ``--help-msg`` now accepts a comma-separated list of message IDs again. + + Closes #7471 (`#7471 `_) + +What's new in Pylint 2.15.2? +---------------------------- +Release date: 2022-09-07 + +- Fixed a case where custom plugins specified by command line could silently fail. + + Specifically, if a plugin relies on the ``init-hook`` option changing ``sys.path`` before + it can be imported, this will now emit a ``bad-plugin-value`` message. Before this + change, it would silently fail to register the plugin for use, but would load + any configuration, which could have unintended effects. + + Fixes part of #7264. (`#7264 `_) +- Fix ``used-before-assignment`` for functions/classes defined in type checking guard. + + Closes #7368 (`#7368 `_) +- Update ``modified_iterating`` checker to fix a crash with ``for`` loops on empty list. + + Closes #7380 (`#7380 `_) +- The ``docparams`` extension now considers typing in Numpy style docstrings + as "documentation" for the ``missing-param-doc`` message. + + Refs #7398 (`#7398 `_) +- Fix false positive for ``unused-variable`` and ``unused-import`` when a name is only used in a string literal type annotation. + + Closes #3299 (`#3299 `_) +- Fix false positive for ``too-many-function-args`` when a function call is assigned to a class attribute inside the class where the function is defined. + + Closes #6592 (`#6592 `_) +- Fix ``used-before-assignment`` for functions/classes defined in type checking guard. + + Closes #7368 (`#7368 `_) +- Fix ignored files being linted when passed on stdin. + + Closes #4354 (`#4354 `_) +- ``missing-return-doc``, ``missing-raises-doc`` and ``missing-yields-doc`` now respect + the ``no-docstring-rgx`` option. + + Closes #4743 (`#4743 `_) +- Don't crash on ``OSError`` in config file discovery. + + Closes #7169 (`#7169 `_) +- ``disable-next`` is now correctly scoped to only the succeeding line. + + Closes #7401 (`#7401 `_) +- Update ``modified_iterating`` checker to fix a crash with ``for`` loops on empty list. + + Closes #7380 (`#7380 `_) + +What's new in Pylint 2.15.1? +---------------------------- +Release date: 2022-09-06 + +This is a "github only release", it was mistakenly released as ``2.16.0-dev`` on pypi. Replaced by ``2.15.2``. + +What's new in Pylint 2.15.0? +---------------------------- + +New Checks +---------- + +- Added new checker ``missing-timeout`` to warn of default timeout values that could cause + a program to be hanging indefinitely. + + Refs #6780 (`#6780 `_) + + +False Positives Fixed +--------------------- + +- Don't report ``super-init-not-called`` for abstract ``__init__`` methods. + + Closes #3975 (`#3975 `_) +- Don't report ``unsupported-binary-operation`` on Python <= 3.9 when using the ``|`` operator + with types, if one has a metaclass that overloads ``__or__`` or ``__ror__`` as appropriate. + + Closes #4951 (`#4951 `_) +- Don't report ``no-value-for-parameter`` for dataclasses fields annotated with ``KW_ONLY``. + + Closes #5767 (`#5767 `_) +- Fixed inference of ``Enums`` when they are imported under an alias. + + Closes #5776 (`#5776 `_) +- Prevent false positives when accessing ``PurePath.parents`` by index (not slice) on Python 3.10+. + + Closes #5832 (`#5832 `_) +- ``unnecessary-list-index-lookup`` is now more conservative to avoid potential false positives. + + Closes #6896 (`#6896 `_) +- Fix double emitting ``trailing-whitespace`` for multi-line docstrings. + + Closes #6936 (`#6936 `_) +- ``import-error`` now correctly checks for ``contextlib.suppress`` guards on import statements. + + Closes #7270 (`#7270 `_) +- Fix false positive for `no-self-argument`/`no-method-argument` when a staticmethod is applied to a function but uses a different name. + + Closes #7300 (`#7300 `_) +- Fix `undefined-loop-variable` with `break` and `continue` statements in `else` blocks. + + Refs #7311 (`#7311 `_) +- Improve default TypeVar name regex. Disallow names prefixed with ``T``. + E.g. use ``AnyStrT`` instead of ``TAnyStr``. + + Refs #7322 (`#7322 `_`) + + +False Negatives Fixed +--------------------- + +- Emit ``used-before-assignment`` when relying on a name that is reimported later in a function. + + Closes #4624 (`#4624 `_) +- Emit ``used-before-assignment`` for self-referencing named expressions (``:=``) lacking + prior assignments. + + Closes #5653 (`#5653 `_) +- Emit ``used-before-assignment`` for self-referencing assignments under if conditions. + + Closes #6643 (`#6643 `_) +- Emit ``modified-iterating-list`` and analogous messages for dicts and sets when iterating + literals, or when using the ``del`` keyword. + + Closes #6648 (`#6648 `_) +- Emit ``used-before-assignment`` when calling nested functions before assignment. + + Closes #6812 (`#6812 `_) +- Emit ``nonlocal-without-binding`` when a nonlocal name has been assigned at a later point in the same scope. + + Closes #6883 (`#6883 `_) +- Emit ``using-constant-test`` when testing the truth value of a variable or call result + holding a generator. + + Closes #6909 (`#6909 `_) +- Rename ``unhashable-dict-key`` to ``unhashable-member`` and emit when creating sets and dicts, + not just when accessing dicts. + + Closes #7034, Closes #7055 (`#7034 `_) + + +Other Bug Fixes +--------------- + +- Fix a failure to lint packages with ``__init__.py`` contained in directories lacking ``__init__.py``. + + Closes #1667 (`#1667 `_) +- Fixed a syntax-error crash that was not handled properly when the declared encoding of a file + was ``utf-9``. + + Closes #3860 (`#3860 `_) +- Fix a crash in the ``not-callable`` check when there is ambiguity whether an instance is being incorrectly provided to ``__new__()``. + + Closes #7109 (`#7109 `_) +- Fix crash when regex option raises a `re.error` exception. + + Closes #7202 (`#7202 `_) +- Fix `undefined-loop-variable` from walrus in comprehension test. + + Closes #7222 (`#7222 `_) +- Check for `` before removing first item from `sys.path` in `modify_sys_path`. + + Closes #7231 (`#7231 `_) +- Fix sys.path pollution in parallel mode. + + Closes #7246 (`#7246 `_) +- Prevent `useless-parent-delegation` for delegating to a builtin + written in C (e.g. `Exception.__init__`) with non-self arguments. + + Closes #7319 (`#7319 `_) Other Changes -============= +------------- + +- ``bad-exception-context`` has been renamed to ``bad-exception-cause`` as it is about the cause and not the context. + + Closes #3694 (`#3694 `_) +- The message for ``literal-comparison`` is now more explicit about the problem and the + solution. + + Closes #5237 (`#5237 `_) +- ``useless-super-delegation`` has been renamed to ``useless-parent-delegation`` in order to be more generic. + + Closes #6953 (`#6953 `_) +- Pylint now uses ``towncrier`` for changelog generation. + + Refs #6974 (`#6974 `_) +- Update ``astroid`` to 2.12. + + Refs #7153 (`#7153 `_) +- Fix crash when a type-annotated `__slots__` with no value is declared. + + Closes #7280 (`#7280 `_) + + +Internal Changes +---------------- + +- Fixed an issue where it was impossible to update functional tests output when the existing + output was impossible to parse. Instead of raising an error we raise a warning message and + let the functional test fail with a default value. + + Refs #6891 (`#6891 `_) +- ``pylint.testutils.primer`` is now a private API. + + Refs #6905 (`#6905 `_) +- We changed the way we handle the changelog internally by using towncrier. + If you're a contributor you won't have to fix merge conflicts in the + changelog anymore. + Closes #6974 (`#6974 `_) +- Pylint is now using Scorecards to implement security recommendations from the + `OpenSSF `_. This is done in order to secure our supply chains using a combination + of automated tooling and best practices, most of which were already implemented before. -Internal changes -================ + Refs #7267 (`#7267 `_) diff --git a/doc/whatsnew/2/2.16/index.rst b/doc/whatsnew/2/2.16/index.rst new file mode 100644 index 0000000000..d2ad86ab3e --- /dev/null +++ b/doc/whatsnew/2/2.16/index.rst @@ -0,0 +1,839 @@ +*************************** + What's New in Pylint 2.16 +*************************** + +.. toctree:: + :maxdepth: 2 + +:Release: 2.16 +:Date: 2023-02-01 + +Summary -- Release highlights +============================= + +In 2.16.0 we added aggregation and composition understanding in ``pyreverse``, and a way to clear +the cache in between run in server mode (originally for the VS Code integration). Apart from the bug +fixes there's also a lot of new checks, and new extensions that have been asked for for a long time +that were implemented. + +If you want to benefit from all the new checks load the following plugins:: + + pylint.extensions.dict_init_mutate, + pylint.extensions.dunder, + pylint.extensions.typing, + pylint.extensions.magic_value, + +We still welcome any community effort to help review, integrate, and add good/bad examples to the doc for +`_. This should be doable without any ``pylint`` or ``astroid`` +knowledge, so this is the perfect entrypoint if you want to contribute to ``pylint`` or open source without +any experience with our code! + +Last but not least @clavedeluna and @nickdrozd became triagers, welcome to the team ! + +.. towncrier release notes start + +What's new in Pylint 2.16.3? +---------------------------- +Release date: 2023-03-03 + + +False Positives Fixed +--------------------- + +- Fix false positive for ``wrong-spelling-in-comment`` with class names in a + python 2 type comment. + + Closes #8370 (`#8370 `_) + + + +Other Bug Fixes +--------------- + +- Prevent emitting ``invalid-name`` for the line on which a ``global`` + statement is declared. + + Closes #8307 (`#8307 `_) + + +What's new in Pylint 2.16.2? +---------------------------- +Release date: 2023-02-13 + + +New Features +------------ + +- Add `--version` option to `pyreverse`. + + Refs #7851 (`#7851 `_) + + + +False Positives Fixed +--------------------- + +- Fix false positive for ``used-before-assignment`` when + ``typing.TYPE_CHECKING`` is used with if/elif/else blocks. + + Closes #7574 (`#7574 `_) + +- Fix false positive for ``used-before-assignment`` for named expressions + appearing after the first element in a list, tuple, or set. + + Closes #8252 (`#8252 `_) + + + +Other Bug Fixes +--------------- + +- Fix ``used-before-assignment`` false positive when the walrus operator + is used with a ternary operator in dictionary key/value initialization. + + Closes #8125 (`#8125 `_) + +- Fix ``no-name-in-module`` false positive raised when a package defines a + variable with the + same name as one of its submodules. + + Closes #8148 (`#8148 `_) + +- Fix ``nested-min-max`` suggestion message to indicate it's possible to splat + iterable objects. + + Closes #8168 (`#8168 `_) + +- Fix a crash happening when a class attribute was negated in the start + argument of an enumerate. + + Closes #8207 (`#8207 `_) + + +What's new in Pylint 2.16.1? +---------------------------- +Release date: 2023-02-02 + + +Other Bug Fixes +--------------- + +- Fix a crash happening for python interpreter < 3.9 following a failed typing + update. + + Closes #8161 (`#8161 `_) + + +What's new in Pylint 2.16.0? +---------------------------- +Release date: 2023-02-01 + + +Changes requiring user actions +------------------------------ + +- The ``accept-no-raise-doc`` option related to ``missing-raises-doc`` will now + be correctly taken into account all the time. + + Pylint will no longer raise missing-raises-doc (W9006) when no exceptions are + documented and accept-no-raise-doc is true (issue #7208). + If you were expecting missing-raises-doc errors to be raised in that case, + you + will now have to add ``accept-no-raise-doc=no`` in your configuration to keep + the same behavior. + + Closes #7208 (`#7208 `_) + + + +New Features +------------ + +- Added the ``no-header`` output format. If enabled with + ``--output-format=no-header``, it will not include the module name in the + output. + + Closes #5362 (`#5362 `_) + +- Added configuration option ``clear-cache-post-run`` to support server-like + usage. + Use this flag if you expect the linted files to be altered between runs. + + Refs #5401 (`#5401 `_) + +- Add ``--allow-reexport-from-package`` option to configure the + ``useless-import-alias`` check not to emit a warning if a name + is reexported from a package. + + Closes #6006 (`#6006 `_) + +- Update ``pyreverse`` to differentiate between aggregations and compositions. + ``pyreverse`` checks if it's an Instance or a Call of an object via method + parameters (via type hints) + to decide if it's a composition or an aggregation. + + Refs #6543 (`#6543 `_) + + + +New Checks +---------- + +- Adds a ``pointless-exception-statement`` check that emits a warning when an + Exception is created and not assigned, raised or returned. + + Refs #3110 (`#3110 `_) + +- Add a ``shadowed-import`` message for aliased imports. + + Closes #4836 (`#4836 `_) + +- Add new check called ``unbalanced-dict-unpacking`` to check for unbalanced + dict unpacking + in assignment and for loops. + + Closes #5797 (`#5797 `_) + +- Add new checker ``positional-only-arguments-expected`` to check for cases + when + positional-only arguments have been passed as keyword arguments. + + Closes #6489 (`#6489 `_) + +- Added ``singledispatch-method`` which informs that ``@singledispatch`` should + decorate functions and not class/instance methods. + Added ``singledispatchmethod-function`` which informs that + ``@singledispatchmethod`` should decorate class/instance methods and not + functions. + + Closes #6917 (`#6917 `_) + +- Rename ``broad-except`` to ``broad-exception-caught`` and add new checker + ``broad-exception-raised`` + which will warn if general exceptions ``BaseException`` or ``Exception`` are + raised. + + Closes #7494 (`#7494 `_) + +- Added ``nested-min-max`` which flags ``min(1, min(2, 3))`` to simplify to + ``min(1, 2, 3)``. + + Closes #7546 (`#7546 `_) + +- Extended ``use-dict-literal`` to also warn about call to ``dict()`` when + passing keyword arguments. + + Closes #7690 (`#7690 `_) + +- Add ``named-expr-without-context`` check to emit a warning if a named + expression is used outside a context like ``if``, ``for``, ``while``, or + a comprehension. + + Refs #7760 (`#7760 `_) + +- Add ``invalid-slice-step`` check to warn about a slice step value of ``0`` + for common builtin sequences. + + Refs #7762 (`#7762 `_) + +- Add ``consider-refactoring-into-while-condition`` check to recommend + refactoring when + a while loop is defined with a constant condition with an immediate ``if`` + statement to check for ``break`` condition as a first statement. + + Closes #8015 (`#8015 `_) + + + +Extensions +---------- + +- Add new extension checker ``dict-init-mutate`` that flags mutating a + dictionary immediately + after the dictionary was created. + + Closes #2876 (`#2876 `_) + +- Added ``bad-dunder-name`` extension check, which flags bad or misspelled + dunder methods. + You can use the ``good-dunder-names`` option to allow specific dunder names. + + Closes #3038 (`#3038 `_) + +- Added ``consider-using-augmented-assign`` check for ``CodeStyle`` extension + which flags ``x = x + 1`` to simplify to ``x += 1``. + This check is disabled by default. To use it, load the code style extension + with ``load-plugins=pylint.extensions.code_style`` and add + ``consider-using-augmented-assign`` in the ``enable`` option. + + Closes #3391 (`#3391 `_) + +- Add ``magic-number`` plugin checker for comparison with constants instead of + named constants or enums. + You can use it with ``--load-plugins=pylint.extensions.magic_value``. + + Closes #7281 (`#7281 `_) + +- Add ``redundant-typehint-argument`` message for `typing` plugin for duplicate + assign typehints. + Enable the plugin to enable the message with: + ``--load-plugins=pylint.extensions.typing``. + + Closes #7636 (`#7636 `_) + + + +False Positives Fixed +--------------------- + +- Fix false positive for ``unused-variable`` and ``unused-import`` when a name + is only used in a string literal type annotation. + + Closes #3299 (`#3299 `_) + +- Document a known false positive for ``useless-suppression`` when disabling + ``line-too-long`` in a module with only comments and no code. + + Closes #3368 (`#3368 `_) + +- ``trailing-whitespaces`` is no longer reported within strings. + + Closes #3822 (`#3822 `_) + +- Fix false positive for ``global-variable-not-assigned`` when a global + variable is re-assigned via an ``ImportFrom`` node. + + Closes #4809 (`#4809 `_) + +- Fix false positive for ``use-maxsplit-arg`` with custom split method. + + Closes #4857 (`#4857 `_) + +- Fix ``logging-fstring-interpolation`` false positive raised when logging and + f-string with ``%s`` formatting. + + Closes #4984 (`#4984 `_) + +- Fix false-positive for ``used-before-assignment`` in pattern matching + with a guard. + + Closes #5327 (`#5327 `_) + +- Fix ``use-sequence-for-iteration`` when unpacking a set with ``*``. + + Closes #5788 (`#5788 `_) + +- Fix ``deprecated-method`` false positive when alias for method is similar to + name of deprecated method. + + Closes #5886 (`#5886 `_) + +- Fix false positive ``assigning-non-slot`` when a class attribute is + re-assigned. + + Closes #6001 (`#6001 `_) + +- Fix false positive for ``too-many-function-args`` when a function call is + assigned to a class attribute inside the class where the function is defined. + + Closes #6592 (`#6592 `_) + +- Fixes false positive ``abstract-method`` on Protocol classes. + + Closes #7209 (`#7209 `_) + +- Pylint now understands the ``kw_only`` keyword argument for ``dataclass``. + + Closes #7290, closes #6550, closes #5857 (`#7290 + `_) + +- Fix false positive for ``undefined-loop-variable`` in ``for-else`` loops that + use a function + having a return type annotation of ``NoReturn`` or ``Never``. + + Closes #7311 (`#7311 `_) + +- Fix ``used-before-assignment`` for functions/classes defined in type checking + guard. + + Closes #7368 (`#7368 `_) + +- Fix false positive for ``unhashable-member`` when subclassing ``dict`` and + using the subclass as a dictionary key. + + Closes #7501 (`#7501 `_) + +- Fix the message for ``unnecessary-dunder-call`` for ``__aiter__`` and + ``__aneext__``. Also + only emit the warning when ``py-version`` >= 3.10. + + Closes #7529 (`#7529 `_) + +- Fix ``used-before-assignment`` false positive when else branch calls + ``sys.exit`` or similar terminating functions. + + Closes #7563 (`#7563 `_) + +- Fix a false positive for ``used-before-assignment`` for imports guarded by + ``typing.TYPE_CHECKING`` later used in variable annotations. + + Closes #7609 (`#7609 `_) + +- Fix a false positive for ``simplify-boolean-expression`` when multiple values + are inferred for a constant. + + Closes #7626 (`#7626 `_) + +- ``unnecessary-list-index-lookup`` will not be wrongly emitted if + ``enumerate`` is called with ``start``. + + Closes #7682 (`#7682 `_) + +- Don't warn about ``stop-iteration-return`` when using ``next()`` over + ``itertools.cycle``. + + Closes #7765 (`#7765 `_) + +- Fixes ``used-before-assignment`` false positive when the walrus operator + is used in a ternary operator. + + Closes #7779 (`#7779 `_) + +- Fix ``missing-param-doc`` false positive when function parameter has an + escaped underscore. + + Closes #7827 (`#7827 `_) + +- Fixes ``method-cache-max-size-none`` false positive for methods inheriting + from ``Enum``. + + Closes #7857 (`#7857 `_) + +- ``multiple-statements`` no longer triggers for function stubs using inlined + ``...``. + + Closes #7860 (`#7860 `_) + +- Fix a false positive for ``used-before-assignment`` when a name guarded by + ``if TYPE_CHECKING:`` is used as a type annotation in a function body and + later re-imported in the same scope. + + Closes #7882 (`#7882 `_) + +- Prevent ``used-before-assignment`` when imports guarded by ``if + TYPE_CHECKING`` + are guarded again when used. + + Closes #7979 (`#7979 `_) + +- Fixes false positive for ``try-except-raise`` with multiple exceptions in one + except statement if exception are in different namespace. + + Closes #8051 (`#8051 `_) + +- Fix ``invalid-name`` errors for ``typing_extension.TypeVar``. + + Refs #8089 (`#8089 `_) + +- Fix ``no-kwoa`` false positive for context managers. + + Closes #8100 (`#8100 `_) + +- Fix a false positive for ``redefined-variable-type`` when ``async`` methods + are present. + + Closes #8120 (`#8120 `_) + + + +False Negatives Fixed +--------------------- + +- Code following a call to ``quit``, ``exit``, ``sys.exit`` or ``os._exit`` + will be marked as `unreachable`. + + Refs #519 (`#519 `_) + +- Emit ``used-before-assignment`` when function arguments are redefined inside + an inner function and accessed there before assignment. + + Closes #2374 (`#2374 `_) + +- Fix a false negative for ``unused-import`` when one module used an import in + a type annotation that was also used in another module. + + Closes #4150 (`#4150 `_) + +- Flag ``superfluous-parens`` if parentheses are used during string + concatenation. + + Closes #4792 (`#4792 `_) + +- Emit ``used-before-assignment`` when relying on names only defined under + conditions always testing false. + + Closes #4913 (`#4913 `_) + +- ``consider-using-join`` can now be emitted for non-empty string separators. + + Closes #6639 (`#6639 `_) + +- Emit ``used-before-assignment`` for further imports guarded by + ``TYPE_CHECKING`` + + Previously, this message was only emitted for imports guarded directly under + ``TYPE_CHECKING``, not guarded two if-branches deep, nor when + ``TYPE_CHECKING`` + was imported from ``typing`` under an alias. + + Closes #7539 (`#7539 `_) + +- Fix a false negative for ``unused-import`` when a constant inside + ``typing.Annotated`` was treated as a reference to an import. + + Closes #7547 (`#7547 `_) + +- ``consider-using-any-or-all`` message will now be raised in cases when + boolean is initialized, reassigned during loop, and immediately returned. + + Closes #7699 (`#7699 `_) + +- Extend ``invalid-slice-index`` to emit an warning for invalid slice indices + used with string and byte sequences, and range objects. + + Refs #7762 (`#7762 `_) + +- Fixes ``unnecessary-list-index-lookup`` false negative when ``enumerate`` is + called with ``iterable`` as a kwarg. + + Closes #7770 (`#7770 `_) + +- ``no-else-return`` or ``no-else-raise`` will be emitted if ``except`` block + always returns or raises. + + Closes #7788 (`#7788 `_) + +- Fix ``dangerous-default-value`` false negative when ``*`` is used. + + Closes #7818 (`#7818 `_) + +- ``consider-using-with`` now triggers for ``pathlib.Path.open``. + + Closes #7964 (`#7964 `_) + + + +Other Bug Fixes +--------------- + +- Fix bug in detecting ``unused-variable`` when iterating on variable. + + Closes #3044 (`#3044 `_) + +- Fix bug in scanning of names inside arguments to ``typing.Literal``. + See https://peps.python.org/pep-0586/#literals-enums-and-forward-references + for details. + + Refs #3299 (`#3299 `_) + +- Update ``disallowed-name`` check to flag module-level variables. + + Closes #3701 (`#3701 `_) + +- Pylint will no longer deadlock if a parallel job is killed but fail + immediately instead. + + Closes #3899 (`#3899 `_) + +- Fix ignored files being linted when passed on stdin. + + Closes #4354 (`#4354 `_) + +- Fix ``no-member`` false negative when augmented assign is done manually, + without ``+=``. + + Closes #4562 (`#4562 `_) + +- Any assertion on a populated tuple will now receive a ``assert-on-tuple`` + warning. + + Closes #4655 (`#4655 `_) + +- ``missing-return-doc``, ``missing-raises-doc`` and ``missing-yields-doc`` now + respect + the ``no-docstring-rgx`` option. + + Closes #4743 (`#4743 `_) + +- Update ``reimported`` help message for clarity. + + Closes #4836 (`#4836 `_) + +- ``consider-iterating-dictionary`` will no longer be raised if bitwise + operations are used. + + Closes #5478 (`#5478 `_) + +- Using custom braces in ``msg-template`` will now work properly. + + Closes #5636 (`#5636 `_) + +- Pylint will now filter duplicates given to it before linting. The output + should + be the same whether a file is given/discovered multiple times or not. + + Closes #6242, #4053 (`#6242 `_) + +- Remove ``__index__`` dunder method call from ``unnecessary-dunder-call`` + check. + + Closes #6795 (`#6795 `_) + +- Fixed handling of ``--`` as separator between positional arguments and flags. + This was not actually fixed in 2.14.5. + + Closes #7003, Refs #7096 (`#7003 + `_) + +- Don't crash on ``OSError`` in config file discovery. + + Closes #7169 (`#7169 `_) + +- Messages sent to reporter are now copied so a reporter cannot modify the + message sent to other reporters. + + Closes #7214 (`#7214 `_) + +- Fixed a case where custom plugins specified by command line could silently + fail. + + Specifically, if a plugin relies on the ``init-hook`` option changing + ``sys.path`` before + it can be imported, this will now emit a ``bad-plugin-value`` message. Before + this + change, it would silently fail to register the plugin for use, but would load + any configuration, which could have unintended effects. + + Fixes part of #7264. (`#7264 `_) + +- Update ``modified_iterating`` checker to fix a crash with ``for`` loops on + empty list. + + Closes #7380 (`#7380 `_) + +- Update wording for ``arguments-differ`` and ``arguments-renamed`` to clarify + overriding object. + + Closes #7390 (`#7390 `_) + +- ``disable-next`` is now correctly scoped to only the succeeding line. + + Closes #7401 (`#7401 `_) + +- Fixed a crash in the ``unhashable-member`` checker when using a ``lambda`` as + a dict key. + + Closes #7453 (`#7453 `_) + +- Add ``mailcap`` to deprecated modules list. + + Closes #7457 (`#7457 `_) + +- Fix a crash in the ``modified-iterating-dict`` checker involving instance + attributes. + + Closes #7461 (`#7461 `_) + +- ``invalid-class-object`` does not crash anymore when ``__class__`` is + assigned alongside another variable. + + Closes #7467 (`#7467 `_) + +- ``--help-msg`` now accepts a comma-separated list of message IDs again. + + Closes #7471 (`#7471 `_) + +- Allow specifying non-builtin exceptions in the ``overgeneral-exception`` + option + using an exception's qualified name. + + Closes #7495 (`#7495 `_) + +- Report ``no-self-argument`` rather than ``no-method-argument`` for methods + with variadic arguments. + + Closes #7507 (`#7507 `_) + +- Fixed an issue where ``syntax-error`` couldn't be raised on files with + invalid encodings. + + Closes #7522 (`#7522 `_) + +- Fix false positive for ``redefined-outer-name`` when aliasing ``typing`` + e.g. as ``t`` and guarding imports under ``t.TYPE_CHECKING``. + + Closes #7524 (`#7524 `_) + +- Fixed a crash of the ``modified_iterating`` checker when iterating on a set + defined as a class attribute. + + Closes #7528 (`#7528 `_) + +- Use ``py-version`` to determine if a message should be emitted for messages + defined with ``max-version`` or ``min-version``. + + Closes #7569 (`#7569 `_) + +- Improve ``bad-thread-instantiation`` check to warn if ``target`` is not + passed in as a keyword argument + or as a second argument. + + Closes #7570 (`#7570 `_) + +- Fixes edge case of custom method named ``next`` raised an astroid error. + + Closes #7610 (`#7610 `_) + +- Fixed a multi-processing crash that prevents using any more than 1 thread on + MacOS. + + The returned module objects and errors that were cached by the linter plugin + loader + cannot be reliably pickled. This means that ``dill`` would throw an error + when + attempting to serialise the linter object for multi-processing use. + + Closes #7635. (`#7635 `_) + +- Fix crash that happened when parsing files with unexpected encoding starting + with 'utf' like ``utf13``. + + Closes #7661 (`#7661 `_) + +- Fix a crash when a child class with an ``__init__`` method inherits from a + parent class with an ``__init__`` class attribute. + + Closes #7742 (`#7742 `_) + +- Fix ``valid-metaclass-classmethod-first-arg`` default config value from "cls" + to "mcs" + which would cause both a false-positive and false-negative. + + Closes #7782 (`#7782 `_) + +- Fixes a crash in the ``unnecessary_list_index_lookup`` check when using + ``enumerate`` with ``start`` and a class attribute. + + Closes #7821 (`#7821 `_) + +- Fixes a crash in ``stop-iteration-return`` when the ``next`` builtin is + called without arguments. + + Closes #7828 (`#7828 `_) + +- When pylint exit due to bad arguments being provided the exit code will now + be the expected ``32``. + + Refs #7931 (`#7931 `_) + +- Fixes a ``ModuleNotFound`` exception when running pylint on a Django project + with the ``pylint_django`` plugin enabled. + + Closes #7938 (`#7938 `_) + +- Fixed a crash when inferring a value and using its qname on a slice that was + being incorrectly called. + + Closes #8067 (`#8067 `_) + +- Use better regex to check for private attributes. + + Refs #8081 (`#8081 `_) + +- Fix issue with new typing Union syntax in runtime context for Python 3.10+. + + Closes #8119 (`#8119 `_) + + + +Other Changes +------------- + +- Pylint now provides basic support for Python 3.11. + + Closes #5920 (`#5920 `_) + +- Update message for ``abstract-method`` to include child class name. + + Closes #7124 (`#7124 `_) + +- Update Pyreverse's dot and plantuml printers to detect when class methods are + abstract and show them with italic font. + For the dot printer update the label to use html-like syntax. + + Closes #7346 (`#7346 `_) + +- The ``docparams`` extension now considers typing in Numpy style docstrings + as "documentation" for the ``missing-param-doc`` message. + + Refs #7398 (`#7398 `_) + +- Relevant ``DeprecationWarnings`` are now raised with ``stacklevel=2``, so + they have the callsite attached in the message. + + Closes #7463 (`#7463 `_) + +- Add a ``minimal`` option to ``pylint-config`` and its toml generator. + + Closes #7485 (`#7485 `_) + +- Add method name to the error messages of ``no-method-argument`` and + ``no-self-argument``. + + Closes #7507 (`#7507 `_) + +- Prevent leaving the pip install cache in the Docker image. + + Refs #7544 (`#7544 `_) + +- Add a keyword-only ``compare_constants`` argument to ``safe_infer``. + + Refs #7626 (`#7626 `_) + +- Add ``default_enabled`` option to optional message dict. Provides an option + to disable a checker message by default. + To use a disabled message, the user must enable it explicitly by adding the + message to the ``enable`` option. + + Refs #7629 (`#7629 `_) + +- Sort ``--generated-rcfile`` output. + + Refs #7655 (`#7655 `_) + +- epylint is now deprecated and will be removed in pylint 3.0.0. All emacs and + flymake related + files were removed and their support will now happen in an external + repository : + https://github.com/emacsorphanage/pylint. + + Closes #7737 (`#7737 `_) + +- Adds test for existing preferred-modules configuration functionality. + + Refs #7957 (`#7957 `_) + + + +Internal Changes +---------------- + +- Add and fix regression tests for plugin loading. + + This shores up the tests that cover the loading of custom plugins as affected + by any changes made to the ``sys.path`` during execution of an ``init-hook``. + Given the existing contract of allowing plugins to be loaded by fiddling with + the path in this way, this is now the last bit of work needed to close Github + issue #7264. + + Closes #7264 (`#7264 `_) diff --git a/doc/whatsnew/2/index.rst b/doc/whatsnew/2/index.rst index 4c1e938b32..596c1c3ca6 100644 --- a/doc/whatsnew/2/index.rst +++ b/doc/whatsnew/2/index.rst @@ -7,6 +7,7 @@ .. toctree:: :maxdepth: 2 + 2.16/index 2.15/index 2.14/index 2.13/index diff --git a/doc/whatsnew/fragments/_template.rst b/doc/whatsnew/fragments/_template.rst new file mode 100644 index 0000000000..c6e410b9c9 --- /dev/null +++ b/doc/whatsnew/fragments/_template.rst @@ -0,0 +1,39 @@ +{% set title = "What's new in Pylint " + versiondata.version + "?" %} +{{ title }} +{{ underlines[0] * (title|length) }} +Release date: {{ versiondata.date }} + +{% for section, _ in sections.items() %} +{% set underline = underlines[0] %}{% if section %}{{section}} +{{ underline * section|length }}{% set underline = underlines[1] %} + +{% endif %} + +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section]%} +{{ definitions[category]['name'] }} +{{ underline * definitions[category]['name']|length }} + +{% if definitions[category]['showcontent'] %} +{% for text, values in sections[section][category].items() %} +- {{ text }} ({{ values|join(', ') }}) + +{% endfor %} + +{% else %} +- {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% if sections[section][category]|length == 0 %} +No significant changes. + +{% else %} +{% endif %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} +{% endfor %} diff --git a/elisp/pylint-flymake.el b/elisp/pylint-flymake.el deleted file mode 100644 index ed213bf467..0000000000 --- a/elisp/pylint-flymake.el +++ /dev/null @@ -1,15 +0,0 @@ -;; Configure Flymake for python -(when (load "flymake" t) - (defun flymake-pylint-init () - (let* ((temp-file (flymake-init-create-temp-buffer-copy - 'flymake-create-temp-inplace)) - (local-file (file-relative-name - temp-file - (file-name-directory buffer-file-name)))) - (list "epylint" (list local-file)))) - - (add-to-list 'flymake-allowed-file-name-masks - '("\\.py\\'" flymake-pylint-init))) - -;; Set as a minor mode for python -(add-hook 'python-mode-hook '(lambda () (flymake-mode))) diff --git a/elisp/pylint.el b/elisp/pylint.el deleted file mode 100644 index 327da0fcbe..0000000000 --- a/elisp/pylint.el +++ /dev/null @@ -1,255 +0,0 @@ -;;; pylint.el --- minor mode for running `pylint' - -;; Copyright (c) 2009, 2010 Ian Eure -;; Author: Ian Eure -;; Maintainer: Jonathan Kotta - -;; Keywords: languages python -;; Version: 1.02 - -;; pylint.el is free software; you can redistribute it and/or modify it -;; under the terms of the GNU General Public License as published by the Free -;; Software Foundation; either version 2, or (at your option) any later -;; version. -;; -;; It is distributed in the hope that it will be useful, but WITHOUT ANY -;; WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -;; FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -;; details. -;; -;; You should have received a copy of the GNU General Public License along -;; with your copy of Emacs; see the file COPYING. If not, write to the Free -;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -;; MA 02110-1301, USA - -;;; Commentary: -;; -;; Specialized compile mode for pylint. You may want to add the -;; following to your init.el: -;; -;; (autoload 'pylint "pylint") -;; (add-hook 'python-mode-hook 'pylint-add-menu-items) -;; (add-hook 'python-mode-hook 'pylint-add-key-bindings) -;; -;; There is also a handy command `pylint-insert-ignore-comment' that -;; makes it easy to insert comments of the form `# pylint: -;; ignore=msg1,msg2,...'. - -;;; Code: - -(require 'compile) -(require 'tramp) - -(defgroup pylint nil - "Minor mode for running the Pylint Python checker" - :prefix "pylint-" - :group 'tools - :group 'languages) - -(defvar pylint-last-buffer nil - "The most recent PYLINT buffer. -A PYLINT buffer becomes most recent when you select PYLINT mode in it. -Notice that using \\[next-error] or \\[compile-goto-error] modifies -`completion-last-buffer' rather than `pylint-last-buffer'.") - -(defconst pylint-regexp-alist - (let ((base "^\\(.*\\):\\([0-9]+\\):\s+\\(\\[%s.*\\)$")) - (list - (list (format base "[FE]") 1 2) - (list (format base "[RWC]") 1 2 nil 1))) - "Regexp used to match PYLINT hits. See `compilation-error-regexp-alist'.") - -(defcustom pylint-options '("--reports=n" "--output-format=parseable") - "Options to pass to pylint.py" - :type '(repeat string) - :group 'pylint) - -(defcustom pylint-use-python-indent-offset nil - "If non-nil, use `python-indent-offset' to set indent-string." - :type 'boolean - :group 'pylint) - -(defcustom pylint-command "pylint" - "PYLINT command." - :type '(file) - :group 'pylint) - -(defcustom pylint-alternate-pylint-command "pylint2" - "Command for pylint when invoked with C-u." - :type '(file) - :group 'pylint) - -(defcustom pylint-ask-about-save nil - "Non-nil means \\[pylint] asks which buffers to save before compiling. -Otherwise, it saves all modified buffers without asking." - :type 'boolean - :group 'pylint) - -(defvar pylint--messages-list () - "A list of strings of all pylint messages.") - -(defvar pylint--messages-list-hist () - "Completion history for `pylint--messages-list'.") - -(defun pylint--sort-messages (a b) - "Compare function for sorting `pylint--messages-list'. - -Sorts most recently used elements first using `pylint--messages-list-hist'." - (let ((idx 0) - (a-idx most-positive-fixnum) - (b-idx most-positive-fixnum)) - (dolist (e pylint--messages-list-hist) - (when (string= e a) - (setq a-idx idx)) - (when (string= e b) - (setq b-idx idx)) - (setq idx (1+ idx))) - (< a-idx b-idx))) - -(defun pylint--create-messages-list () - "Use `pylint-command' to populate `pylint--messages-list'." - ;; example output: - ;; |--we want this--| - ;; v v - ;; :using-cmp-argument (W1640): *Using the cmp argument for list.sort / sorted* - ;; Using the cmp argument for list.sort or the sorted builtin should be avoided, - ;; since it was removed in Python 3. Using either `key` or `functools.cmp_to_key` - ;; should be preferred. This message can't be emitted when using Python >= 3.0. - (setq pylint--messages-list - (split-string - (with-temp-buffer - (shell-command (concat pylint-command " --list-msgs") (current-buffer)) - (flush-lines "^[^:]") - (goto-char (point-min)) - (while (not (eobp)) - (delete-char 1) ;; delete ";" - (re-search-forward " ") - (delete-region (point) (line-end-position)) - (forward-line 1)) - (buffer-substring-no-properties (point-min) (point-max)))))) - -;;;###autoload -(defun pylint-insert-ignore-comment (&optional arg) - "Insert a comment like \"# pylint: disable=msg1,msg2,...\". - -This command repeatedly uses `completing-read' to match known -messages, and ultimately inserts a comma-separated list of all -the selected messages. - -With prefix argument, only insert a comma-separated list (for -appending to an existing list)." - (interactive "*P") - (unless pylint--messages-list - (pylint--create-messages-list)) - (setq pylint--messages-list - (sort pylint--messages-list #'pylint--sort-messages)) - (let ((msgs ()) - (msg "") - (prefix (if arg - "," - "# pylint: disable=")) - (sentinel "[DONE]")) - (while (progn - (setq msg (completing-read - "Message: " - pylint--messages-list - nil t nil 'pylint--messages-list-hist sentinel)) - (unless (string= sentinel msg) - (add-to-list 'msgs msg 'append)))) - (setq pylint--messages-list-hist - (delete sentinel pylint--messages-list-hist)) - (insert prefix (mapconcat 'identity msgs ",")))) - -(define-compilation-mode pylint-mode "PYLINT" - (setq pylint-last-buffer (current-buffer)) - (set (make-local-variable 'compilation-error-regexp-alist) - pylint-regexp-alist) - (set (make-local-variable 'compilation-disable-input) t)) - -(defvar pylint-mode-map - (let ((map (make-sparse-keymap))) - (set-keymap-parent map compilation-minor-mode-map) - (define-key map " " 'scroll-up) - (define-key map "\^?" 'scroll-down) - (define-key map "\C-c\C-f" 'next-error-follow-minor-mode) - - (define-key map "\r" 'compile-goto-error) ;; ? - (define-key map "n" 'next-error-no-select) - (define-key map "p" 'previous-error-no-select) - (define-key map "{" 'compilation-previous-file) - (define-key map "}" 'compilation-next-file) - (define-key map "\t" 'compilation-next-error) - (define-key map [backtab] 'compilation-previous-error) - map) - "Keymap for PYLINT buffers. -`compilation-minor-mode-map' is a cdr of this.") - -(defun pylint--make-indent-string () - "Make a string for the `--indent-string' option." - (format "--indent-string='%s'" - (make-string python-indent-offset ?\ ))) - -;;;###autoload -(defun pylint (&optional arg) - "Run PYLINT, and collect output in a buffer, much like `compile'. - -While pylint runs asynchronously, you can use \\[next-error] (M-x next-error), -or \\\\[compile-goto-error] in the grep \ -output buffer, to go to the lines where pylint found matches. - -\\{pylint-mode-map}" - (interactive "P") - - (save-some-buffers (not pylint-ask-about-save) nil) - (let* ((filename (buffer-file-name)) - (localname-offset (cl-struct-slot-offset 'tramp-file-name 'localname)) - (filename (or (and (tramp-tramp-file-p filename) - (elt (tramp-dissect-file-name filename) localname-offset)) - filename)) - (filename (shell-quote-argument filename)) - (pylint-command (if arg - pylint-alternate-pylint-command - pylint-command)) - (pylint-options (if (not pylint-use-python-indent-offset) - pylint-options - (append pylint-options - (list (pylint--make-indent-string))))) - (command (mapconcat - 'identity - (append `(,pylint-command) pylint-options `(,filename)) - " "))) - - (compilation-start command 'pylint-mode))) - -;;;###autoload -(defun pylint-add-key-bindings () - (let ((map (cond - ((boundp 'py-mode-map) py-mode-map) - ((boundp 'python-mode-map) python-mode-map)))) - - ;; shortcuts in the tradition of python-mode and ropemacs - (define-key map (kbd "C-c m l") 'pylint) - (define-key map (kbd "C-c m p") 'previous-error) - (define-key map (kbd "C-c m n") 'next-error) - (define-key map (kbd "C-c m i") 'pylint-insert-ignore-comment) - nil)) - -;;;###autoload -(defun pylint-add-menu-items () - (let ((map (cond - ((boundp 'py-mode-map) py-mode-map) - ((boundp 'python-mode-map) python-mode-map)))) - - (define-key map [menu-bar Python pylint-separator] - '("--" . pylint-separator)) - (define-key map [menu-bar Python next-error] - '("Next error" . next-error)) - (define-key map [menu-bar Python prev-error] - '("Previous error" . previous-error)) - (define-key map [menu-bar Python lint] - '("Pylint" . pylint)) - nil)) - -(provide 'pylint) - -;;; pylint.el ends here diff --git a/elisp/startup b/elisp/startup deleted file mode 100644 index 2f8fed1d4b..0000000000 --- a/elisp/startup +++ /dev/null @@ -1,17 +0,0 @@ -;; -*-emacs-lisp-*- -;; -;; Emacs startup file for the Debian GNU/Linux %PACKAGE% package -;; -;; Originally contributed by Nils Naumann -;; Modified by Dirk Eddelbuettel -;; Adapted for dh-make by Jim Van Zandt - -;; The %PACKAGE% package follows the Debian/GNU Linux 'emacsen' policy and -;; byte-compiles its elisp files for each 'Emacs flavor' (emacs19, -;; xemacs19, emacs20, xemacs20...). The compiled code is then -;; installed in a subdirectory of the respective site-lisp directory. -;; We have to add this to the load-path: -(setq load-path (cons (concat "/usr/share/" - (symbol-name debian-emacs-flavor) - "/site-lisp/%PACKAGE%") load-path)) -(load-library "pylint") diff --git a/examples/custom_raw.py b/examples/custom_raw.py index 1cfeae3f9c..090f87ea8f 100644 --- a/examples/custom_raw.py +++ b/examples/custom_raw.py @@ -32,7 +32,7 @@ def process_module(self, node: nodes.Module) -> None: the module's content is accessible via node.stream() function """ with node.stream() as stream: - for (lineno, line) in enumerate(stream): + for lineno, line in enumerate(stream): if line.rstrip().endswith("\\"): self.add_message("backslash-line-continuation", line=lineno) diff --git a/examples/deprecation_checker.py b/examples/deprecation_checker.py index 52244edf60..6fb21eedd5 100644 --- a/examples/deprecation_checker.py +++ b/examples/deprecation_checker.py @@ -51,14 +51,20 @@ def mymethod(self, arg0, arg1, deprecated1=None, arg2='foo', deprecated2='bar', class DeprecationChecker(DeprecatedMixin, BaseChecker): """Class implementing deprecation checker.""" - # DeprecationMixin class is Mixin class implementing logic for searching deprecated methods and functions. - # The list of deprecated methods/functions is defined by implementing class via deprecated_methods callback. + # DeprecatedMixin class is Mixin class implementing logic for searching deprecated methods and functions. + # The list of deprecated methods/functions is defined by the implementing class via deprecated_methods callback. # DeprecatedMixin class is overriding attributes of BaseChecker hence must be specified *before* BaseChecker # in list of base classes. # The name defines a custom section of the config for this checker. name = "deprecated" + # Register messages emitted by the checker. + msgs = { + **DeprecatedMixin.DEPRECATED_METHOD_MESSAGE, + **DeprecatedMixin.DEPRECATED_ARGUMENT_MESSAGE, + } + def deprecated_methods(self) -> set[str]: """Callback method called by DeprecatedMixin for every method/function found in the code. diff --git a/examples/pylintrc b/examples/pylintrc index a74d47a6f4..1cf9639a13 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -5,6 +5,10 @@ # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + # Load and enable all available extensions. Use --list-extensions to see a list # all available extensions. #enable-all-extensions= @@ -34,7 +38,7 @@ extension-pkg-whitelist= # specified are enabled, while categories only check already-enabled messages. fail-on= -# Specify a score threshold to be exceeded before program exits with error. +# Specify a score threshold under which the program will exit with error. fail-under=10 # Interpret the stdin as a python script, whose filename needs to be passed as @@ -44,13 +48,15 @@ fail-under=10 # Files or directories to be skipped. They should be base names, not paths. ignore=CVS -# Add files or directories matching the regex patterns to the ignore-list. The -# regex matches against paths and can be in Posix or Windows format. +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. ignore-paths= -# Files or directories matching the regex patterns are skipped. The regex -# matches against base names, not paths. The default value ignores Emacs file -# locks +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks ignore-patterns=^\.# # List of module names for which member attributes should not be checked @@ -99,189 +105,6 @@ unsafe-load-any-extension=no #verbose= -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'fatal', 'error', 'warning', 'refactor', -# 'convention', and 'info' which contain the number of messages in each -# category, as well as 'statement' which is the total number of statements -# analyzed. This score is used by the global evaluation report (RP0004). -evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -#output-format= - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, -# UNDEFINED. -confidence=HIGH, - CONTROL_FLOW, - INFERENCE, - INFERENCE_FAILURE, - UNDEFINED - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then re-enable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when caught. -overgeneral-exceptions=BaseException, - Exception - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit,argparse.parse_error - - -[DESIGN] - -# List of regular expressions of class ancestor names to ignore when counting -# public methods (see R0903) -exclude-too-few-public-methods= - -# List of qualified class names to ignore when counting class parents (see -# R0901) -ignored-parents= - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules= - -# Output a graph (.gv or any supported image format) of external dependencies -# to the given file (report RP0402 must not be disabled). -ext-import-graph= - -# Output a graph (.gv or any supported image format) of all (i.e. internal and -# external) dependencies to the given file (report RP0402 must not be -# disabled). -import-graph= - -# Output a graph (.gv or any supported image format) of internal dependencies -# to the given file (report RP0402 must not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[CLASSES] - -# Warn about protected attribute access inside special methods -check-protected-access-in-special-methods=no - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - [BASIC] # Naming style matching correct argument names. @@ -418,90 +241,77 @@ variable-naming-style=snake_case #variable-rgx= -[SIMILARITIES] - -# Comments are removed from the similarity computation -ignore-comments=yes - -# Docstrings are removed from the similarity computation -ignore-docstrings=yes - -# Imports are removed from the similarity computation -ignore-imports=yes - -# Signatures are removed from the similarity computation -ignore-signatures=yes - -# Minimum lines number of a similarity. -min-similarity-lines=4 +[CLASSES] +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no -[LOGGING] +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs -[VARIABLES] -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= +[DESIGN] -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= -# List of names allowed to shadow builtins -allowed-redefined-builtins= +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb +# Maximum number of arguments for function / method. +max-args=5 -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ +# Maximum number of attributes for a class (see R0902). +max-attributes=7 -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 -# Tells whether we should check for unused import in __init__ files. -init-import=no +# Maximum number of branch for function / method body. +max-branches=12 -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io +# Maximum number of locals for function / method body. +max-locals=15 +# Maximum number of parents for a class (see R0901). +max-parents=7 -[SPELLING] +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 +# Maximum number of return / yield for function / method body. +max-returns=6 -# Spelling dictionary name. Available dictionaries: en (aspell), en_AU -# (aspell), en_CA (aspell), en_GB (aspell), en_US (aspell). -spelling-dict= +# Maximum number of statements in function / method body. +max-statements=50 -# List of comma separated words that should be considered directives if they -# appear at the beginning of a comment and should not be checked. -spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 -# List of comma separated words that should not be checked. -spelling-ignore-words= -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= +[EXCEPTIONS] -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception [FORMAT] @@ -534,6 +344,99 @@ single-line-class-stmt=no single-line-if-stmt=no +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. @@ -545,6 +448,96 @@ notes=FIXME, notes-rgx= +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en (aspell), en_AU +# (aspell), en_CA (aspell), en_GB (aspell), en_US (aspell). +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + [TYPECHECK] # List of decorators that produce context managers, such as @@ -599,12 +592,33 @@ mixin-class-rgx=.*[Mm]ixin signature-mutators= -[STRING] +[VARIABLES] -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/examples/pyproject.toml b/examples/pyproject.toml index 6c4e0755de..71490d860d 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -4,6 +4,10 @@ # one or another interpreter, leading to false positives when analysed. # analyse-fallback-blocks = +# Clear in-memory caches upon conclusion of linting. Useful if running pylint in +# a server-like mode. +# clear-cache-post-run = + # Always return a 0 (non-error) status code, even if lint errors are found. This # is primarily useful in continuous integration scripts. # exit-zero = @@ -24,7 +28,7 @@ # specified are enabled, while categories only check already-enabled messages. # fail-on = -# Specify a score threshold to be exceeded before program exits with error. +# Specify a score threshold under which the program will exit with error. fail-under = 10 # Interpret the stdin as a python script, whose filename needs to be passed as @@ -34,12 +38,15 @@ fail-under = 10 # Files or directories to be skipped. They should be base names, not paths. ignore = ["CVS"] -# Add files or directories matching the regex patterns to the ignore-list. The -# regex matches against paths and can be in Posix or Windows format. +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, it +# can't be used as an escape character. # ignore-paths = -# Files or directories matching the regex patterns are skipped. The regex matches -# against base names, not paths. The default value ignores Emacs file locks +# Files or directories matching the regular expression patterns are skipped. The +# regex matches against base names, not paths. The default value ignores Emacs +# file locks ignore-patterns = ["^\\.#"] # List of module names for which member attributes should not be checked (useful @@ -219,7 +226,7 @@ exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make"] valid-classmethod-first-arg = ["cls"] # List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg = ["cls"] +valid-metaclass-classmethod-first-arg = ["mcs"] [tool.pylint.design] # List of regular expressions of class ancestor names to ignore when counting @@ -261,7 +268,7 @@ min-public-methods = 2 [tool.pylint.exceptions] # Exceptions that will emit a warning when caught. -overgeneral-exceptions = ["BaseException", "Exception"] +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] [tool.pylint.format] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. @@ -294,6 +301,9 @@ max-module-lines = 1000 # List of modules that can be imported at any level, not just the top level one. # allow-any-import-level = +# Allow explicit reexports by alias from a package __init__. +# allow-reexport-from-package = + # Allow wildcard imports from modules that define __all__. # allow-wildcard-with-all = @@ -353,6 +363,11 @@ disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file- # should appear only once). See also the "--disable" option for examples. enable = ["c-extension-no-member"] +[tool.pylint.method_args] +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods = ["requests.api.delete", "requests.api.get", "requests.api.head", "requests.api.options", "requests.api.patch", "requests.api.post", "requests.api.put", "requests.api.request"] + [tool.pylint.miscellaneous] # List of note tags to take in consideration, separated by a comma. notes = ["FIXME", "XXX", "TODO"] @@ -430,15 +445,6 @@ spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:sk # --spelling-private-dict-file option) instead of raising a message. # spelling-store-unknown-words = -[tool.pylint.string] -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -# check-quote-consistency = - -# This flag controls whether the implicit-str-concat should generate a warning on -# implicit string concatenation in sequences defined over several lines. -# check-str-concat-over-line-jumps = - [tool.pylint.typecheck] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that @@ -509,8 +515,7 @@ callbacks = ["cb_", "_cb"] # be used). dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" -# Argument names that match this expression will be ignored. Default to name with -# leading underscore. +# Argument names that match this expression will be ignored. ignored-argument-names = "_.*|^ignored_|^unused_" # Tells whether we should check for unused import in __init__ files. diff --git a/pylint/__init__.py b/pylint/__init__.py index 7004a64386..2cc7edadbb 100644 --- a/pylint/__init__.py +++ b/pylint/__init__.py @@ -4,8 +4,19 @@ from __future__ import annotations +__all__ = [ + "__version__", + "version", + "modify_sys_path", + "run_pylint", + "run_epylint", + "run_symilar", + "run_pyreverse", +] + import os import sys +import warnings from collections.abc import Sequence from typing import NoReturn @@ -44,10 +55,16 @@ def run_epylint(argv: Sequence[str] | None = None) -> NoReturn: """ from pylint.epylint import Run as EpylintRun + warnings.warn( + "'run_epylint' will be removed in pylint 3.0, use " + "https://github.com/emacsorphanage/pylint instead.", + DeprecationWarning, + stacklevel=1, + ) EpylintRun(argv) -def run_pyreverse(argv: Sequence[str] | None = None) -> NoReturn: # type: ignore[misc] +def run_pyreverse(argv: Sequence[str] | None = None) -> NoReturn: """Run pyreverse. argv can be a sequence of strings normally supplied as arguments on the command line @@ -86,9 +103,10 @@ def modify_sys_path() -> None: if pylint is installed in an editable configuration (as the last item). https://github.com/PyCQA/pylint/issues/4161 """ - sys.path.pop(0) - env_pythonpath = os.environ.get("PYTHONPATH", "") cwd = os.getcwd() + if sys.path[0] in ("", ".", cwd): + sys.path.pop(0) + env_pythonpath = os.environ.get("PYTHONPATH", "") if env_pythonpath.startswith(":") and env_pythonpath not in (f":{cwd}", ":."): sys.path.pop(0) elif env_pythonpath.endswith(":") and env_pythonpath not in (f"{cwd}:", ".:"): @@ -96,4 +114,3 @@ def modify_sys_path() -> None: version = __version__ -__all__ = ["__version__", "version", "modify_sys_path"] diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index ea4243d4cc..a67fa48537 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "2.14.5" +__version__ = "2.16.3" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/pylint/checkers/__init__.py b/pylint/checkers/__init__.py index 7e39c6877c..ed641d8e5b 100644 --- a/pylint/checkers/__init__.py +++ b/pylint/checkers/__init__.py @@ -127,7 +127,7 @@ def table_lines_from_stats( ) new_str = f"{new_value:.3f}" if isinstance(new_value, float) else str(new_value) old_str = f"{old_value:.3f}" if isinstance(old_value, float) else str(old_value) - lines.extend((value[0].replace("_", " "), new_str, old_str, diff_str)) + lines.extend((value[0].replace("_", " "), new_str, old_str, diff_str)) # type: ignore[arg-type] return lines diff --git a/pylint/checkers/base/__init__.py b/pylint/checkers/base/__init__.py index 38b43baddb..f427cbf21b 100644 --- a/pylint/checkers/base/__init__.py +++ b/pylint/checkers/base/__init__.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# pylint: disable=duplicate-code # This is similar to the __init__ of .name_checker + from __future__ import annotations __all__ = [ diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index bd78c2d4f1..fcc980d229 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -13,11 +13,11 @@ from typing import TYPE_CHECKING, cast import astroid -from astroid import nodes +from astroid import nodes, objects from pylint import utils as lint_utils from pylint.checkers import BaseChecker, utils -from pylint.interfaces import HIGH +from pylint.interfaces import HIGH, INFERENCE, Confidence from pylint.reporters.ureports import nodes as reporter_nodes from pylint.utils import LinterStats @@ -104,6 +104,7 @@ def report_by_type_stats( sect.append(reporter_nodes.Table(children=lines, cols=6, rheaders=1)) +# pylint: disable-next = too-many-public-methods class BasicChecker(_BasicChecker): """Basic checker. @@ -166,9 +167,10 @@ class BasicChecker(_BasicChecker): "W0122": ( "Use of exec", "exec-used", - 'Used when you use the "exec" statement (function for Python ' - "3), to discourage its usage. That doesn't " - "mean you cannot use it !", + "Raised when the 'exec' statement is used. It's dangerous to use this " + "function for a user input, and it's also slower than actual code in " + "general. This doesn't mean you should never use it, but you should " + "consider alternatives first and restrict the functions available.", ), "W0123": ( "Use of eval", @@ -187,7 +189,7 @@ class BasicChecker(_BasicChecker): "re-raised.", ), "W0199": ( - "Assert called on a 2-item-tuple. Did you mean 'assert x,y'?", + "Assert called on a populated tuple. Did you mean 'assert x,y'?", "assert-on-tuple", "A call of assert on a tuple will always evaluate to true if " "the tuple is not empty, and will always evaluate to false if " @@ -251,6 +253,18 @@ class BasicChecker(_BasicChecker): "duplicate-value", "This message is emitted when a set contains the same value two or more times.", ), + "W0131": ( + "Named expression used without context", + "named-expr-without-context", + "Emitted if named expression is used to do a regular assignment " + "outside a context like if, for, while, or a comprehension.", + ), + "W0133": ( + "Exception statement has no effect", + "pointless-exception-statement", + "Used when an exception is created without being assigned, raised or returned " + "for subsequent use elsewhere.", + ), } reports = (("RP0101", "Statistics by type", report_by_type_stats),) @@ -315,11 +329,34 @@ def _check_using_constant_test( ) inferred = None emit = isinstance(test, (nodes.Const,) + structs + const_nodes) + maybe_generator_call = None if not isinstance(test, except_nodes): inferred = utils.safe_infer(test) + if inferred is astroid.Uninferable and isinstance(test, nodes.Name): + emit, maybe_generator_call = BasicChecker._name_holds_generator(test) + + # Emit if calling a function that only returns GeneratorExp (always tests True) + elif isinstance(test, nodes.Call): + maybe_generator_call = test + if maybe_generator_call: + inferred_call = utils.safe_infer(maybe_generator_call.func) + if isinstance(inferred_call, nodes.FunctionDef): + # Can't use all(x) or not any(not x) for this condition, because it + # will return True for empty generators, which is not what we want. + all_returns_were_generator = None + for return_node in inferred_call._get_return_nodes_skip_functions(): + if not isinstance(return_node.value, nodes.GeneratorExp): + all_returns_were_generator = False + break + all_returns_were_generator = True + if all_returns_were_generator: + self.add_message( + "using-constant-test", node=node, confidence=INFERENCE + ) + return if emit: - self.add_message("using-constant-test", node=test) + self.add_message("using-constant-test", node=test, confidence=INFERENCE) elif isinstance(inferred, const_nodes): # If the constant node is a FunctionDef or Lambda then # it may be an illicit function call due to missing parentheses @@ -336,12 +373,44 @@ def _check_using_constant_test( for inf_call in call_inferred: if inf_call != astroid.Uninferable: self.add_message( - "missing-parentheses-for-call-in-test", node=test + "missing-parentheses-for-call-in-test", + node=test, + confidence=INFERENCE, ) break except astroid.InferenceError: pass - self.add_message("using-constant-test", node=test) + self.add_message("using-constant-test", node=test, confidence=INFERENCE) + + @staticmethod + def _name_holds_generator(test: nodes.Name) -> tuple[bool, nodes.Call | None]: + """Return whether `test` tests a name certain to hold a generator, or optionally + a call that should be then tested to see if *it* returns only generators. + """ + assert isinstance(test, nodes.Name) + emit = False + maybe_generator_call = None + lookup_result = test.frame(future=True).lookup(test.name) + if not lookup_result: + return emit, maybe_generator_call + maybe_generator_assigned = ( + isinstance(assign_name.parent.value, nodes.GeneratorExp) + for assign_name in lookup_result[1] + if isinstance(assign_name.parent, nodes.Assign) + ) + first_item = next(maybe_generator_assigned, None) + if first_item is not None: + # Emit if this variable is certain to hold a generator + if all(itertools.chain((first_item,), maybe_generator_assigned)): + emit = True + # If this variable holds the result of a call, save it for next test + elif ( + len(lookup_result[1]) == 1 + and isinstance(lookup_result[1][0].parent, nodes.Assign) + and isinstance(lookup_result[1][0].parent.value, nodes.Call) + ): + maybe_generator_call = lookup_result[1][0].parent.value + return emit, maybe_generator_call def visit_module(self, _: nodes.Module) -> None: """Check module name, docstring and required arguments.""" @@ -354,7 +423,11 @@ def visit_classdef(self, _: nodes.ClassDef) -> None: self.linter.stats.node_count["klass"] += 1 @utils.only_required_for_messages( - "pointless-statement", "pointless-string-statement", "expression-not-assigned" + "pointless-statement", + "pointless-exception-statement", + "pointless-string-statement", + "expression-not-assigned", + "named-expr-without-context", ) def visit_expr(self, node: nodes.Expr) -> None: """Check for various kind of statements without effect.""" @@ -379,20 +452,39 @@ def visit_expr(self, node: nodes.Expr) -> None: self.add_message("pointless-string-statement", node=node) return + # Warn W0133 for exceptions that are used as statements + if isinstance(expr, nodes.Call): + name = "" + if isinstance(expr.func, nodes.Name): + name = expr.func.name + elif isinstance(expr.func, nodes.Attribute): + name = expr.func.attrname + + # Heuristic: only run inference for names that begin with an uppercase char + # This reduces W0133's coverage, but retains acceptable runtime performance + # For more details, see: https://github.com/PyCQA/pylint/issues/8073 + inferred = utils.safe_infer(expr) if name[:1].isupper() else None + if isinstance(inferred, objects.ExceptionInstance): + self.add_message( + "pointless-exception-statement", node=node, confidence=INFERENCE + ) + return + # Ignore if this is : - # * a direct function call # * the unique child of a try/except body # * a yield statement # * an ellipsis (which can be used on Python 3 instead of pass) # warn W0106 if we have any underlying function call (we can't predict # side effects), else pointless-statement if ( - isinstance(expr, (nodes.Yield, nodes.Await, nodes.Call)) + isinstance(expr, (nodes.Yield, nodes.Await)) or (isinstance(node.parent, nodes.TryExcept) and node.parent.body == [node]) or (isinstance(expr, nodes.Const) and expr.value is Ellipsis) ): return - if any(expr.nodes_of_class(nodes.Call)): + if isinstance(expr, nodes.NamedExpr): + self.add_message("named-expr-without-context", node=node, confidence=HIGH) + elif any(expr.nodes_of_class(nodes.Call)): self.add_message( "expression-not-assigned", node=node, args=expr.as_string() ) @@ -505,7 +597,7 @@ def _check_dangerous_default(self, node: nodes.FunctionDef) -> None: def is_iterable(internal_node: nodes.NodeNG) -> bool: return isinstance(internal_node, (nodes.List, nodes.Set, nodes.Dict)) - defaults = node.args.defaults or [] + node.args.kw_defaults or [] + defaults = (node.args.defaults or []) + (node.args.kw_defaults or []) for default in defaults: if not default: continue @@ -602,12 +694,16 @@ def _check_misplaced_format_function(self, call_node: nodes.Call) -> None: self.add_message("misplaced-format-function", node=call_node) @utils.only_required_for_messages( - "eval-used", "exec-used", "bad-reversed-sequence", "misplaced-format-function" + "eval-used", + "exec-used", + "bad-reversed-sequence", + "misplaced-format-function", + "unreachable", ) def visit_call(self, node: nodes.Call) -> None: - """Visit a Call node -> check if this is not a disallowed builtin - call and check for * or ** use. - """ + """Visit a Call node.""" + if utils.is_terminating_func(node): + self._check_unreachable(node, confidence=INFERENCE) self._check_misplaced_format_function(node) if isinstance(node.func, nodes.Name): name = node.func.name @@ -624,12 +720,8 @@ def visit_call(self, node: nodes.Call) -> None: @utils.only_required_for_messages("assert-on-tuple", "assert-on-string-literal") def visit_assert(self, node: nodes.Assert) -> None: """Check whether assert is used on a tuple or string literal.""" - if ( - node.fail is None - and isinstance(node.test, nodes.Tuple) - and len(node.test.elts) == 2 - ): - self.add_message("assert-on-tuple", node=node) + if isinstance(node.test, nodes.Tuple) and len(node.test.elts) > 0: + self.add_message("assert-on-tuple", node=node, confidence=HIGH) if isinstance(node.test, nodes.Const) and isinstance(node.test.value, str): if node.test.value: @@ -679,22 +771,26 @@ def leave_tryfinally(self, _: nodes.TryFinally) -> None: self._tryfinallys.pop() def _check_unreachable( - self, node: nodes.Return | nodes.Continue | nodes.Break | nodes.Raise + self, + node: nodes.Return | nodes.Continue | nodes.Break | nodes.Raise | nodes.Call, + confidence: Confidence = HIGH, ) -> None: """Check unreachable code.""" - unreach_stmt = node.next_sibling() - if unreach_stmt is not None: + unreachable_statement = node.next_sibling() + if unreachable_statement is not None: if ( isinstance(node, nodes.Return) - and isinstance(unreach_stmt, nodes.Expr) - and isinstance(unreach_stmt.value, nodes.Yield) + and isinstance(unreachable_statement, nodes.Expr) + and isinstance(unreachable_statement.value, nodes.Yield) ): # Don't add 'unreachable' for empty generators. # Only add warning if 'yield' is followed by another node. - unreach_stmt = unreach_stmt.next_sibling() - if unreach_stmt is None: + unreachable_statement = unreachable_statement.next_sibling() + if unreachable_statement is None: return - self.add_message("unreachable", node=unreach_stmt) + self.add_message( + "unreachable", node=unreachable_statement, confidence=confidence + ) def _check_not_in_finally( self, diff --git a/pylint/checkers/base/basic_error_checker.py b/pylint/checkers/base/basic_error_checker.py index 97176fec2b..abd34da1b9 100644 --- a/pylint/checkers/base/basic_error_checker.py +++ b/pylint/checkers/base/basic_error_checker.py @@ -12,6 +12,7 @@ import astroid from astroid import nodes +from astroid.typing import InferenceResult from pylint.checkers import utils from pylint.checkers.base.basic_checker import _BasicChecker @@ -68,7 +69,7 @@ def _loop_exits_early(loop: nodes.For | nodes.While) -> bool: ) -def _has_abstract_methods(node): +def _has_abstract_methods(node: nodes.ClassDef) -> bool: """Determine if the given `node` has abstract methods. The methods should be made abstract by decorating them @@ -189,7 +190,6 @@ class BasicErrorChecker(_BasicChecker): "continue-in-finally", "Emitted when the `continue` keyword is found " "inside a finally clause, which is a SyntaxError.", - {"maxversion": (3, 8)}, ), "E0117": ( "nonlocal name %s found without binding", @@ -206,6 +206,10 @@ class BasicErrorChecker(_BasicChecker): ), } + def open(self) -> None: + py_version = self.linter.config.py_version + self._py38_plus = py_version >= (3, 8) + @utils.only_required_for_messages("function-redefined") def visit_classdef(self, node: nodes.ClassDef) -> None: self._check_redefinition("class", node) @@ -297,7 +301,6 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: visit_asyncfunctiondef = visit_functiondef def _check_name_used_prior_global(self, node: nodes.FunctionDef) -> None: - scope_globals = { name: child for child in node.nodes_of_class(nodes.Global) @@ -393,15 +396,16 @@ def visit_unaryop(self, node: nodes.UnaryOp) -> None: def _check_nonlocal_without_binding(self, node: nodes.Nonlocal, name: str) -> None: current_scope = node.scope() - while True: - if current_scope.parent is None: - break + while current_scope.parent is not None: if not isinstance(current_scope, (nodes.ClassDef, nodes.FunctionDef)): self.add_message("nonlocal-without-binding", args=(name,), node=node) return - if name not in current_scope.locals: + # Search for `name` in the parent scope if: + # `current_scope` is the same scope in which the `nonlocal` name is declared + # or `name` is not in `current_scope.locals`. + if current_scope is node.scope() or name not in current_scope.locals: current_scope = current_scope.parent.scope() continue @@ -409,7 +413,9 @@ def _check_nonlocal_without_binding(self, node: nodes.Nonlocal, name: str) -> No return if not isinstance(current_scope, nodes.FunctionDef): - self.add_message("nonlocal-without-binding", args=(name,), node=node) + self.add_message( + "nonlocal-without-binding", args=(name,), node=node, confidence=HIGH + ) @utils.only_required_for_messages("nonlocal-without-binding") def visit_nonlocal(self, node: nodes.Nonlocal) -> None: @@ -424,7 +430,9 @@ def visit_call(self, node: nodes.Call) -> None: for inferred in infer_all(node.func): self._check_inferred_class_is_abstract(inferred, node) - def _check_inferred_class_is_abstract(self, inferred, node: nodes.Call): + def _check_inferred_class_is_abstract( + self, inferred: InferenceResult, node: nodes.Call + ) -> None: if not isinstance(inferred, nodes.ClassDef): return @@ -492,6 +500,7 @@ def _check_in_loop( isinstance(parent, nodes.TryFinally) and node in parent.finalbody and isinstance(node, nodes.Continue) + and not self._py38_plus ): self.add_message("continue-in-finally", node=node) diff --git a/pylint/checkers/base/comparison_checker.py b/pylint/checkers/base/comparison_checker.py index 3398a54077..ffbd273744 100644 --- a/pylint/checkers/base/comparison_checker.py +++ b/pylint/checkers/base/comparison_checker.py @@ -48,7 +48,7 @@ class ComparisonChecker(_BasicChecker): {"old_names": [("W0154", "old-unidiomatic-typecheck")]}, ), "R0123": ( - "Comparison to literal", + "In '%s', use '%s' when comparing constant literals not '%s' ('%s')", "literal-comparison", "Used when comparing an object to a literal, which is usually " "what you do not want to do, since you can compare to a different " @@ -76,8 +76,8 @@ class ComparisonChecker(_BasicChecker): "W0177": ( "Comparison %s should be %s", "nan-comparison", - "Used when an expression is compared to NaN" - "values like numpy.NaN and float('nan')", + "Used when an expression is compared to NaN " + "values like numpy.NaN and float('nan').", ), } @@ -89,16 +89,10 @@ def _check_singleton_comparison( checking_for_absence: bool = False, ) -> None: """Check if == or != is being used to compare a singleton value.""" - singleton_values = (True, False, None) - def _is_singleton_const(node: nodes.NodeNG) -> bool: - return isinstance(node, nodes.Const) and any( - node.value is value for value in singleton_values - ) - - if _is_singleton_const(left_value): + if utils.is_singleton_const(left_value): singleton, other_value = left_value.value, right_value - elif _is_singleton_const(right_value): + elif utils.is_singleton_const(right_value): singleton, other_value = right_value.value, left_value else: return @@ -201,7 +195,27 @@ def _check_literal_comparison( is_const = isinstance(literal.value, (bytes, str, int, float)) if is_const or is_other_literal: - self.add_message("literal-comparison", node=node) + incorrect_node_str = node.as_string() + if "is not" in incorrect_node_str: + equal_or_not_equal = "!=" + is_or_is_not = "is not" + else: + equal_or_not_equal = "==" + is_or_is_not = "is" + fixed_node_str = incorrect_node_str.replace( + is_or_is_not, equal_or_not_equal + ) + self.add_message( + "literal-comparison", + args=( + incorrect_node_str, + equal_or_not_equal, + is_or_is_not, + fixed_node_str, + ), + node=node, + confidence=HIGH, + ) def _check_logical_tautology(self, node: nodes.Compare) -> None: """Check if identifier is compared against itself. @@ -230,8 +244,8 @@ def _check_logical_tautology(self, node: nodes.Compare) -> None: suggestion = f"{left_operand} {operator} {right_operand}" self.add_message("comparison-with-itself", node=node, args=(suggestion,)) - def _check_two_literals_being_compared(self, node: nodes.Compare) -> None: - """Check if two literals are being compared; this is always a logical tautology.""" + def _check_constants_comparison(self, node: nodes.Compare) -> None: + """When two constants are being compared it is always a logical tautology.""" left_operand = node.left if not isinstance(left_operand, nodes.Const): return @@ -284,7 +298,7 @@ def visit_compare(self, node: nodes.Compare) -> None: self._check_callable_comparison(node) self._check_logical_tautology(node) self._check_unidiomatic_typecheck(node) - self._check_two_literals_being_compared(node) + self._check_constants_comparison(node) # NOTE: this checker only works with binary comparisons like 'x == 42' # but not 'x == y == 42' if len(node.ops) != 1: diff --git a/pylint/checkers/base/docstring_checker.py b/pylint/checkers/base/docstring_checker.py index 3b53ff8827..791b085b5c 100644 --- a/pylint/checkers/base/docstring_checker.py +++ b/pylint/checkers/base/docstring_checker.py @@ -59,21 +59,21 @@ class DocStringChecker(_BasicChecker): "C0114": ( "Missing module docstring", "missing-module-docstring", - "Used when a module has no docstring." + "Used when a module has no docstring. " "Empty modules do not require a docstring.", {"old_names": [("C0111", "missing-docstring")]}, ), "C0115": ( "Missing class docstring", "missing-class-docstring", - "Used when a class has no docstring." + "Used when a class has no docstring. " "Even an empty class must have a docstring.", {"old_names": [("C0111", "missing-docstring")]}, ), "C0116": ( "Missing function or method docstring", "missing-function-docstring", - "Used when a function or method has no docstring." + "Used when a function or method has no docstring. " "Some special methods like __init__ do not require a " "docstring.", {"old_names": [("C0111", "missing-docstring")]}, @@ -108,16 +108,16 @@ class DocStringChecker(_BasicChecker): def open(self) -> None: self.linter.stats.reset_undocumented() - @utils.only_required_for_messages("missing-docstring", "empty-docstring") + @utils.only_required_for_messages("missing-module-docstring", "empty-docstring") def visit_module(self, node: nodes.Module) -> None: self._check_docstring("module", node) - @utils.only_required_for_messages("missing-docstring", "empty-docstring") + @utils.only_required_for_messages("missing-class-docstring", "empty-docstring") def visit_classdef(self, node: nodes.ClassDef) -> None: if self.linter.config.no_docstring_rgx.match(node.name) is None: self._check_docstring("class", node) - @utils.only_required_for_messages("missing-docstring", "empty-docstring") + @utils.only_required_for_messages("missing-function-docstring", "empty-docstring") def visit_functiondef(self, node: nodes.FunctionDef) -> None: if self.linter.config.no_docstring_rgx.match(node.name) is None: ftype = "method" if node.is_method() else "function" diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 3f30fd8f01..c18af89cc9 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -39,12 +39,17 @@ # Default patterns for name types that do not have styles DEFAULT_PATTERNS = { "typevar": re.compile( - r"^_{0,2}(?:[^\W\da-z_]+|(?:[^\W\da-z_]+[^\WA-Z_]+)+T?(? None: if len(groups[min_warnings]) > 1: by_line = sorted( groups[min_warnings], - key=lambda group: min( + key=lambda group: min( # type: ignore[no-any-return] warning[0].lineno for warning in group if warning[0].lineno is not None @@ -383,11 +388,6 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: visit_asyncfunctiondef = visit_functiondef - @utils.only_required_for_messages("disallowed-name", "invalid-name") - def visit_global(self, node: nodes.Global) -> None: - for name in node.names: - self._check_name("const", name, node) - @utils.only_required_for_messages( "disallowed-name", "invalid-name", @@ -439,6 +439,11 @@ def visit_assignname(self, node: nodes.AssignName) -> None: inferred_assign_type, nodes.Const ): self._check_name("const", node.name, node) + else: + self._check_name( + "variable", node.name, node, disallowed_check_only=True + ) + # Check names defined in AnnAssign nodes elif isinstance( assign_type, nodes.AnnAssign @@ -456,10 +461,8 @@ def visit_assignname(self, node: nodes.AssignName) -> None: elif isinstance(frame, nodes.ClassDef): if not list(frame.local_attr_ancestors(node.name)): for ancestor in frame.ancestors(): - if ( - ancestor.name == "Enum" - and ancestor.root().name == "enum" - or utils.is_assign_name_annotated_with(node, "Final") + if utils.is_enum(ancestor) or utils.is_assign_name_annotated_with( + node, "Final" ): self._check_name("class_const", node.name, node) break @@ -517,6 +520,7 @@ def _check_name( name: str, node: nodes.NodeNG, confidence: interfaces.Confidence = interfaces.HIGH, + disallowed_check_only: bool = False, ) -> None: """Check for a name using the type's regexp.""" @@ -531,7 +535,9 @@ def _should_exempt_from_invalid_name(node: nodes.NodeNG) -> bool: return if self._name_disallowed_by_regex(name=name): self.linter.stats.increase_bad_name(node_type, 1) - self.add_message("disallowed-name", node=node, args=name) + self.add_message( + "disallowed-name", node=node, args=name, confidence=interfaces.HIGH + ) return regexp = self._name_regexps[node_type] match = regexp.match(name) @@ -543,7 +549,11 @@ def _should_exempt_from_invalid_name(node: nodes.NodeNG) -> bool: warnings = bad_name_group.setdefault(match.lastgroup, []) # type: ignore[union-attr, arg-type] warnings.append((node, node_type, name, confidence)) - if match is None and not _should_exempt_from_invalid_name(node): + if ( + match is None + and not disallowed_check_only + and not _should_exempt_from_invalid_name(node) + ): self._raise_name_warning(None, node, node_type, name, confidence) # Check TypeVar names for variance suffixes @@ -557,7 +567,7 @@ def _assigns_typevar(node: nodes.NodeNG | None) -> bool: inferred = utils.safe_infer(node.func) if ( isinstance(inferred, astroid.ClassDef) - and inferred.qname() == TYPING_TYPE_VAR_QNAME + and inferred.qname() in TYPE_VAR_QNAME ): return True return False diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py index 6515d107ef..dd7a0222fc 100644 --- a/pylint/checkers/base_checker.py +++ b/pylint/checkers/base_checker.py @@ -7,7 +7,7 @@ import abc import functools import warnings -from collections.abc import Iterator +from collections.abc import Iterable, Sequence from inspect import cleandoc from tokenize import TokenInfo from typing import TYPE_CHECKING, Any @@ -34,7 +34,6 @@ @functools.total_ordering class BaseChecker(_ArgumentsProvider): - # checker name (you may reuse an existing one) name: str = "" # ordered list of options to control the checker behaviour @@ -54,6 +53,7 @@ def __init__(self, linter: PyLinter) -> None: "longer supported. Child classes should only inherit BaseChecker or any " "of the other checker types from pylint.checkers.", DeprecationWarning, + stacklevel=2, ) if self.name is not None: self.name = self.name.lower() @@ -105,10 +105,11 @@ def __str__(self) -> str: def get_full_documentation( self, msgs: dict[str, MessageDefinitionTuple], - options: Iterator[tuple[str, OptionDict, Any]], - reports: tuple[tuple[str, str, ReportsCallable], ...], + options: Iterable[tuple[str, OptionDict, Any]], + reports: Sequence[tuple[str, str, ReportsCallable]], doc: str | None = None, module: str | None = None, + show_options: bool = True, ) -> str: result = "" checker_title = f"{self.name.replace('_', ' ').title()} checker" @@ -126,8 +127,11 @@ def get_full_documentation( # options might be an empty generator and not be False when cast to boolean options_list = list(options) if options_list: - result += get_rst_title(f"{checker_title} Options", "^") - result += f"{get_rst_section(None, options_list)}\n" + if show_options: + result += get_rst_title(f"{checker_title} Options", "^") + result += f"{get_rst_section(None, options_list)}\n" + else: + result += f"See also :ref:`{self.name} checker's options' documentation <{self.name}-options>`\n\n" if msgs: result += get_rst_title(f"{checker_title} Messages", "^") for msgid, msg in sorted( @@ -174,6 +178,10 @@ def check_consistency(self) -> None: checker_id = None existing_ids = [] for message in self.messages: + # Id's for shared messages such as the 'deprecated-*' messages + # can be inconsistent with their checker id. + if message.shared: + continue if checker_id is not None and checker_id != message.msgid[1:3]: error_msg = "Inconsistent checker part in message id " error_msg += f"'{message.msgid}' (expected 'x{checker_id}xx' " @@ -192,8 +200,8 @@ def create_message_definition_from_tuple( # TODO: 3.0: Remove deprecated if-statement elif implements(self, (IRawChecker, ITokenChecker)): warnings.warn( # pragma: no cover - "Checkers should subclass BaseTokenChecker or BaseRawFileChecker" - "instead of using the __implements__ mechanism. Use of __implements__" + "Checkers should subclass BaseTokenChecker or BaseRawFileChecker " + "instead of using the __implements__ mechanism. Use of __implements__ " "will no longer be supported in pylint 3.0", DeprecationWarning, ) @@ -227,6 +235,12 @@ def messages(self) -> list[MessageDefinition]: ] def get_message_definition(self, msgid: str) -> MessageDefinition: + # TODO: 3.0: Remove deprecated method + warnings.warn( + "'get_message_definition' is deprecated and will be removed in 3.0.", + DeprecationWarning, + stacklevel=2, + ) for message_definition in self.messages: if message_definition.msgid == msgid: return message_definition diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index 933e65c6c7..cf2fc39e25 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -8,11 +8,16 @@ import collections import sys +from collections import defaultdict +from collections.abc import Callable, Sequence from itertools import chain, zip_longest from re import Pattern +from typing import TYPE_CHECKING, Any, Union import astroid from astroid import bases, nodes +from astroid.nodes import LocalsDictNodeNG +from astroid.typing import SuccessfulInferenceResult from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import ( @@ -38,11 +43,17 @@ from pylint.interfaces import HIGH, INFERENCE from pylint.typing import MessageDefinitionTuple +if TYPE_CHECKING: + from pylint.lint.pylinter import PyLinter + + if sys.version_info >= (3, 8): from functools import cached_property else: from astroid.decorators import cachedproperty as cached_property +_AccessNodes = Union[nodes.Attribute, nodes.AssignAttr] + INVALID_BASE_CLASSES = {"bool", "range", "slice", "memoryview"} BUILTIN_DECORATORS = {"builtins.property", "builtins.classmethod"} ASTROID_TYPE_COMPARATORS = { @@ -65,7 +76,7 @@ ) -def _signature_from_call(call): +def _signature_from_call(call: nodes.Call) -> _CallSignature: kws = {} args = [] starred_kws = [] @@ -73,7 +84,7 @@ def _signature_from_call(call): for keyword in call.keywords or []: arg, value = keyword.arg, keyword.value if arg is None and isinstance(value, nodes.Name): - # Starred node and we are interested only in names, + # Starred node, and we are interested only in names, # otherwise some transformation might occur for the parameter. starred_kws.append(value.name) elif isinstance(value, nodes.Name): @@ -94,7 +105,7 @@ def _signature_from_call(call): return _CallSignature(args, kws, starred_args, starred_kws) -def _signature_from_arguments(arguments): +def _signature_from_arguments(arguments: nodes.Arguments) -> _ParameterSignature: kwarg = arguments.kwarg vararg = arguments.vararg args = [ @@ -106,7 +117,9 @@ def _signature_from_arguments(arguments): return _ParameterSignature(args, kwonlyargs, vararg, kwarg) -def _definition_equivalent_to_call(definition, call): +def _definition_equivalent_to_call( + definition: _ParameterSignature, call: _CallSignature +) -> bool: """Check if a definition signature is equivalent to a call.""" if definition.kwargs: if definition.kwargs not in call.starred_kws: @@ -183,11 +196,11 @@ def _is_trivial_super_delegation(function: nodes.FunctionDef) -> bool: # Deal with parameters overriding in two methods. -def _positional_parameters(method): +def _positional_parameters(method: nodes.FunctionDef) -> list[nodes.AssignName]: positional = method.args.args if method.is_bound() and method.type in {"classmethod", "method"}: positional = positional[1:] - return positional + return positional # type: ignore[no-any-return] class _DefaultMissing: @@ -197,7 +210,9 @@ class _DefaultMissing: _DEFAULT_MISSING = _DefaultMissing() -def _has_different_parameters_default_value(original, overridden): +def _has_different_parameters_default_value( + original: nodes.Arguments, overridden: nodes.Arguments +) -> bool: """Check if original and overridden methods arguments have different default values. Return True if one of the overridden arguments has a default @@ -229,7 +244,9 @@ def _has_different_parameters_default_value(original, overridden): if not isinstance(overridden_default, original_type): # Two args with same name but different types return True - is_same_fn = ASTROID_TYPE_COMPARATORS.get(original_type) + is_same_fn: Callable[[Any, Any], bool] | None = ASTROID_TYPE_COMPARATORS.get( + original_type + ) if is_same_fn is None: # If the default value comparison is unhandled, assume the value is different return True @@ -242,7 +259,7 @@ def _has_different_parameters_default_value(original, overridden): def _has_different_parameters( original: list[nodes.AssignName], overridden: list[nodes.AssignName], - dummy_parameter_regex: Pattern, + dummy_parameter_regex: Pattern[str], ) -> list[str]: result: list[str] = [] zipped = zip_longest(original, overridden) @@ -296,7 +313,7 @@ def _has_different_keyword_only_parameters( def _different_parameters( original: nodes.FunctionDef, overridden: nodes.FunctionDef, - dummy_parameter_regex: Pattern, + dummy_parameter_regex: Pattern[str], ) -> list[str]: """Determine if the two methods have different parameters. @@ -370,11 +387,11 @@ def _different_parameters( return output_messages -def _is_invalid_base_class(cls): +def _is_invalid_base_class(cls: nodes.ClassDef) -> bool: return cls.name in INVALID_BASE_CLASSES and is_builtin_object(cls) -def _has_data_descriptor(cls, attr): +def _has_data_descriptor(cls: nodes.ClassDef, attr: str) -> bool: attributes = cls.getattr(attr) for attribute in attributes: try: @@ -393,7 +410,11 @@ def _has_data_descriptor(cls, attr): return False -def _called_in_methods(func, klass, methods): +def _called_in_methods( + func: LocalsDictNodeNG, + klass: nodes.ClassDef, + methods: Sequence[str], +) -> bool: """Check if the func was called in any of the given methods, belonging to the *klass*. @@ -422,7 +443,7 @@ def _called_in_methods(func, klass, methods): return False -def _is_attribute_property(name, klass): +def _is_attribute_property(name: str, klass: nodes.ClassDef) -> bool: """Check if the given attribute *name* is a property in the given *klass*. It will look for `property` calls or for functions @@ -458,7 +479,9 @@ def _is_attribute_property(name, klass): return False -def _has_same_layout_slots(slots, assigned_value): +def _has_same_layout_slots( + slots: list[nodes.Const | None], assigned_value: nodes.Name +) -> bool: inferred = next(assigned_value.infer()) if isinstance(inferred, nodes.ClassDef): other_slots = inferred.slots() @@ -470,9 +493,7 @@ def _has_same_layout_slots(slots, assigned_value): return False -MSGS: dict[ - str, MessageDefinitionTuple -] = { # pylint: disable=consider-using-namedtuple-or-dataclass +MSGS: dict[str, MessageDefinitionTuple] = { "F0202": ( "Unable to check methods signature (%s / %s)", "method-check-failed", @@ -505,13 +526,13 @@ def _has_same_layout_slots(slots, assigned_value): "descendant of the class where it's defined.", ), "E0211": ( - "Method has no argument", + "Method %r has no argument", "no-method-argument", "Used when a method which should have the bound instance as " "first argument has no argument defined.", ), "E0213": ( - 'Method should have "self" as first argument', + 'Method %r should have "self" as first argument', "no-self-argument", 'Used when a method has an attribute different the "self" as ' "first argument. This is considered as an error since this is " @@ -562,7 +583,7 @@ def _has_same_layout_slots(slots, assigned_value): "implemented interface or in an overridden method.", ), "W0223": ( - "Method %r is abstract in class %r but is not overridden", + "Method %r is abstract in class %r but is not overridden in child class %r", "abstract-method", "Used when an abstract method (i.e. raise NotImplementedError) is " "not overridden in concrete class.", @@ -579,12 +600,13 @@ def _has_same_layout_slots(slots, assigned_value): "Used when an __init__ method is called on a class which is not " "in the direct ancestors for the analysed class.", ), - "W0235": ( - "Useless super delegation in method %r", - "useless-super-delegation", + "W0246": ( + "Useless parent or super() delegation in method %r", + "useless-parent-delegation", "Used whenever we can detect that an overridden method is useless, " - "relying on super() delegation to do the same thing as another method " + "relying on parent or super() delegation to do the same thing as another method " "from the MRO.", + {"old_names": [("W0235", "useless-super-delegation")]}, ), "W0236": ( "Method %r was expected to be %r, found it instead as %r", @@ -662,7 +684,7 @@ def _has_same_layout_slots(slots, assigned_value): "Used when a value in __slots__ conflicts with a class variable, property or method.", ), "E0243": ( - "Invalid __class__ object", + "Invalid assignment to '__class__'. Should be a class definition but got a '%s'", "invalid-class-object", "Used when an invalid object is assigned to a __class__ property. " "Only a class is permitted.", @@ -703,17 +725,20 @@ def _has_same_layout_slots(slots, assigned_value): } -def _scope_default(): - return collections.defaultdict(list) +def _scope_default() -> defaultdict[str, list[_AccessNodes]]: + # It's impossible to nest defaultdicts so we must use a function + return defaultdict(list) class ScopeAccessMap: """Store the accessed variables per scope.""" - def __init__(self): - self._scopes = collections.defaultdict(_scope_default) + def __init__(self) -> None: + self._scopes: defaultdict[ + nodes.ClassDef, defaultdict[str, list[_AccessNodes]] + ] = defaultdict(_scope_default) - def set_accessed(self, node): + def set_accessed(self, node: _AccessNodes) -> None: """Set the given node as accessed.""" frame = node_frame_class(node) @@ -722,7 +747,7 @@ def set_accessed(self, node): return self._scopes[frame][node.attrname].append(node) - def accessed(self, scope): + def accessed(self, scope: nodes.ClassDef) -> dict[str, list[_AccessNodes]]: """Get the accessed variables for the given scope.""" return self._scopes.get(scope, {}) @@ -767,7 +792,7 @@ class ClassChecker(BaseChecker): ( "valid-metaclass-classmethod-first-arg", { - "default": ("cls",), + "default": ("mcs",), "type": "csv", "metavar": "", "help": "List of valid names for the first argument in \ @@ -804,10 +829,10 @@ class ClassChecker(BaseChecker): ), ) - def __init__(self, linter=None): + def __init__(self, linter: PyLinter) -> None: super().__init__(linter) self._accessed = ScopeAccessMap() - self._first_attrs = [] + self._first_attrs: list[str | None] = [] def open(self) -> None: self._mixin_class_rgx = self.linter.config.mixin_class_rgx @@ -815,8 +840,8 @@ def open(self) -> None: self._py38_plus = py_version >= (3, 8) @cached_property - def _dummy_rgx(self): - return self.linter.config.dummy_variables_rgx + def _dummy_rgx(self) -> Pattern[str]: + return self.linter.config.dummy_variables_rgx # type: ignore[no-any-return] @only_required_for_messages( "abstract-method", @@ -840,7 +865,7 @@ def visit_classdef(self, node: nodes.ClassDef) -> None: self._check_typing_final(node) self._check_consistent_mro(node) - def _check_consistent_mro(self, node): + def _check_consistent_mro(self, node: nodes.ClassDef) -> None: """Detect that a class has a consistent mro or duplicate bases.""" try: node.mro() @@ -848,11 +873,8 @@ def _check_consistent_mro(self, node): self.add_message("inconsistent-mro", args=node.name, node=node) except astroid.DuplicateBasesError: self.add_message("duplicate-bases", args=node.name, node=node) - except NotImplementedError: - # Old style class, there's no mro so don't do anything. - pass - def _check_proper_bases(self, node): + def _check_proper_bases(self, node: nodes.ClassDef) -> None: """Detect that a class inherits something which is not a class or a type. """ @@ -953,7 +975,6 @@ def _check_unused_private_functions(self, node: nodes.ClassDef) -> None: "cls", node.name, }: - break # Check type(self).__attrname @@ -1129,6 +1150,7 @@ def _check_attribute_defined_outside_init(self, cnode: nodes.ClassDef) -> None: "attribute-defined-outside-init", args=attr, node=node ) + # pylint: disable = too-many-branches def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Check method arguments, overriding.""" # ignore actual functions @@ -1157,7 +1179,7 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: continue if not isinstance(parent_function, nodes.FunctionDef): continue - self._check_signature(node, parent_function, "overridden", klass) + self._check_signature(node, parent_function, klass) self._check_invalid_overridden_method(node, parent_function) break @@ -1197,6 +1219,7 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: pass # check if the method is hidden by an attribute + # pylint: disable = too-many-try-statements try: overridden = klass.instance_attr(node.name)[0] overridden_frame = overridden.frame(future=True) @@ -1239,7 +1262,7 @@ def _check_useless_super_delegation(self, function: nodes.FunctionDef) -> None: if not _is_trivial_super_delegation(function): return - call = function.body[0].value + call: nodes.Call = function.body[0].value # Classes that override __eq__ should also override # __hash__, even a trivial override is meaningful @@ -1267,6 +1290,8 @@ def _check_useless_super_delegation(self, function: nodes.FunctionDef) -> None: or _has_different_parameters_default_value( meth_node.args, function.args ) + # arguments to builtins such as Exception.__init__() cannot be inspected + or (meth_node.args.args is None and function.argnames() != ["self"]) ): return break @@ -1283,7 +1308,7 @@ def _check_useless_super_delegation(self, function: nodes.FunctionDef) -> None: ): return - def form_annotations(arguments): + def form_annotations(arguments: nodes.Arguments) -> list[str]: annotations = chain( (arguments.posonlyargs_annotations or []), arguments.annotations ) @@ -1305,10 +1330,13 @@ def form_annotations(arguments): if _definition_equivalent_to_call(params, args): self.add_message( - "useless-super-delegation", node=function, args=(function.name,) + "useless-parent-delegation", + node=function, + args=(function.name,), + confidence=INFERENCE, ) - def _check_property_with_parameters(self, node): + def _check_property_with_parameters(self, node: nodes.FunctionDef) -> None: if ( node.args.args and len(node.args.args) > 1 @@ -1317,7 +1345,11 @@ def _check_property_with_parameters(self, node): ): self.add_message("property-with-parameters", node=node) - def _check_invalid_overridden_method(self, function_node, parent_function_node): + def _check_invalid_overridden_method( + self, + function_node: nodes.FunctionDef, + parent_function_node: nodes.FunctionDef, + ) -> None: parent_is_property = decorated_with_property( parent_function_node ) or is_property_setter_or_deleter(parent_function_node) @@ -1366,7 +1398,8 @@ def _check_invalid_overridden_method(self, function_node, parent_function_node): def _check_slots(self, node: nodes.ClassDef) -> None: if "__slots__" not in node.locals: return - for slots in node.igetattr("__slots__"): + + for slots in node.ilookup("__slots__"): # check if __slots__ is a valid type if slots is astroid.Uninferable: continue @@ -1387,7 +1420,7 @@ def _check_slots(self, node: nodes.ClassDef) -> None: else: values = slots.itered() if values is astroid.Uninferable: - return + continue for elt in values: try: self._check_slots_elt(elt, node) @@ -1429,7 +1462,9 @@ def _check_redefined_slots( node=slots_node, ) - def _check_slots_elt(self, elt, node): + def _check_slots_elt( + self, elt: SuccessfulInferenceResult, node: nodes.ClassDef + ) -> None: for inferred in elt.infer(): if inferred is astroid.Uninferable: continue @@ -1523,7 +1558,18 @@ def visit_assignattr(self, node: nodes.AssignAttr) -> None: def _check_invalid_class_object(self, node: nodes.AssignAttr) -> None: if not node.attrname == "__class__": return - inferred = safe_infer(node.parent.value) + if isinstance(node.parent, nodes.Tuple): + class_index = -1 + for i, elt in enumerate(node.parent.elts): + if hasattr(elt, "attrname") and elt.attrname == "__class__": + class_index = i + if class_index == -1: + # This should not happen because we checked that the node name + # is '__class__' earlier, but let's not be too confident here + return # pragma: no cover + inferred = safe_infer(node.parent.parent.value.elts[class_index]) + else: + inferred = safe_infer(node.parent.value) if ( isinstance(inferred, nodes.ClassDef) or inferred is astroid.Uninferable @@ -1531,9 +1577,14 @@ def _check_invalid_class_object(self, node: nodes.AssignAttr) -> None: ): # If is uninferable, we allow it to prevent false positives return - self.add_message("invalid-class-object", node=node) + self.add_message( + "invalid-class-object", + node=node, + args=inferred.__class__.__name__, + confidence=INFERENCE, + ) - def _check_in_slots(self, node): + def _check_in_slots(self, node: nodes.AssignAttr) -> None: """Check that the given AssignAttr node is defined in the class slots. """ @@ -1581,6 +1632,10 @@ def _check_in_slots(self, node): # Properties circumvent the slots mechanism, # so we should not emit a warning for them. return + if node.attrname != "__class__" and utils.is_class_attr( + node.attrname, klass + ): + return if node.attrname in klass.locals: for local_name in klass.locals.get(node.attrname): statement = local_name.statement(future=True) @@ -1596,7 +1651,12 @@ def _check_in_slots(self, node): slots, node.parent.value ): return - self.add_message("assigning-non-slot", args=(node.attrname,), node=node) + self.add_message( + "assigning-non-slot", + args=(node.attrname,), + node=node, + confidence=INFERENCE, + ) @only_required_for_messages( "protected-access", "no-classmethod-decorator", "no-staticmethod-decorator" @@ -1611,7 +1671,7 @@ def visit_assign(self, assign_node: nodes.Assign) -> None: return self._check_protected_attribute_access(node) - def _check_classmethod_declaration(self, node): + def _check_classmethod_declaration(self, node: nodes.Assign) -> None: """Checks for uses of classmethod() or staticmethod(). When a @classmethod or @staticmethod decorator should be used instead. @@ -1650,7 +1710,9 @@ def _check_classmethod_declaration(self, node): if any(method_name == member.name for member in parent_class.mymethods()): self.add_message(msg, node=node.targets[0]) - def _check_protected_attribute_access(self, node: nodes.Attribute): + def _check_protected_attribute_access( + self, node: nodes.Attribute | nodes.AssignAttr + ) -> None: """Given an attribute access node (set or get), check if attribute access is legitimate. @@ -1663,12 +1725,10 @@ def _check_protected_attribute_access(self, node: nodes.Attribute): Klass. """ attrname = node.attrname - if ( is_attr_protected(attrname) and attrname not in self.linter.config.exclude_protected ): - klass = node_frame_class(node) # In classes, check we are not getting a parent method @@ -1731,7 +1791,7 @@ def _check_protected_attribute_access(self, node: nodes.Attribute): if ( self._is_classmethod(node.frame(future=True)) and self._is_inferred_instance(node.expr, klass) - and self._is_class_attribute(attrname, klass) + and self._is_class_or_instance_attribute(attrname, klass) ): return @@ -1761,27 +1821,24 @@ def _is_type_self_call(self, expr: nodes.NodeNG) -> bool: ) @staticmethod - def _is_classmethod(func): + def _is_classmethod(func: LocalsDictNodeNG) -> bool: """Check if the given *func* node is a class method.""" - return isinstance(func, nodes.FunctionDef) and ( func.type == "classmethod" or func.name == "__class_getitem__" ) @staticmethod - def _is_inferred_instance(expr, klass): + def _is_inferred_instance(expr: nodes.NodeNG, klass: nodes.ClassDef) -> bool: """Check if the inferred value of the given *expr* is an instance of *klass*. """ - inferred = safe_infer(expr) if not isinstance(inferred, astroid.Instance): return False - return inferred._proxied is klass @staticmethod - def _is_class_attribute(name, klass): + def _is_class_or_instance_attribute(name: str, klass: nodes.ClassDef) -> bool: """Check if the given attribute *name* is a class or instance member of the given *klass*. @@ -1789,11 +1846,8 @@ def _is_class_attribute(name, klass): ``False`` otherwise. """ - try: - klass.getattr(name) + if utils.is_class_attr(name, klass): return True - except astroid.NotFoundError: - pass try: klass.instance_attr(name) @@ -1801,7 +1855,9 @@ def _is_class_attribute(name, klass): except astroid.NotFoundError: return False - def _check_accessed_members(self, node, accessed): + def _check_accessed_members( + self, node: nodes.ClassDef, accessed: dict[str, list[_AccessNodes]] + ) -> None: """Check that accessed members are defined.""" excs = ("AttributeError", "Exception", "BaseException") for attr, nodes_lst in accessed.items(): @@ -1861,7 +1917,9 @@ def _check_accessed_members(self, node, accessed): args=(attr, lno), ) - def _check_first_arg_for_type(self, node, metaclass=0): + def _check_first_arg_for_type( + self, node: nodes.FunctionDef, metaclass: bool + ) -> None: """Check the name of first argument, expect:. * 'self' for a regular method @@ -1892,9 +1950,17 @@ def _check_first_arg_for_type(self, node, metaclass=0): self.add_message("bad-staticmethod-argument", args=first, node=node) return self._first_attrs[-1] = None + elif "builtins.staticmethod" in node.decoratornames(): + # Check if there is a decorator which is not named `staticmethod` but is assigned to one. + return # class / regular method with no args - elif not node.args.args and not node.args.posonlyargs: - self.add_message("no-method-argument", node=node) + elif not ( + node.args.args + or node.args.posonlyargs + or node.args.vararg + or node.args.kwarg + ): + self.add_message("no-method-argument", node=node, args=node.name) # metaclass elif metaclass: # metaclass __new__ or classmethod @@ -1926,9 +1992,16 @@ def _check_first_arg_for_type(self, node, metaclass=0): ) # regular class with regular method without self as argument elif first != "self": - self.add_message("no-self-argument", node=node) + self.add_message("no-self-argument", node=node, args=node.name) - def _check_first_arg_config(self, first, config, node, message, method_name): + def _check_first_arg_config( + self, + first: str | None, + config: Sequence[str], + node: nodes.FunctionDef, + message: str, + method_name: str, + ) -> None: if first not in config: if len(config) == 1: valid = repr(config[0]) @@ -1937,13 +2010,13 @@ def _check_first_arg_config(self, first, config, node, message, method_name): valid = f"{valid} or {config[-1]!r}" self.add_message(message, args=(method_name, valid), node=node) - def _check_bases_classes(self, node): + def _check_bases_classes(self, node: nodes.ClassDef) -> None: """Check that the given class node implements abstract methods from base classes. """ - def is_abstract(method): - return method.is_abstract(pass_is_abstract=False) + def is_abstract(method: nodes.FunctionDef) -> bool: + return method.is_abstract(pass_is_abstract=False) # type: ignore[no-any-return] # check if this class abstract if class_is_abstract(node): @@ -1962,7 +2035,13 @@ def is_abstract(method): if name in node.locals: # it is redefined as an attribute or with a descriptor continue - self.add_message("abstract-method", node=node, args=(name, owner.name)) + + self.add_message( + "abstract-method", + node=node, + args=(name, owner.name, node.name), + confidence=INFERENCE, + ) def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> None: """Check that the __init__ method call super or ancestors'__init__ @@ -1986,6 +2065,7 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No and expr.expr.func.name == "super" ): return + # pylint: disable = too-many-try-statements try: for klass in expr.expr.infer(): if klass is astroid.Uninferable: @@ -2011,7 +2091,7 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No # Record that the class' init has been called parents_with_called_inits.add(node_frame_class(method)) except KeyError: - if klass not in to_call: + if klass not in klass_node.ancestors(recurs=False): self.add_message( "non-parent-init-called", node=expr, args=klass.name ) @@ -2023,24 +2103,11 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No if node_frame_class(method) in parents_with_called_inits: return - # Return if klass is protocol - if klass.qname() in utils.TYPING_PROTOCOLS: + if utils.is_protocol_class(klass): return - # Return if any of the klass' first-order bases is protocol - for base in klass.bases: - try: - for inf_base in base.infer(): - if inf_base.qname() in utils.TYPING_PROTOCOLS: - return - except astroid.InferenceError: - continue - if decorated_with(node, ["typing.overload"]): continue - cls = node_frame_class(method) - if klass.name == "object" or (cls and cls.name == "object"): - continue self.add_message( "super-init-not-called", args=klass.name, @@ -2048,7 +2115,12 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No confidence=INFERENCE, ) - def _check_signature(self, method1, refmethod, class_type, cls): + def _check_signature( + self, + method1: nodes.FunctionDef, + refmethod: nodes.FunctionDef, + cls: nodes.ClassDef, + ) -> None: """Check that the signature of the two given methods match.""" if not ( isinstance(method1, nodes.FunctionDef) @@ -2078,6 +2150,9 @@ def _check_signature(self, method1, refmethod, class_type, cls): arg_differ_output = _different_parameters( refmethod, method1, dummy_parameter_regex=self._dummy_rgx ) + + class_type = "overriding" + if len(arg_differ_output) > 0: for msg in arg_differ_output: if "Number" in msg: @@ -2122,11 +2197,14 @@ def _check_signature(self, method1, refmethod, class_type, cls): len(method1.args.defaults) < len(refmethod.args.defaults) and not method1.args.vararg ): + class_type = "overridden" self.add_message( "signature-differs", args=(class_type, method1.name), node=method1 ) - def _uses_mandatory_method_param(self, node): + def _uses_mandatory_method_param( + self, node: nodes.Attribute | nodes.Assign | nodes.AssignAttr + ) -> bool: """Check that attribute lookup name use first attribute variable name. Name is `self` for method, `cls` for classmethod and `mcs` for metaclass. @@ -2157,7 +2235,7 @@ def _is_mandatory_method_param(self, node: nodes.NodeNG) -> bool: def _ancestors_to_call( - klass_node: nodes.ClassDef, method="__init__" + klass_node: nodes.ClassDef, method_name: str = "__init__" ) -> dict[nodes.ClassDef, bases.UnboundMethod]: """Return a dictionary where keys are the list of base classes providing the queried method, and so that should/may be called from the method node. @@ -2165,7 +2243,12 @@ def _ancestors_to_call( to_call: dict[nodes.ClassDef, bases.UnboundMethod] = {} for base_node in klass_node.ancestors(recurs=False): try: - to_call[base_node] = next(base_node.igetattr(method)) + init_node = next(base_node.igetattr(method_name)) + if not isinstance(init_node, astroid.UnboundMethod): + continue + if init_node.is_abstract(): + continue + to_call[base_node] = init_node except astroid.InferenceError: continue return to_call diff --git a/pylint/checkers/classes/special_methods_checker.py b/pylint/checkers/classes/special_methods_checker.py index f232d86915..fe69062650 100644 --- a/pylint/checkers/classes/special_methods_checker.py +++ b/pylint/checkers/classes/special_methods_checker.py @@ -4,8 +4,14 @@ """Special methods checker and helper function's module.""" +from __future__ import annotations + +from collections.abc import Callable + import astroid -from astroid import nodes +from astroid import bases, nodes +from astroid.context import InferenceContext +from astroid.typing import InferenceResult from pylint.checkers import BaseChecker from pylint.checkers.utils import ( @@ -16,11 +22,16 @@ only_required_for_messages, safe_infer, ) +from pylint.lint.pylinter import PyLinter NEXT_METHOD = "__next__" -def _safe_infer_call_result(node, caller, context=None): +def _safe_infer_call_result( + node: nodes.FunctionDef, + caller: nodes.FunctionDef, + context: InferenceContext | None = None, +) -> InferenceResult | None: """Safely infer the return value of a function. Returns None if inference failed or if there is some ambiguity (more than @@ -131,9 +142,11 @@ class SpecialMethodsChecker(BaseChecker): ), } - def __init__(self, linter=None): + def __init__(self, linter: PyLinter) -> None: super().__init__(linter) - self._protocol_map = { + self._protocol_map: dict[ + str, Callable[[nodes.FunctionDef, InferenceResult], None] + ] = { "__iter__": self._check_iter, "__len__": self._check_len, "__bool__": self._check_bool, @@ -181,7 +194,7 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: visit_asyncfunctiondef = visit_functiondef - def _check_unexpected_method_signature(self, node): + def _check_unexpected_method_signature(self, node: nodes.FunctionDef) -> None: expected_params = SPECIAL_METHODS_PARAMS[node.name] if expected_params is None: @@ -207,8 +220,9 @@ def _check_unexpected_method_signature(self, node): # tuple, although the user should implement the method # to take all of them in consideration. emit = mandatory not in expected_params - # pylint: disable-next=consider-using-f-string - expected_params = "between %d or %d" % expected_params + # mypy thinks that expected_params has type tuple[int, int] | int | None + # But at this point it must be 'tuple[int, int]' because of the type check + expected_params = f"between {expected_params[0]} or {expected_params[1]}" # type: ignore[assignment] else: # If the number of mandatory parameters doesn't # suffice, the expected parameters for this @@ -231,68 +245,65 @@ def _check_unexpected_method_signature(self, node): ) @staticmethod - def _is_wrapped_type(node, type_): + def _is_wrapped_type(node: InferenceResult, type_: str) -> bool: return ( - isinstance(node, astroid.Instance) + isinstance(node, bases.Instance) and node.name == type_ and not isinstance(node, nodes.Const) ) @staticmethod - def _is_int(node): + def _is_int(node: InferenceResult) -> bool: if SpecialMethodsChecker._is_wrapped_type(node, "int"): return True return isinstance(node, nodes.Const) and isinstance(node.value, int) @staticmethod - def _is_str(node): + def _is_str(node: InferenceResult) -> bool: if SpecialMethodsChecker._is_wrapped_type(node, "str"): return True return isinstance(node, nodes.Const) and isinstance(node.value, str) @staticmethod - def _is_bool(node): + def _is_bool(node: InferenceResult) -> bool: if SpecialMethodsChecker._is_wrapped_type(node, "bool"): return True return isinstance(node, nodes.Const) and isinstance(node.value, bool) @staticmethod - def _is_bytes(node): + def _is_bytes(node: InferenceResult) -> bool: if SpecialMethodsChecker._is_wrapped_type(node, "bytes"): return True return isinstance(node, nodes.Const) and isinstance(node.value, bytes) @staticmethod - def _is_tuple(node): + def _is_tuple(node: InferenceResult) -> bool: if SpecialMethodsChecker._is_wrapped_type(node, "tuple"): return True return isinstance(node, nodes.Const) and isinstance(node.value, tuple) @staticmethod - def _is_dict(node): + def _is_dict(node: InferenceResult) -> bool: if SpecialMethodsChecker._is_wrapped_type(node, "dict"): return True return isinstance(node, nodes.Const) and isinstance(node.value, dict) @staticmethod - def _is_iterator(node): - if node is astroid.Uninferable: - # Just ignore Uninferable objects. - return True - if isinstance(node, astroid.bases.Generator): + def _is_iterator(node: InferenceResult) -> bool: + if isinstance(node, bases.Generator): # Generators can be iterated. return True if isinstance(node, nodes.ComprehensionScope): # Comprehensions can be iterated. return True - if isinstance(node, astroid.Instance): + if isinstance(node, bases.Instance): try: node.local_attr(NEXT_METHOD) return True @@ -308,55 +319,61 @@ def _is_iterator(node): pass return False - def _check_iter(self, node, inferred): + def _check_iter(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_iterator(inferred): self.add_message("non-iterator-returned", node=node) - def _check_len(self, node, inferred): + def _check_len(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_int(inferred): self.add_message("invalid-length-returned", node=node) elif isinstance(inferred, nodes.Const) and inferred.value < 0: self.add_message("invalid-length-returned", node=node) - def _check_bool(self, node, inferred): + def _check_bool(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_bool(inferred): self.add_message("invalid-bool-returned", node=node) - def _check_index(self, node, inferred): + def _check_index(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_int(inferred): self.add_message("invalid-index-returned", node=node) - def _check_repr(self, node, inferred): + def _check_repr(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_str(inferred): self.add_message("invalid-repr-returned", node=node) - def _check_str(self, node, inferred): + def _check_str(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_str(inferred): self.add_message("invalid-str-returned", node=node) - def _check_bytes(self, node, inferred): + def _check_bytes(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_bytes(inferred): self.add_message("invalid-bytes-returned", node=node) - def _check_hash(self, node, inferred): + def _check_hash(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_int(inferred): self.add_message("invalid-hash-returned", node=node) - def _check_length_hint(self, node, inferred): + def _check_length_hint( + self, node: nodes.FunctionDef, inferred: InferenceResult + ) -> None: if not self._is_int(inferred): self.add_message("invalid-length-hint-returned", node=node) elif isinstance(inferred, nodes.Const) and inferred.value < 0: self.add_message("invalid-length-hint-returned", node=node) - def _check_format(self, node, inferred): + def _check_format(self, node: nodes.FunctionDef, inferred: InferenceResult) -> None: if not self._is_str(inferred): self.add_message("invalid-format-returned", node=node) - def _check_getnewargs(self, node, inferred): + def _check_getnewargs( + self, node: nodes.FunctionDef, inferred: InferenceResult + ) -> None: if not self._is_tuple(inferred): self.add_message("invalid-getnewargs-returned", node=node) - def _check_getnewargs_ex(self, node, inferred): + def _check_getnewargs_ex( + self, node: nodes.FunctionDef, inferred: InferenceResult + ) -> None: if not self._is_tuple(inferred): self.add_message("invalid-getnewargs-ex-returned", node=node) return @@ -374,7 +391,6 @@ def _check_getnewargs_ex(self, node, inferred): (inferred.elts[0], self._is_tuple), (inferred.elts[1], self._is_dict), ): - if isinstance(arg, nodes.Call): arg = safe_infer(arg) diff --git a/pylint/checkers/deprecated.py b/pylint/checkers/deprecated.py index ea33ff6bc3..f6a82b1dac 100644 --- a/pylint/checkers/deprecated.py +++ b/pylint/checkers/deprecated.py @@ -31,31 +31,48 @@ class DeprecatedMixin(BaseChecker): A class implementing mixin must define "deprecated-method" Message. """ - msgs: dict[str, MessageDefinitionTuple] = { - "W1505": ( + DEPRECATED_MODULE_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4901": ( + "Deprecated module %r", + "deprecated-module", + "A module marked as deprecated is imported.", + {"old_names": [("W0402", "old-deprecated-module")], "shared": True}, + ), + } + + DEPRECATED_METHOD_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4902": ( "Using deprecated method %s()", "deprecated-method", "The method is marked as deprecated and will be removed in the future.", + {"old_names": [("W1505", "old-deprecated-method")], "shared": True}, ), - "W1511": ( + } + + DEPRECATED_ARGUMENT_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4903": ( "Using deprecated argument %s of method %s()", "deprecated-argument", "The argument is marked as deprecated and will be removed in the future.", + {"old_names": [("W1511", "old-deprecated-argument")], "shared": True}, ), - "W0402": ( - "Deprecated module %r", - "deprecated-module", - "A module marked as deprecated is imported.", - ), - "W1512": ( + } + + DEPRECATED_CLASS_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4904": ( "Using deprecated class %s of module %s", "deprecated-class", "The class is marked as deprecated and will be removed in the future.", + {"old_names": [("W1512", "old-deprecated-class")], "shared": True}, ), - "W1513": ( + } + + DEPRECATED_DECORATOR_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4905": ( "Using deprecated decorator %s()", "deprecated-decorator", "The decorator is marked as deprecated and will be removed in the future.", + {"old_names": [("W1513", "old-deprecated-decorator")], "shared": True}, ), } @@ -172,10 +189,10 @@ def deprecated_classes(self, module: str) -> Iterable[str]: # pylint: disable=unused-argument return () - def check_deprecated_module(self, node: nodes.Import, mod_path: str) -> None: + def check_deprecated_module(self, node: nodes.Import, mod_path: str | None) -> None: """Checks if the module is deprecated.""" for mod_name in self.deprecated_modules(): - if mod_path == mod_name or mod_path.startswith(mod_name + "."): + if mod_path == mod_name or mod_path and mod_path.startswith(mod_name + "."): self.add_message("deprecated-module", node=node, args=mod_path) def check_deprecated_method(self, node: nodes.Call, inferred: nodes.NodeNG) -> None: @@ -196,16 +213,7 @@ def check_deprecated_method(self, node: nodes.Call, inferred: nodes.NodeNG) -> N # Not interested in other nodes. return - if hasattr(inferred.parent, "qname") and inferred.parent.qname(): - # Handling the situation when deprecated function is - # alias to existing function. - qnames = { - inferred.qname(), - f"{inferred.parent.qname()}.{func_name}", - func_name, - } - else: - qnames = {inferred.qname(), func_name} + qnames = {inferred.qname(), func_name} if any(name in self.deprecated_methods() for name in qnames): self.add_message("deprecated-method", node=node, args=(func_name,)) return diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index c97393bcc2..11ff7a5a1e 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -9,13 +9,13 @@ import re from collections import defaultdict from collections.abc import Iterator -from typing import TYPE_CHECKING, List, cast +from typing import TYPE_CHECKING import astroid from astroid import nodes from pylint.checkers import BaseChecker -from pylint.checkers.utils import only_required_for_messages +from pylint.checkers.utils import is_enum, only_required_for_messages from pylint.typing import MessageDefinitionTuple if TYPE_CHECKING: @@ -175,7 +175,7 @@ def _is_exempt_from_public_methods(node: astroid.ClassDef) -> bool: # If it's a typing.Namedtuple, typing.TypedDict or an Enum for ancestor in node.ancestors(): - if ancestor.name == "Enum" and ancestor.root().name == "enum": + if is_enum(ancestor): return True if ancestor.qname() in (TYPING_NAMEDTUPLE, TYPING_TYPEDDICT): return True @@ -245,7 +245,7 @@ def _get_parents_iter( ``{A, B, C, D}`` -- both ``E`` and its ancestors are excluded. """ parents: set[nodes.ClassDef] = set() - to_explore = cast(List[nodes.ClassDef], list(node.ancestors(recurs=False))) + to_explore = list(node.ancestors(recurs=False)) while to_explore: parent = to_explore.pop() if parent.qname() in ignored_parents: diff --git a/pylint/checkers/dunder_methods.py b/pylint/checkers/dunder_methods.py index f6520ba958..987e539aab 100644 --- a/pylint/checkers/dunder_methods.py +++ b/pylint/checkers/dunder_methods.py @@ -10,118 +10,19 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import safe_infer +from pylint.constants import DUNDER_METHODS from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter -DUNDER_METHODS: dict[str, str] = { - "__init__": "Instantiate class directly", - "__del__": "Use del keyword", - "__repr__": "Use repr built-in function", - "__str__": "Use str built-in function", - "__bytes__": "Use bytes built-in function", - "__format__": "Use format built-in function, format string method, or f-string", - "__lt__": "Use < operator", - "__le__": "Use <= operator", - "__eq__": "Use == operator", - "__ne__": "Use != operator", - "__gt__": "Use > operator", - "__ge__": "Use >= operator", - "__hash__": "Use hash built-in function", - "__bool__": "Use bool built-in function", - "__getattr__": "Access attribute directly or use getattr built-in function", - "__getattribute__": "Access attribute directly or use getattr built-in function", - "__setattr__": "Set attribute directly or use setattr built-in function", - "__delattr__": "Use del keyword", - "__dir__": "Use dir built-in function", - "__get__": "Use get method", - "__set__": "Use set method", - "__delete__": "Use del keyword", - "__instancecheck__": "Use isinstance built-in function", - "__subclasscheck__": "Use issubclass built-in function", - "__call__": "Invoke instance directly", - "__len__": "Use len built-in function", - "__length_hint__": "Use length_hint method", - "__getitem__": "Access item via subscript", - "__setitem__": "Set item via subscript", - "__delitem__": "Use del keyword", - "__iter__": "Use iter built-in function", - "__next__": "Use next built-in function", - "__reversed__": "Use reversed built-in funciton", - "__contains__": "Use in keyword", - "__add__": "Use + operator", - "__sub__": "Use - operator", - "__mul__": "Use * operator", - "__matmul__": "Use @ operator", - "__truediv__": "Use / operator", - "__floordiv__": "Use // operator", - "__mod__": "Use % operator", - "__divmod__": "Use divmod built-in function", - "__pow__": "Use ** operator or pow built-in function", - "__lshift__": "Use << operator", - "__rshift__": "Use >> operator", - "__and__": "Use & operator", - "__xor__": "Use ^ operator", - "__or__": "Use | operator", - "__radd__": "Use + operator", - "__rsub__": "Use - operator", - "__rmul__": "Use * operator", - "__rmatmul__": "Use @ operator", - "__rtruediv__": "Use / operator", - "__rfloordiv__": "Use // operator", - "__rmod__": "Use % operator", - "__rdivmod__": "Use divmod built-in function", - "__rpow__": "Use ** operator or pow built-in function", - "__rlshift__": "Use << operator", - "__rrshift__": "Use >> operator", - "__rand__": "Use & operator", - "__rxor__": "Use ^ operator", - "__ror__": "Use | operator", - "__iadd__": "Use += operator", - "__isub__": "Use -= operator", - "__imul__": "Use *= operator", - "__imatmul__": "Use @= operator", - "__itruediv__": "Use /= operator", - "__ifloordiv__": "Use //= operator", - "__imod__": "Use %= operator", - "__ipow__": "Use **= operator", - "__ilshift__": "Use <<= operator", - "__irshift__": "Use >>= operator", - "__iand__": "Use &= operator", - "__ixor__": "Use ^= operator", - "__ior__": "Use |= operator", - "__neg__": "Multiply by -1 instead", - "__pos__": "Multiply by +1 instead", - "__abs__": "Use abs built-in function", - "__invert__": "Use ~ operator", - "__complex__": "Use complex built-in function", - "__int__": "Use int built-in function", - "__float__": "Use float built-in function", - "__index__": "Use index method", - "__round__": "Use round built-in function", - "__trunc__": "Use math.trunc function", - "__floor__": "Use math.floor function", - "__ceil__": "Use math.ceil function", - "__enter__": "Invoke context manager directly", - "__aiter__": "Use iter built-in function", - "__anext__": "Use next built-in function", - "__aenter__": "Invoke context manager directly", - "__copy__": "Use copy.copy function", - "__deepcopy__": "Use copy.deepcopy function", - "__fspath__": "Use os.fspath function instead", -} - - class DunderCallChecker(BaseChecker): """Check for unnecessary dunder method calls. Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization - We exclude __new__, __subclasses__, __init_subclass__, __set_name__, - __class_getitem__, __missing__, __exit__, __await__, - __aexit__, __getnewargs_ex__, __getnewargs__, __getstate__, - __setstate__, __reduce__, __reduce_ex__ + We exclude names in list pylint.constants.EXTRA_DUNDER_METHODS such as + __index__ (see https://github.com/PyCQA/pylint/issues/6795) since these either have no alternative method of being called or have a genuine use case for being called manually. @@ -143,6 +44,12 @@ class DunderCallChecker(BaseChecker): } options = () + def open(self) -> None: + self._dunder_methods: dict[str, str] = {} + for since_vers, dunder_methods in DUNDER_METHODS.items(): + if since_vers <= self.linter.config.py_version: + self._dunder_methods.update(dunder_methods) + @staticmethod def within_dunder_def(node: nodes.NodeNG) -> bool: """Check if dunder method call is within a dunder method definition.""" @@ -161,7 +68,7 @@ def visit_call(self, node: nodes.Call) -> None: """Check if method being called is an unnecessary dunder method.""" if ( isinstance(node.func, nodes.Attribute) - and node.func.attrname in DUNDER_METHODS + and node.func.attrname in self._dunder_methods and not self.within_dunder_def(node) and not ( isinstance(node.func.expr, nodes.Call) @@ -177,7 +84,7 @@ def visit_call(self, node: nodes.Call) -> None: self.add_message( "unnecessary-dunder-call", node=node, - args=(node.func.attrname, DUNDER_METHODS[node.func.attrname]), + args=(node.func.attrname, self._dunder_methods[node.func.attrname]), confidence=HIGH, ) diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index 52ef41a806..67735b3536 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -8,29 +8,35 @@ import builtins import inspect +import warnings +from collections.abc import Generator from typing import TYPE_CHECKING, Any import astroid from astroid import nodes, objects +from astroid.context import InferenceContext +from astroid.typing import InferenceResult, SuccessfulInferenceResult from pylint import checkers from pylint.checkers import utils -from pylint.interfaces import HIGH +from pylint.interfaces import HIGH, INFERENCE from pylint.typing import MessageDefinitionTuple if TYPE_CHECKING: from pylint.lint import PyLinter -def _builtin_exceptions(): - def predicate(obj): +def _builtin_exceptions() -> set[str]: + def predicate(obj: Any) -> bool: return isinstance(obj, type) and issubclass(obj, BaseException) members = inspect.getmembers(builtins, predicate) return {exc.__name__ for (_, exc) in members} -def _annotated_unpack_infer(stmt, context=None): +def _annotated_unpack_infer( + stmt: nodes.NodeNG, context: InferenceContext | None = None +) -> Generator[tuple[nodes.NodeNG, SuccessfulInferenceResult], None, None]: """Recursively generate nodes inferred by the given statement. If the inferred value is a list or a tuple, recurse on the elements. @@ -49,16 +55,12 @@ def _annotated_unpack_infer(stmt, context=None): yield stmt, inferred -def _is_raising(body: list) -> bool: +def _is_raising(body: list[nodes.NodeNG]) -> bool: """Return whether the given statement node raises an exception.""" return any(isinstance(node, nodes.Raise) for node in body) -OVERGENERAL_EXCEPTIONS = ("BaseException", "Exception") - -MSGS: dict[ - str, MessageDefinitionTuple -] = { # pylint: disable=consider-using-namedtuple-or-dataclass +MSGS: dict[str, MessageDefinitionTuple] = { "E0701": ( "Bad except clauses order (%s)", "bad-except-order", @@ -72,13 +74,6 @@ def _is_raising(body: list) -> bool: "Used when something which is neither a class nor an instance " "is raised (i.e. a `TypeError` will be raised).", ), - "E0703": ( - "Exception context set to something which is not an exception, nor None", - "bad-exception-context", - 'Used when using the syntax "raise ... from ...", ' - "where the exception context is not an exception, " - "nor None.", - ), "E0704": ( "The raise statement is not inside an except clause", "misplaced-bare-raise", @@ -89,6 +84,14 @@ def _is_raising(body: list) -> bool: "as an exception is raised inside the try block, but it is " "nevertheless a code smell that must not be relied upon.", ), + "E0705": ( + "Exception cause set to something which is not an exception, nor None", + "bad-exception-cause", + 'Used when using the syntax "raise ... from ...", ' + "where the exception cause is not an exception, " + "nor None.", + {"old_names": [("E0703", "bad-exception-context")]}, + ), "E0710": ( "Raising a new style class which doesn't inherit from BaseException", "raising-non-exception", @@ -109,13 +112,19 @@ def _is_raising(body: list) -> bool: "W0702": ( "No exception type(s) specified", "bare-except", - "Used when an except clause doesn't specify exceptions type to catch.", + "A bare ``except:`` clause will catch ``SystemExit`` and " + "``KeyboardInterrupt`` exceptions, making it harder to interrupt a program " + "with ``Control-C``, and can disguise other problems. If you want to catch " + "all exceptions that signal program errors, use ``except Exception:`` (bare " + "except is equivalent to ``except BaseException:``).", ), - "W0703": ( + "W0718": ( "Catching too general exception %s", - "broad-except", - "Used when an except catches a too general exception, " - "possibly burying unrelated errors.", + "broad-exception-caught", + "If you use a naked ``except Exception:`` clause, you might end up catching " + "exceptions other than the ones you expect to catch. This can hide bugs or " + "make it harder to debug programs when unrelated errors are hidden.", + {"old_names": [("W0703", "broad-except")]}, ), "W0705": ( "Catching previously caught exception type %s", @@ -161,17 +170,26 @@ def _is_raising(body: list) -> bool: "is not valid for the exception in question. Usually emitted when having " "binary operations between exceptions in except handlers.", ), + "W0719": ( + "Raising too general exception: %s", + "broad-exception-raised", + "Raising exceptions that are too generic force you to catch exceptions " + "generically too. It will force you to use a naked ``except Exception:`` " + "clause. You might then end up catching exceptions other than the ones " + "you expect to catch. This can hide bugs or make it harder to debug programs " + "when unrelated errors are hidden.", + ), } class BaseVisitor: """Base class for visitors defined in this module.""" - def __init__(self, checker, node): + def __init__(self, checker: ExceptionsChecker, node: nodes.Raise) -> None: self._checker = checker self._node = node - def visit(self, node): + def visit(self, node: SuccessfulInferenceResult) -> None: name = node.__class__.__name__.lower() dispatch_meth = getattr(self, "visit_" + name, None) if dispatch_meth: @@ -188,7 +206,26 @@ class ExceptionRaiseRefVisitor(BaseVisitor): def visit_name(self, node: nodes.Name) -> None: if node.name == "NotImplemented": - self._checker.add_message("notimplemented-raised", node=self._node) + self._checker.add_message( + "notimplemented-raised", node=self._node, confidence=HIGH + ) + return + + try: + exceptions = list(_annotated_unpack_infer(node)) + except astroid.InferenceError: + return + + for _, exception in exceptions: + if isinstance( + exception, nodes.ClassDef + ) and self._checker._is_overgeneral_exception(exception): + self._checker.add_message( + "broad-exception-raised", + args=exception.name, + node=self._node, + confidence=INFERENCE, + ) def visit_call(self, node: nodes.Call) -> None: if isinstance(node.func, nodes.Name): @@ -200,7 +237,9 @@ def visit_call(self, node: nodes.Call) -> None: ): msg = node.args[0].value if "%" in msg or ("{" in msg and "}" in msg): - self._checker.add_message("raising-format-tuple", node=self._node) + self._checker.add_message( + "raising-format-tuple", node=self._node, confidence=HIGH + ) class ExceptionRaiseLeafVisitor(BaseVisitor): @@ -208,7 +247,10 @@ class ExceptionRaiseLeafVisitor(BaseVisitor): def visit_const(self, node: nodes.Const) -> None: self._checker.add_message( - "raising-bad-type", node=self._node, args=node.value.__class__.__name__ + "raising-bad-type", + node=self._node, + args=node.value.__class__.__name__, + confidence=INFERENCE, ) def visit_instance(self, instance: objects.ExceptionInstance) -> None: @@ -221,14 +263,28 @@ def visit_instance(self, instance: objects.ExceptionInstance) -> None: def visit_classdef(self, node: nodes.ClassDef) -> None: if not utils.inherit_from_std_ex(node) and utils.has_known_bases(node): if node.newstyle: - self._checker.add_message("raising-non-exception", node=self._node) + self._checker.add_message( + "raising-non-exception", + node=self._node, + confidence=INFERENCE, + ) def visit_tuple(self, _: nodes.Tuple) -> None: - self._checker.add_message("raising-bad-type", node=self._node, args="tuple") + self._checker.add_message( + "raising-bad-type", + node=self._node, + args="tuple", + confidence=INFERENCE, + ) def visit_default(self, node: nodes.NodeNG) -> None: name = getattr(node, "name", node.__class__.__name__) - self._checker.add_message("raising-bad-type", node=self._node, args=name) + self._checker.add_message( + "raising-bad-type", + node=self._node, + args=name, + confidence=INFERENCE, + ) class ExceptionsChecker(checkers.BaseChecker): @@ -240,7 +296,7 @@ class ExceptionsChecker(checkers.BaseChecker): ( "overgeneral-exceptions", { - "default": OVERGENERAL_EXCEPTIONS, + "default": ("builtins.BaseException", "builtins.Exception"), "type": "csv", "metavar": "", "help": "Exceptions that will emit a warning when caught.", @@ -248,8 +304,20 @@ class ExceptionsChecker(checkers.BaseChecker): ), ) - def open(self): + def open(self) -> None: self._builtin_exceptions = _builtin_exceptions() + for exc_name in self.linter.config.overgeneral_exceptions: + if "." not in exc_name: + warnings.warn_explicit( + "Specifying exception names in the overgeneral-exceptions option" + " without module name is deprecated and support for it" + " will be removed in pylint 3.0." + f" Use fully qualified name (maybe 'builtins.{exc_name}' ?) instead.", + category=UserWarning, + filename="pylint: Command line or configuration file", + lineno=1, + module="pylint", + ) super().open() @utils.only_required_for_messages( @@ -257,9 +325,10 @@ def open(self): "raising-bad-type", "raising-non-exception", "notimplemented-raised", - "bad-exception-context", + "bad-exception-cause", "raising-format-tuple", "raise-missing-from", + "broad-exception-raised", ) def visit_raise(self, node: nodes.Raise) -> None: if node.exc is None: @@ -269,7 +338,7 @@ def visit_raise(self, node: nodes.Raise) -> None: if node.cause is None: self._check_raise_missing_from(node) else: - self._check_bad_exception_context(node) + self._check_bad_exception_cause(node) expr = node.exc ExceptionRaiseRefVisitor(self, node).visit(expr) @@ -279,7 +348,7 @@ def visit_raise(self, node: nodes.Raise) -> None: return ExceptionRaiseLeafVisitor(self, node).visit(inferred) - def _check_misplaced_bare_raise(self, node): + def _check_misplaced_bare_raise(self, node: nodes.Raise) -> None: # Filter out if it's present in __exit__. scope = node.scope() if ( @@ -298,12 +367,12 @@ def _check_misplaced_bare_raise(self, node): expected = (nodes.ExceptHandler,) if not current or not isinstance(current.parent, expected): - self.add_message("misplaced-bare-raise", node=node) + self.add_message("misplaced-bare-raise", node=node, confidence=HIGH) - def _check_bad_exception_context(self, node: nodes.Raise) -> None: - """Verify that the exception context is properly set. + def _check_bad_exception_cause(self, node: nodes.Raise) -> None: + """Verify that the exception cause is properly set. - An exception context can be only `None` or an exception. + An exception cause can be only `None` or an exception. """ cause = utils.safe_infer(node.cause) if cause in (astroid.Uninferable, None): @@ -311,11 +380,11 @@ def _check_bad_exception_context(self, node: nodes.Raise) -> None: if isinstance(cause, nodes.Const): if cause.value is not None: - self.add_message("bad-exception-context", node=node) + self.add_message("bad-exception-cause", node=node, confidence=INFERENCE) elif not isinstance(cause, nodes.ClassDef) and not utils.inherit_from_std_ex( cause ): - self.add_message("bad-exception-context", node=node) + self.add_message("bad-exception-cause", node=node, confidence=INFERENCE) def _check_raise_missing_from(self, node: nodes.Raise) -> None: if node.exc is None: @@ -363,7 +432,12 @@ def _check_raise_missing_from(self, node: nodes.Raise) -> None: confidence=HIGH, ) - def _check_catching_non_exception(self, handler, exc, part): + def _check_catching_non_exception( + self, + handler: nodes.ExceptHandler, + exc: SuccessfulInferenceResult, + part: nodes.NodeNG, + ) -> None: if isinstance(exc, nodes.Tuple): # Check if it is a tuple of exceptions. inferred = [utils.safe_infer(elt) for elt in exc.elts] @@ -411,11 +485,11 @@ def _check_catching_non_exception(self, handler, exc, part): "catching-non-exception", node=handler.type, args=(exc.name,) ) - def _check_try_except_raise(self, node): + def _check_try_except_raise(self, node: nodes.TryExcept) -> None: def gather_exceptions_from_handler( - handler, - ) -> list[nodes.NodeNG] | None: - exceptions: list[nodes.NodeNG] = [] + handler: nodes.ExceptHandler, + ) -> list[InferenceResult] | None: + exceptions: list[InferenceResult] = [] if handler.type: exceptions_in_handler = utils.safe_infer(handler.type) if isinstance(exceptions_in_handler, nodes.Tuple): @@ -423,7 +497,7 @@ def gather_exceptions_from_handler( { exception for exception in exceptions_in_handler.elts - if isinstance(exception, nodes.Name) + if isinstance(exception, (nodes.Name, nodes.Attribute)) } ) elif exceptions_in_handler: @@ -435,7 +509,7 @@ def gather_exceptions_from_handler( bare_raise = False handler_having_bare_raise = None - exceptions_in_bare_handler = [] + exceptions_in_bare_handler: list[InferenceResult] | None = [] for handler in node.handlers: if bare_raise: # check that subsequent handler is not parent of handler which had bare raise. @@ -479,12 +553,15 @@ def visit_binop(self, node: nodes.BinOp) -> None: def visit_compare(self, node: nodes.Compare) -> None: if isinstance(node.parent, nodes.ExceptHandler): # except (V < A) - suggestion = f"Did you mean '({node.left.as_string()}, {', '.join(operand.as_string() for _, operand in node.ops)})' instead?" + suggestion = ( + f"Did you mean '({node.left.as_string()}, " + f"{', '.join(o.as_string() for _, o in node.ops)})' instead?" + ) self.add_message("wrong-exception-operation", node=node, args=(suggestion,)) @utils.only_required_for_messages( "bare-except", - "broad-except", + "broad-exception-caught", "try-except-raise", "binary-op-exception", "bad-except-order", @@ -499,17 +576,22 @@ def visit_tryexcept(self, node: nodes.TryExcept) -> None: for index, handler in enumerate(node.handlers): if handler.type is None: if not _is_raising(handler.body): - self.add_message("bare-except", node=handler) + self.add_message("bare-except", node=handler, confidence=HIGH) # check if an "except:" is followed by some other # except if index < (nb_handlers - 1): msg = "empty except clause should always appear last" - self.add_message("bad-except-order", node=node, args=msg) + self.add_message( + "bad-except-order", node=node, args=msg, confidence=HIGH + ) elif isinstance(handler.type, nodes.BoolOp): self.add_message( - "binary-op-exception", node=handler, args=handler.type.op + "binary-op-exception", + node=handler, + args=handler.type.op, + confidence=HIGH, ) else: try: @@ -518,8 +600,6 @@ def visit_tryexcept(self, node: nodes.TryExcept) -> None: continue for part, exception in exceptions: - if exception is astroid.Uninferable: - continue if isinstance( exception, astroid.Instance ) and utils.inherit_from_std_ex(exception): @@ -540,24 +620,40 @@ def visit_tryexcept(self, node: nodes.TryExcept) -> None: if previous_exc in exc_ancestors: msg = f"{previous_exc.name} is an ancestor class of {exception.name}" self.add_message( - "bad-except-order", node=handler.type, args=msg + "bad-except-order", + node=handler.type, + args=msg, + confidence=INFERENCE, ) - if ( - exception.name in self.linter.config.overgeneral_exceptions - and exception.root().name == utils.EXCEPTIONS_MODULE - and not _is_raising(handler.body) + if self._is_overgeneral_exception(exception) and not _is_raising( + handler.body ): self.add_message( - "broad-except", args=exception.name, node=handler.type + "broad-exception-caught", + args=exception.name, + node=handler.type, + confidence=INFERENCE, ) if exception in exceptions_classes: self.add_message( - "duplicate-except", args=exception.name, node=handler.type + "duplicate-except", + args=exception.name, + node=handler.type, + confidence=INFERENCE, ) exceptions_classes += [exc for _, exc in exceptions] + def _is_overgeneral_exception(self, exception: nodes.ClassDef) -> bool: + return ( + exception.qname() in self.linter.config.overgeneral_exceptions + # TODO: 3.0: not a qualified name, deprecated + or "." not in exception.name + and exception.name in self.linter.config.overgeneral_exceptions + and exception.root().name == utils.EXCEPTIONS_MODULE + ) + def register(linter: PyLinter) -> None: linter.register_checker(ExceptionsChecker(linter)) diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index 5f14ae2e11..562c48f5d6 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -13,26 +13,29 @@ from __future__ import annotations +import sys import tokenize from functools import reduce +from re import Match from typing import TYPE_CHECKING from astroid import nodes from pylint.checkers import BaseRawFileChecker, BaseTokenChecker -from pylint.checkers.utils import ( - is_overload_stub, - is_protocol_class, - node_frame_class, - only_required_for_messages, -) +from pylint.checkers.utils import only_required_for_messages from pylint.constants import WarningScope +from pylint.interfaces import HIGH from pylint.typing import MessageDefinitionTuple from pylint.utils.pragma_parser import OPTION_PO, PragmaParserError, parse_pragma if TYPE_CHECKING: from pylint.lint import PyLinter +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + _KEYWORD_TOKENS = { "assert", "del", @@ -47,6 +50,8 @@ "while", "yield", "with", + "=", + ":=", } _JUNK_TOKENS = {tokenize.COMMENT, tokenize.NL} @@ -114,7 +119,7 @@ } -def _last_token_on_line_is(tokens, line_end, token): +def _last_token_on_line_is(tokens: TokenWrapper, line_end: int, token: str) -> bool: return ( line_end > 0 and tokens.token(line_end - 1) == token @@ -127,22 +132,22 @@ def _last_token_on_line_is(tokens, line_end, token): class TokenWrapper: """A wrapper for readable access to token information.""" - def __init__(self, tokens): + def __init__(self, tokens: list[tokenize.TokenInfo]) -> None: self._tokens = tokens - def token(self, idx): + def token(self, idx: int) -> str: return self._tokens[idx][1] - def type(self, idx): + def type(self, idx: int) -> int: return self._tokens[idx][0] - def start_line(self, idx): + def start_line(self, idx: int) -> int: return self._tokens[idx][2][0] - def start_col(self, idx): + def start_col(self, idx: int) -> int: return self._tokens[idx][2][1] - def line(self, idx): + def line(self, idx: int) -> str: return self._tokens[idx][4] @@ -251,13 +256,12 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): ), ) - def __init__(self, linter=None): + def __init__(self, linter: PyLinter) -> None: super().__init__(linter) - self._lines = None - self._visited_lines = None - self._bracket_stack = [None] + self._lines: dict[int, str] = {} + self._visited_lines: dict[int, Literal[1, 2]] = {} - def new_line(self, tokens, line_end, line_start): + def new_line(self, tokens: TokenWrapper, line_end: int, line_start: int) -> None: """A new line has been encountered, process it if necessary.""" if _last_token_on_line_is(tokens, line_end, ";"): self.add_message("unnecessary-semicolon", line=tokens.start_line(line_end)) @@ -266,28 +270,25 @@ def new_line(self, tokens, line_end, line_start): line = tokens.line(line_start) if tokens.type(line_start) not in _JUNK_TOKENS: self._lines[line_num] = line.split("\n")[0] - self.check_lines(line, line_num) + self.check_lines(tokens, line_start, line, line_num) def process_module(self, node: nodes.Module) -> None: pass - # pylint: disable-next=too-many-return-statements + # pylint: disable-next = too-many-return-statements, too-many-branches def _check_keyword_parentheses( self, tokens: list[tokenize.TokenInfo], start: int ) -> None: """Check that there are not unnecessary parentheses after a keyword. Parens are unnecessary if there is exactly one balanced outer pair on a - line, and it is followed by a colon, and contains no commas (i.e. is not a - tuple). + line and contains no commas (i.e. is not a tuple). Args: - tokens: list of Tokens; the entire list of Tokens. - start: int; the position of the keyword in the token list. + tokens: The entire list of Tokens. + start: The position of the keyword in the token list. """ # If the next token is not a paren, we're fine. - if self._bracket_stack[-1] == ":" and tokens[start].string == "for": - self._bracket_stack.pop() if tokens[start + 1].string != "(": return if ( @@ -379,21 +380,20 @@ def _check_keyword_parentheses( return def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: - """Process tokens and search for : + """Process tokens and search for: - _ too long lines (i.e. longer than ) - _ optionally bad construct (if given, bad_construct must be a compiled + - too long lines (i.e. longer than ) + - optionally bad construct (if given, bad_construct must be a compiled regular expression). """ - self._bracket_stack = [None] indents = [0] check_equal = False line_num = 0 self._lines = {} self._visited_lines = {} - self._last_line_ending = None + self._last_line_ending: str | None = None last_blank_line_num = 0 - for idx, (tok_type, token, start, _, line) in enumerate(tokens): + for idx, (tok_type, string, start, _, line) in enumerate(tokens): if start[0] != line_num: line_num = start[0] # A tokenizer oddity: if an indented line contains a multi-line @@ -411,10 +411,10 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: # If an INDENT appears, setting check_equal is wrong, and will # be undone when we see the INDENT. check_equal = True - self._check_line_ending(token, line_num) + self._check_line_ending(string, line_num) elif tok_type == tokenize.INDENT: check_equal = False - self.check_indent_level(token, indents[-1] + 1, line_num) + self.check_indent_level(string, indents[-1] + 1, line_num) indents.append(indents[-1] + 1) elif tok_type == tokenize.DEDENT: # there's nothing we need to check here! what's important is @@ -438,10 +438,10 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: check_equal = False self.check_indent_level(line, indents[-1], line_num) - if tok_type == tokenize.NUMBER and token.endswith("l"): + if tok_type == tokenize.NUMBER and string.endswith("l"): self.add_message("lowercase-l-suffix", line=line_num) - if token in _KEYWORD_TOKENS: + if string in _KEYWORD_TOKENS: self._check_keyword_parentheses(tokens, idx) line_num -= 1 # to be ok with "wc -l" @@ -467,7 +467,7 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: if line_num == last_blank_line_num and line_num > 0: self.add_message("trailing-newlines", line=line_num) - def _check_line_ending(self, line_ending, line_num): + def _check_line_ending(self, line_ending: str, line_num: int) -> None: # check if line endings are mixed if self._last_line_ending is not None: # line_ending == "" indicates a synthetic newline added at @@ -525,15 +525,15 @@ def visit_default(self, node: nodes.NodeNG) -> None: except AttributeError: tolineno = node.tolineno assert tolineno, node - lines = [] - for line in range(line, tolineno + 1): + lines: list[str] = [] + for line in range(line, tolineno + 1): # noqa: B020 self._visited_lines[line] = 1 try: lines.append(self._lines[line].rstrip()) except KeyError: lines.append("") - def _check_multi_statement_line(self, node, line): + def _check_multi_statement_line(self, node: nodes.NodeNG, line: int) -> None: """Check for lines containing multiple statements.""" # Do not warn about multiple nested context managers # in with statements. @@ -558,31 +558,28 @@ def _check_multi_statement_line(self, node, line): ): return - # Function overloads that use ``Ellipsis`` are exempted. + # Functions stubs with ``Ellipsis`` as body are exempted. if ( - isinstance(node, nodes.Expr) + isinstance(node.parent, nodes.FunctionDef) + and isinstance(node, nodes.Expr) and isinstance(node.value, nodes.Const) and node.value.value is Ellipsis ): - frame = node.frame(future=True) - if is_overload_stub(frame) or is_protocol_class(node_frame_class(frame)): - return + return self.add_message("multiple-statements", node=node) self._visited_lines[line] = 2 - def check_line_ending(self, line: str, i: int) -> None: - """Check that the final newline is not missing and that there is no trailing - white-space. - """ - if not line.endswith("\n"): - self.add_message("missing-final-newline", line=i) - return + def check_trailing_whitespace_ending(self, line: str, i: int) -> None: + """Check that there is no trailing white-space.""" # exclude \f (formfeed) from the rstrip stripped_line = line.rstrip("\t\n\r\v ") if line[len(stripped_line) :] not in ("\n", "\r\n"): self.add_message( - "trailing-whitespace", line=i, col_offset=len(stripped_line) + "trailing-whitespace", + line=i, + col_offset=len(stripped_line), + confidence=HIGH, ) def check_line_length(self, line: str, i: int, checker_off: bool) -> None: @@ -597,7 +594,7 @@ def check_line_length(self, line: str, i: int, checker_off: bool) -> None: self.add_message("line-too-long", line=i, args=(len(line), max_chars)) @staticmethod - def remove_pylint_option_from_lines(options_pattern_obj) -> str: + def remove_pylint_option_from_lines(options_pattern_obj: Match[str]) -> str: """Remove the `# pylint ...` pattern from lines.""" lines = options_pattern_obj.string purged_lines = ( @@ -607,8 +604,8 @@ def remove_pylint_option_from_lines(options_pattern_obj) -> str: return purged_lines @staticmethod - def is_line_length_check_activated(pylint_pattern_match_object) -> bool: - """Return true if the line length check is activated.""" + def is_line_length_check_activated(pylint_pattern_match_object: Match[str]) -> bool: + """Return True if the line length check is activated.""" try: for pragma in parse_pragma(pylint_pattern_match_object.group(2)): if pragma.action == "disable" and "line-too-long" in pragma.messages: @@ -633,7 +630,7 @@ def specific_splitlines(lines: str) -> list[str]: "\u2028", "\u2029", } - res = [] + res: list[str] = [] buffer = "" for atomic_line in lines.splitlines(True): if atomic_line[-1] not in unsplit_ends: @@ -643,10 +640,12 @@ def specific_splitlines(lines: str) -> list[str]: buffer += atomic_line return res - def check_lines(self, lines: str, lineno: int) -> None: + def check_lines( + self, tokens: TokenWrapper, line_start: int, lines: str, lineno: int + ) -> None: """Check given lines for potential messages. - Check lines have : + Check if lines have: - a final newline - no trailing white-space - less than a maximum number of characters @@ -664,17 +663,20 @@ def check_lines(self, lines: str, lineno: int) -> None: split_lines = self.specific_splitlines(lines) for offset, line in enumerate(split_lines): - self.check_line_ending(line, lineno + offset) - - # hold onto the initial lineno for later - potential_line_length_warning = False - for offset, line in enumerate(split_lines): - # this check is purposefully simple and doesn't rstrip - # since this is running on every line you're checking it's - # advantageous to avoid doing a lot of work - if len(line) > max_chars: - potential_line_length_warning = True - break + if not line.endswith("\n"): + self.add_message("missing-final-newline", line=lineno + offset) + continue + # We don't test for trailing whitespaces in strings + # See https://github.com/PyCQA/pylint/issues/6936 + # and https://github.com/PyCQA/pylint/issues/3822 + if tokens.type(line_start) != tokenize.STRING: + self.check_trailing_whitespace_ending(line, lineno + offset) + + # This check is purposefully simple and doesn't rstrip since this is running + # on every line you're checking it's advantageous to avoid doing a lot of work + potential_line_length_warning = any( + len(line) > max_chars for line in split_lines + ) # if there were no lines passing the max_chars config, we don't bother # running the full line check (as we've met an even more strict condition) @@ -694,7 +696,7 @@ def check_lines(self, lines: str, lineno: int) -> None: for offset, line in enumerate(self.specific_splitlines(lines)): self.check_line_length(line, lineno + offset, checker_off) - def check_indent_level(self, string, expected, line_num): + def check_indent_level(self, string: str, expected: int, line_num: int) -> None: """Return the indent level of the string.""" indent = self.linter.config.indent_string if indent == "\\t": # \t is not interpreted in the configuration file diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 0119c9bcff..d29056b8c2 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -10,10 +10,13 @@ import copy import os import sys -from typing import TYPE_CHECKING, Any +from collections import defaultdict +from collections.abc import ItemsView, Sequence +from typing import TYPE_CHECKING, Any, Dict, List, Union import astroid from astroid import nodes +from astroid.nodes._base_nodes import ImportNode from pylint.checkers import BaseChecker, DeprecatedMixin from pylint.checkers.utils import ( @@ -25,13 +28,18 @@ ) from pylint.exceptions import EmptyReportError from pylint.graph import DotBackend, get_cycles +from pylint.interfaces import HIGH from pylint.reporters.ureports.nodes import Paragraph, Section, VerbatimText from pylint.typing import MessageDefinitionTuple from pylint.utils import IsortDriver +from pylint.utils.linterstats import LinterStats if TYPE_CHECKING: from pylint.lint import PyLinter +# The dictionary with Any should actually be a _ImportTree again +# but mypy doesn't support recursive types yet +_ImportTree = Dict[str, Union[List[Dict[str, Any]], List[str]]] DEPRECATED_MODULES = { (0, 0, 0): {"tkinter.tix", "fpectl"}, @@ -42,7 +50,7 @@ (3, 6, 0): {"asynchat", "asyncore", "smtpd"}, (3, 7, 0): {"macpath"}, (3, 9, 0): {"lib2to3", "parser", "symbol", "binhex"}, - (3, 10, 0): {"distutils"}, + (3, 10, 0): {"distutils", "typing.io", "typing.re"}, (3, 11, 0): { "aifc", "audioop", @@ -52,6 +60,7 @@ "crypt", "imghdr", "msilib", + "mailcap", "nis", "nntplib", "ossaudiodev", @@ -69,7 +78,7 @@ } -def _qualified_names(modname): +def _qualified_names(modname: str | None) -> list[str]: """Split the names of the given module into subparts. For example, @@ -77,16 +86,25 @@ def _qualified_names(modname): returns ['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker'] """ - names = modname.split(".") + names = modname.split(".") if modname is not None else "" return [".".join(names[0 : i + 1]) for i in range(len(names))] -def _get_first_import(node, context, name, base, level, alias): +def _get_first_import( + node: ImportNode, + context: nodes.LocalsDictNodeNG, + name: str, + base: str | None, + level: int | None, + alias: str | None, +) -> tuple[nodes.Import | nodes.ImportFrom | None, str | None]: """Return the node where [base.] is imported or None if not found.""" fullname = f"{base}.{name}" if base else name first = None found = False + msg = "reimported" + for first in context.body: if first is node: continue @@ -96,6 +114,13 @@ def _get_first_import(node, context, name, base, level, alias): if any(fullname == iname[0] for iname in first.names): found = True break + for imported_name, imported_alias in first.names: + if not imported_alias and imported_name == alias: + found = True + msg = "shadowed-import" + break + if found: + break elif isinstance(first, nodes.ImportFrom): if level == first.level: for imported_name, imported_alias in first.names: @@ -109,14 +134,22 @@ def _get_first_import(node, context, name, base, level, alias): ): found = True break + if not imported_alias and imported_name == alias: + found = True + msg = "shadowed-import" + break if found: break if found and not astroid.are_exclusive(first, node): - return first - return None + return first, msg + return None, None -def _ignore_import_failure(node, modname, ignored_modules): +def _ignore_import_failure( + node: ImportNode, + modname: str | None, + ignored_modules: Sequence[str], +) -> bool: for submodule in _qualified_names(modname): if submodule in ignored_modules: return True @@ -132,35 +165,37 @@ def _ignore_import_failure(node, modname, ignored_modules): # utilities to represents import dependencies as tree and dot graph ########### -def _make_tree_defs(mod_files_list): +def _make_tree_defs(mod_files_list: ItemsView[str, set[str]]) -> _ImportTree: """Get a list of 2-uple (module, list_of_files_which_import_this_module), it will return a dictionary to represent this as a tree. """ - tree_defs = {} + tree_defs: _ImportTree = {} for mod, files in mod_files_list: - node = (tree_defs, ()) + node: list[_ImportTree | list[str]] = [tree_defs, []] for prefix in mod.split("."): - node = node[0].setdefault(prefix, [{}, []]) - node[1] += files + assert isinstance(node[0], dict) + node = node[0].setdefault(prefix, ({}, [])) # type: ignore[arg-type,assignment] + assert isinstance(node[1], list) + node[1].extend(files) return tree_defs -def _repr_tree_defs(data, indent_str=None): +def _repr_tree_defs(data: _ImportTree, indent_str: str | None = None) -> str: """Return a string which represents imports as a tree.""" lines = [] nodes_items = data.items() for i, (mod, (sub, files)) in enumerate(sorted(nodes_items, key=lambda x: x[0])): - files = "" if not files else f"({','.join(sorted(files))})" + files_list = "" if not files else f"({','.join(sorted(files))})" if indent_str is None: - lines.append(f"{mod} {files}") + lines.append(f"{mod} {files_list}") sub_indent_str = " " else: - lines.append(rf"{indent_str}\-{mod} {files}") + lines.append(rf"{indent_str}\-{mod} {files_list}") if i == len(nodes_items) - 1: sub_indent_str = f"{indent_str} " else: sub_indent_str = f"{indent_str}| " - if sub: + if sub and isinstance(sub, dict): lines.append(_repr_tree_defs(sub, sub_indent_str)) return "\n".join(lines) @@ -186,7 +221,7 @@ def _dependencies_graph(filename: str, dep_info: dict[str, set[str]]) -> str: def _make_graph( filename: str, dep_info: dict[str, set[str]], sect: Section, gtype: str -): +) -> None: """Generate a dependencies graph and add some information about it in the report's section. """ @@ -197,7 +232,6 @@ def _make_graph( # the import checker itself ################################################### MSGS: dict[str, MessageDefinitionTuple] = { - **{k: v for k, v in DeprecatedMixin.msgs.items() if k[1:3] == "04"}, "E0401": ( "Unable to import %s", "import-error", @@ -219,9 +253,9 @@ def _make_graph( "Use 'from %s import %s' instead", "consider-using-from-import", "Emitted when a submodule of a package is imported and " - "aliased with the same name. " - "E.g., instead of ``import concurrent.futures as futures`` use " - "``from concurrent import futures``", + "aliased with the same name, " + "e.g., instead of ``import concurrent.futures as futures`` use " + "``from concurrent import futures``.", ), "W0401": ( "Wildcard import %s", @@ -231,7 +265,7 @@ def _make_graph( "W0404": ( "Reimport %r (imported line %s)", "reimported", - "Used when a module is reimported multiple times.", + "Used when a module is imported more than once.", ), "W0406": ( "Module import itself", @@ -258,23 +292,23 @@ def _make_graph( "%s should be placed before %s", "wrong-import-order", "Used when PEP8 import order is not respected (standard imports " - "first, then third-party libraries, then local imports)", + "first, then third-party libraries, then local imports).", ), "C0412": ( "Imports from package %s are not grouped", "ungrouped-imports", - "Used when imports are not grouped by packages", + "Used when imports are not grouped by packages.", ), "C0413": ( 'Import "%s" should be placed at the top of the module', "wrong-import-position", - "Used when code and imports are mixed", + "Used when code and imports are mixed.", ), "C0414": ( "Import alias does not rename original package", "useless-import-alias", - "Used when an import alias is same as original package." - "e.g using import numpy as numpy instead of import numpy as np", + "Used when an import alias is same as original package, " + "e.g., using import numpy as numpy instead of import numpy as np.", ), "C0415": ( "Import outside toplevel (%s)", @@ -282,6 +316,11 @@ def _make_graph( "Used when an import statement is used anywhere other than the module " "toplevel. Move this import to the top of the file.", ), + "W0416": ( + "Shadowed %r (imported line %s)", + "shadowed-import", + "Used when a module is aliased with a name that shadows another import.", + ), } @@ -302,7 +341,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): """ name = "imports" - msgs = MSGS + msgs = {**DeprecatedMixin.DEPRECATED_MODULE_MESSAGE, **MSGS} default_deprecated_modules = () options = ( @@ -400,12 +439,21 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): "help": "Allow wildcard imports from modules that define __all__.", }, ), + ( + "allow-reexport-from-package", + { + "default": False, + "type": "yn", + "metavar": "", + "help": "Allow explicit reexports by alias from a package __init__.", + }, + ), ) def __init__(self, linter: PyLinter) -> None: BaseChecker.__init__(self, linter) - self.import_graph: collections.defaultdict = collections.defaultdict(set) - self._imports_stack: list[tuple[Any, Any]] = [] + self.import_graph: defaultdict[str, set[str]] = defaultdict(set) + self._imports_stack: list[tuple[ImportNode, str]] = [] self._first_non_import_node = None self._module_pkg: dict[ Any, Any @@ -416,14 +464,15 @@ def __init__(self, linter: PyLinter) -> None: ("RP0402", "Modules dependencies graph", self._report_dependencies_graph), ) - def open(self): + def open(self) -> None: """Called before visiting project (i.e set of modules).""" self.linter.stats.dependencies = {} self.linter.stats = self.linter.stats - self.import_graph = collections.defaultdict(set) + self.import_graph = defaultdict(set) self._module_pkg = {} # mapping of modules to the pkg they belong in - self._excluded_edges = collections.defaultdict(set) - self._ignored_modules = self.linter.config.ignored_modules + self._current_module_package = False + self._excluded_edges: defaultdict[str, set[str]] = defaultdict(set) + self._ignored_modules: Sequence[str] = self.linter.config.ignored_modules # Build a mapping {'module': 'preferred-module'} self.preferred_modules = dict( module.split(":") @@ -431,14 +480,15 @@ def open(self): if ":" in module ) self._allow_any_import_level = set(self.linter.config.allow_any_import_level) + self._allow_reexport_package = self.linter.config.allow_reexport_from_package - def _import_graph_without_ignored_edges(self): + def _import_graph_without_ignored_edges(self) -> defaultdict[str, set[str]]: filtered_graph = copy.deepcopy(self.import_graph) for node in filtered_graph: filtered_graph[node].difference_update(self._excluded_edges[node]) return filtered_graph - def close(self): + def close(self) -> None: """Called before visiting project (i.e set of modules).""" if self.linter.is_message_enabled("cyclic-import"): graph = self._import_graph_without_ignored_edges() @@ -456,6 +506,10 @@ def deprecated_modules(self) -> set[str]: all_deprecated_modules = all_deprecated_modules.union(mod_set) return all_deprecated_modules + def visit_module(self, node: nodes.Module) -> None: + """Store if current module is a package, i.e. an __init__ file.""" + self._current_module_package = node.package + def visit_import(self, node: nodes.Import) -> None: """Triggered when an import statement is seen.""" self._check_reimport(node) @@ -537,7 +591,17 @@ def leave_module(self, node: nodes.Module) -> None: self._imports_stack = [] self._first_non_import_node = None - def compute_first_non_import_node(self, node): + def compute_first_non_import_node( + self, + node: nodes.If + | nodes.Expr + | nodes.Comprehension + | nodes.IfExp + | nodes.Assign + | nodes.AssignAttr + | nodes.TryExcept + | nodes.TryFinally, + ) -> None: # if the node does not contain an import instruction, and if it is the # first node of the module, keep a track of it (all the import positions # of the module will be compared to the position of this first @@ -577,7 +641,9 @@ def compute_first_non_import_node(self, node): visit_ifexp ) = visit_comprehension = visit_expr = visit_if = compute_first_non_import_node - def visit_functiondef(self, node: nodes.FunctionDef) -> None: + def visit_functiondef( + self, node: nodes.FunctionDef | nodes.While | nodes.For | nodes.ClassDef + ) -> None: # If it is the first non import instruction of the module, record it. if self._first_non_import_node: return @@ -599,7 +665,7 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: visit_classdef = visit_for = visit_while = visit_functiondef - def _check_misplaced_future(self, node): + def _check_misplaced_future(self, node: nodes.ImportFrom) -> None: basename = node.modname if basename == "__future__": # check if this is the first non-docstring statement in the module @@ -612,7 +678,7 @@ def _check_misplaced_future(self, node): self.add_message("misplaced-future", node=node) return - def _check_same_line_imports(self, node): + def _check_same_line_imports(self, node: nodes.ImportFrom) -> None: # Detect duplicate imports on the same line. names = (name for name, _ in node.names) counter = collections.Counter(names) @@ -620,7 +686,7 @@ def _check_same_line_imports(self, node): if count > 1: self.add_message("reimported", node=node, args=(name, node.fromlineno)) - def _check_position(self, node): + def _check_position(self, node: ImportNode) -> None: """Check `node` import or importfrom node position is correct. Send a message if `node` comes before another instruction @@ -639,7 +705,11 @@ def _check_position(self, node): "wrong-import-position", node.fromlineno, node ) - def _record_import(self, node, importedmodnode): + def _record_import( + self, + node: ImportNode, + importedmodnode: nodes.Module | None, + ) -> None: """Record the package `node` imports from.""" if isinstance(node, nodes.ImportFrom): importedname = node.modname @@ -661,24 +731,33 @@ def _record_import(self, node, importedmodnode): self._imports_stack.append((node, importedname)) @staticmethod - def _is_fallback_import(node, imports): + def _is_fallback_import( + node: ImportNode, imports: list[tuple[ImportNode, str]] + ) -> bool: imports = [import_node for (import_node, _) in imports] return any(astroid.are_exclusive(import_node, node) for import_node in imports) - def _check_imports_order(self, _module_node): + # pylint: disable = too-many-statements + def _check_imports_order( + self, _module_node: nodes.Module + ) -> tuple[ + list[tuple[ImportNode, str]], + list[tuple[ImportNode, str]], + list[tuple[ImportNode, str]], + ]: """Checks imports of module `node` are grouped by category. Imports must follow this order: standard, 3rd party, local """ - std_imports = [] - third_party_imports = [] - first_party_imports = [] + std_imports: list[tuple[ImportNode, str]] = [] + third_party_imports: list[tuple[ImportNode, str]] = [] + first_party_imports: list[tuple[ImportNode, str]] = [] # need of a list that holds third or first party ordered import - external_imports = [] - local_imports = [] - third_party_not_ignored = [] - first_party_not_ignored = [] - local_not_ignored = [] + external_imports: list[tuple[ImportNode, str]] = [] + local_imports: list[tuple[ImportNode, str]] = [] + third_party_not_ignored: list[tuple[ImportNode, str]] = [] + first_party_not_ignored: list[tuple[ImportNode, str]] = [] + local_not_ignored: list[tuple[ImportNode, str]] = [] isort_driver = IsortDriver(self.linter.config) for node, modname in self._imports_stack: if modname.startswith("."): @@ -760,7 +839,9 @@ def _check_imports_order(self, _module_node): ) return std_imports, external_imports, local_imports - def _get_imported_module(self, importnode, modname): + def _get_imported_module( + self, importnode: ImportNode, modname: str | None + ) -> nodes.Module | None: try: return importnode.do_import_module(modname) except astroid.TooManyLevelsError: @@ -768,8 +849,10 @@ def _get_imported_module(self, importnode, modname): return None self.add_message("relative-beyond-top-level", node=importnode) except astroid.AstroidSyntaxError as exc: - message = f"Cannot import {modname!r} due to syntax error {str(exc.error)!r}" # pylint: disable=no-member; false positive - self.add_message("syntax-error", line=importnode.lineno, args=message) + message = f"Cannot import {modname!r} due to '{exc.error}'" + self.add_message( + "syntax-error", line=importnode.lineno, args=message, confidence=HIGH + ) except astroid.AstroidBuildingError: if not self.linter.is_message_enabled("import-error"): @@ -788,9 +871,7 @@ def _get_imported_module(self, importnode, modname): raise astroid.AstroidError from e return None - def _add_imported_module( - self, node: nodes.Import | nodes.ImportFrom, importedmodname: str - ) -> None: + def _add_imported_module(self, node: ImportNode, importedmodname: str) -> None: """Notify an imported module, used to analyze dependencies.""" module_file = node.root().file context_name = node.root().name @@ -831,7 +912,7 @@ def _add_imported_module( ): self._excluded_edges[context_name].add(importedmodname) - def _check_preferred_module(self, node, mod_path): + def _check_preferred_module(self, node: ImportNode, mod_path: str) -> None: """Check if the module has a preferred replacement.""" if mod_path in self.preferred_modules: self.add_message( @@ -840,7 +921,7 @@ def _check_preferred_module(self, node, mod_path): args=(self.preferred_modules[mod_path], mod_path), ) - def _check_import_as_rename(self, node: nodes.Import | nodes.ImportFrom) -> None: + def _check_import_as_rename(self, node: ImportNode) -> None: names = node.names for name in names: if not all(name): @@ -852,8 +933,11 @@ def _check_import_as_rename(self, node: nodes.Import | nodes.ImportFrom) -> None if import_name != aliased_name: continue - if len(splitted_packages) == 1: - self.add_message("useless-import-alias", node=node) + if len(splitted_packages) == 1 and ( + self._allow_reexport_package is False + or self._current_module_package is False + ): + self.add_message("useless-import-alias", node=node, confidence=HIGH) elif len(splitted_packages) == 2: self.add_message( "consider-using-from-import", @@ -861,9 +945,16 @@ def _check_import_as_rename(self, node: nodes.Import | nodes.ImportFrom) -> None args=(splitted_packages[0], import_name), ) - def _check_reimport(self, node, basename=None, level=None): - """Check if the import is necessary (i.e. not already done).""" - if not self.linter.is_message_enabled("reimported"): + def _check_reimport( + self, + node: ImportNode, + basename: str | None = None, + level: int | None = None, + ) -> None: + """Check if a module with the same name is already imported or aliased.""" + if not self.linter.is_message_enabled( + "reimported" + ) and not self.linter.is_message_enabled("shadowed-import"): return frame = node.frame(future=True) @@ -874,15 +965,18 @@ def _check_reimport(self, node, basename=None, level=None): for known_context, known_level in contexts: for name, alias in node.names: - first = _get_first_import( + first, msg = _get_first_import( node, known_context, name, basename, known_level, alias ) - if first is not None: + if first is not None and msg is not None: + name = name if msg == "reimported" else alias self.add_message( - "reimported", node=node, args=(name, first.fromlineno) + msg, node=node, args=(name, first.fromlineno), confidence=HIGH ) - def _report_external_dependencies(self, sect, _, _dummy): + def _report_external_dependencies( + self, sect: Section, _: LinterStats, _dummy: LinterStats | None + ) -> None: """Return a verbatim layout for displaying dependencies.""" dep_info = _make_tree_defs(self._external_dependencies_info().items()) if not dep_info: @@ -890,7 +984,9 @@ def _report_external_dependencies(self, sect, _, _dummy): tree_str = _repr_tree_defs(dep_info) sect.append(VerbatimText(tree_str)) - def _report_dependencies_graph(self, sect, _, _dummy): + def _report_dependencies_graph( + self, sect: Section, _: LinterStats, _dummy: LinterStats | None + ) -> None: """Write dependencies as a dot (graphviz) file.""" dep_info = self.linter.stats.dependencies if not dep_info or not ( @@ -909,9 +1005,9 @@ def _report_dependencies_graph(self, sect, _, _dummy): if filename: _make_graph(filename, self._internal_dependencies_info(), sect, "internal ") - def _filter_dependencies_graph(self, internal): + def _filter_dependencies_graph(self, internal: bool) -> defaultdict[str, set[str]]: """Build the internal or the external dependency graph.""" - graph = collections.defaultdict(set) + graph: defaultdict[str, set[str]] = defaultdict(set) for importee, importers in self.linter.stats.dependencies.items(): for importer in importers: package = self._module_pkg.get(importer, importer) @@ -921,20 +1017,22 @@ def _filter_dependencies_graph(self, internal): return graph @astroid.decorators.cached - def _external_dependencies_info(self): + def _external_dependencies_info(self) -> defaultdict[str, set[str]]: """Return cached external dependencies information or build and cache them. """ return self._filter_dependencies_graph(internal=False) @astroid.decorators.cached - def _internal_dependencies_info(self): + def _internal_dependencies_info(self) -> defaultdict[str, set[str]]: """Return cached internal dependencies information or build and cache them. """ return self._filter_dependencies_graph(internal=True) - def _check_wildcard_imports(self, node, imported_module): + def _check_wildcard_imports( + self, node: nodes.ImportFrom, imported_module: nodes.Module | None + ) -> None: if node.root().package: # Skip the check if in __init__.py issue #2026 return @@ -944,14 +1042,14 @@ def _check_wildcard_imports(self, node, imported_module): if name == "*" and not wildcard_import_is_allowed: self.add_message("wildcard-import", args=node.modname, node=node) - def _wildcard_import_is_allowed(self, imported_module): + def _wildcard_import_is_allowed(self, imported_module: nodes.Module | None) -> bool: return ( self.linter.config.allow_wildcard_with_all and imported_module is not None and "__all__" in imported_module.locals ) - def _check_toplevel(self, node): + def _check_toplevel(self, node: ImportNode) -> None: """Check whether the import is made outside the module toplevel.""" # If the scope of the import is a module, then obviously it is # not outside the module toplevel. diff --git a/pylint/checkers/logging.py b/pylint/checkers/logging.py index a82afcdef0..1cfa33c0f6 100644 --- a/pylint/checkers/logging.py +++ b/pylint/checkers/logging.py @@ -7,16 +7,23 @@ from __future__ import annotations import string +import sys from typing import TYPE_CHECKING import astroid -from astroid import nodes +from astroid import bases, nodes +from astroid.typing import InferenceResult from pylint import checkers from pylint.checkers import utils from pylint.checkers.utils import infer_all from pylint.typing import MessageDefinitionTuple +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + if TYPE_CHECKING: from pylint.lint import PyLinter @@ -98,17 +105,21 @@ "warning", } +MOST_COMMON_FORMATTING = frozenset(["%s", "%d", "%f", "%r"]) + -def is_method_call(func, types=(), methods=()): +def is_method_call( + func: bases.BoundMethod, types: tuple[str, ...] = (), methods: tuple[str, ...] = () +) -> bool: """Determines if a BoundMethod node represents a method call. Args: - func (astroid.BoundMethod): The BoundMethod AST node to check. - types (Optional[String]): Optional sequence of caller type names to restrict check. - methods (Optional[String]): Optional sequence of method names to restrict check. + func: The BoundMethod AST node to check. + types: Optional sequence of caller type names to restrict check. + methods: Optional sequence of method names to restrict check. Returns: - bool: true if the node represents a method call for the given type and + true if the node represents a method call for the given type and method names, False otherwise. """ return ( @@ -185,14 +196,14 @@ def visit_import(self, node: nodes.Import) -> None: def visit_call(self, node: nodes.Call) -> None: """Checks calls to logging methods.""" - def is_logging_name(): + def is_logging_name() -> bool: return ( isinstance(node.func, nodes.Attribute) and isinstance(node.func.expr, nodes.Name) and node.func.expr.name in self._logging_names ) - def is_logger_class(): + def is_logger_class() -> tuple[bool, str | None]: for inferred in infer_all(node.func): if isinstance(inferred, astroid.BoundMethod): parent = inferred._proxied.parent @@ -214,14 +225,14 @@ def is_logger_class(): return self._check_log_method(node, name) - def _check_log_method(self, node, name): + def _check_log_method(self, node: nodes.Call, name: str) -> None: """Checks calls to logging.log(level, format, *format_args).""" if name == "log": if node.starargs or node.kwargs or len(node.args) < 2: # Either a malformed call, star args, or double-star args. Beyond # the scope of this checker. return - format_pos = 1 + format_pos: Literal[0, 1] = 1 elif name in CHECKED_CONVENIENCE_FUNCTIONS: if node.starargs or node.kwargs or not node.args: # Either no args, star args, or double-star args. Beyond the @@ -231,8 +242,9 @@ def _check_log_method(self, node, name): else: return - if isinstance(node.args[format_pos], nodes.BinOp): - binop = node.args[format_pos] + format_arg = node.args[format_pos] + if isinstance(format_arg, nodes.BinOp): + binop = format_arg emit = binop.op == "%" if binop.op == "+": total_number_of_strings = sum( @@ -247,18 +259,20 @@ def _check_log_method(self, node, name): node=node, args=(self._helper_string(node),), ) - elif isinstance(node.args[format_pos], nodes.Call): - self._check_call_func(node.args[format_pos]) - elif isinstance(node.args[format_pos], nodes.Const): + elif isinstance(format_arg, nodes.Call): + self._check_call_func(format_arg) + elif isinstance(format_arg, nodes.Const): self._check_format_string(node, format_pos) - elif isinstance(node.args[format_pos], nodes.JoinedStr): + elif isinstance(format_arg, nodes.JoinedStr): + if str_formatting_in_f_string(format_arg): + return self.add_message( "logging-fstring-interpolation", node=node, args=(self._helper_string(node),), ) - def _helper_string(self, node): + def _helper_string(self, node: nodes.Call) -> str: """Create a string that lists the valid types of formatting for this node.""" valid_types = ["lazy %"] @@ -276,11 +290,11 @@ def _helper_string(self, node): return " or ".join(valid_types) @staticmethod - def _is_operand_literal_str(operand): + def _is_operand_literal_str(operand: InferenceResult | None) -> bool: """Return True if the operand in argument is a literal string.""" return isinstance(operand, nodes.Const) and operand.name == "str" - def _check_call_func(self, node: nodes.Call): + def _check_call_func(self, node: nodes.Call) -> None: """Checks that function call is not format_string.format().""" func = utils.safe_infer(node.func) types = ("str", "unicode") @@ -296,12 +310,12 @@ def _check_call_func(self, node: nodes.Call): args=(self._helper_string(node),), ) - def _check_format_string(self, node, format_arg): + def _check_format_string(self, node: nodes.Call, format_arg: Literal[0, 1]) -> None: """Checks that format string tokens match the supplied arguments. Args: - node (nodes.NodeNG): AST node to be checked. - format_arg (int): Index of the format string in the node arguments. + node: AST node to be checked. + format_arg: Index of the format string in the node arguments. """ num_args = _count_supplied_tokens(node.args[format_arg + 1 :]) if not num_args: @@ -368,7 +382,7 @@ def is_complex_format_str(node: nodes.NodeNG) -> bool: return any(format_spec for (_, _, format_spec, _) in parsed) -def _count_supplied_tokens(args): +def _count_supplied_tokens(args: list[nodes.NodeNG]) -> int: """Counts the number of tokens in an args list. The Python log functions allow for special keyword arguments: func, @@ -376,13 +390,26 @@ def _count_supplied_tokens(args): arguments that aren't keywords. Args: - args (list): AST nodes that are arguments for a log format string. + args: AST nodes that are arguments for a log format string. Returns: - int: Number of AST nodes that aren't keywords. + Number of AST nodes that aren't keywords. """ return sum(1 for arg in args if not isinstance(arg, nodes.Keyword)) +def str_formatting_in_f_string(node: nodes.JoinedStr) -> bool: + """Determine whether the node represents an f-string with string formatting. + + For example: `f'Hello %s'` + """ + # Check "%" presence first for performance. + return any( + "%" in val.value and any(x in val.value for x in MOST_COMMON_FORMATTING) + for val in node.values + if isinstance(val, nodes.Const) + ) + + def register(linter: PyLinter) -> None: linter.register_checker(LoggingChecker(linter)) diff --git a/pylint/checkers/mapreduce_checker.py b/pylint/checkers/mapreduce_checker.py index 9d721aa490..96e86d7c0e 100644 --- a/pylint/checkers/mapreduce_checker.py +++ b/pylint/checkers/mapreduce_checker.py @@ -20,6 +20,7 @@ def __init__(self) -> None: "MapReduceMixin has been deprecated and will be removed in pylint 3.0. " "To make a checker reduce map data simply implement get_map_data and reduce_map_data.", DeprecationWarning, + stacklevel=2, ) @abc.abstractmethod diff --git a/pylint/checkers/method_args.py b/pylint/checkers/method_args.py new file mode 100644 index 0000000000..e882482190 --- /dev/null +++ b/pylint/checkers/method_args.py @@ -0,0 +1,128 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Variables checkers for Python code.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import astroid +from astroid import arguments, bases, nodes + +from pylint.checkers import BaseChecker, utils +from pylint.interfaces import INFERENCE + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class MethodArgsChecker(BaseChecker): + """BaseChecker for method_args. + + Checks for + * missing-timeout + * positional-only-arguments-expected + """ + + name = "method_args" + msgs = { + "W3101": ( + "Missing timeout argument for method '%s' can cause your program to hang indefinitely", + "missing-timeout", + "Used when a method needs a 'timeout' parameter in order to avoid waiting " + "for a long time. If no timeout is specified explicitly the default value " + "is used. For example for 'requests' the program will never time out " + "(i.e. hang indefinitely).", + ), + "E3102": ( + "`%s()` got some positional-only arguments passed as keyword arguments: %s", + "positional-only-arguments-expected", + "Emitted when positional-only arguments have been passed as keyword arguments. " + "Remove the keywords for the affected arguments in the function call.", + {"minversion": (3, 8)}, + ), + } + options = ( + ( + "timeout-methods", + { + "default": ( + "requests.api.delete", + "requests.api.get", + "requests.api.head", + "requests.api.options", + "requests.api.patch", + "requests.api.post", + "requests.api.put", + "requests.api.request", + ), + "type": "csv", + "metavar": "", + "help": "List of qualified names (i.e., library.method) which require a timeout parameter " + "e.g. 'requests.api.get,requests.api.post'", + }, + ), + ) + + @utils.only_required_for_messages( + "missing-timeout", "positional-only-arguments-expected" + ) + def visit_call(self, node: nodes.Call) -> None: + self._check_missing_timeout(node) + self._check_positional_only_arguments_expected(node) + + def _check_missing_timeout(self, node: nodes.Call) -> None: + """Check if the call needs a timeout parameter based on package.func_name + configured in config.timeout_methods. + + Package uses inferred node in order to know the package imported. + """ + inferred = utils.safe_infer(node.func) + call_site = arguments.CallSite.from_call(node) + if ( + inferred + and not call_site.has_invalid_keywords() + and isinstance( + inferred, (nodes.FunctionDef, nodes.ClassDef, bases.UnboundMethod) + ) + and inferred.qname() in self.linter.config.timeout_methods + ): + keyword_arguments = [keyword.arg for keyword in node.keywords] + keyword_arguments.extend(call_site.keyword_arguments) + if "timeout" not in keyword_arguments: + self.add_message( + "missing-timeout", + node=node, + args=(node.func.as_string(),), + confidence=INFERENCE, + ) + + def _check_positional_only_arguments_expected(self, node: nodes.Call) -> None: + """Check if positional only arguments have been passed as keyword arguments by + inspecting its method definition. + """ + inferred_func = utils.safe_infer(node.func) + while isinstance(inferred_func, (astroid.BoundMethod, astroid.UnboundMethod)): + inferred_func = inferred_func._proxied + if not ( + isinstance(inferred_func, (nodes.FunctionDef)) + and inferred_func.args.posonlyargs + ): + return + pos_args = [a.name for a in inferred_func.args.posonlyargs] + kws = [k.arg for k in node.keywords if k.arg in pos_args] + if not kws: + return + + self.add_message( + "positional-only-arguments-expected", + node=node, + args=(node.func.as_string(), ", ".join(f"'{k}'" for k in kws)), + confidence=INFERENCE, + ) + + +def register(linter: PyLinter) -> None: + linter.register_checker(MethodArgsChecker(linter)) diff --git a/pylint/checkers/misc.py b/pylint/checkers/misc.py index b48d302d8c..8f64957358 100644 --- a/pylint/checkers/misc.py +++ b/pylint/checkers/misc.py @@ -42,7 +42,7 @@ def _get_by_id_managed_msgs(self) -> list[ManagedMessage]: def process_module(self, node: nodes.Module) -> None: """Inspect the source file to find messages activated or deactivated by id.""" managed_msgs = self._get_by_id_managed_msgs() - for (mod_name, msgid, symbol, lineno, is_disabled) in managed_msgs: + for mod_name, msgid, symbol, lineno, is_disabled in managed_msgs: if mod_name == node.name: verb = "disable" if is_disabled else "enable" txt = f"'{msgid}' is cryptic: use '# pylint: {verb}={symbol}' instead" diff --git a/pylint/checkers/modified_iterating_checker.py b/pylint/checkers/modified_iterating_checker.py index c8ae50eb7c..bdc8fff7f9 100644 --- a/pylint/checkers/modified_iterating_checker.py +++ b/pylint/checkers/modified_iterating_checker.py @@ -56,9 +56,8 @@ class ModifiedIterationChecker(checkers.BaseChecker): ) def visit_for(self, node: nodes.For) -> None: iter_obj = node.iter - if isinstance(iter_obj, nodes.Name): - for body_node in node.body: - self._modified_iterating_check_on_node_and_children(body_node, iter_obj) + for body_node in node.body: + self._modified_iterating_check_on_node_and_children(body_node, iter_obj) def _modified_iterating_check_on_node_and_children( self, body_node: nodes.NodeNG, iter_obj: nodes.NodeNG @@ -72,17 +71,33 @@ def _modified_iterating_check( self, node: nodes.NodeNG, iter_obj: nodes.NodeNG ) -> None: msg_id = None - if self._modified_iterating_list_cond(node, iter_obj): + if isinstance(node, nodes.Delete) and any( + self._deleted_iteration_target_cond(t, iter_obj) for t in node.targets + ): + inferred = utils.safe_infer(iter_obj) + if isinstance(inferred, nodes.List): + msg_id = "modified-iterating-list" + elif isinstance(inferred, nodes.Dict): + msg_id = "modified-iterating-dict" + elif isinstance(inferred, nodes.Set): + msg_id = "modified-iterating-set" + elif not isinstance(iter_obj, (nodes.Name, nodes.Attribute)): + pass + elif self._modified_iterating_list_cond(node, iter_obj): msg_id = "modified-iterating-list" elif self._modified_iterating_dict_cond(node, iter_obj): msg_id = "modified-iterating-dict" elif self._modified_iterating_set_cond(node, iter_obj): msg_id = "modified-iterating-set" if msg_id: + if isinstance(iter_obj, nodes.Attribute): + obj_name = iter_obj.attrname + else: + obj_name = iter_obj.name self.add_message( msg_id, node=node, - args=(iter_obj.name,), + args=(obj_name,), confidence=interfaces.INFERENCE, ) @@ -98,11 +113,16 @@ def _is_node_expr_that_calls_attribute_name(node: nodes.NodeNG) -> bool: @staticmethod def _common_cond_list_set( node: nodes.Expr, - iter_obj: nodes.NodeNG, + iter_obj: nodes.Name | nodes.Attribute, infer_val: nodes.List | nodes.Set, ) -> bool: - return (infer_val == utils.safe_infer(iter_obj)) and ( - node.value.func.expr.name == iter_obj.name + iter_obj_name = ( + iter_obj.attrname + if isinstance(iter_obj, nodes.Attribute) + else iter_obj.name + ) + return (infer_val == utils.safe_infer(iter_obj)) and ( # type: ignore[no-any-return] + node.value.func.expr.name == iter_obj_name ) @staticmethod @@ -113,7 +133,7 @@ def _is_node_assigns_subscript_name(node: nodes.NodeNG) -> bool: ) def _modified_iterating_list_cond( - self, node: nodes.NodeNG, iter_obj: nodes.NodeNG + self, node: nodes.NodeNG, iter_obj: nodes.Name | nodes.Attribute ) -> bool: if not self._is_node_expr_that_calls_attribute_name(node): return False @@ -126,7 +146,7 @@ def _modified_iterating_list_cond( ) def _modified_iterating_dict_cond( - self, node: nodes.NodeNG, iter_obj: nodes.NodeNG + self, node: nodes.NodeNG, iter_obj: nodes.Name | nodes.Attribute ) -> bool: if not self._is_node_assigns_subscript_name(node): return False @@ -144,10 +164,14 @@ def _modified_iterating_dict_cond( return False if infer_val != utils.safe_infer(iter_obj): return False - return node.targets[0].value.name == iter_obj.name + if isinstance(iter_obj, nodes.Attribute): + iter_obj_name = iter_obj.attrname + else: + iter_obj_name = iter_obj.name + return node.targets[0].value.name == iter_obj_name # type: ignore[no-any-return] def _modified_iterating_set_cond( - self, node: nodes.NodeNG, iter_obj: nodes.NodeNG + self, node: nodes.NodeNG, iter_obj: nodes.Name | nodes.Attribute ) -> bool: if not self._is_node_expr_that_calls_attribute_name(node): return False @@ -159,6 +183,22 @@ def _modified_iterating_set_cond( and node.value.func.attrname in _SET_MODIFIER_METHODS ) + def _deleted_iteration_target_cond( + self, node: nodes.DelName, iter_obj: nodes.NodeNG + ) -> bool: + if not isinstance(node, nodes.DelName): + return False + if not isinstance(iter_obj.parent, nodes.For): + return False + if not isinstance( + iter_obj.parent.target, (nodes.AssignName, nodes.BaseContainer) + ): + return False + return any( + t == node.name + for t in utils.find_assigned_names_recursive(iter_obj.parent.target) + ) + def register(linter: PyLinter) -> None: linter.register_checker(ModifiedIterationChecker(linter)) diff --git a/pylint/checkers/nested_min_max.py b/pylint/checkers/nested_min_max.py new file mode 100644 index 0000000000..e9aa409f04 --- /dev/null +++ b/pylint/checkers/nested_min_max.py @@ -0,0 +1,116 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Check for use of nested min/max functions.""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +from astroid import nodes, objects + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import only_required_for_messages, safe_infer +from pylint.interfaces import INFERENCE + +if TYPE_CHECKING: + from pylint.lint import PyLinter + +DICT_TYPES = ( + objects.DictValues, + objects.DictKeys, + objects.DictItems, + nodes.node_classes.Dict, +) + + +class NestedMinMaxChecker(BaseChecker): + """Multiple nested min/max calls on the same line will raise multiple messages. + + This behaviour is intended as it would slow down the checker to check + for nested call with minimal benefits. + """ + + FUNC_NAMES = ("builtins.min", "builtins.max") + + name = "nested_min_max" + msgs = { + "W3301": ( + "Do not use nested call of '%s'; it's possible to do '%s' instead", + "nested-min-max", + "Nested calls ``min(1, min(2, 3))`` can be rewritten as ``min(1, 2, 3)``.", + ) + } + + @classmethod + def is_min_max_call(cls, node: nodes.NodeNG) -> bool: + if not isinstance(node, nodes.Call): + return False + + inferred = safe_infer(node.func) + return ( + isinstance(inferred, nodes.FunctionDef) + and inferred.qname() in cls.FUNC_NAMES + ) + + @classmethod + def get_redundant_calls(cls, node: nodes.Call) -> list[nodes.Call]: + return [ + arg + for arg in node.args + if cls.is_min_max_call(arg) and arg.func.name == node.func.name + ] + + @only_required_for_messages("nested-min-max") + def visit_call(self, node: nodes.Call) -> None: + if not self.is_min_max_call(node): + return + + redundant_calls = self.get_redundant_calls(node) + if not redundant_calls: + return + + fixed_node = copy.copy(node) + while len(redundant_calls) > 0: + for i, arg in enumerate(fixed_node.args): + # Exclude any calls with generator expressions as there is no + # clear better suggestion for them. + if isinstance(arg, nodes.Call) and any( + isinstance(a, nodes.GeneratorExp) for a in arg.args + ): + return + + if arg in redundant_calls: + fixed_node.args = ( + fixed_node.args[:i] + arg.args + fixed_node.args[i + 1 :] + ) + break + + redundant_calls = self.get_redundant_calls(fixed_node) + + for idx, arg in enumerate(fixed_node.args): + if not isinstance(arg, nodes.Const): + inferred = safe_infer(arg) + if isinstance( + inferred, (nodes.List, nodes.Tuple, nodes.Set, *DICT_TYPES) + ): + splat_node = nodes.Starred(lineno=inferred.lineno) + splat_node.value = arg + fixed_node.args = ( + fixed_node.args[:idx] + + [splat_node] + + fixed_node.args[idx + 1 : idx] + ) + + self.add_message( + "nested-min-max", + node=node, + args=(node.func.name, fixed_node.as_string()), + confidence=INFERENCE, + ) + + +def register(linter: PyLinter) -> None: + linter.register_checker(NestedMinMaxChecker(linter)) diff --git a/pylint/checkers/refactoring/implicit_booleaness_checker.py b/pylint/checkers/refactoring/implicit_booleaness_checker.py index 47c8c01815..2ad038619b 100644 --- a/pylint/checkers/refactoring/implicit_booleaness_checker.py +++ b/pylint/checkers/refactoring/implicit_booleaness_checker.py @@ -9,6 +9,7 @@ from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import HIGH, INFERENCE class ImplicitBooleanessChecker(checkers.BaseChecker): @@ -50,7 +51,6 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): * comparison such as variable != empty_literal: """ - # configuration section name name = "refactoring" msgs = { "C1802": ( @@ -64,10 +64,10 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): {"old_names": [("C1801", "len-as-condition")]}, ), "C1803": ( - "'%s' can be simplified to '%s' as an empty sequence is falsey", + "'%s' can be simplified to '%s' as an empty %s is falsey", "use-implicit-booleaness-not-comparison", "Used when Pylint detects that collection literal comparison is being " - "used to check for emptiness; Use implicit booleaness instead" + "used to check for emptiness; Use implicit booleaness instead " "of a collection classes; empty collections are considered as false", ), } @@ -99,7 +99,11 @@ def visit_call(self, node: nodes.Call) -> None: ) if isinstance(len_arg, generator_or_comprehension): # The node is a generator or comprehension as in len([x for x in ...]) - self.add_message("use-implicit-booleaness-not-len", node=node) + self.add_message( + "use-implicit-booleaness-not-len", + node=node, + confidence=HIGH, + ) return try: instance = next(len_arg.infer()) @@ -113,7 +117,11 @@ def visit_call(self, node: nodes.Call) -> None: if "range" in mother_classes or ( affected_by_pep8 and not self.instance_has_bool(instance) ): - self.add_message("use-implicit-booleaness-not-len", node=node) + self.add_message( + "use-implicit-booleaness-not-len", + node=node, + confidence=INFERENCE, + ) @staticmethod def instance_has_bool(class_def: nodes.ClassDef) -> bool: @@ -134,7 +142,9 @@ def visit_unaryop(self, node: nodes.UnaryOp) -> None: and node.op == "not" and utils.is_call_of_name(node.operand, "len") ): - self.add_message("use-implicit-booleaness-not-len", node=node) + self.add_message( + "use-implicit-booleaness-not-len", node=node, confidence=HIGH + ) @utils.only_required_for_messages("use-implicit-booleaness-not-comparison") def visit_compare(self, node: nodes.Compare) -> None: @@ -177,35 +187,42 @@ def _check_use_implicit_booleaness_not_comparison( # No need to check for operator when visiting compare node if operator in {"==", "!=", ">=", ">", "<=", "<"}: - collection_literal = "{}" - if isinstance(literal_node, nodes.List): - collection_literal = "[]" - if isinstance(literal_node, nodes.Tuple): - collection_literal = "()" - - instance_name = "x" - if isinstance(target_node, nodes.Call) and target_node.func: - instance_name = f"{target_node.func.as_string()}(...)" - elif isinstance(target_node, (nodes.Attribute, nodes.Name)): - instance_name = target_node.as_string() - - original_comparison = ( - f"{instance_name} {operator} {collection_literal}" - ) - suggestion = ( - f"{instance_name}" - if operator == "!=" - else f"not {instance_name}" - ) self.add_message( "use-implicit-booleaness-not-comparison", - args=( - original_comparison, - suggestion, + args=self._implicit_booleaness_message_args( + literal_node, operator, target_node ), node=node, + confidence=HIGH, ) + def _get_node_description(self, node: nodes.NodeNG) -> str: + return { + nodes.List: "list", + nodes.Tuple: "tuple", + nodes.Dict: "dict", + nodes.Const: "str", + }.get(type(node), "iterable") + + def _implicit_booleaness_message_args( + self, literal_node: nodes.NodeNG, operator: str, target_node: nodes.NodeNG + ) -> tuple[str, str, str]: + """Helper to get the right message for "use-implicit-booleaness-not-comparison".""" + description = self._get_node_description(literal_node) + collection_literal = { + "list": "[]", + "tuple": "()", + "dict": "{}", + }.get(description, "iterable") + instance_name = "x" + if isinstance(target_node, nodes.Call) and target_node.func: + instance_name = f"{target_node.func.as_string()}(...)" + elif isinstance(target_node, (nodes.Attribute, nodes.Name)): + instance_name = target_node.as_string() + original_comparison = f"{instance_name} {operator} {collection_literal}" + suggestion = f"{instance_name}" if operator == "!=" else f"not {instance_name}" + return original_comparison, suggestion, description + @staticmethod def base_names_of_instance(node: bases.Uninferable | bases.Instance) -> list[str]: """Return all names inherited by a class instance or those returned by a diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 7873dc25ed..3a6d1033c3 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -9,10 +9,10 @@ from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import HIGH, INFERENCE class RecommendationChecker(checkers.BaseChecker): - name = "refactoring" msgs = { "C0200": ( @@ -67,7 +67,7 @@ def open(self) -> None: self._py36_plus = py_version >= (3, 6) @staticmethod - def _is_builtin(node, function): + def _is_builtin(node: nodes.NodeNG, function: str) -> bool: inferred = utils.safe_infer(node) if not inferred: return False @@ -85,6 +85,10 @@ def _check_consider_iterating_dictionary(self, node: nodes.Call) -> None: return if node.func.attrname != "keys": return + + if isinstance(node.parent, nodes.BinOp) and node.parent.op in {"&", "|", "^"}: + return + comp_ancestor = utils.get_node_first_ancestor_of_type(node, nodes.Compare) if ( isinstance(node.parent, (nodes.For, nodes.Comprehension)) @@ -101,7 +105,9 @@ def _check_consider_iterating_dictionary(self, node: nodes.Call) -> None: inferred.bound, nodes.Dict ): return - self.add_message("consider-iterating-dictionary", node=node) + self.add_message( + "consider-iterating-dictionary", node=node, confidence=INFERENCE + ) def _check_use_maxsplit_arg(self, node: nodes.Call) -> None: """Add message when accessing first or last elements of a str.split() or @@ -115,6 +121,11 @@ def _check_use_maxsplit_arg(self, node: nodes.Call) -> None: and isinstance(utils.safe_infer(node.func), astroid.BoundMethod) ): return + inferred_expr = utils.safe_infer(node.func.expr) + if isinstance(inferred_expr, astroid.Instance) and any( + inferred_expr.nodes_of_class(nodes.ClassDef) + ): + return try: sep = utils.get_argument_from_call(node, 0, "sep") @@ -326,9 +337,16 @@ def _check_consider_using_dict_items_comprehension( def _check_use_sequence_for_iteration( self, node: nodes.For | nodes.Comprehension ) -> None: - """Check if code iterates over an in-place defined set.""" - if isinstance(node.iter, nodes.Set): - self.add_message("use-sequence-for-iteration", node=node.iter) + """Check if code iterates over an in-place defined set. + + Sets using `*` are not considered in-place. + """ + if isinstance(node.iter, nodes.Set) and not any( + utils.has_starred_node_recursive(node) + ): + self.add_message( + "use-sequence-for-iteration", node=node.iter, confidence=HIGH + ) @utils.only_required_for_messages("consider-using-f-string") def visit_const(self, node: nodes.Const) -> None: diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 265b59d992..0caa2fbb53 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -11,23 +11,31 @@ import tokenize from collections.abc import Iterator from functools import reduce -from typing import NamedTuple +from re import Pattern +from typing import TYPE_CHECKING, Any, NamedTuple, Union, cast import astroid -from astroid import nodes +from astroid import bases, nodes from astroid.util import Uninferable from pylint import checkers from pylint.checkers import utils from pylint.checkers.utils import node_frame_class -from pylint.interfaces import HIGH +from pylint.interfaces import HIGH, INFERENCE, Confidence + +if TYPE_CHECKING: + from pylint.lint import PyLinter if sys.version_info >= (3, 8): from functools import cached_property else: from astroid.decorators import cachedproperty as cached_property -KNOWN_INFINITE_ITERATORS = {"itertools.count"} +NodesWithNestedBlocks = Union[ + nodes.TryExcept, nodes.TryFinally, nodes.While, nodes.For, nodes.If +] + +KNOWN_INFINITE_ITERATORS = {"itertools.count", "itertools.cycle"} BUILTIN_EXIT_FUNCS = frozenset(("quit", "exit")) CALLS_THAT_COULD_BE_REPLACED_BY_WITH = frozenset( ( @@ -41,6 +49,7 @@ CALLS_RETURNING_CONTEXT_MANAGERS = frozenset( ( "_io.open", # regular 'open()' call + "pathlib.Path.open", "codecs.open", "urllib.request.urlopen", "tempfile.NamedTemporaryFile", @@ -59,10 +68,22 @@ ) -def _if_statement_is_always_returning(if_node, returning_node_class) -> bool: +def _if_statement_is_always_returning( + if_node: nodes.If, returning_node_class: nodes.NodeNG +) -> bool: return any(isinstance(node, returning_node_class) for node in if_node.body) +def _except_statement_is_always_returning( + node: nodes.TryExcept, returning_node_class: nodes.NodeNG +) -> bool: + """Detect if all except statements return.""" + return all( + any(isinstance(child, returning_node_class) for child in handler.body) + for handler in node.handlers + ) + + def _is_trailing_comma(tokens: list[tokenize.TokenInfo], index: int) -> bool: """Check if the given token is a trailing comma. @@ -90,7 +111,7 @@ def _is_trailing_comma(tokens: list[tokenize.TokenInfo], index: int) -> bool: if not more_tokens_on_line: return False - def get_curline_index_start(): + def get_curline_index_start() -> int: """Get the index denoting the start of the current line.""" for subindex, token in enumerate(reversed(tokens[:index])): # See Lib/tokenize.py and Lib/token.py in cpython for more info @@ -137,7 +158,7 @@ def _is_part_of_with_items(node: nodes.Call) -> bool: if isinstance(current, nodes.With): items_start = current.items[0][0].lineno items_end = current.items[-1][0].tolineno - return items_start <= node.lineno <= items_end + return items_start <= node.lineno <= items_end # type: ignore[no-any-return] current = current.parent return False @@ -171,7 +192,7 @@ def _is_part_of_assignment_target(node: nodes.NodeNG) -> bool: return node in node.parent.targets if isinstance(node.parent, nodes.AugAssign): - return node == node.parent.target + return node == node.parent.target # type: ignore[no-any-return] if isinstance(node.parent, (nodes.Tuple, nodes.List)): return _is_part_of_assignment_target(node.parent) @@ -193,7 +214,7 @@ def __iter__(self) -> Iterator[dict[str, nodes.NodeNG]]: def get_stack_for_frame( self, frame: nodes.FunctionDef | nodes.ClassDef | nodes.Module - ): + ) -> dict[str, nodes.NodeNG]: """Get the stack corresponding to the scope of the given frame.""" if isinstance(frame, nodes.FunctionDef): return self.function_scope @@ -321,11 +342,12 @@ class RefactoringChecker(checkers.BaseTokenChecker): "and increases readability compared to for-loop iteration.", ), "R1714": ( - 'Consider merging these comparisons with "in" to %r', + "Consider merging these comparisons with 'in' by using '%s %sin (%s)'." + " Use a set instead if elements are hashable.", "consider-using-in", - "To check if a variable is equal to one of many values," - 'combine the values into a tuple and check if the variable is contained "in" it ' - "instead of checking for equality against each of the values." + "To check if a variable is equal to one of many values, " + 'combine the values into a set or tuple and check if the variable is contained "in" it ' + "instead of checking for equality against each of the values. " "This is faster and less verbose.", ), "R1715": ( @@ -339,7 +361,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): "R1716": ( "Simplify chained comparison between the operands", "chained-comparison", - "This message is emitted when pylint encounters boolean operation like" + "This message is emitted when pylint encounters boolean operation like " '"a < b and b < c", suggesting instead to refactor it to "a < b < c"', ), "R1717": ( @@ -348,7 +370,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): "Emitted when we detect the creation of a dictionary " "using the dict() callable and a transient list. " "Although there is nothing syntactically wrong with this code, " - "it is hard to read and can be simplified to a dict comprehension." + "it is hard to read and can be simplified to a dict comprehension. " "Also it is faster since you don't need to create another " "transient list", ), @@ -356,7 +378,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): "Consider using a set comprehension", "consider-using-set-comprehension", "Although there is nothing syntactically wrong with this code, " - "it is hard to read and can be simplified to a set comprehension." + "it is hard to read and can be simplified to a set comprehension. " "Also it is faster since you don't need to create another " "transient list", ), @@ -383,9 +405,10 @@ class RefactoringChecker(checkers.BaseTokenChecker): "It is faster and simpler.", ), "R1722": ( - "Consider using sys.exit()", + "Consider using 'sys.exit' instead", "consider-using-sys-exit", - "Instead of using exit() or quit(), consider using the sys.exit().", + "Contrary to 'exit()' or 'quit()', 'sys.exit' does not rely on the " + "site module being available (as the 'sys' module is always available).", ), "R1723": ( 'Unnecessary "%s" after "break", %s', @@ -453,9 +476,9 @@ class RefactoringChecker(checkers.BaseTokenChecker): "The literal is faster as it avoids an additional function call.", ), "R1735": ( - "Consider using {} instead of dict()", + "Consider using '%s' instead of a call to 'dict'.", "use-dict-literal", - "Emitted when using dict() to create an empty dictionary instead of the literal {}. " + "Emitted when using dict() to create a dictionary instead of a literal '{ ... }'. " "The literal is faster as it avoids an additional function call.", ), "R1736": ( @@ -490,38 +513,37 @@ class RefactoringChecker(checkers.BaseTokenChecker): ), ) - def __init__(self, linter): + def __init__(self, linter: PyLinter) -> None: super().__init__(linter) - self._return_nodes = {} + self._return_nodes: dict[str, list[nodes.Return]] = {} self._consider_using_with_stack = ConsiderUsingWithStack() self._init() - self._never_returning_functions = None + self._never_returning_functions: set[str] = set() - def _init(self): - self._nested_blocks = [] - self._elifs = [] - self._nested_blocks_msg = None - self._reported_swap_nodes = set() - self._can_simplify_bool_op = False + def _init(self) -> None: + self._nested_blocks: list[NodesWithNestedBlocks] = [] + self._elifs: list[tuple[int, int]] = [] + self._reported_swap_nodes: set[nodes.NodeNG] = set() + self._can_simplify_bool_op: bool = False self._consider_using_with_stack.clear_all() - def open(self): + def open(self) -> None: # do this in open since config not fully initialized in __init__ self._never_returning_functions = set( self.linter.config.never_returning_functions ) @cached_property - def _dummy_rgx(self): - return self.linter.config.dummy_variables_rgx + def _dummy_rgx(self) -> Pattern[str]: + return self.linter.config.dummy_variables_rgx # type: ignore[no-any-return] @staticmethod - def _is_bool_const(node): + def _is_bool_const(node: nodes.Return | nodes.Assign) -> bool: return isinstance(node.value, nodes.Const) and isinstance( node.value.value, bool ) - def _is_actual_elif(self, node): + def _is_actual_elif(self, node: nodes.If | nodes.TryExcept) -> bool: """Check if the given node is an actual elif. This is a problem we're having with the builtin ast module, @@ -537,7 +559,7 @@ def _is_actual_elif(self, node): return True return False - def _check_simplifiable_if(self, node): + def _check_simplifiable_if(self, node: nodes.If) -> None: """Check if the given if node can be simplified. The if statement can be reduced to a boolean expression @@ -632,14 +654,18 @@ def leave_module(self, _: nodes.Module) -> None: ) self._init() - @utils.only_required_for_messages("too-many-nested-blocks") - def visit_tryexcept(self, node: nodes.TryExcept) -> None: + @utils.only_required_for_messages("too-many-nested-blocks", "no-else-return") + def visit_tryexcept(self, node: nodes.TryExcept | nodes.TryFinally) -> None: self._check_nested_blocks(node) + if isinstance(node, nodes.TryExcept): + self._check_superfluous_else_return(node) + self._check_superfluous_else_raise(node) + visit_tryfinally = visit_tryexcept visit_while = visit_tryexcept - def _check_redefined_argument_from_local(self, name_node): + def _check_redefined_argument_from_local(self, name_node: nodes.AssignName) -> None: if self._dummy_rgx and self._dummy_rgx.match(name_node.name): return if not name_node.lineno: @@ -696,54 +722,73 @@ def visit_with(self, node: nodes.With) -> None: for name in names.nodes_of_class(nodes.AssignName): self._check_redefined_argument_from_local(name) - def _check_superfluous_else(self, node, msg_id, returning_node_class): + def _check_superfluous_else( + self, + node: nodes.If | nodes.TryExcept, + msg_id: str, + returning_node_class: nodes.NodeNG, + ) -> None: + if isinstance(node, nodes.TryExcept) and isinstance( + node.parent, nodes.TryFinally + ): + # Not interested in try/except/else/finally statements. + return + if not node.orelse: - # Not interested in if statements without else. + # Not interested in if/try statements without else. return if self._is_actual_elif(node): # Not interested in elif nodes; only if return - if _if_statement_is_always_returning(node, returning_node_class): + if ( + isinstance(node, nodes.If) + and _if_statement_is_always_returning(node, returning_node_class) + ) or ( + isinstance(node, nodes.TryExcept) + and _except_statement_is_always_returning(node, returning_node_class) + ): orelse = node.orelse[0] if (orelse.lineno, orelse.col_offset) in self._elifs: args = ("elif", 'remove the leading "el" from "elif"') else: args = ("else", 'remove the "else" and de-indent the code inside it') - self.add_message(msg_id, node=node, args=args) + self.add_message(msg_id, node=node, args=args, confidence=HIGH) - def _check_superfluous_else_return(self, node): + def _check_superfluous_else_return(self, node: nodes.If) -> None: return self._check_superfluous_else( node, msg_id="no-else-return", returning_node_class=nodes.Return ) - def _check_superfluous_else_raise(self, node): + def _check_superfluous_else_raise(self, node: nodes.If) -> None: return self._check_superfluous_else( node, msg_id="no-else-raise", returning_node_class=nodes.Raise ) - def _check_superfluous_else_break(self, node): + def _check_superfluous_else_break(self, node: nodes.If) -> None: return self._check_superfluous_else( node, msg_id="no-else-break", returning_node_class=nodes.Break ) - def _check_superfluous_else_continue(self, node): + def _check_superfluous_else_continue(self, node: nodes.If) -> None: return self._check_superfluous_else( node, msg_id="no-else-continue", returning_node_class=nodes.Continue ) @staticmethod - def _type_and_name_are_equal(node_a, node_b): - for _type in (nodes.Name, nodes.AssignName): - if all(isinstance(_node, _type) for _node in (node_a, node_b)): - return node_a.name == node_b.name - if all(isinstance(_node, nodes.Const) for _node in (node_a, node_b)): - return node_a.value == node_b.value + def _type_and_name_are_equal(node_a: Any, node_b: Any) -> bool: + if isinstance(node_a, nodes.Name) and isinstance(node_b, nodes.Name): + return node_a.name == node_b.name # type: ignore[no-any-return] + if isinstance(node_a, nodes.AssignName) and isinstance( + node_b, nodes.AssignName + ): + return node_a.name == node_b.name # type: ignore[no-any-return] + if isinstance(node_a, nodes.Const) and isinstance(node_b, nodes.Const): + return node_a.value == node_b.value # type: ignore[no-any-return] return False - def _is_dict_get_block(self, node): - + def _is_dict_get_block(self, node: nodes.If) -> bool: # "if " if not isinstance(node.test, nodes.Compare): return False @@ -773,7 +818,7 @@ def _is_dict_get_block(self, node): # The object needs to be a dictionary instance return isinstance(utils.safe_infer(node.test.ops[0][1]), nodes.Dict) - def _check_consider_get(self, node): + def _check_consider_get(self, node: nodes.If) -> None: if_block_ok = self._is_dict_get_block(node) if if_block_ok and not node.orelse: self.add_message("consider-using-get", node=node) @@ -809,7 +854,8 @@ def visit_if(self, node: nodes.If) -> None: self._check_consider_get(node) self._check_consider_using_min_max_builtin(node) - def _check_consider_using_min_max_builtin(self, node: nodes.If): + # pylint: disable = too-many-branches + def _check_consider_using_min_max_builtin(self, node: nodes.If) -> None: """Check if the given if node can be refactored as a min/max python builtin.""" if self._is_actual_elif(node) or node.orelse: # Not interested in if statements with multiple branches. @@ -892,7 +938,7 @@ def _check_consider_using_min_max_builtin(self, node: nodes.If): def visit_ifexp(self, node: nodes.IfExp) -> None: self._check_simplifiable_ifexp(node) - def _check_simplifiable_ifexp(self, node): + def _check_simplifiable_ifexp(self, node: nodes.IfExp) -> None: if not isinstance(node.body, nodes.Const) or not isinstance( node.orelse, nodes.Const ): @@ -951,7 +997,7 @@ def leave_classdef(self, _: nodes.ClassDef) -> None: def visit_raise(self, node: nodes.Raise) -> None: self._check_stop_iteration_inside_generator(node) - def _check_stop_iteration_inside_generator(self, node): + def _check_stop_iteration_inside_generator(self, node: nodes.Raise) -> None: """Check if an exception of type StopIteration is raised inside a generator.""" frame = node.frame(future=True) if not isinstance(frame, nodes.FunctionDef) or not frame.is_generator(): @@ -961,18 +1007,20 @@ def _check_stop_iteration_inside_generator(self, node): if not node.exc: return exc = utils.safe_infer(node.exc) - if not exc or not isinstance(exc, (astroid.Instance, nodes.ClassDef)): + if not exc or not isinstance(exc, (bases.Instance, nodes.ClassDef)): return if self._check_exception_inherit_from_stopiteration(exc): - self.add_message("stop-iteration-return", node=node) + self.add_message("stop-iteration-return", node=node, confidence=INFERENCE) @staticmethod - def _check_exception_inherit_from_stopiteration(exc): + def _check_exception_inherit_from_stopiteration( + exc: nodes.ClassDef | bases.Instance, + ) -> bool: """Return True if the exception node in argument inherit from StopIteration.""" stopiteration_qname = f"{utils.EXCEPTIONS_MODULE}.StopIteration" return any(_class.qname() == stopiteration_qname for _class in exc.mro()) - def _check_consider_using_comprehension_constructor(self, node): + def _check_consider_using_comprehension_constructor(self, node: nodes.Call) -> None: if ( isinstance(node.func, nodes.Name) and node.args @@ -1006,7 +1054,7 @@ def _check_consider_using_comprehension_constructor(self, node): message_name = "consider-using-set-comprehension" self.add_message(message_name, node=node) - def _check_consider_using_generator(self, node): + def _check_consider_using_generator(self, node: nodes.Call) -> None: # 'any', 'all', definitely should use generator, while 'list', 'tuple', # 'sum', 'max', and 'min' need to be considered first # See https://github.com/PyCQA/pylint/pull/3309#discussion_r576683109 @@ -1057,17 +1105,17 @@ def visit_call(self, node: nodes.Call) -> None: self._check_super_with_arguments(node) self._check_consider_using_generator(node) self._check_consider_using_with(node) - self._check_use_list_or_dict_literal(node) + self._check_use_list_literal(node) + self._check_use_dict_literal(node) @staticmethod - def _has_exit_in_scope(scope): + def _has_exit_in_scope(scope: nodes.LocalsDictNodeNG) -> bool: exit_func = scope.locals.get("exit") return bool( exit_func and isinstance(exit_func[0], (nodes.ImportFrom, nodes.Import)) ) - def _check_quit_exit_call(self, node): - + def _check_quit_exit_call(self, node: nodes.Call) -> None: if isinstance(node.func, nodes.Name) and node.func.name in BUILTIN_EXIT_FUNCS: # If we have `exit` imported from `sys` in the current or global scope, exempt this instance. local_scope = node.scope() @@ -1075,9 +1123,9 @@ def _check_quit_exit_call(self, node): node.root() ): return - self.add_message("consider-using-sys-exit", node=node) + self.add_message("consider-using-sys-exit", node=node, confidence=HIGH) - def _check_super_with_arguments(self, node): + def _check_super_with_arguments(self, node: nodes.Call) -> None: if not isinstance(node.func, nodes.Name) or node.func.name != "super": return @@ -1089,13 +1137,16 @@ def _check_super_with_arguments(self, node): or not isinstance(node.args[0], nodes.Name) or not isinstance(node.args[1], nodes.Name) or node_frame_class(node) is None - or node.args[0].name != node_frame_class(node).name + # TODO: PY38: Use walrus operator, this will also fix the mypy issue + or node.args[0].name != node_frame_class(node).name # type: ignore[union-attr] ): return self.add_message("super-with-arguments", node=node) - def _check_raising_stopiteration_in_generator_next_call(self, node): + def _check_raising_stopiteration_in_generator_next_call( + self, node: nodes.Call + ) -> None: """Check if a StopIteration exception is raised by the call to next function. If the next value has a default value, then do not add message. @@ -1104,9 +1155,9 @@ def _check_raising_stopiteration_in_generator_next_call(self, node): :type node: :class:`nodes.Call` """ - def _looks_like_infinite_iterator(param): + def _looks_like_infinite_iterator(param: nodes.NodeNG) -> bool: inferred = utils.safe_infer(param) - if inferred: + if isinstance(inferred, bases.Instance): return inferred.qname() in KNOWN_INFINITE_ITERATORS return False @@ -1114,8 +1165,17 @@ def _looks_like_infinite_iterator(param): # A next() method, which is now what we want. return + if len(node.args) == 0: + # handle case when builtin.next is called without args. + # see https://github.com/PyCQA/pylint/issues/7828 + return + inferred = utils.safe_infer(node.func) - if getattr(inferred, "name", "") == "next": + + if ( + isinstance(inferred, nodes.FunctionDef) + and inferred.qname() == "builtins.next" + ): frame = node.frame(future=True) # The next builtin can only have up to two # positional arguments and no keyword arguments @@ -1127,9 +1187,14 @@ def _looks_like_infinite_iterator(param): and not utils.node_ignores_exception(node, StopIteration) and not _looks_like_infinite_iterator(node.args[0]) ): - self.add_message("stop-iteration-return", node=node) + self.add_message( + "stop-iteration-return", node=node, confidence=INFERENCE + ) - def _check_nested_blocks(self, node): + def _check_nested_blocks( + self, + node: NodesWithNestedBlocks, + ) -> None: """Update and check the number of nested blocks.""" # only check block levels inside functions or methods if not isinstance(node.scope(), nodes.FunctionDef): @@ -1155,7 +1220,9 @@ def _check_nested_blocks(self, node): if len(nested_blocks) > len(self._nested_blocks): self._emit_nested_blocks_message_if_needed(nested_blocks) - def _emit_nested_blocks_message_if_needed(self, nested_blocks): + def _emit_nested_blocks_message_if_needed( + self, nested_blocks: list[NodesWithNestedBlocks] + ) -> None: if len(nested_blocks) > self.linter.config.max_nested_blocks: self.add_message( "too-many-nested-blocks", @@ -1163,12 +1230,14 @@ def _emit_nested_blocks_message_if_needed(self, nested_blocks): args=(len(nested_blocks), self.linter.config.max_nested_blocks), ) - def _emit_consider_using_with_if_needed(self, stack: dict[str, nodes.NodeNG]): + def _emit_consider_using_with_if_needed( + self, stack: dict[str, nodes.NodeNG] + ) -> None: for node in stack.values(): self.add_message("consider-using-with", node=node) @staticmethod - def _duplicated_isinstance_types(node): + def _duplicated_isinstance_types(node: nodes.BoolOp) -> dict[str, set[str]]: """Get the duplicated types from the underlying isinstance calls. :param nodes.BoolOp node: Node which should contain a bunch of isinstance calls. @@ -1176,8 +1245,8 @@ def _duplicated_isinstance_types(node): to duplicate values from consecutive calls. :rtype: dict """ - duplicated_objects = set() - all_types = collections.defaultdict(set) + duplicated_objects: set[str] = set() + all_types: collections.defaultdict[str, set[str]] = collections.defaultdict(set) for call in node.values: if not isinstance(call, nodes.Call) or len(call.args) != 2: @@ -1209,7 +1278,7 @@ def _duplicated_isinstance_types(node): key: value for key, value in all_types.items() if key in duplicated_objects } - def _check_consider_merging_isinstance(self, node): + def _check_consider_merging_isinstance(self, node: nodes.BoolOp) -> None: """Check isinstance calls which can be merged together.""" if node.op != "or": return @@ -1223,7 +1292,7 @@ def _check_consider_merging_isinstance(self, node): args=(duplicated_name, ", ".join(names)), ) - def _check_consider_using_in(self, node): + def _check_consider_using_in(self, node: nodes.BoolOp) -> None: allowed_ops = {"or": "==", "and": "!="} if node.op not in allowed_ops or len(node.values) < 2: @@ -1258,15 +1327,18 @@ def _check_consider_using_in(self, node): # Gather information for the suggestion common_variable = sorted(list(common_variables))[0] - comprehension = "in" if node.op == "or" else "not in" values = list(collections.OrderedDict.fromkeys(values)) values.remove(common_variable) values_string = ", ".join(values) if len(values) != 1 else values[0] + "," - suggestion = f"{common_variable} {comprehension} ({values_string})" - - self.add_message("consider-using-in", node=node, args=(suggestion,)) + maybe_not = "" if node.op == "or" else "not " + self.add_message( + "consider-using-in", + node=node, + args=(common_variable, maybe_not, values_string), + confidence=HIGH, + ) - def _check_chained_comparison(self, node): + def _check_chained_comparison(self, node: nodes.BoolOp) -> None: """Check if there is any chained comparison in the expression. Add a refactoring message if a boolOp contains comparison like a < b and b < c, @@ -1277,7 +1349,10 @@ def _check_chained_comparison(self, node): if node.op != "and" or len(node.values) < 2: return - def _find_lower_upper_bounds(comparison_node, uses): + def _find_lower_upper_bounds( + comparison_node: nodes.Compare, + uses: collections.defaultdict[str, dict[str, set[nodes.Compare]]], + ) -> None: left_operand = comparison_node.left for operator, right_operand in comparison_node.ops: for operand in (left_operand, right_operand): @@ -1302,7 +1377,9 @@ def _find_lower_upper_bounds(comparison_node, uses): uses[value]["lower_bound"].add(comparison_node) left_operand = right_operand - uses = collections.defaultdict( + uses: collections.defaultdict[ + str, dict[str, set[nodes.Compare]] + ] = collections.defaultdict( lambda: {"lower_bound": set(), "upper_bound": set()} ) for comparison_node in node.values: @@ -1318,7 +1395,9 @@ def _find_lower_upper_bounds(comparison_node, uses): break @staticmethod - def _apply_boolean_simplification_rules(operator, values): + def _apply_boolean_simplification_rules( + operator: str, values: list[nodes.NodeNG] + ) -> list[nodes.NodeNG]: """Removes irrelevant values or returns short-circuiting values. This function applies the following two rules: @@ -1328,7 +1407,7 @@ def _apply_boolean_simplification_rules(operator, values): 2) False values in OR expressions are only relevant if all values are false, and the reverse for AND """ - simplified_values = [] + simplified_values: list[nodes.NodeNG] = [] for subnode in values: inferred_bool = None @@ -1344,7 +1423,7 @@ def _apply_boolean_simplification_rules(operator, values): return simplified_values or [nodes.Const(operator == "and")] - def _simplify_boolean_operation(self, bool_op): + def _simplify_boolean_operation(self, bool_op: nodes.BoolOp) -> nodes.BoolOp: """Attempts to simplify a boolean operation. Recursively applies simplification on the operator terms, @@ -1366,7 +1445,7 @@ def _simplify_boolean_operation(self, bool_op): simplified_bool_op.postinit(result) return simplified_bool_op - def _check_simplifiable_condition(self, node): + def _check_simplifiable_condition(self, node: nodes.BoolOp) -> None: """Check if a boolean condition can be simplified. Variables will not be simplified, even if the value can be inferred, @@ -1408,7 +1487,7 @@ def visit_boolop(self, node: nodes.BoolOp) -> None: self._check_simplifiable_condition(node) @staticmethod - def _is_simple_assignment(node): + def _is_simple_assignment(node: nodes.NodeNG | None) -> bool: return ( isinstance(node, nodes.Assign) and len(node.targets) == 1 @@ -1416,7 +1495,7 @@ def _is_simple_assignment(node): and isinstance(node.value, nodes.Name) ) - def _check_swap_variables(self, node): + def _check_swap_variables(self, node: nodes.Return | nodes.Assign) -> None: if not node.next_sibling() or not node.next_sibling().next_sibling(): return assignments = [node, node.next_sibling(), node.next_sibling().next_sibling()] @@ -1446,7 +1525,7 @@ def visit_assign(self, node: nodes.Assign) -> None: "consider-using-ternary", "consider-swap-variables", ) - def visit_return(self, node: nodes.Return) -> None: + def visit_return(self, node: nodes.Return | nodes.Assign) -> None: self._check_swap_variables(node) if self._is_and_or_ternary(node.value): cond, truth_value, false_value = self._and_or_ternary_arguments(node.value) @@ -1458,11 +1537,10 @@ def visit_return(self, node: nodes.Return) -> None: ): return - inferred_truth_value = utils.safe_infer(truth_value) + inferred_truth_value = utils.safe_infer(truth_value, compare_constants=True) if inferred_truth_value is None or inferred_truth_value == astroid.Uninferable: - truth_boolean_value = True - else: - truth_boolean_value = inferred_truth_value.bool_value() + return + truth_boolean_value = inferred_truth_value.bool_value() if truth_boolean_value is False: message = "simplify-boolean-expression" @@ -1470,7 +1548,7 @@ def visit_return(self, node: nodes.Return) -> None: else: message = "consider-using-ternary" suggestion = f"{truth_value.as_string()} if {cond.as_string()} else {false_value.as_string()}" - self.add_message(message, node=node, args=(suggestion,)) + self.add_message(message, node=node, args=(suggestion,), confidence=INFERENCE) def _append_context_managers_to_stack(self, node: nodes.Assign) -> None: if _is_inside_context_manager(node): @@ -1519,7 +1597,7 @@ def _append_context_managers_to_stack(self, node: nodes.Assign) -> None: ) stack[varname] = value - def _check_consider_using_with(self, node: nodes.Call): + def _check_consider_using_with(self, node: nodes.Call) -> None: if _is_inside_context_manager(node) or _is_a_return_statement(node): # If we are inside a context manager itself, we assume that it will handle the resource management itself. # If the node is a child of a return, we assume that the caller knows he is getting a context manager @@ -1534,7 +1612,9 @@ def _check_consider_using_with(self, node: nodes.Call): # the result of this call was already assigned to a variable and will be checked when leaving the scope. return inferred = utils.safe_infer(node.func) - if not inferred: + if not inferred or not isinstance( + inferred, (nodes.FunctionDef, nodes.ClassDef, bases.UnboundMethod) + ): return could_be_used_in_with = ( # things like ``lock.acquire()`` @@ -1548,17 +1628,63 @@ def _check_consider_using_with(self, node: nodes.Call): if could_be_used_in_with and not _will_be_released_automatically(node): self.add_message("consider-using-with", node=node) - def _check_use_list_or_dict_literal(self, node: nodes.Call) -> None: - """Check if empty list or dict is created by using the literal [] or {}.""" - if node.as_string() in {"list()", "dict()"}: + def _check_use_list_literal(self, node: nodes.Call) -> None: + """Check if empty list is created by using the literal [].""" + if node.as_string() == "list()": inferred = utils.safe_infer(node.func) if isinstance(inferred, nodes.ClassDef) and not node.args: if inferred.qname() == "builtins.list": self.add_message("use-list-literal", node=node) - elif inferred.qname() == "builtins.dict" and not node.keywords: - self.add_message("use-dict-literal", node=node) - def _check_consider_using_join(self, aug_assign): + def _check_use_dict_literal(self, node: nodes.Call) -> None: + """Check if dict is created by using the literal {}.""" + if not isinstance(node.func, astroid.Name) or node.func.name != "dict": + return + inferred = utils.safe_infer(node.func) + if ( + isinstance(inferred, nodes.ClassDef) + and inferred.qname() == "builtins.dict" + and not node.args + ): + self.add_message( + "use-dict-literal", + args=(self._dict_literal_suggestion(node),), + node=node, + confidence=INFERENCE, + ) + + @staticmethod + def _dict_literal_suggestion(node: nodes.Call) -> str: + """Return a suggestion of reasonable length.""" + elements: list[str] = [] + for keyword in node.keywords: + if len(", ".join(elements)) >= 64: + break + if keyword not in node.kwargs: + elements.append(f'"{keyword.arg}": {keyword.value.as_string()}') + for keyword in node.kwargs: + if len(", ".join(elements)) >= 64: + break + elements.append(f"**{keyword.value.as_string()}") + suggestion = ", ".join(elements) + return f"{{{suggestion}{', ... ' if len(suggestion) > 64 else ''}}}" + + @staticmethod + def _name_to_concatenate(node: nodes.NodeNG) -> str | None: + """Try to extract the name used in a concatenation loop.""" + if isinstance(node, nodes.Name): + return cast("str | None", node.name) + if not isinstance(node, nodes.JoinedStr): + return None + + values = [ + value for value in node.values if isinstance(value, nodes.FormattedValue) + ] + if len(values) != 1 or not isinstance(values[0].value, nodes.Name): + return None + return cast("str | None", values[0].value.name) + + def _check_consider_using_join(self, aug_assign: nodes.AugAssign) -> None: """We start with the augmented assignment and work our way upwards. Names of variables for nodes if match successful: @@ -1585,8 +1711,7 @@ def _check_consider_using_join(self, aug_assign): and aug_assign.target.name in result_assign_names and isinstance(assign.value, nodes.Const) and isinstance(assign.value.value, str) - and isinstance(aug_assign.value, nodes.Name) - and aug_assign.value.name == for_loop.target.name + and self._name_to_concatenate(aug_assign.value) == for_loop.target.name ) if is_concat_loop: self.add_message("consider-using-join", node=aug_assign) @@ -1687,7 +1812,7 @@ def _check_unnecessary_comprehension(self, node: nodes.Comprehension) -> None: ) @staticmethod - def _is_and_or_ternary(node): + def _is_and_or_ternary(node: nodes.NodeNG | None) -> bool: """Returns true if node is 'condition and true_value or false_value' form. All of: condition, true_value and false_value should not be a complex boolean expression @@ -1704,7 +1829,9 @@ def _is_and_or_ternary(node): ) @staticmethod - def _and_or_ternary_arguments(node): + def _and_or_ternary_arguments( + node: nodes.BoolOp, + ) -> tuple[nodes.NodeNG, nodes.NodeNG, nodes.NodeNG]: false_value = node.values[1] condition, true_value = node.values[0].values return condition, true_value, false_value @@ -1874,10 +2001,10 @@ def _is_function_def_never_returning(self, node: nodes.FunctionDef) -> bool: ) try: return node.qname() in self._never_returning_functions - except TypeError: + except (TypeError, AttributeError): return False - def _check_return_at_the_end(self, node): + def _check_return_at_the_end(self, node: nodes.FunctionDef) -> None: """Check for presence of a *single* return statement at the end of a function. @@ -2056,11 +2183,19 @@ def _check_unnecessary_list_index_lookup( not isinstance(node.iter, nodes.Call) or not isinstance(node.iter.func, nodes.Name) or not node.iter.func.name == "enumerate" - or not node.iter.args - or not isinstance(node.iter.args[0], nodes.Name) ): return + try: + iterable_arg = utils.get_argument_from_call( + node.iter, position=0, keyword="iterable" + ) + except utils.NoSuchArgumentError: + return + + if not isinstance(iterable_arg, nodes.Name): + return + if not isinstance(node.target, nodes.Tuple) or len(node.target.elts) < 2: # enumerate() result is being assigned without destructuring return @@ -2070,7 +2205,13 @@ def _check_unnecessary_list_index_lookup( # destructured, so we can't necessarily use it. return - iterating_object_name = node.iter.args[0].name + has_start_arg, confidence = self._enumerate_with_start(node) + if has_start_arg: + # enumerate is being called with start arg/kwarg so resulting index lookup + # is not redundant, hence we should not report an error. + return + + iterating_object_name = iterable_arg.name value_variable = node.target.elts[1] # Store potential violations. These will only be reported if we don't @@ -2092,6 +2233,15 @@ def _check_unnecessary_list_index_lookup( ) has_nested_loops = next(nested_loops, None) is not None + # Check if there are any if statements within the loop in question; + # If so, we will be more conservative about reporting errors as we + # can't yet do proper control flow analysis to be sure when + # reassignment will affect us + if_statements = itertools.chain.from_iterable( + child.nodes_of_class(nodes.If) for child in children + ) + has_if_statements = next(if_statements, None) is not None + for child in children: for subscript in child.nodes_of_class(nodes.Subscript): if isinstance(node, nodes.For) and _is_part_of_assignment_target( @@ -2135,12 +2285,14 @@ def _check_unnecessary_list_index_lookup( # loops we don't want to report this unless we get to the # end of the loop without updating the collection bad_nodes.append(subscript) + elif has_if_statements: + continue else: self.add_message( "unnecessary-list-index-lookup", node=subscript, args=(node.target.elts[1].name,), - confidence=HIGH, + confidence=confidence, ) for subscript in bad_nodes: @@ -2148,5 +2300,54 @@ def _check_unnecessary_list_index_lookup( "unnecessary-list-index-lookup", node=subscript, args=(node.target.elts[1].name,), - confidence=HIGH, + confidence=confidence, ) + + def _enumerate_with_start( + self, node: nodes.For | nodes.Comprehension + ) -> tuple[bool, Confidence]: + """Check presence of `start` kwarg or second argument to enumerate. + + For example: + + `enumerate([1,2,3], start=1)` + `enumerate([1,2,3], 1)` + + If `start` is assigned to `0`, the default value, this is equivalent to + not calling `enumerate` with start. + """ + confidence = HIGH + + if len(node.iter.args) > 1: + # We assume the second argument to `enumerate` is the `start` int arg. + # It's a reasonable assumption for now as it's the only possible argument: + # https://docs.python.org/3/library/functions.html#enumerate + start_arg = node.iter.args[1] + start_val, confidence = self._get_start_value(start_arg) + if start_val is None: + return False, confidence + return not start_val == 0, confidence + + for keyword in node.iter.keywords: + if keyword.arg == "start": + start_val, confidence = self._get_start_value(keyword.value) + if start_val is None: + return False, confidence + return not start_val == 0, confidence + + return False, confidence + + def _get_start_value(self, node: nodes.NodeNG) -> tuple[int | None, Confidence]: + if ( + isinstance(node, (nodes.Name, nodes.Call, nodes.Attribute)) + or isinstance(node, nodes.UnaryOp) + and isinstance(node.operand, nodes.Attribute) + ): + inferred = utils.safe_infer(node) + start_val = inferred.value if inferred else None + return start_val, INFERENCE + if isinstance(node, nodes.UnaryOp): + return node.operand.value, HIGH + if isinstance(node, nodes.Const): + return node.value, HIGH + return None, HIGH diff --git a/pylint/checkers/similar.py b/pylint/checkers/similar.py index 77f2fc8352..bc485a981d 100644 --- a/pylint/checkers/similar.py +++ b/pylint/checkers/similar.py @@ -5,16 +5,26 @@ """A similarities / code duplication command line tool and pylint checker. The algorithm is based on comparing the hash value of n successive lines of a file. -First the files are read and any line that doesn't fulfill requirement are removed (comments, docstrings...) +First the files are read and any line that doesn't fulfill requirement are removed +(comments, docstrings...) + Those stripped lines are stored in the LineSet class which gives access to them. -Then each index of the stripped lines collection is associated with the hash of n successive entries of the stripped lines starting at the current index -(n is the minimum common lines option). -The common hashes between both linesets are then looked for. If there are matches, then the match indices in both linesets are stored and associated -with the corresponding couples (start line number/end line number) in both files. -This association is then post-processed to handle the case of successive matches. For example if the minimum common lines setting is set to four, then -the hashes are computed with four lines. If one of match indices couple (12, 34) is the successor of another one (11, 33) then it means that there are -in fact five lines which are common. -Once post-processed the values of association table are the result looked for, i.e start and end lines numbers of common lines in both files. +Then each index of the stripped lines collection is associated with the hash of n +successive entries of the stripped lines starting at the current index (n is the +minimum common lines option). + +The common hashes between both linesets are then looked for. If there are matches, then +the match indices in both linesets are stored and associated with the corresponding +couples (start line number/end line number) in both files. + +This association is then post-processed to handle the case of successive matches. For +example if the minimum common lines setting is set to four, then the hashes are +computed with four lines. If one of match indices couple (12, 34) is the +successor of another one (11, 33) then it means that there are in fact five lines which +are common. + +Once post-processed the values of association table are the result looked for, i.e. +start and end lines numbers of common lines in both files. """ from __future__ import annotations @@ -28,7 +38,7 @@ import sys import warnings from collections import defaultdict -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Generator, Iterable, Sequence from getopt import getopt from io import BufferedIOBase, BufferedReader, BytesIO from itertools import chain, groupby @@ -49,7 +59,7 @@ from astroid import nodes from pylint.checkers import BaseChecker, BaseRawFileChecker, table_lines_from_stats -from pylint.reporters.ureports.nodes import Table +from pylint.reporters.ureports.nodes import Section, Table from pylint.typing import MessageDefinitionTuple, Options from pylint.utils import LinterStats, decoding_stream @@ -185,7 +195,7 @@ def __repr__(self) -> str: f">" ) - def __eq__(self, other) -> bool: + def __eq__(self, other: Any) -> bool: if not isinstance(other, LineSetStartCouple): return NotImplemented return ( @@ -229,18 +239,16 @@ def hash_lineset( # Need different iterators on same lines but each one is shifted 1 from the precedent shifted_lines = [iter(lines[i:]) for i in range(min_common_lines)] - for index_i, *succ_lines in enumerate(zip(*shifted_lines)): - start_linenumber = lineset.stripped_lines[index_i].line_number + for i, *succ_lines in enumerate(zip(*shifted_lines)): + start_linenumber = LineNumber(lineset.stripped_lines[i].line_number) try: - end_linenumber = lineset.stripped_lines[ - index_i + min_common_lines - ].line_number + end_linenumber = lineset.stripped_lines[i + min_common_lines].line_number except IndexError: - end_linenumber = lineset.stripped_lines[-1].line_number + 1 + end_linenumber = LineNumber(lineset.stripped_lines[-1].line_number + 1) - index = Index(index_i) + index = Index(i) index2lines[index] = SuccessiveLinesLimits( - start=LineNumber(start_linenumber), end=LineNumber(end_linenumber) + start=start_linenumber, end=end_linenumber ) l_c = LinesChunk(lineset.name, index, *succ_lines) @@ -384,7 +392,7 @@ def append_stream( self.namespace.ignore_docstrings, self.namespace.ignore_imports, self.namespace.ignore_signatures, - line_enabled_callback=self.linter._is_one_message_enabled # type: ignore[attr-defined] + line_enabled_callback=self.linter._is_one_message_enabled if hasattr(self, "linter") else None, ) @@ -459,20 +467,28 @@ def _get_similarity_report( report += f" {line.rstrip()}\n" if line.rstrip() else "\n" duplicated_line_number += number * (len(couples_l) - 1) total_line_number: int = sum(len(lineset) for lineset in self.linesets) - report += f"TOTAL lines={total_line_number} duplicates={duplicated_line_number} percent={duplicated_line_number * 100.0 / total_line_number:.2f}\n" + report += ( + f"TOTAL lines={total_line_number} " + f"duplicates={duplicated_line_number} " + f"percent={duplicated_line_number * 100.0 / total_line_number:.2f}\n" + ) return report + # pylint: disable = too-many-locals def _find_common( self, lineset1: LineSet, lineset2: LineSet ) -> Generator[Commonality, None, None]: """Find similarities in the two given linesets. - This the core of the algorithm. - The idea is to compute the hashes of a minimal number of successive lines of each lineset and then compare the hashes. - Every match of such comparison is stored in a dict that links the couple of starting indices in both linesets to - the couple of corresponding starting and ending lines in both files. - Last regroups all successive couples in a bigger one. It allows to take into account common chunk of lines that have more - than the minimal number of successive lines required. + This the core of the algorithm. The idea is to compute the hashes of a + minimal number of successive lines of each lineset and then compare the + hashes. Every match of such comparison is stored in a dict that links the + couple of starting indices in both linesets to the couple of corresponding + starting and ending lines in both files. + + Last regroups all successive couples in a bigger one. It allows to take into + account common chunk of lines that have more than the minimal number of + successive lines required. """ hash_to_index_1: HashToIndex_T hash_to_index_2: HashToIndex_T @@ -542,7 +558,7 @@ def _iter_sims(self) -> Generator[Commonality, None, None]: for lineset2 in self.linesets[idx + 1 :]: yield from self._find_common(lineset, lineset2) - def get_map_data(self): + def get_map_data(self) -> list[LineSet]: """Returns the data we can use for a map/reduce process. In this case we are returning this instance's Linesets, that is all file @@ -550,7 +566,7 @@ def get_map_data(self): """ return self.linesets - def combine_mapreduce_data(self, linesets_collection): + def combine_mapreduce_data(self, linesets_collection: list[list[LineSet]]) -> None: """Reduces and recombines data into a format that we can report on. The partner function of get_map_data() @@ -587,7 +603,7 @@ def stripped_lines( line_begins_import = { lineno: all(is_import for _, is_import in node_is_import_group) for lineno, node_is_import_group in groupby( - node_is_import_by_lineno, key=lambda x: x[0] + node_is_import_by_lineno, key=lambda x: x[0] # type: ignore[no-any-return] ) } current_line_is_import = False @@ -691,32 +707,32 @@ def __init__( line_enabled_callback=line_enabled_callback, ) - def __str__(self): + def __str__(self) -> str: return f"" - def __len__(self): + def __len__(self) -> int: return len(self._real_lines) - def __getitem__(self, index): + def __getitem__(self, index: int) -> LineSpecifs: return self._stripped_lines[index] - def __lt__(self, other): + def __lt__(self, other: LineSet) -> bool: return self.name < other.name - def __hash__(self): + def __hash__(self) -> int: return id(self) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if not isinstance(other, LineSet): return False return self.__dict__ == other.__dict__ @property - def stripped_lines(self): + def stripped_lines(self) -> list[LineSpecifs]: return self._stripped_lines @property - def real_lines(self): + def real_lines(self) -> list[str]: return self._real_lines @@ -732,7 +748,7 @@ def real_lines(self): def report_similarities( - sect, + sect: Section, stats: LinterStats, old_stats: LinterStats | None, ) -> None: @@ -817,7 +833,7 @@ def __init__(self, linter: PyLinter) -> None: ignore_signatures=self.linter.config.ignore_signatures, ) - def open(self): + def open(self) -> None: """Init the checkers: reset linesets and statistics information.""" self.linesets = [] self.linter.stats.reset_duplicated_lines() @@ -840,7 +856,7 @@ def process_module(self, node: nodes.Module) -> None: with node.stream() as stream: self.append_stream(self.linter.current_name, stream, node.file_encoding) # type: ignore[arg-type] - def close(self): + def close(self) -> None: """Compute and display similarities on closing (i.e. end of parsing).""" total = sum(len(lineset) for lineset in self.linesets) duplicated = 0 @@ -861,11 +877,11 @@ def close(self): stats.nb_duplicated_lines += int(duplicated) stats.percent_duplicated_lines += float(total and duplicated * 100.0 / total) - def get_map_data(self): + def get_map_data(self) -> list[LineSet]: """Passthru override.""" return Similar.get_map_data(self) - def reduce_map_data(self, linter, data): + def reduce_map_data(self, linter: PyLinter, data: list[list[LineSet]]) -> None: """Reduces and recombines data into a format that we can report on. The partner function of get_map_data() @@ -877,7 +893,7 @@ def register(linter: PyLinter) -> None: linter.register_checker(SimilarChecker(linter)) -def usage(status=0): +def usage(status: int = 0) -> NoReturn: """Display command line usage information.""" print("finds copy pasted blocks in a set of files") print() @@ -888,7 +904,7 @@ def usage(status=0): sys.exit(status) -def Run(argv=None) -> NoReturn: +def Run(argv: Sequence[str] | None = None) -> NoReturn: """Standalone command line access point.""" if argv is None: argv = sys.argv[1:] @@ -907,7 +923,7 @@ def Run(argv=None) -> NoReturn: ignore_docstrings = False ignore_imports = False ignore_signatures = False - opts, args = getopt(argv, s_opts, l_opts) + opts, args = getopt(list(argv), s_opts, l_opts) for opt, val in opts: if opt in {"-d", "--duplicates"}: min_lines = int(val) diff --git a/pylint/checkers/spelling.py b/pylint/checkers/spelling.py index 23300135ad..274fc7599c 100644 --- a/pylint/checkers/spelling.py +++ b/pylint/checkers/spelling.py @@ -7,6 +7,7 @@ from __future__ import annotations import re +import sys import tokenize from re import Pattern from typing import TYPE_CHECKING @@ -16,6 +17,11 @@ from pylint.checkers import BaseTokenChecker from pylint.checkers.utils import only_required_for_messages +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + if TYPE_CHECKING: from pylint.lint import PyLinter @@ -42,15 +48,17 @@ class WikiWordFilter: # type: ignore[no-redef] ... class Filter: # type: ignore[no-redef] - def _skip(self, word): + def _skip(self, word: str) -> bool: raise NotImplementedError class Chunker: # type: ignore[no-redef] pass def get_tokenizer( - tag=None, chunkers=None, filters=None - ): # pylint: disable=unused-argument + tag: str | None = None, # pylint: disable=unused-argument + chunkers: list[Chunker] | None = None, # pylint: disable=unused-argument + filters: list[Filter] | None = None, # pylint: disable=unused-argument + ) -> Filter: return Filter() @@ -67,24 +75,24 @@ def get_tokenizer( instr = " To make it work, install the 'python-enchant' package." -class WordsWithDigitsFilter(Filter): +class WordsWithDigitsFilter(Filter): # type: ignore[misc] """Skips words with digits.""" - def _skip(self, word): + def _skip(self, word: str) -> bool: return any(char.isdigit() for char in word) -class WordsWithUnderscores(Filter): +class WordsWithUnderscores(Filter): # type: ignore[misc] """Skips words with underscores. They are probably function parameter names. """ - def _skip(self, word): + def _skip(self, word: str) -> bool: return "_" in word -class RegExFilter(Filter): +class RegExFilter(Filter): # type: ignore[misc] """Parent class for filters using regular expressions. This filter skips any words the match the expression @@ -93,7 +101,7 @@ class RegExFilter(Filter): _pattern: Pattern[str] - def _skip(self, word) -> bool: + def _skip(self, word: str) -> bool: return bool(self._pattern.match(word)) @@ -120,12 +128,14 @@ class SphinxDirectives(RegExFilter): _pattern = re.compile(r"^(:([a-z]+)){1,2}:`([^`]+)(`)?") -class ForwardSlashChunker(Chunker): +class ForwardSlashChunker(Chunker): # type: ignore[misc] """This chunker allows splitting words like 'before/after' into 'before' and 'after'. """ - def next(self): + _text: str + + def next(self) -> tuple[str, int]: while True: if not self._text: raise StopIteration() @@ -148,7 +158,7 @@ def next(self): return f"{pre_text}/{post_text}", 0 return pre_text, 0 - def _next(self): + def _next(self) -> tuple[str, Literal[0]]: while True: if "/" not in self._text: return self._text, 0 @@ -162,7 +172,6 @@ def _next(self): CODE_FLANKED_IN_BACKTICK_REGEX = re.compile(r"(\s|^)(`{1,2})([^`]+)(\2)([^`]|$)") -MYPY_IGNORE_DIRECTIVE_RULE_REGEX = re.compile(r"(\s|^)(type\: ignore\[[^\]]+\])(.*)") def _strip_code_flanked_in_backticks(line: str) -> str: @@ -172,7 +181,7 @@ def _strip_code_flanked_in_backticks(line: str) -> str: so this cannot be done at the individual filter level. """ - def replace_code_but_leave_surrounding_characters(match_obj) -> str: + def replace_code_but_leave_surrounding_characters(match_obj: re.Match[str]) -> str: return match_obj.group(1) + match_obj.group(5) return CODE_FLANKED_IN_BACKTICK_REGEX.sub( @@ -180,21 +189,6 @@ def replace_code_but_leave_surrounding_characters(match_obj) -> str: ) -def _strip_mypy_ignore_directive_rule(line: str) -> str: - """Alter line so mypy rule name is ignored. - - Pyenchant parses anything flanked by spaces as an individual token, - so this cannot be done at the individual filter level. - """ - - def replace_rule_name_but_leave_surrounding_characters(match_obj) -> str: - return match_obj.group(1) + match_obj.group(3) - - return MYPY_IGNORE_DIRECTIVE_RULE_REGEX.sub( - replace_rule_name_but_leave_surrounding_characters, line - ) - - class SpellingChecker(BaseTokenChecker): """Check spelling in comments and docstrings.""" @@ -328,6 +322,7 @@ def open(self) -> None: ) self.initialized = True + # pylint: disable = too-many-statements def _check_spelling(self, msgid: str, line: str, line_num: int) -> None: original_line = line try: @@ -349,7 +344,6 @@ def _check_spelling(self, msgid: str, line: str, line_num: int) -> None: starts_with_comment = False line = _strip_code_flanked_in_backticks(line) - line = _strip_mypy_ignore_directive_rule(line) for word, word_start_at in self.tokenizer(line.strip()): word_start_at += initial_space @@ -415,7 +409,7 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: return # Process tokens and look for comments. - for (tok_type, token, (start_row, _), _, _) in tokens: + for tok_type, token, (start_row, _), _, _ in tokens: if tok_type == tokenize.COMMENT: if start_row == 1 and token.startswith("#!/"): # Skip shebang lines @@ -423,26 +417,24 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: if token.startswith("# pylint:"): # Skip pylint enable/disable comments continue + if token.startswith("# type: "): + # Skip python 2 type comments and mypy type ignore comments + # mypy do not support additional text in type comments + continue self._check_spelling("wrong-spelling-in-comment", token, start_row) @only_required_for_messages("wrong-spelling-in-docstring") def visit_module(self, node: nodes.Module) -> None: - if not self.initialized: - return self._check_docstring(node) @only_required_for_messages("wrong-spelling-in-docstring") def visit_classdef(self, node: nodes.ClassDef) -> None: - if not self.initialized: - return self._check_docstring(node) @only_required_for_messages("wrong-spelling-in-docstring") def visit_functiondef( self, node: nodes.FunctionDef | nodes.AsyncFunctionDef ) -> None: - if not self.initialized: - return self._check_docstring(node) visit_asyncfunctiondef = visit_functiondef @@ -454,12 +446,12 @@ def _check_docstring( | nodes.ClassDef | nodes.Module, ) -> None: - """Check the node has any spelling errors.""" + """Check if the node has any spelling errors.""" + if not self.initialized: + return if not node.doc_node: return - start_line = node.lineno + 1 - # Go through lines of docstring for idx, line in enumerate(node.doc_node.value.splitlines()): self._check_spelling("wrong-spelling-in-docstring", line, start_line + idx) diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 8aa5fc8f6d..c9bec38232 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -8,17 +8,22 @@ import sys from collections.abc import Iterable -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, Set, Tuple import astroid from astroid import nodes +from astroid.typing import InferenceResult from pylint import interfaces from pylint.checkers import BaseChecker, DeprecatedMixin, utils +from pylint.interfaces import INFERENCE +from pylint.typing import MessageDefinitionTuple if TYPE_CHECKING: from pylint.lint import PyLinter +DeprecationDict = Dict[Tuple[int, int, int], Set[str]] + OPEN_FILES_MODE = ("open", "file") OPEN_FILES_FUNCS = OPEN_FILES_MODE + ("read_text", "write_text") UNITTEST_CASE = "unittest.case" @@ -40,7 +45,9 @@ # For modules, see ImportsChecker -DEPRECATED_ARGUMENTS = { +DEPRECATED_ARGUMENTS: dict[ + tuple[int, int, int], dict[str, tuple[tuple[int | None, str], ...]] +] = { (0, 0, 0): { "int": ((None, "x"),), "bool": ((None, "x"),), @@ -78,7 +85,7 @@ (3, 9, 0): {"random.Random.shuffle": ((1, "random"),)}, } -DEPRECATED_DECORATORS = { +DEPRECATED_DECORATORS: DeprecationDict = { (3, 8, 0): {"asyncio.coroutine"}, (3, 3, 0): { "abc.abstractclassmethod", @@ -89,15 +96,17 @@ } -DEPRECATED_METHODS: dict = { +DEPRECATED_METHODS: dict[int, DeprecationDict] = { 0: { - "cgi.parse_qs", - "cgi.parse_qsl", - "ctypes.c_buffer", - "distutils.command.register.register.check_metadata", - "distutils.command.sdist.sdist.check_metadata", - "tkinter.Misc.tk_menuBar", - "tkinter.Menu.tk_bindForTraversal", + (0, 0, 0): { + "cgi.parse_qs", + "cgi.parse_qsl", + "ctypes.c_buffer", + "distutils.command.register.register.check_metadata", + "distutils.command.sdist.sdist.check_metadata", + "tkinter.Misc.tk_menuBar", + "tkinter.Menu.tk_bindForTraversal", + } }, 2: { (2, 6, 0): { @@ -235,7 +244,12 @@ }, (3, 11, 0): { "locale.getdefaultlocale", - "unittest.TestLoader.findTestCases", + "locale.resetlocale", + "re.template", + "unittest.findTestCases", + "unittest.makeSuite", + "unittest.getTestCaseNames", + "unittest.TestLoader.loadTestsFromModule", "unittest.TestLoader.loadTestsFromTestCase", "unittest.TestLoader.getTestCaseNames", }, @@ -243,7 +257,7 @@ } -DEPRECATED_CLASSES = { +DEPRECATED_CLASSES: dict[tuple[int, int, int], dict[str, set[str]]] = { (3, 2, 0): { "configparser": { "LegacyInterpolation", @@ -292,6 +306,9 @@ } }, (3, 11, 0): { + "typing": { + "Text", + }, "webbrowser": { "MacOSX", }, @@ -299,7 +316,7 @@ } -def _check_mode_str(mode): +def _check_mode_str(mode: Any) -> bool: # check type if not isinstance(mode, str): return False @@ -332,8 +349,11 @@ def _check_mode_str(mode): class StdlibChecker(DeprecatedMixin, BaseChecker): name = "stdlib" - msgs = { - **{k: v for k, v in DeprecatedMixin.msgs.items() if k[1:3] == "15"}, + msgs: dict[str, MessageDefinitionTuple] = { + **DeprecatedMixin.DEPRECATED_METHOD_MESSAGE, + **DeprecatedMixin.DEPRECATED_ARGUMENT_MESSAGE, + **DeprecatedMixin.DEPRECATED_CLASS_MESSAGE, + **DeprecatedMixin.DEPRECATED_DECORATOR_MESSAGE, "W1501": ( '"%s" is not a valid mode for open.', "bad-open-mode", @@ -362,7 +382,7 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "threading.Thread needs the target function", "bad-thread-instantiation", "The warning is emitted when a threading.Thread class " - "is instantiated without the target function being passed. " + "is instantiated without the target function being passed as a kwarg or as a second argument. " "By default, the first parameter is the group param, not the target param.", ), "W1507": ( @@ -378,6 +398,20 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "Env manipulation functions support only string type arguments. " "See https://docs.python.org/3/library/os.html#os.getenv.", ), + "E1519": ( + "singledispatch decorator should not be used with methods, " + "use singledispatchmethod instead.", + "singledispatch-method", + "singledispatch should decorate functions and not class/instance methods. " + "Use singledispatchmethod for those cases.", + ), + "E1520": ( + "singledispatchmethod decorator should not be used with functions, " + "use singledispatch instead.", + "singledispatchmethod-function", + "singledispatchmethod should decorate class/instance methods and not functions. " + "Use singledispatch for those cases.", + ), "W1508": ( "%s default type is %s. Expected str or None.", "invalid-envvar-default", @@ -392,15 +426,16 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "The preexec_fn parameter is not safe to use in the presence " "of threads in your application. The child process could " "deadlock before exec is called. If you must use it, keep it " - "trivial! Minimize the number of libraries you call into." - "https://docs.python.org/3/library/subprocess.html#popen-constructor", + "trivial! Minimize the number of libraries you call into. " + "See https://docs.python.org/3/library/subprocess.html#popen-constructor", ), "W1510": ( - "Using subprocess.run without explicitly set `check` is not recommended.", + "'subprocess.run' used without explicitly defining the value for 'check'.", "subprocess-run-check", - "The check parameter should always be used with explicitly set " - "`check` keyword to make clear what the error-handling behavior is." - "https://docs.python.org/3/library/subprocess.html#subprocess.run", + "The ``check`` keyword is set to False by default. It means the process " + "launched by ``subprocess.run`` can exit with a non-zero exit code and " + "fail silently. It's better to set it explicitly to make clear what the " + "error-handling behavior is.", ), "W1514": ( "Using open without explicitly specifying an encoding", @@ -421,7 +456,7 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "By decorating a method with lru_cache or cache the 'self' argument will be linked to " "the function and therefore never garbage collected. Unless your instance " "will never need to be garbage collected (singleton) it is recommended to refactor " - "code to avoid this pattern or add a maxsize to the cache." + "code to avoid this pattern or add a maxsize to the cache. " "The default value for maxsize is 128.", { "old_names": [ @@ -442,9 +477,9 @@ def __init__(self, linter: PyLinter) -> None: for since_vers, func_list in DEPRECATED_METHODS[sys.version_info[0]].items(): if since_vers <= sys.version_info: self._deprecated_methods.update(func_list) - for since_vers, func_list in DEPRECATED_ARGUMENTS.items(): + for since_vers, args_list in DEPRECATED_ARGUMENTS.items(): if since_vers <= sys.version_info: - self._deprecated_arguments.update(func_list) + self._deprecated_arguments.update(args_list) for since_vers, class_list in DEPRECATED_CLASSES.items(): if since_vers <= sys.version_info: self._deprecated_classes.update(class_list) @@ -454,20 +489,26 @@ def __init__(self, linter: PyLinter) -> None: # Modules are checked by the ImportsChecker, because the list is # synced with the config argument deprecated-modules - def _check_bad_thread_instantiation(self, node): - if not node.kwargs and not node.keywords and len(node.args) <= 1: - self.add_message("bad-thread-instantiation", node=node) + def _check_bad_thread_instantiation(self, node: nodes.Call) -> None: + func_kwargs = {key.arg for key in node.keywords} + if "target" in func_kwargs: + return - def _check_for_preexec_fn_in_popen(self, node): + if len(node.args) < 2 and (not node.kwargs or "target" not in func_kwargs): + self.add_message( + "bad-thread-instantiation", node=node, confidence=interfaces.HIGH + ) + + def _check_for_preexec_fn_in_popen(self, node: nodes.Call) -> None: if node.keywords: for keyword in node.keywords: if keyword.arg == "preexec_fn": self.add_message("subprocess-popen-preexec-fn", node=node) - def _check_for_check_kw_in_run(self, node): + def _check_for_check_kw_in_run(self, node: nodes.Call) -> None: kwargs = {keyword.arg for keyword in (node.keywords or ())} if "check" not in kwargs: - self.add_message("subprocess-run-check", node=node) + self.add_message("subprocess-run-check", node=node, confidence=INFERENCE) def _check_shallow_copy_environ(self, node: nodes.Call) -> None: arg = utils.get_argument_from_call(node, position=0) @@ -546,15 +587,25 @@ def visit_boolop(self, node: nodes.BoolOp) -> None: for value in node.values: self._check_datetime(value) - @utils.only_required_for_messages("method-cache-max-size-none") + @utils.only_required_for_messages( + "method-cache-max-size-none", + "singledispatch-method", + "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.decorators) + self._check_lru_cache_decorators(node) + self._check_dispatch_decorators(node) - def _check_lru_cache_decorators(self, decorators: nodes.Decorators) -> None: + def _check_lru_cache_decorators(self, node: nodes.FunctionDef) -> None: """Check if instance methods are decorated with functools.lru_cache.""" + if any(utils.is_enum(ancestor) for ancestor in node.parent.ancestors()): + # method of class inheriting from Enum is exempt from this check. + return + lru_cache_nodes: list[nodes.NodeNG] = [] - for d_node in decorators.nodes: + for d_node in node.decorators.nodes: + # pylint: disable = too-many-try-statements try: for infered_node in d_node.infer(): q_name = infered_node.qname() @@ -588,7 +639,37 @@ def _check_lru_cache_decorators(self, decorators: nodes.Decorators) -> None: confidence=interfaces.INFERENCE, ) - def _check_redundant_assert(self, node, infer): + def _check_dispatch_decorators(self, node: nodes.FunctionDef) -> None: + decorators_map: dict[str, tuple[nodes.NodeNG, interfaces.Confidence]] = {} + + for decorator in node.decorators.nodes: + if isinstance(decorator, nodes.Name) and decorator.name: + decorators_map[decorator.name] = (decorator, interfaces.HIGH) + elif utils.is_registered_in_singledispatch_function(node): + decorators_map["singledispatch"] = (decorator, interfaces.INFERENCE) + elif utils.is_registered_in_singledispatchmethod_function(node): + decorators_map["singledispatchmethod"] = ( + decorator, + 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 + ): + self.add_message( + "singledispatchmethod-function", + node=decorators_map["singledispatchmethod"][0], + confidence=decorators_map["singledispatchmethod"][1], + ) + + def _check_redundant_assert(self, node: nodes.Call, infer: InferenceResult) -> None: if ( isinstance(infer, astroid.BoundMethod) and node.args @@ -601,7 +682,7 @@ def _check_redundant_assert(self, node, infer): node=node, ) - def _check_datetime(self, node): + def _check_datetime(self, node: nodes.NodeNG) -> None: """Check that a datetime was inferred, if so, emit boolean-datetime warning.""" try: inferred = next(node.infer()) @@ -677,7 +758,7 @@ def _check_open_call( if isinstance(encoding_arg, nodes.Const) and encoding_arg.value is None: self.add_message("unspecified-encoding", node=node) - def _check_env_function(self, node, infer): + def _check_env_function(self, node: nodes.Call, infer: nodes.FunctionDef) -> None: env_name_kwarg = "key" env_value_kwarg = "default" if node.keywords: @@ -716,7 +797,14 @@ def _check_env_function(self, node, infer): allow_none=True, ) - def _check_invalid_envvar_value(self, node, infer, message, call_arg, allow_none): + def _check_invalid_envvar_value( + self, + node: nodes.Call, + infer: nodes.FunctionDef, + message: str, + call_arg: InferenceResult | None, + allow_none: bool, + ) -> None: if call_arg in (astroid.Uninferable, None): return @@ -730,18 +818,18 @@ def _check_invalid_envvar_value(self, node, infer, message, call_arg, allow_none if emit: self.add_message(message, node=node, args=(name, call_arg.pytype())) else: - self.add_message(message, node=node, args=(name, call_arg.pytype())) + self.add_message(message, node=node, args=(name, call_arg.pytype())) # type: ignore[union-attr] - def deprecated_methods(self): + def deprecated_methods(self) -> set[str]: return self._deprecated_methods - def deprecated_arguments(self, method: str): + def deprecated_arguments(self, method: str) -> tuple[tuple[int | None, str], ...]: return self._deprecated_arguments.get(method, ()) - def deprecated_classes(self, module: str): + def deprecated_classes(self, module: str) -> Iterable[str]: return self._deprecated_classes.get(module, ()) - def deprecated_decorators(self) -> Iterable: + def deprecated_decorators(self) -> Iterable[str]: return self._deprecated_decorators diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index e7f14e20b1..d5be5aa531 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -7,15 +7,16 @@ from __future__ import annotations import collections -import numbers import re +import sys import tokenize from collections import Counter from collections.abc import Iterable, Sequence from typing import TYPE_CHECKING import astroid -from astroid import nodes +from astroid import bases, nodes +from astroid.typing import SuccessfulInferenceResult from pylint.checkers import BaseChecker, BaseRawFileChecker, BaseTokenChecker, utils from pylint.checkers.utils import only_required_for_messages @@ -25,6 +26,12 @@ if TYPE_CHECKING: from pylint.lint import PyLinter +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + + _AST_NODE_STR_TYPES = ("__builtin__.unicode", "__builtin__.str", "builtins.str") # Prefixes for both strings and bytes literals per # https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals @@ -200,7 +207,7 @@ ) -def get_access_path(key, parts): +def get_access_path(key: str | Literal[0], parts: list[tuple[bool, str]]) -> str: """Given a list of format specifiers, returns the final access path (e.g. a.b.c[0][1]). """ @@ -213,7 +220,9 @@ def get_access_path(key, parts): return str(key) + "".join(path) -def arg_matches_format_type(arg_type, format_type): +def arg_matches_format_type( + arg_type: SuccessfulInferenceResult, format_type: str +) -> bool: if format_type in "sr": # All types can be printed with %s and %r return True @@ -238,7 +247,7 @@ class StringFormatChecker(BaseChecker): name = "string" msgs = MSGS - # pylint: disable=too-many-branches + # pylint: disable = too-many-branches, too-many-locals, too-many-statements @only_required_for_messages( "bad-format-character", "truncated-format-string", @@ -427,7 +436,9 @@ def visit_call(self, node: nodes.Call) -> None: elif func.name == "format": self._check_new_format(node, func) - def _detect_vacuous_formatting(self, node, positional_arguments): + def _detect_vacuous_formatting( + self, node: nodes.Call, positional_arguments: list[SuccessfulInferenceResult] + ) -> None: counter = collections.Counter( arg.name for arg in positional_arguments if isinstance(arg, nodes.Name) ) @@ -438,7 +449,7 @@ def _detect_vacuous_formatting(self, node, positional_arguments): "duplicate-string-formatting-argument", node=node, args=(name,) ) - def _check_new_format(self, node, func): + def _check_new_format(self, node: nodes.Call, func: bases.BoundMethod) -> None: """Check the new string formatting.""" # Skip format nodes which don't have an explicit string on the # left side of the format operation. @@ -483,7 +494,7 @@ def _check_new_format(self, node, func): check_args = False # Consider "{[0]} {[1]}" as num_args. - num_args += sum(1 for field in named_fields if field == "") + num_args += sum(1 for field in named_fields if not field) if named_fields: for field in named_fields: if field and field not in named_arguments: @@ -498,7 +509,7 @@ def _check_new_format(self, node, func): # num_args can be 0 if manual_pos is not. num_args = num_args or manual_pos if positional_arguments or num_args: - empty = any(field == "" for field in named_fields) + empty = not all(field for field in named_fields) if named_arguments or empty: # Verify the required number of positional arguments # only if the .format got at least one keyword argument. @@ -522,19 +533,26 @@ def _check_new_format(self, node, func): self._detect_vacuous_formatting(node, positional_arguments) self._check_new_format_specifiers(node, fields, named_arguments) - def _check_new_format_specifiers(self, node, fields, named): + # pylint: disable = too-many-statements + def _check_new_format_specifiers( + self, + node: nodes.Call, + fields: list[tuple[str, list[tuple[bool, str]]]], + named: dict[str, SuccessfulInferenceResult], + ) -> None: """Check attribute and index access in the format string ("{0.a}" and "{0[a]}"). """ + key: Literal[0] | str for key, specifiers in fields: # Obtain the argument. If it can't be obtained # or inferred, skip this check. - if key == "": + if not key: # {[0]} will have an unnamed argument, defaulting # to 0. It will not be present in `named`, so use the value # 0 for it. key = 0 - if isinstance(key, numbers.Number): + if isinstance(key, int): try: argname = utils.get_argument_from_call(node, key) except utils.NoSuchArgumentError: @@ -558,7 +576,7 @@ def _check_new_format_specifiers(self, node, fields, named): # because we can't infer its value properly. continue previous = argument - parsed = [] + parsed: list[tuple[bool, str]] = [] for is_attribute, specifier in specifiers: if previous is astroid.Uninferable: break @@ -692,9 +710,12 @@ class StringConstantChecker(BaseTokenChecker, BaseRawFileChecker): # Unicode strings. UNICODE_ESCAPE_CHARACTERS = "uUN" - def __init__(self, linter): + def __init__(self, linter: PyLinter) -> None: super().__init__(linter) - self.string_tokens = {} # token position -> (token value, next token) + self.string_tokens: dict[ + tuple[int, int], tuple[str, tokenize.TokenInfo | None] + ] = {} + """Token position -> (token value, next token).""" def process_module(self, node: nodes.Module) -> None: self._unicode_literals = "unicode_literals" in node.future_imports @@ -812,18 +833,18 @@ def check_for_concatenated_strings( confidence=HIGH, ) - def process_string_token(self, token, start_row, start_col): + def process_string_token(self, token: str, start_row: int, start_col: int) -> None: quote_char = None - index = None - for index, char in enumerate(token): + for _index, char in enumerate(token): if char in "'\"": quote_char = char break if quote_char is None: return - - prefix = token[:index].lower() # markers like u, b, r. - after_prefix = token[index:] + # pylint: disable=undefined-loop-variable + prefix = token[:_index].lower() # markers like u, b, r. + after_prefix = token[_index:] + # pylint: enable=undefined-loop-variable # Chop off quotes quote_length = ( 3 if after_prefix[:3] == after_prefix[-3:] == 3 * quote_char else 1 @@ -839,15 +860,15 @@ def process_string_token(self, token, start_row, start_col): ) def process_non_raw_string_token( - self, prefix, string_body, start_row, string_start_col - ): + self, prefix: str, string_body: str, start_row: int, string_start_col: int + ) -> None: """Check for bad escapes in a non-raw string. prefix: lowercase string of string prefix markers ('ur'). string_body: the un-parsed body of the string, not including the quote marks. - start_row: integer line number in the source. - string_start_col: integer col number of the string start in the source. + start_row: line number in the source. + string_start_col: col number of the string start in the source. """ # Walk through the string; if we see a backslash then escape the next # character, and skip over it. If we see a non-escaped character, @@ -907,7 +928,7 @@ def visit_const(self, node: nodes.Const) -> None: ): self._detect_u_string_prefix(node) - def _detect_u_string_prefix(self, node: nodes.Const): + def _detect_u_string_prefix(self, node: nodes.Const) -> None: """Check whether strings include a 'u' prefix like u'String'.""" if node.kind == "u": self.add_message( @@ -922,7 +943,7 @@ def register(linter: PyLinter) -> None: linter.register_checker(StringConstantChecker(linter)) -def str_eval(token): +def str_eval(token: str) -> str: """Mostly replicate `ast.literal_eval(token)` manually to avoid any performance hit. This supports f-strings, contrary to `ast.literal_eval`. diff --git a/pylint/checkers/threading_checker.py b/pylint/checkers/threading_checker.py index 308b6ca26d..df0dfe7cf2 100644 --- a/pylint/checkers/threading_checker.py +++ b/pylint/checkers/threading_checker.py @@ -44,7 +44,6 @@ class ThreadingChecker(BaseChecker): @only_required_for_messages("useless-with-lock") def visit_with(self, node: nodes.With) -> None: - context_managers = (c for c, _ in node.items if isinstance(c, nodes.Call)) for context_manager in context_managers: if isinstance(context_manager, nodes.Call): diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 0a1307ffed..bfd415923a 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -6,7 +6,6 @@ from __future__ import annotations -import fnmatch import heapq import itertools import operator @@ -14,14 +13,16 @@ import shlex import sys import types -from collections import deque -from collections.abc import Callable, Iterator, Sequence +from collections.abc import Callable, Iterable, Iterator, Sequence from functools import singledispatch from re import Pattern -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, TypeVar, Union +import astroid import astroid.exceptions -from astroid import bases, nodes +import astroid.helpers +from astroid import arguments, bases, nodes +from astroid.typing import InferenceResult, SuccessfulInferenceResult from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import ( @@ -29,11 +30,12 @@ decorated_with_property, has_known_bases, is_builtin_object, - is_classdef_type, is_comprehension, + is_hashable, is_inside_abstract_class, is_iterable, is_mapping, + is_module_ignored, is_node_in_type_annotation_context, is_overload_stub, is_postponed_evaluation_enabled, @@ -46,13 +48,15 @@ supports_membership_test, supports_setitem, ) -from pylint.interfaces import INFERENCE +from pylint.interfaces import HIGH, INFERENCE from pylint.typing import MessageDefinitionTuple if sys.version_info >= (3, 8): from functools import cached_property + from typing import Literal else: from astroid.decorators import cachedproperty as cached_property + from typing_extensions import Literal if TYPE_CHECKING: from pylint.lint import PyLinter @@ -65,6 +69,8 @@ nodes.ClassDef, ] +_T = TypeVar("_T") + STR_FORMAT = {"builtins.str.format"} ASYNCIO_COROUTINE = "asyncio.coroutines.coroutine" BUILTIN_TUPLE = "builtins.tuple" @@ -75,16 +81,23 @@ ) -def _unflatten(iterable): +class VERSION_COMPATIBLE_OVERLOAD: + pass + + +VERSION_COMPATIBLE_OVERLOAD_SENTINEL = VERSION_COMPATIBLE_OVERLOAD() + + +def _unflatten(iterable: Iterable[_T]) -> Iterator[_T]: for index, elem in enumerate(iterable): if isinstance(elem, Sequence) and not isinstance(elem, str): yield from _unflatten(elem) elif elem and not index: # We're interested only in the first element. - yield elem + yield elem # type: ignore[misc] -def _flatten_container(iterable): +def _flatten_container(iterable: Iterable[_T]) -> Iterator[_T]: # Flatten nested containers into a single iterable for item in iterable: if isinstance(item, (list, tuple, types.GeneratorType)): @@ -93,7 +106,12 @@ def _flatten_container(iterable): yield item -def _is_owner_ignored(owner, attrname, ignored_classes, ignored_modules): +def _is_owner_ignored( + owner: SuccessfulInferenceResult, + attrname: str | None, + ignored_classes: Iterable[str], + ignored_modules: Iterable[str], +) -> bool: """Check if the given owner should be ignored. This will verify if the owner's module is in *ignored_modules* @@ -105,32 +123,8 @@ def _is_owner_ignored(owner, attrname, ignored_classes, ignored_modules): matches any name from the *ignored_classes* or if its qualified name can be found in *ignored_classes*. """ - ignored_modules = set(ignored_modules) - module_name = owner.root().name - module_qname = owner.root().qname() - - for ignore in ignored_modules: - # Try to match the module name / fully qualified name directly - if module_qname in ignored_modules or module_name in ignored_modules: - return True - - # Try to see if the ignores pattern match against the module name. - if fnmatch.fnmatch(module_qname, ignore): - return True - - # Otherwise, we might have a root module name being ignored, - # and the qualified owner has more levels of depth. - parts = deque(module_name.split(".")) - current_module = "" - - while parts: - part = parts.popleft() - if not current_module: - current_module = part - else: - current_module += f".{part}" - if current_module in ignored_modules: - return True + if is_module_ignored(owner.root(), ignored_modules): + return True # Match against ignored classes. ignored_classes = set(ignored_classes) @@ -139,15 +133,15 @@ def _is_owner_ignored(owner, attrname, ignored_classes, ignored_modules): @singledispatch -def _node_names(node): +def _node_names(node: SuccessfulInferenceResult) -> Iterable[str]: if not hasattr(node, "locals"): return [] - return node.locals.keys() + return node.locals.keys() # type: ignore[no-any-return] @_node_names.register(nodes.ClassDef) @_node_names.register(astroid.Instance) -def _(node): +def _(node: nodes.ClassDef | bases.Instance) -> Iterable[str]: values = itertools.chain(node.instance_attrs.keys(), node.locals.keys()) try: @@ -159,7 +153,7 @@ def _(node): return itertools.chain(values, other_values) -def _string_distance(seq1, seq2): +def _string_distance(seq1: str, seq2: str) -> int: seq2_length = len(seq2) row = list(range(1, seq2_length + 1)) + [0] @@ -177,20 +171,25 @@ def _string_distance(seq1, seq2): return row[seq2_length - 1] -def _similar_names(owner, attrname, distance_threshold, max_choices): +def _similar_names( + owner: SuccessfulInferenceResult, + attrname: str | None, + distance_threshold: int, + max_choices: int, +) -> list[str]: """Given an owner and a name, try to find similar names. The similar names are searched given a distance metric and only a given number of choices will be returned. """ - possible_names = [] + possible_names: list[tuple[str, int]] = [] names = _node_names(owner) for name in names: if name == attrname: continue - distance = _string_distance(attrname, name) + distance = _string_distance(attrname or "", name) if distance <= distance_threshold: possible_names.append((name, distance)) @@ -205,7 +204,12 @@ def _similar_names(owner, attrname, distance_threshold, max_choices): return sorted(picked) -def _missing_member_hint(owner, attrname, distance_threshold, max_choices): +def _missing_member_hint( + owner: SuccessfulInferenceResult, + attrname: str | None, + distance_threshold: int, + max_choices: int, +) -> str: names = _similar_names(owner, attrname, distance_threshold, max_choices) if not names: # No similar name. @@ -353,12 +357,6 @@ def _missing_member_hint(owner, attrname, distance_threshold, max_choices): "as a metaclass, something which might be invalid for using as " "a metaclass.", ), - "E1140": ( - "Dict key is unhashable", - "unhashable-dict-key", - "Emitted when a dict key is not hashable " - "(i.e. doesn't define __hash__ method).", - ), "E1141": ( "Unpacking a dictionary in iteration without calling .items()", "dict-iter-missing-items", @@ -369,6 +367,19 @@ def _missing_member_hint(owner, attrname, distance_threshold, max_choices): "await-outside-async", "Emitted when await is used outside an async function.", ), + "E1143": ( + "'%s' is unhashable and can't be used as a %s in a %s", + "unhashable-member", + "Emitted when a dict key or set member is not hashable " + "(i.e. doesn't define __hash__ method).", + {"old_names": [("E1140", "unhashable-dict-key")]}, + ), + "E1144": ( + "Slice step cannot be 0", + "invalid-slice-step", + "Used when a slice step is 0 and the object doesn't implement " + "a custom __getitem__ method.", + ), "W1113": ( "Keyword argument before variable positional arguments list " "in the definition of %s function", @@ -410,13 +421,13 @@ def _missing_member_hint(owner, attrname, distance_threshold, max_choices): def _emit_no_member( - node, - owner, - owner_name, + node: nodes.Attribute | nodes.AssignAttr | nodes.DelAttr, + owner: InferenceResult, + owner_name: str | None, mixin_class_rgx: Pattern[str], - ignored_mixins=True, - ignored_none=True, -): + ignored_mixins: bool = True, + ignored_none: bool = True, +) -> bool: """Try to see if no-member should be emitted for the given owner. The following cases are ignored: @@ -429,7 +440,7 @@ def _emit_no_member( AttributeError, Exception or bare except. * The node is guarded behind and `IF` or `IFExp` node """ - # pylint: disable=too-many-return-statements + # pylint: disable = too-many-return-statements, too-many-branches if node_ignores_exception(node, AttributeError): return False if ignored_none and isinstance(owner, nodes.Const) and owner.value is None: @@ -488,16 +499,7 @@ def _emit_no_member( return False except astroid.NotFoundError: return True - if ( - owner.parent - and isinstance(owner.parent, nodes.ClassDef) - and owner.parent.name == "EnumMeta" - and owner_name == "__members__" - and node.attrname in {"items", "values", "keys"} - ): - # Avoid false positive on Enum.__members__.{items(), values, keys} - # See https://github.com/PyCQA/pylint/issues/4123 - return False + # Don't emit no-member if guarded behind `IF` or `IFExp` # * Walk up recursively until if statement is found. # * Check if condition can be inferred as `Const`, @@ -653,7 +655,11 @@ def _determine_callable( raise ValueError -def _has_parent_of_type(node, node_type, statement): +def _has_parent_of_type( + node: nodes.Call, + node_type: nodes.Keyword | nodes.Starred, + statement: nodes.Statement, +) -> bool: """Check if the given node has a parent of the given type.""" parent = node.parent while not isinstance(parent, node_type) and statement.parent_of(parent): @@ -661,11 +667,15 @@ def _has_parent_of_type(node, node_type, statement): return isinstance(parent, node_type) -def _no_context_variadic_keywords(node, scope): +def _no_context_variadic_keywords(node: nodes.Call, scope: nodes.Lambda) -> bool: statement = node.statement(future=True) - variadics = () + variadics = [] - if isinstance(scope, nodes.Lambda) and not isinstance(scope, nodes.FunctionDef): + if ( + isinstance(scope, nodes.Lambda) + and not isinstance(scope, nodes.FunctionDef) + or isinstance(statement, nodes.With) + ): variadics = list(node.keywords or []) + node.kwargs elif isinstance(statement, (nodes.Return, nodes.Expr, nodes.Assign)) and isinstance( statement.value, nodes.Call @@ -676,12 +686,17 @@ def _no_context_variadic_keywords(node, scope): return _no_context_variadic(node, scope.args.kwarg, nodes.Keyword, variadics) -def _no_context_variadic_positional(node, scope): +def _no_context_variadic_positional(node: nodes.Call, scope: nodes.Lambda) -> bool: variadics = node.starargs + node.kwargs return _no_context_variadic(node, scope.args.vararg, nodes.Starred, variadics) -def _no_context_variadic(node, variadic_name, variadic_type, variadics): +def _no_context_variadic( + node: nodes.Call, + variadic_name: str | None, + variadic_type: nodes.Keyword | nodes.Starred, + variadics: list[nodes.Keyword | nodes.Starred], +) -> bool: """Verify if the given call node has variadic nodes without context. This is a workaround for handling cases of nested call functions @@ -727,19 +742,14 @@ def _no_context_variadic(node, variadic_name, variadic_type, variadics): return False -def _is_invalid_metaclass(metaclass): - try: - mro = metaclass.mro() - except NotImplementedError: - # Cannot have a metaclass which is not a newstyle class. - return True - else: - if not any(is_builtin_object(cls) and cls.name == "type" for cls in mro): - return True - return False +def _is_invalid_metaclass(metaclass: nodes.ClassDef) -> bool: + mro = metaclass.mro() + return not any(is_builtin_object(cls) and cls.name == "type" for cls in mro) -def _infer_from_metaclass_constructor(cls, func: nodes.FunctionDef): +def _infer_from_metaclass_constructor( + cls: nodes.ClassDef, func: nodes.FunctionDef +) -> InferenceResult | None: """Try to infer what the given *func* constructor is building. :param astroid.FunctionDef func: @@ -776,14 +786,15 @@ def _infer_from_metaclass_constructor(cls, func: nodes.FunctionDef): return inferred or None -def _is_c_extension(module_node): +def _is_c_extension(module_node: InferenceResult) -> bool: return ( - not astroid.modutils.is_standard_module(module_node.name) + isinstance(module_node, nodes.Module) + and not astroid.modutils.is_standard_module(module_node.name) and not module_node.fully_defined() ) -def _is_invalid_isinstance_type(arg): +def _is_invalid_isinstance_type(arg: nodes.NodeNG) -> bool: # Return True if we are sure that arg is not a type inferred = utils.safe_infer(arg) if not inferred: @@ -958,11 +969,11 @@ def open(self) -> None: self._mixin_class_rgx = self.linter.config.mixin_class_rgx @cached_property - def _suggestion_mode(self): - return self.linter.config.suggestion_mode + def _suggestion_mode(self) -> bool: + return self.linter.config.suggestion_mode # type: ignore[no-any-return] @cached_property - def _compiled_generated_members(self) -> tuple[Pattern, ...]: + def _compiled_generated_members(self) -> tuple[Pattern[str], ...]: # do this lazily since config not fully initialized in __init__ # generated_members may contain regular expressions # (surrounded by quote `"` and followed by a comma `,`) @@ -986,15 +997,15 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: @only_required_for_messages("invalid-metaclass") def visit_classdef(self, node: nodes.ClassDef) -> None: - def _metaclass_name(metaclass): + def _metaclass_name(metaclass: InferenceResult) -> str | None: # pylint: disable=unidiomatic-typecheck if isinstance(metaclass, (nodes.ClassDef, nodes.FunctionDef)): - return metaclass.name + return metaclass.name # type: ignore[no-any-return] if type(metaclass) is bases.Instance: # Really do mean type, not isinstance, since subclasses of bases.Instance # like Const or Dict should use metaclass.as_string below. return str(metaclass) - return metaclass.as_string() + return metaclass.as_string() # type: ignore[no-any-return] metaclass = node.declared_metaclass() if not metaclass: @@ -1024,8 +1035,11 @@ def visit_assignattr(self, node: nodes.AssignAttr) -> None: def visit_delattr(self, node: nodes.DelAttr) -> None: self.visit_attribute(node) + # pylint: disable = too-many-branches @only_required_for_messages("no-member", "c-extension-no-member") - def visit_attribute(self, node: nodes.Attribute) -> None: + def visit_attribute( + self, node: nodes.Attribute | nodes.AssignAttr | nodes.DelAttr + ) -> None: """Check that the accessed attribute exists. to avoid too much false positives for now, we'll consider the code as @@ -1051,9 +1065,9 @@ def visit_attribute(self, node: nodes.Attribute) -> None: return # list of (node, nodename) which are missing the attribute - missingattr = set() + missingattr: set[tuple[SuccessfulInferenceResult, str | None]] = set() - non_opaque_inference_results = [ + non_opaque_inference_results: list[SuccessfulInferenceResult] = [ owner for owner in inferred if owner is not astroid.Uninferable and not isinstance(owner, nodes.Unknown) @@ -1116,6 +1130,9 @@ def visit_attribute(self, node: nodes.Attribute) -> None: try: if isinstance( attr_node.statement(future=True), nodes.AugAssign + ) or ( + isinstance(attr_parent, nodes.Assign) + and utils.is_augmented_assign(attr_parent)[0] ): continue except astroid.exceptions.StatementMissing: @@ -1150,7 +1167,11 @@ def visit_attribute(self, node: nodes.Attribute) -> None: confidence=INFERENCE, ) - def _get_nomember_msgid_hint(self, node, owner): + def _get_nomember_msgid_hint( + self, + node: nodes.Attribute | nodes.AssignAttr | nodes.DelAttr, + owner: SuccessfulInferenceResult, + ) -> tuple[Literal["c-extension-no-member", "no-member"], str]: suggestions_are_possible = self._suggestion_mode and isinstance( owner, nodes.Module ) @@ -1168,7 +1189,7 @@ def _get_nomember_msgid_hint(self, node, owner): ) else: hint = "" - return msg, hint + return msg, hint # type: ignore[return-value] @only_required_for_messages( "assignment-from-no-return", @@ -1251,7 +1272,7 @@ def _is_list_sort_method(node: nodes.Call) -> bool: and isinstance(utils.safe_infer(node.func.expr), nodes.List) ) - def _check_dundername_is_string(self, node) -> None: + 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__ @@ -1272,7 +1293,7 @@ def _check_dundername_is_string(self, node) -> None: # Add the message self.add_message("non-str-assignment-to-dunder-name", node=node) - def _check_uninferable_call(self, node): + def _check_uninferable_call(self, node: nodes.Call) -> None: """Check that the given uninferable Call node does not call an actual function. """ @@ -1324,7 +1345,13 @@ def _check_uninferable_call(self, node): self.add_message("not-callable", node=node, args=node.func.as_string()) - def _check_argument_order(self, node, call_site, called, called_param_names): + def _check_argument_order( + self, + node: nodes.Call, + call_site: arguments.CallSite, + called: CallableObjects, + called_param_names: list[str | None], + ) -> None: """Match the supplied argument names against the function parameters. Warn if some argument names are not in the same order as they are in @@ -1364,7 +1391,7 @@ def _check_argument_order(self, node, call_site, called, called_param_names): if calling_parg_names != called_param_names[: len(calling_parg_names)]: self.add_message("arguments-out-of-order", node=node, args=()) - def _check_isinstance_args(self, node): + def _check_isinstance_args(self, node: nodes.Call) -> None: if len(node.args) != 2: # isinstance called with wrong number of args return @@ -1373,7 +1400,7 @@ def _check_isinstance_args(self, node): if _is_invalid_isinstance_type(second_arg): self.add_message("isinstance-second-argument-not-valid-type", node=node) - # pylint: disable=too-many-branches,too-many-locals + # pylint: disable = too-many-branches, too-many-locals, too-many-statements def visit_call(self, node: nodes.Call) -> None: """Check that called functions/methods are inferred to callable objects, and that passed arguments match the parameters in the inferred function. @@ -1444,10 +1471,23 @@ def visit_call(self, node: nodes.Call) -> None: keyword_args += list(already_filled_keywords) num_positional_args += implicit_args + already_filled_positionals + # Decrement `num_positional_args` by 1 when a function call is assigned to a class attribute + # inside the class where the function is defined. + # This avoids emitting `too-many-function-args` since `num_positional_args` + # includes an implicit `self` argument which is not present in `called.args`. + if ( + isinstance(node.frame(), nodes.ClassDef) + and isinstance(node.parent, (nodes.Assign, nodes.AnnAssign)) + and isinstance(called, nodes.FunctionDef) + and called in node.frame().body + and num_positional_args > 0 + ): + num_positional_args -= 1 + # Analyze the list of formal parameters. args = list(itertools.chain(called.args.posonlyargs or (), called.args.args)) num_mandatory_parameters = len(args) - len(called.args.defaults) - parameters: list[list[Any]] = [] + parameters: list[tuple[tuple[str | None, nodes.NodeNG | None], bool]] = [] parameter_name_to_index = {} for i, arg in enumerate(args): if isinstance(arg, nodes.Tuple): @@ -1464,7 +1504,7 @@ def visit_call(self, node: nodes.Call) -> None: defval = called.args.defaults[i - num_mandatory_parameters] else: defval = None - parameters.append([(name, defval), False]) + parameters.append(((name, defval), False)) kwparams = {} for i, arg in enumerate(called.args.kwonlyargs): @@ -1482,7 +1522,7 @@ def visit_call(self, node: nodes.Call) -> None: # 1. Match the positional arguments. for i in range(num_positional_args): if i < len(parameters): - parameters[i][1] = True + parameters[i] = (parameters[i][0], True) elif called.args.vararg is not None: # The remaining positional arguments get assigned to the *args # parameter. @@ -1513,7 +1553,7 @@ def visit_call(self, node: nodes.Call) -> None: args=(keyword, callable_name), ) else: - parameters[i][1] = True + parameters[i] = (parameters[i][0], True) elif keyword in kwparams: if kwparams[keyword][1]: # Duplicate definition of function parameter. @@ -1539,11 +1579,11 @@ def visit_call(self, node: nodes.Call) -> None: # 3. Match the **kwargs, if any. if node.kwargs: - for i, [(name, defval), assigned] in enumerate(parameters): + for i, [(name, _defval), _assigned] in enumerate(parameters): # Assume that *kwargs provides values for all remaining # unassigned named parameters. if name is not None: - parameters[i][1] = True + parameters[i] = (parameters[i][0], True) else: # **kwargs can't assign to tuples. pass @@ -1568,7 +1608,12 @@ def visit_call(self, node: nodes.Call) -> None: and not has_no_context_keywords_variadic and not overload_function ): - self.add_message("missing-kwoa", node=node, args=(name, callable_name)) + self.add_message( + "missing-kwoa", + node=node, + args=(name, callable_name), + confidence=INFERENCE, + ) @staticmethod def _keyword_argument_is_in_all_decorator_returns( @@ -1610,7 +1655,7 @@ def _keyword_argument_is_in_all_decorator_returns( return True - def _check_invalid_sequence_index(self, subscript: nodes.Subscript): + def _check_invalid_sequence_index(self, subscript: nodes.Subscript) -> None: # Look for index operations where the parent is a sequence type. # If the types can be determined, only allow indices to be int, # slice or instances with __index__. @@ -1652,13 +1697,7 @@ def _check_invalid_sequence_index(self, subscript: nodes.Subscript): ): return None - # For ExtSlice objects coming from visit_extslice, no further - # inference is necessary, since if we got this far the ExtSlice - # is an error. - if isinstance(subscript.value, nodes.ExtSlice): - index_type = subscript.value - else: - index_type = safe_infer(subscript.slice) + index_type = safe_infer(subscript.slice) if index_type is None or index_type is astroid.Uninferable: return None # Constants must be of type int @@ -1715,14 +1754,6 @@ def _check_not_callable( self.add_message("not-callable", node=node, args=node.func.as_string()) - @only_required_for_messages("invalid-sequence-index") - def visit_extslice(self, node: nodes.ExtSlice) -> None: - if not node.parent or not hasattr(node.parent, "value"): - return None - # Check extended slice objects as if they were used as a sequence - # index to check if the object being sliced can support them - return self._check_invalid_sequence_index(node.parent) - def _check_invalid_slice_index(self, node: nodes.Slice) -> None: # Check the type of each part of the slice invalid_slices_nodes: list[nodes.NodeNG] = [] @@ -1751,14 +1782,16 @@ def _check_invalid_slice_index(self, node: nodes.Slice) -> None: pass invalid_slices_nodes.append(index) - if not invalid_slices_nodes: + invalid_slice_step = ( + node.step and isinstance(node.step, nodes.Const) and node.step.value == 0 + ) + + if not (invalid_slices_nodes or invalid_slice_step): return # Anything else is an error, unless the object that is indexed # is a custom object, which knows how to handle this kind of slices parent = node.parent - if isinstance(parent, nodes.ExtSlice): - parent = parent.parent if isinstance(parent, nodes.Subscript): inferred = safe_infer(parent.value) if inferred is None or inferred is astroid.Uninferable: @@ -1771,11 +1804,19 @@ def _check_invalid_slice_index(self, node: nodes.Slice) -> None: astroid.objects.FrozenSet, nodes.Set, ) - if not isinstance(inferred, known_objects): + if not ( + isinstance(inferred, known_objects) + or isinstance(inferred, nodes.Const) + and inferred.pytype() in {"builtins.str", "builtins.bytes"} + or isinstance(inferred, astroid.bases.Instance) + and inferred.pytype() == "builtins.range" + ): # Might be an instance that knows how to handle this slice object return for snode in invalid_slices_nodes: self.add_message("invalid-slice-index", node=snode) + if invalid_slice_step: + self.add_message("invalid-slice-step", node=node.step, confidence=HIGH) @only_required_for_messages("not-context-manager") def visit_with(self, node: nodes.With) -> None: @@ -1901,14 +1942,59 @@ def _detect_unsupported_alternative_union_syntax(self, node: nodes.BinOp) -> Non if not allowed_nested_syntax: self._check_unsupported_alternative_union_syntax(node) + def _includes_version_compatible_overload(self, attrs: list[nodes.NodeNG]) -> bool: + """Check if a set of overloads of an operator includes one that + can be relied upon for our configured Python version. + + If we are running under a Python 3.10+ runtime but configured for + pre-3.10 compatibility then Astroid will have inferred the + existence of __or__ / __ror__ on builtins.type, but these aren't + available in the configured version of Python. + """ + is_py310_builtin = all( + isinstance(attr, (nodes.FunctionDef, astroid.BoundMethod)) + and attr.parent.qname() == "builtins.type" + for attr in attrs + ) + return not is_py310_builtin or self._py310_plus + + def _recursive_search_for_classdef_type( + self, node: nodes.ClassDef, operation: Literal["__or__", "__ror__"] + ) -> bool | VERSION_COMPATIBLE_OVERLOAD: + if not isinstance(node, nodes.ClassDef): + return False + try: + attrs = node.getattr(operation) + except astroid.NotFoundError: + return True + if self._includes_version_compatible_overload(attrs): + return VERSION_COMPATIBLE_OVERLOAD_SENTINEL + return True + def _check_unsupported_alternative_union_syntax(self, node: nodes.BinOp) -> None: - """Check if left or right node is of type `type`.""" + """Check if left or right node is of type `type`. + + If either is, and doesn't support an or operator via a metaclass, + infer that this is a mistaken attempt to use alternative union + syntax when not supported. + """ msg = "unsupported operand type(s) for |" - for n in (node.left, node.right): - n = astroid.helpers.object_type(n) - if isinstance(n, nodes.ClassDef) and is_classdef_type(n): - self.add_message("unsupported-binary-operation", args=msg, node=node) - break + left_obj = astroid.helpers.object_type(node.left) + right_obj = astroid.helpers.object_type(node.right) + left_is_type = self._recursive_search_for_classdef_type(left_obj, "__or__") + if left_is_type is VERSION_COMPATIBLE_OVERLOAD_SENTINEL: + return + right_is_type = self._recursive_search_for_classdef_type(right_obj, "__ror__") + if right_is_type is VERSION_COMPATIBLE_OVERLOAD_SENTINEL: + return + + if left_is_type or right_is_type: + self.add_message( + "unsupported-binary-operation", + args=msg, + node=node, + confidence=INFERENCE, + ) # TODO: This check was disabled (by adding the leading underscore) # due to false positives several years ago - can we re-enable it? @@ -1926,7 +2012,7 @@ def _visit_augassign(self, node: nodes.AugAssign) -> None: """Detect TypeErrors for augmented binary arithmetic operands.""" self._check_binop_errors(node) - def _check_binop_errors(self, node): + def _check_binop_errors(self, node: nodes.BinOp | nodes.AugAssign) -> None: for error in node.type_errors(): # Let the error customize its output. if any( @@ -1936,7 +2022,7 @@ def _check_binop_errors(self, node): continue self.add_message("unsupported-binary-operation", args=str(error), node=node) - def _check_membership_test(self, node): + def _check_membership_test(self, node: nodes.NodeNG) -> None: if is_inside_abstract_class(node): return if is_comprehension(node): @@ -1958,13 +2044,36 @@ def visit_compare(self, node: nodes.Compare) -> None: if op in {"in", "not in"}: self._check_membership_test(right) + @only_required_for_messages("unhashable-member") + def visit_dict(self, node: nodes.Dict) -> None: + for k, _ in node.items: + if not is_hashable(k): + self.add_message( + "unhashable-member", + node=k, + args=(k.as_string(), "key", "dict"), + confidence=INFERENCE, + ) + + @only_required_for_messages("unhashable-member") + def visit_set(self, node: nodes.Set) -> None: + for element in node.elts: + if not is_hashable(element): + self.add_message( + "unhashable-member", + node=element, + args=(element.as_string(), "member", "set"), + confidence=INFERENCE, + ) + @only_required_for_messages( "unsubscriptable-object", "unsupported-assignment-operation", "unsupported-delete-operation", - "unhashable-dict-key", + "unhashable-member", "invalid-sequence-index", "invalid-slice-index", + "invalid-slice-step", ) def visit_subscript(self, node: nodes.Subscript) -> None: self._check_invalid_sequence_index(node) @@ -1975,15 +2084,13 @@ def visit_subscript(self, node: nodes.Subscript) -> None: if isinstance(node.value, nodes.Dict): # Assert dict key is hashable - inferred = safe_infer(node.slice) - if inferred and inferred != astroid.Uninferable: - try: - hash_fn = next(inferred.igetattr("__hash__")) - except astroid.InferenceError: - pass - else: - if getattr(hash_fn, "value", True) is None: - self.add_message("unhashable-dict-key", node=node.value) + if not is_hashable(node.slice): + self.add_message( + "unhashable-member", + node=node.value, + args=(node.slice.as_string(), "key", "dict"), + confidence=INFERENCE, + ) if node.ctx == astroid.Load: supported_protocol = supports_getitem @@ -2094,7 +2201,7 @@ class IterableChecker(BaseChecker): } @staticmethod - def _is_asyncio_coroutine(node): + def _is_asyncio_coroutine(node: nodes.NodeNG) -> bool: if not isinstance(node, nodes.Call): return False @@ -2112,7 +2219,7 @@ def _is_asyncio_coroutine(node): return True return False - def _check_iterable(self, node, check_async=False): + def _check_iterable(self, node: nodes.NodeNG, check_async: bool = False) -> None: if is_inside_abstract_class(node): return inferred = safe_infer(node) @@ -2121,7 +2228,7 @@ def _check_iterable(self, node, check_async=False): if not is_iterable(inferred, check_async=check_async): self.add_message("not-an-iterable", args=node.as_string(), node=node) - def _check_mapping(self, node): + def _check_mapping(self, node: nodes.NodeNG) -> None: if is_inside_abstract_class(node): return if isinstance(node, nodes.DictComp): diff --git a/pylint/checkers/unicode.py b/pylint/checkers/unicode.py index b5123ef17c..6d7b253954 100644 --- a/pylint/checkers/unicode.py +++ b/pylint/checkers/unicode.py @@ -218,7 +218,7 @@ def _normalize_codec_name(codec: str) -> str: def _remove_bom(encoded: bytes, encoding: str) -> bytes: """Remove the bom if given from a line.""" - if not encoding.startswith("utf"): + if encoding not in UNICODE_BOMS: return encoded bom = UNICODE_BOMS[encoding] if encoded.startswith(bom): @@ -524,7 +524,7 @@ def process_module(self, node: nodes.Module) -> None: stream.seek(0) # Check for invalid content (controls/chars) - for (lineno, line) in enumerate( + for lineno, line in enumerate( _fix_utf16_32_line_stream(stream, codec), start=1 ): if lineno == 1: diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index ce46dfcbbf..357cc702ef 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -7,12 +7,14 @@ from __future__ import annotations import builtins +import fnmatch import itertools import numbers import re import string import warnings -from collections.abc import Iterable +from collections import deque +from collections.abc import Iterable, Iterator from functools import lru_cache, partial from re import Match from typing import TYPE_CHECKING, Callable, TypeVar @@ -22,6 +24,8 @@ from astroid import TooManyLevelsError, nodes from astroid.context import InferenceContext from astroid.exceptions import AstroidError +from astroid.nodes._base_nodes import ImportNode +from astroid.typing import InferenceResult, SuccessfulInferenceResult if TYPE_CHECKING: from pylint.checkers import BaseChecker @@ -47,6 +51,7 @@ TYPING_PROTOCOLS = frozenset( {"typing.Protocol", "typing_extensions.Protocol", ".Protocol"} ) +COMMUTATIVE_OPERATORS = frozenset({"*", "+", "^", "&", "|"}) ITER_METHOD = "__iter__" AITER_METHOD = "__aiter__" NEXT_METHOD = "__next__" @@ -227,6 +232,12 @@ ) ) +SINGLETON_VALUES = {True, False, None} + +TERMINATING_FUNCS_QNAMES = frozenset( + {"_sitebuiltins.Quitter", "sys.exit", "posix._exit", "nt._exit"} +) + class NoSuchArgumentError(Exception): pass @@ -242,6 +253,7 @@ def is_inside_lambda(node: nodes.NodeNG) -> bool: "utils.is_inside_lambda will be removed in favour of calling " "utils.get_node_first_ancestor_of_type(x, nodes.Lambda) in pylint 3.0", DeprecationWarning, + stacklevel=2, ) return any(isinstance(parent, nodes.Lambda) for parent in node.node_ancestors()) @@ -275,12 +287,12 @@ def is_error(node: nodes.FunctionDef) -> bool: def is_builtin_object(node: nodes.NodeNG) -> bool: """Returns True if the given node is an object from the __builtin__ module.""" - return node and node.root().name == "builtins" + return node and node.root().name == "builtins" # type: ignore[no-any-return] def is_builtin(name: str) -> bool: """Return true if could be considered as a builtin defined by python.""" - return name in builtins or name in SPECIAL_BUILTINS # type: ignore[attr-defined] + return name in builtins or name in SPECIAL_BUILTINS # type: ignore[operator] def is_defined_in_scope( @@ -288,26 +300,33 @@ def is_defined_in_scope( varname: str, scope: nodes.NodeNG, ) -> bool: + return defnode_in_scope(var_node, varname, scope) is not None + + +# pylint: disable = too-many-branches +def defnode_in_scope( + var_node: nodes.NodeNG, + varname: str, + scope: nodes.NodeNG, +) -> nodes.NodeNG | None: if isinstance(scope, nodes.If): for node in scope.body: - if ( - isinstance(node, nodes.Assign) - and any( - isinstance(target, nodes.AssignName) and target.name == varname - for target in node.targets - ) - ) or (isinstance(node, nodes.Nonlocal) and varname in node.names): - return True + if isinstance(node, nodes.Nonlocal) and varname in node.names: + return node + if isinstance(node, nodes.Assign): + for target in node.targets: + if isinstance(target, nodes.AssignName) and target.name == varname: + return target elif isinstance(scope, (COMP_NODE_TYPES, nodes.For)): for ass_node in scope.nodes_of_class(nodes.AssignName): if ass_node.name == varname: - return True + return ass_node elif isinstance(scope, nodes.With): for expr, ids in scope.items: if expr.parent_of(var_node): break if ids and isinstance(ids, nodes.AssignName) and ids.name == varname: - return True + return ids elif isinstance(scope, (nodes.Lambda, nodes.FunctionDef)): if scope.args.is_argument(varname): # If the name is found inside a default value @@ -317,32 +336,65 @@ def is_defined_in_scope( try: scope.args.default_value(varname) scope = scope.parent - is_defined_in_scope(var_node, varname, scope) + defnode = defnode_in_scope(var_node, varname, scope) except astroid.NoDefault: pass - return True + else: + return defnode + return scope if getattr(scope, "name", None) == varname: - return True + return scope elif isinstance(scope, nodes.ExceptHandler): if isinstance(scope.name, nodes.AssignName): ass_node = scope.name if ass_node.name == varname: - return True - return False + return ass_node + return None def is_defined_before(var_node: nodes.Name) -> bool: """Check if the given variable node is defined before. Verify that the variable node is defined by a parent node + (e.g. if or with) earlier than `var_node`, or is defined by a (list, set, dict, or generator comprehension, lambda) or in a previous sibling node on the same line (statement_defining ; statement_using). """ varname = var_node.name for parent in var_node.node_ancestors(): - if is_defined_in_scope(var_node, varname, parent): + defnode = defnode_in_scope(var_node, varname, parent) + if defnode is None: + continue + defnode_scope = defnode.scope() + if isinstance(defnode_scope, COMP_NODE_TYPES + (nodes.Lambda,)): + # Avoid the case where var_node_scope is a nested function + # FunctionDef is a Lambda until https://github.com/PyCQA/astroid/issues/291 + if isinstance(defnode_scope, nodes.FunctionDef): + var_node_scope = var_node.scope() + if var_node_scope is not defnode_scope and isinstance( + var_node_scope, nodes.FunctionDef + ): + return False return True + if defnode.lineno < var_node.lineno: + return True + # `defnode` and `var_node` on the same line + for defnode_anc in defnode.node_ancestors(): + if defnode_anc.lineno != var_node.lineno: + continue + if isinstance( + defnode_anc, + ( + nodes.For, + nodes.While, + nodes.With, + nodes.TryExcept, + nodes.TryFinally, + nodes.ExceptHandler, + ), + ): + return True # possibly multiple statements on the same line using semicolon separator stmt = var_node.statement(future=True) _node = stmt.previous_sibling() @@ -445,7 +497,7 @@ def only_required_for_messages( def store_messages( func: AstCallbackMethod[_CheckerT, _NodeT] ) -> AstCallbackMethod[_CheckerT, _NodeT]: - setattr(func, "checks_msgs", messages) + func.checks_msgs = messages # type: ignore[attr-defined] return func return store_messages @@ -464,6 +516,7 @@ def check_messages( "utils.check_messages will be removed in favour of calling " "utils.only_required_for_messages in pylint 3.0", DeprecationWarning, + stacklevel=2, ) return only_required_for_messages(*messages) @@ -563,7 +616,7 @@ def split_format_field_names( format_string: str, ) -> tuple[str, Iterable[tuple[bool, str]]]: try: - return _string.formatter_field_name_split(format_string) + return _string.formatter_field_name_split(format_string) # type: ignore[no-any-return] except ValueError as e: raise IncompleteFormatString() from e @@ -575,6 +628,7 @@ def collect_string_fields(format_string: str) -> Iterable[str | None]: It handles nested fields as well. """ formatter = string.Formatter() + # pylint: disable = too-many-try-statements try: parseiterator = formatter.parse(format_string) for result in parseiterator: @@ -677,7 +731,7 @@ def is_attr_private(attrname: str) -> Match[str] | None: """Check that attribute name is private (at least two leading underscores, at most one trailing underscore). """ - regex = re.compile("^_{2,}.*[^_]+_?$") + regex = re.compile("^_{2,10}.*[^_]+_?$") return regex.match(attrname) @@ -747,7 +801,7 @@ def stringify_error(error: str | type[Exception]) -> str: expected_errors = {stringify_error(error) for error in error_type} if not handler.type: return False - return handler.catch(expected_errors) + return handler.catch(expected_errors) # type: ignore[no-any-return] def decorated_with_property(node: nodes.FunctionDef) -> bool: @@ -763,7 +817,7 @@ def decorated_with_property(node: nodes.FunctionDef) -> bool: return False -def _is_property_kind(node, *kinds: str) -> bool: +def _is_property_kind(node: nodes.NodeNG, *kinds: str) -> bool: if not isinstance(node, (astroid.UnboundMethod, nodes.FunctionDef)): return False if node.decorators: @@ -773,17 +827,17 @@ def _is_property_kind(node, *kinds: str) -> bool: return False -def is_property_setter(node) -> bool: +def is_property_setter(node: nodes.NodeNG) -> bool: """Check if the given node is a property setter.""" return _is_property_kind(node, "setter") -def is_property_deleter(node) -> bool: +def is_property_deleter(node: nodes.NodeNG) -> bool: """Check if the given node is a property deleter.""" return _is_property_kind(node, "deleter") -def is_property_setter_or_deleter(node) -> bool: +def is_property_setter_or_deleter(node: nodes.NodeNG) -> bool: """Check if the given node is either a property setter or a deleter.""" return _is_property_kind(node, "setter", "deleter") @@ -893,25 +947,20 @@ def uninferable_final_decorators( @lru_cache(maxsize=1024) def unimplemented_abstract_methods( node: nodes.ClassDef, is_abstract_cb: nodes.FunctionDef = None -) -> dict[str, nodes.NodeNG]: +) -> dict[str, nodes.FunctionDef]: """Get the unimplemented abstract methods for the given *node*. A method can be considered abstract if the callback *is_abstract_cb* returns a ``True`` value. The check defaults to verifying that a method is decorated with abstract methods. - The function will work only for new-style classes. For old-style - classes, it will simply return an empty dictionary. - For the rest of them, it will return a dictionary of abstract method + It will return a dictionary of abstract method names and their inferred objects. """ if is_abstract_cb is None: is_abstract_cb = partial(decorated_with, qnames=ABC_METHODS) - visited: dict[str, nodes.NodeNG] = {} + visited: dict[str, nodes.FunctionDef] = {} try: mro = reversed(node.mro()) - except NotImplementedError: - # Old style class, it will not have a mro. - return {} except astroid.ResolveError: # Probably inconsistent hierarchy, don't try to figure this out here. return {} @@ -1012,7 +1061,7 @@ def _except_handlers_ignores_exceptions( def get_exception_handlers( - node: nodes.NodeNG, exception: type[Exception] = Exception + node: nodes.NodeNG, exception: type[Exception] | str = Exception ) -> list[nodes.ExceptHandler] | None: """Return the collections of handlers handling the exception in arguments. @@ -1031,6 +1080,59 @@ def get_exception_handlers( return [] +def get_contextlib_with_statements(node: nodes.NodeNG) -> Iterator[nodes.With]: + """Get all contextlib.with statements in the ancestors of the given node.""" + for with_node in node.node_ancestors(): + if isinstance(with_node, nodes.With): + yield with_node + + +def _suppresses_exception( + call: nodes.Call, exception: type[Exception] | str = Exception +) -> bool: + """Check if the given node suppresses the given exception.""" + if not isinstance(exception, str): + exception = exception.__name__ + for arg in call.args: + inferred = safe_infer(arg) + if isinstance(inferred, nodes.ClassDef): + if inferred.name == exception: + return True + elif isinstance(inferred, nodes.Tuple): + for elt in inferred.elts: + inferred_elt = safe_infer(elt) + if ( + isinstance(inferred_elt, nodes.ClassDef) + and inferred_elt.name == exception + ): + return True + return False + + +def get_contextlib_suppressors( + node: nodes.NodeNG, exception: type[Exception] | str = Exception +) -> Iterator[nodes.With]: + """Return the contextlib suppressors handling the exception. + + Args: + node (nodes.NodeNG): A node that is potentially wrapped in a contextlib.suppress. + exception (builtin.Exception): exception or name of the exception. + + Yields: + nodes.With: A with node that is suppressing the exception. + """ + for with_node in get_contextlib_with_statements(node): + for item, _ in with_node.items: + if isinstance(item, nodes.Call): + inferred = safe_infer(item.func) + if ( + isinstance(inferred, nodes.ClassDef) + and inferred.qname() == "contextlib.suppress" + ): + if _suppresses_exception(item, exception): + yield with_node + + def is_node_inside_try_except(node: nodes.Raise) -> bool: """Check if the node is directly under a Try/Except statement (but not under an ExceptHandler!). @@ -1046,7 +1148,7 @@ def is_node_inside_try_except(node: nodes.Raise) -> bool: def node_ignores_exception( - node: nodes.NodeNG, exception: type[Exception] = Exception + node: nodes.NodeNG, exception: type[Exception] | str = Exception ) -> bool: """Check if the node is in a TryExcept which handles the given exception. @@ -1054,15 +1156,19 @@ def node_ignores_exception( excepts. """ managing_handlers = get_exception_handlers(node, exception) - if not managing_handlers: - return False - return any(managing_handlers) + if managing_handlers: + return True + return any(get_contextlib_suppressors(node, exception)) def class_is_abstract(node: nodes.ClassDef) -> bool: """Return true if the given class node should be considered as an abstract class. """ + # Protocol classes are considered "abstract" + if is_protocol_class(node): + return True + # Only check for explicit metaclass=ABCMeta on this specific class meta = node.declared_metaclass() if meta is not None: @@ -1245,12 +1351,18 @@ def _get_python_type_of_node(node: nodes.NodeNG) -> str | None: @lru_cache(maxsize=1024) def safe_infer( - node: nodes.NodeNG, context: InferenceContext | None = None -) -> nodes.NodeNG | type[astroid.Uninferable] | None: + node: nodes.NodeNG, + context: InferenceContext | None = None, + *, + compare_constants: bool = False, +) -> InferenceResult | None: """Return the inferred value for the given node. Return None if inference failed or if there is some ambiguity (more than one node has been inferred of different types). + + If compare_constants is True and if multiple constants are inferred, + unequal inferred values are also considered ambiguous and return None. """ inferred_types: set[str | None] = set() try: @@ -1264,11 +1376,19 @@ def safe_infer( if value is not astroid.Uninferable: inferred_types.add(_get_python_type_of_node(value)) + # pylint: disable = too-many-try-statements try: for inferred in infer_gen: inferred_type = _get_python_type_of_node(inferred) if inferred_type not in inferred_types: return None # If there is ambiguity on the inferred node. + if ( + compare_constants + and isinstance(inferred, nodes.Const) + and isinstance(value, nodes.Const) + and inferred.value != value.value + ): + return None if ( isinstance(inferred, nodes.FunctionDef) and inferred.args.args is not None @@ -1288,8 +1408,8 @@ def safe_infer( @lru_cache(maxsize=512) def infer_all( - node: nodes.NodeNG, context: InferenceContext = None -) -> list[nodes.NodeNG]: + node: nodes.NodeNG, context: InferenceContext | None = None +) -> list[InferenceResult]: try: return list(node.infer(context=context)) except astroid.InferenceError: @@ -1303,7 +1423,7 @@ def has_known_bases( ) -> bool: """Return true if all base classes of a class could be inferred.""" try: - return klass._all_bases_known + return klass._all_bases_known # type: ignore[no-any-return] except AttributeError: pass for base in klass.bases: @@ -1327,7 +1447,7 @@ def is_none(node: nodes.NodeNG) -> bool: ) -def node_type(node: nodes.NodeNG) -> nodes.NodeNG | None: +def node_type(node: nodes.NodeNG) -> SuccessfulInferenceResult | None: """Return the inferred type for `node`. If there is more than one possible type, or if inferred type is Uninferable or None, @@ -1335,7 +1455,7 @@ def node_type(node: nodes.NodeNG) -> nodes.NodeNG | None: """ # check there is only one possible type for the assign node. Else we # don't handle it for now - types: set[nodes.NodeNG] = set() + types: set[SuccessfulInferenceResult] = set() try: for var_type in node.infer(): if var_type == astroid.Uninferable or is_none(var_type): @@ -1361,11 +1481,15 @@ def is_registered_in_singledispatch_function(node: nodes.FunctionDef) -> bool: decorators = node.decorators.nodes if node.decorators else [] for decorator in decorators: - # func.register are function calls - if not isinstance(decorator, nodes.Call): + # func.register are function calls or register attributes + # when the function is annotated with types + if isinstance(decorator, nodes.Call): + func = decorator.func + elif isinstance(decorator, nodes.Attribute): + func = decorator + else: continue - func = decorator.func if not isinstance(func, nodes.Attribute) or func.attrname != "register": continue @@ -1380,6 +1504,43 @@ def is_registered_in_singledispatch_function(node: nodes.FunctionDef) -> bool: return False +def find_inferred_fn_from_register(node: nodes.NodeNG) -> nodes.FunctionDef | None: + # func.register are function calls or register attributes + # when the function is annotated with types + if isinstance(node, nodes.Call): + func = node.func + elif isinstance(node, nodes.Attribute): + func = node + else: + return None + + if not isinstance(func, nodes.Attribute) or func.attrname != "register": + return None + + func_def = safe_infer(func.expr) + if not isinstance(func_def, nodes.FunctionDef): + return None + + return func_def + + +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", + ) + + decorators = node.decorators.nodes if node.decorators else [] + for decorator in decorators: + func_def = find_inferred_fn_from_register(decorator) + if func_def: + return decorated_with(func_def, singledispatchmethod_qnames) + + return False + + def get_node_last_lineno(node: nodes.NodeNG) -> int: """Get the last lineno of the given node. @@ -1401,7 +1562,7 @@ def get_node_last_lineno(node: nodes.NodeNG) -> int: if getattr(node, "body", False): return get_node_last_lineno(node.body[-1]) # Not a compound statement - return node.lineno + return node.lineno # type: ignore[no-any-return] def is_postponed_evaluation_enabled(node: nodes.NodeNG) -> bool: @@ -1422,6 +1583,7 @@ def is_class_subscriptable_pep585_with_postponed_evaluation_enabled( "Use 'is_postponed_evaluation_enabled(node) and " "is_node_in_type_annotation_context(node)' instead.", DeprecationWarning, + stacklevel=2, ) return ( is_postponed_evaluation_enabled(node) @@ -1494,14 +1656,23 @@ def is_protocol_class(cls: nodes.NodeNG) -> bool: """Check if the given node represents a protocol class. :param cls: The node to check - :returns: True if the node is a typing protocol class, false otherwise. + :returns: True if the node is or inherits from typing.Protocol directly, false otherwise. """ if not isinstance(cls, nodes.ClassDef): return False - # Use .ancestors() since not all protocol classes can have - # their mro deduced. - return any(parent.qname() in TYPING_PROTOCOLS for parent in cls.ancestors()) + # Return if klass is protocol + if cls.qname() in TYPING_PROTOCOLS: + return True + + for base in cls.bases: + try: + for inf_base in base.infer(): + if inf_base.qname() in TYPING_PROTOCOLS: + return True + except astroid.InferenceError: + continue + return False def is_call_of_name(node: nodes.NodeNG, name: str) -> bool: @@ -1557,6 +1728,10 @@ def is_attribute_typed_annotation( return False +def is_enum(node: nodes.ClassDef) -> bool: + return node.name == "Enum" and node.root().name == "enum" # type: ignore[no-any-return] + + def is_assign_name_annotated_with(node: nodes.AssignName, typing_name: str) -> bool: """Test if AssignName node has `typing_name` annotation. @@ -1594,14 +1769,14 @@ def get_iterating_dictionary_name(node: nodes.For | nodes.Comprehension) -> str inferred = safe_infer(node.iter.func) if not isinstance(inferred, astroid.BoundMethod): return None - return node.iter.as_string().rpartition(".keys")[0] + return node.iter.as_string().rpartition(".keys")[0] # type: ignore[no-any-return] # Is it a dictionary? if isinstance(node.iter, (nodes.Name, nodes.Attribute)): inferred = safe_infer(node.iter) if not isinstance(inferred, nodes.Dict): return None - return node.iter.as_string() + return node.iter.as_string() # type: ignore[no-any-return] return None @@ -1620,7 +1795,7 @@ def get_subscript_const_value(node: nodes.Subscript) -> nodes.Const: return inferred -def get_import_name(importnode: nodes.Import | nodes.ImportFrom, modname: str) -> str: +def get_import_name(importnode: ImportNode, modname: str | None) -> str | None: """Get a prepared module name from the given import node. In the case of relative imports, this will return the @@ -1637,7 +1812,7 @@ def get_import_name(importnode: nodes.Import | nodes.ImportFrom, modname: str) - root = importnode.root() if isinstance(root, nodes.Module): try: - return root.relative_to_absolute_name(modname, level=importnode.level) + return root.relative_to_absolute_name(modname, level=importnode.level) # type: ignore[no-any-return] except TooManyLevelsError: return modname return modname @@ -1740,11 +1915,20 @@ def is_empty_str_literal(node: nodes.NodeNG | None) -> bool: def returns_bool(node: nodes.NodeNG) -> bool: - """Returns true if a node is a return that returns a constant boolean.""" + """Returns true if a node is a nodes.Return that returns a constant boolean.""" return ( isinstance(node, nodes.Return) and isinstance(node.value, nodes.Const) - and node.value.value in {True, False} + and isinstance(node.value.value, bool) + ) + + +def assigned_bool(node: nodes.NodeNG) -> bool: + """Returns true if a node is a nodes.Assign that returns a constant boolean.""" + return ( + isinstance(node, nodes.Assign) + and isinstance(node.value, nodes.Const) + and isinstance(node.value.value, bool) ) @@ -1754,7 +1938,7 @@ def get_node_first_ancestor_of_type( """Return the first parent node that is any of the provided types (or None).""" for ancestor in node.node_ancestors(): if isinstance(ancestor, ancestor_type): - return ancestor + return ancestor # type: ignore[no-any-return] return None @@ -1799,16 +1983,314 @@ def in_type_checking_block(node: nodes.NodeNG) -> bool: if ( isinstance(inferred_module, nodes.Module) and inferred_module.name == "typing" - and inferred_module.pytype() == "builtins.module" ): return True return False +def is_typing_member(node: nodes.NodeNG, names_to_check: tuple[str, ...]) -> bool: + """Check if `node` is a member of the `typing` module and has one of the names from + `names_to_check`. + """ + if isinstance(node, nodes.Name): + try: + import_from = node.lookup(node.name)[1][0] + except IndexError: + return False + + if isinstance(import_from, nodes.ImportFrom): + return ( + import_from.modname == "typing" + and import_from.real_name(node.name) in names_to_check + ) + elif isinstance(node, nodes.Attribute): + inferred_module = safe_infer(node.expr) + return ( + isinstance(inferred_module, nodes.Module) + and inferred_module.name == "typing" + and node.attrname in names_to_check + ) + return False + + @lru_cache() def in_for_else_branch(parent: nodes.NodeNG, stmt: nodes.Statement) -> bool: """Returns True if stmt is inside the else branch for a parent For stmt.""" return isinstance(parent, nodes.For) and any( else_stmt.parent_of(stmt) or else_stmt == stmt for else_stmt in parent.orelse ) + + +def find_assigned_names_recursive( + target: nodes.AssignName | nodes.BaseContainer, +) -> Iterator[str]: + """Yield the names of assignment targets, accounting for nested ones.""" + if isinstance(target, nodes.AssignName): + if target.name is not None: + yield target.name + elif isinstance(target, nodes.BaseContainer): + for elt in target.elts: + yield from find_assigned_names_recursive(elt) + + +def has_starred_node_recursive( + node: nodes.For | nodes.Comprehension | nodes.Set, +) -> Iterator[bool]: + """Yield ``True`` if a Starred node is found recursively.""" + if isinstance(node, nodes.Starred): + yield True + elif isinstance(node, nodes.Set): + for elt in node.elts: + yield from has_starred_node_recursive(elt) + elif isinstance(node, (nodes.For, nodes.Comprehension)): + for elt in node.iter.elts: + yield from has_starred_node_recursive(elt) + + +def is_hashable(node: nodes.NodeNG) -> bool: + """Return whether any inferred value of `node` is hashable. + + When finding ambiguity, return True. + """ + # pylint: disable = too-many-try-statements + try: + for inferred in node.infer(): + if inferred is astroid.Uninferable or isinstance(inferred, nodes.ClassDef): + return True + if not hasattr(inferred, "igetattr"): + return True + hash_fn = next(inferred.igetattr("__hash__")) + if hash_fn.parent is inferred: + return True + if getattr(hash_fn, "value", True) is not None: + return True + return False + except astroid.InferenceError: + return True + + +def _is_target_name_in_binop_side( + target: nodes.AssignName | nodes.AssignAttr, side: nodes.NodeNG | None +) -> bool: + """Determine whether the target name-like node is referenced in the side node.""" + if isinstance(side, nodes.Name): + if isinstance(target, nodes.AssignName): + return target.name == side.name # type: ignore[no-any-return] + return False + if isinstance(side, nodes.Attribute) and isinstance(target, nodes.AssignAttr): + return target.as_string() == side.as_string() # type: ignore[no-any-return] + return False + + +def is_augmented_assign(node: nodes.Assign) -> tuple[bool, str]: + """Determine if the node is assigning itself (with modifications) to itself. + + For example: x = 1 + x + """ + if not isinstance(node.value, nodes.BinOp): + return False, "" + + binop = node.value + target = node.targets[0] + + if not isinstance(target, (nodes.AssignName, nodes.AssignAttr)): + return False, "" + + # We don't want to catch x = "1" + x or x = "%s" % x + if isinstance(binop.left, nodes.Const) and isinstance( + binop.left.value, (str, bytes) + ): + return False, "" + + # This could probably be improved but for now we disregard all assignments from calls + if isinstance(binop.left, nodes.Call) or isinstance(binop.right, nodes.Call): + return False, "" + + if _is_target_name_in_binop_side(target, binop.left): + return True, binop.op + if ( + # Unless an operator is commutative, we should not raise (i.e. x = 3/x) + binop.op in COMMUTATIVE_OPERATORS + and _is_target_name_in_binop_side(target, binop.right) + ): + inferred_left = safe_infer(binop.left) + if isinstance(inferred_left, nodes.Const) and isinstance( + inferred_left.value, int + ): + return True, binop.op + return False, "" + return False, "" + + +def is_module_ignored( + module: nodes.Module, + ignored_modules: Iterable[str], +) -> bool: + ignored_modules = set(ignored_modules) + module_name = module.name + module_qname = module.qname() + + for ignore in ignored_modules: + # Try to match the module name / fully qualified name directly + if module_qname in ignored_modules or module_name in ignored_modules: + return True + + # Try to see if the ignores pattern match against the module name. + if fnmatch.fnmatch(module_qname, ignore): + return True + + # Otherwise, we might have a root module name being ignored, + # and the qualified owner has more levels of depth. + parts = deque(module_name.split(".")) + current_module = "" + + while parts: + part = parts.popleft() + if not current_module: + current_module = part + else: + current_module += f".{part}" + if current_module in ignored_modules: + return True + + return False + + +def is_singleton_const(node: nodes.NodeNG) -> bool: + return isinstance(node, nodes.Const) and any( + node.value is value for value in SINGLETON_VALUES + ) + + +def is_terminating_func(node: nodes.Call) -> bool: + """Detect call to exit(), quit(), os._exit(), or sys.exit().""" + if ( + not isinstance(node.func, nodes.Attribute) + and not (isinstance(node.func, nodes.Name)) + or isinstance(node.parent, nodes.Lambda) + ): + return False + + try: + for inferred in node.func.infer(): + if ( + hasattr(inferred, "qname") + and inferred.qname() in TERMINATING_FUNCS_QNAMES + ): + return True + except (StopIteration, astroid.InferenceError): + pass + + return False + + +def is_class_attr(name: str, klass: nodes.ClassDef) -> bool: + try: + klass.getattr(name) + return True + except astroid.NotFoundError: + return False + + +def is_defined(name: str, node: nodes.NodeNG) -> bool: + """Searches for a tree node that defines the given variable name.""" + is_defined_so_far = False + + if isinstance(node, nodes.NamedExpr): + is_defined_so_far = node.target.name == name + + if isinstance(node, (nodes.Import, nodes.ImportFrom)): + is_defined_so_far = any(node_name[0] == name for node_name in node.names) + + if isinstance(node, nodes.With): + is_defined_so_far = any( + isinstance(item[1], nodes.AssignName) and item[1].name == name + for item in node.items + ) + + if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)): + is_defined_so_far = node.name == name + + if isinstance(node, nodes.AnnAssign): + is_defined_so_far = ( + node.value + and isinstance(node.target, nodes.AssignName) + and node.target.name == name + ) + + if isinstance(node, nodes.Assign): + is_defined_so_far = any( + any( + ( + ( + isinstance(elt, nodes.Starred) + and isinstance(elt.value, nodes.AssignName) + and elt.value.name == name + ) + or (isinstance(elt, nodes.AssignName) and elt.name == name) + ) + for elt in get_all_elements(target) + ) + for target in node.targets + ) + + return is_defined_so_far or any( + is_defined(name, child) for child in node.get_children() + ) + + +def get_inverse_comparator(op: str) -> str: + """Returns the inverse comparator given a comparator. + + E.g. when given "==", returns "!=" + + :param str op: the comparator to look up. + + :returns: The inverse of the comparator in string format + :raises KeyError: if input is not recognized as a comparator + """ + return { + "==": "!=", + "!=": "==", + "<": ">=", + ">": "<=", + "<=": ">", + ">=": "<", + "in": "not in", + "not in": "in", + "is": "is not", + "is not": "is", + }[op] + + +def not_condition_as_string( + test_node: nodes.Compare | nodes.Name | nodes.UnaryOp | nodes.BoolOp | nodes.BinOp, +) -> str: + msg = f"not {test_node.as_string()}" + if isinstance(test_node, nodes.UnaryOp): + msg = test_node.operand.as_string() + elif isinstance(test_node, nodes.BoolOp): + msg = f"not ({test_node.as_string()})" + elif isinstance(test_node, nodes.Compare): + lhs = test_node.left + ops, rhs = test_node.ops[0] + lower_priority_expressions = ( + nodes.Lambda, + nodes.UnaryOp, + nodes.BoolOp, + nodes.IfExp, + nodes.NamedExpr, + ) + lhs = ( + f"({lhs.as_string()})" + if isinstance(lhs, lower_priority_expressions) + else lhs.as_string() + ) + rhs = ( + f"({rhs.as_string()})" + if isinstance(rhs, lower_priority_expressions) + else rhs.as_string() + ) + msg = f"{lhs} {get_inverse_comparator(ops)} {rhs}" + return msg diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index fe7fdc9674..76faf5cc8c 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -13,20 +13,22 @@ import re import sys from collections import defaultdict -from collections.abc import Iterable, Iterator +from collections.abc import Generator, Iterable, Iterator from enum import Enum from functools import lru_cache from typing import TYPE_CHECKING, Any, NamedTuple import astroid -from astroid import nodes +from astroid import bases, extract_node, nodes +from astroid.nodes import _base_nodes +from astroid.typing import InferenceResult from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import ( in_type_checking_block, is_postponed_evaluation_enabled, ) -from pylint.constants import PY39_PLUS, TYPING_TYPE_CHECKS_GUARDS +from pylint.constants import PY39_PLUS, TYPING_NEVER, TYPING_NORETURN from pylint.interfaces import CONTROL_FLOW, HIGH, INFERENCE, INFERENCE_FAILURE from pylint.typing import MessageDefinitionTuple @@ -106,6 +108,22 @@ } ) +DICT_TYPES = ( + astroid.objects.DictValues, + astroid.objects.DictKeys, + astroid.objects.DictItems, + astroid.nodes.node_classes.Dict, +) + +NODES_WITH_VALUE_ATTR = ( + nodes.Assign, + nodes.AnnAssign, + nodes.AugAssign, + nodes.Expr, + nodes.Return, + nodes.Match, +) + class VariableVisitConsumerAction(Enum): """Reported by _check_consumer() and its sub-methods to determine the @@ -119,7 +137,7 @@ class VariableVisitConsumerAction(Enum): RETURN = 1 -def _is_from_future_import(stmt, name): +def _is_from_future_import(stmt: nodes.ImportFrom, name: str) -> bool | None: """Check if the name is a future import from another module.""" try: module = stmt.do_import_module(stmt.modname) @@ -133,7 +151,9 @@ def _is_from_future_import(stmt, name): @lru_cache(maxsize=1000) -def overridden_method(klass, name): +def overridden_method( + klass: nodes.LocalsDictNodeNG, name: str | None +) -> nodes.FunctionDef | None: """Get overridden method if any.""" try: parent = next(klass.local_attr_ancestors(name)) @@ -150,23 +170,32 @@ def overridden_method(klass, name): return None -def _get_unpacking_extra_info(node, inferred): +def _get_unpacking_extra_info(node: nodes.Assign, inferred: InferenceResult) -> str: """Return extra information to add to the message for unpacking-non-sequence - and unbalanced-tuple-unpacking errors. + and unbalanced-tuple/dict-unpacking errors. """ more = "" + if isinstance(inferred, DICT_TYPES): + if isinstance(node, nodes.Assign): + more = node.value.as_string() + elif isinstance(node, nodes.For): + more = node.iter.as_string() + return more + inferred_module = inferred.root().name if node.root().name == inferred_module: if node.lineno == inferred.lineno: - more = f" {inferred.as_string()}" + more = f"'{inferred.as_string()}'" elif inferred.lineno: - more = f" defined at line {inferred.lineno}" + more = f"defined at line {inferred.lineno}" elif inferred.lineno: - more = f" defined at line {inferred.lineno} of {inferred_module}" + more = f"defined at line {inferred.lineno} of {inferred_module}" return more -def _detect_global_scope(node, frame, defframe): +def _detect_global_scope( + node: nodes.Name, frame: nodes.LocalsDictNodeNG, defframe: nodes.LocalsDictNodeNG +) -> bool: """Detect that the given frames share a global scope. Two frames share a global scope when neither @@ -204,11 +233,12 @@ class C: ... return False if isinstance(frame, nodes.FunctionDef): # If the parent of the current node is a - # function, then it can be under its scope - # (defined in, which doesn't concern us) or + # function, then it can be under its scope (defined in); or # the `->` part of annotations. The same goes # for annotations of function arguments, they'll have # their parent the Arguments node. + if frame.parent_of(defframe): + return node.lineno < defframe.lineno # type: ignore[no-any-return] if not isinstance(node.parent, (nodes.FunctionDef, nodes.Arguments)): return False elif any( @@ -242,16 +272,20 @@ class C: ... return False # At this point, we are certain that frame and defframe share a scope # and the definition of the first depends on the second. - return frame.lineno < defframe.lineno + return frame.lineno < defframe.lineno # type: ignore[no-any-return] -def _infer_name_module(node, name): +def _infer_name_module( + node: nodes.Import, name: str +) -> Generator[InferenceResult, None, None]: context = astroid.context.InferenceContext() context.lookupname = name - return node.infer(context, asname=False) + return node.infer(context, asname=False) # type: ignore[no-any-return] -def _fix_dot_imports(not_consumed): +def _fix_dot_imports( + not_consumed: dict[str, list[nodes.NodeNG]] +) -> list[tuple[str, _base_nodes.ImportNode]]: """Try to fix imports with multiple dots, by returning a dictionary with the import names expanded. @@ -259,7 +293,7 @@ def _fix_dot_imports(not_consumed): like 'xml' (when we have both 'xml.etree' and 'xml.sax'), to 'xml.etree' and 'xml.sax' respectively. """ - names = {} + names: dict[str, _base_nodes.ImportNode] = {} for name, stmts in not_consumed.items(): if any( isinstance(stmt, nodes.AssignName) @@ -292,15 +326,18 @@ def _fix_dot_imports(not_consumed): second_name = import_module_name if second_name and second_name not in names: names[second_name] = stmt - return sorted(names.items(), key=lambda a: a[1].fromlineno) + return sorted(names.items(), key=lambda a: a[1].fromlineno) # type: ignore[no-any-return] -def _find_frame_imports(name, frame): +def _find_frame_imports(name: str, frame: nodes.LocalsDictNodeNG) -> bool: """Detect imports in the frame, with the required *name*. - Such imports can be considered assignments. + Such imports can be considered assignments if they are not globals. Returns True if an import for the given name was found. """ + if name in _flattened_scope_names(frame.nodes_of_class(nodes.Global)): + return False + imports = frame.nodes_of_class((nodes.Import, nodes.ImportFrom)) for import_node in imports: for import_name, import_alias in import_node.names: @@ -311,10 +348,12 @@ def _find_frame_imports(name, frame): return True elif import_name and import_name == name: return True - return None + return False -def _import_name_is_global(stmt, global_names): +def _import_name_is_global( + stmt: nodes.Global | _base_nodes.ImportNode, global_names: set[str] +) -> bool: for import_name, import_alias in stmt.names: # If the import uses an alias, check only that. # Otherwise, check only the import name. @@ -333,13 +372,16 @@ def _flattened_scope_names( return set(itertools.chain.from_iterable(values)) -def _assigned_locally(name_node): +def _assigned_locally(name_node: nodes.Name) -> bool: """Checks if name_node has corresponding assign statement in same scope.""" - assign_stmts = name_node.scope().nodes_of_class(nodes.AssignName) - return any(a.name == name_node.name for a in assign_stmts) + name_node_scope = name_node.scope() + assign_stmts = name_node_scope.nodes_of_class(nodes.AssignName) + return any(a.name == name_node.name for a in assign_stmts) or _find_frame_imports( + name_node.name, name_node_scope + ) -def _has_locals_call_after_node(stmt, scope): +def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) -> bool: skip_nodes = ( nodes.FunctionDef, nodes.ClassDef, @@ -456,9 +498,8 @@ def _has_locals_call_after_node(stmt, scope): "the loop.", ), "W0632": ( - "Possible unbalanced tuple unpacking with " - "sequence%s: " - "left side has %d label(s), right side has %d value(s)", + "Possible unbalanced tuple unpacking with sequence %s: left side has %d " + "label%s, right side has %d value%s", "unbalanced-tuple-unpacking", "Used when there is an unbalanced tuple unpacking in assignment", {"old_names": [("E0632", "old-unbalanced-tuple-unpacking")]}, @@ -466,8 +507,7 @@ def _has_locals_call_after_node(stmt, scope): "E0633": ( "Attempting to unpack a non-sequence%s", "unpacking-non-sequence", - "Used when something which is not " - "a sequence is used in an unpack assignment", + "Used when something which is not a sequence is used in an unpack assignment", {"old_names": [("W0633", "old-unpacking-non-sequence")]}, ), "W0640": ( @@ -496,6 +536,12 @@ def _has_locals_call_after_node(stmt, scope): "Emitted when an index used on an iterable goes beyond the length of that " "iterable.", ), + "W0644": ( + "Possible unbalanced dict unpacking with %s: " + "left side has %d label%s, right side has %d value%s", + "unbalanced-dict-unpacking", + "Used when there is an unbalanced dict unpacking in assignment or for loop", + ), } @@ -511,21 +557,22 @@ class ScopeConsumer(NamedTuple): class NamesConsumer: """A simple class to handle consumed, to consume and scope type info of node locals.""" - def __init__(self, node, scope_type): + def __init__(self, node: nodes.NodeNG, scope_type: str) -> None: self._atomic = ScopeConsumer( copy.copy(node.locals), {}, collections.defaultdict(list), scope_type ) self.node = node + self._if_nodes_deemed_uncertain: set[nodes.If] = set() - def __repr__(self): - to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()] - consumed = [f"{k}->{v}" for k, v in self._atomic.consumed.items()] - consumed_uncertain = [ + def __repr__(self) -> str: + _to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()] + _consumed = [f"{k}->{v}" for k, v in self._atomic.consumed.items()] + _consumed_uncertain = [ f"{k}->{v}" for k, v in self._atomic.consumed_uncertain.items() ] - to_consumes = ", ".join(to_consumes) - consumed = ", ".join(consumed) - consumed_uncertain = ", ".join(consumed_uncertain) + to_consumes = ", ".join(_to_consumes) + consumed = ", ".join(_consumed) + consumed_uncertain = ", ".join(_consumed_uncertain) return f""" to_consume : {to_consumes} consumed : {consumed} @@ -533,15 +580,15 @@ def __repr__(self): scope_type : {self._atomic.scope_type} """ - def __iter__(self): + def __iter__(self) -> Iterator[Any]: return iter(self._atomic) @property - def to_consume(self): + def to_consume(self) -> dict[str, list[nodes.NodeNG]]: return self._atomic.to_consume @property - def consumed(self): + def consumed(self) -> dict[str, list[nodes.NodeNG]]: return self._atomic.consumed @property @@ -557,10 +604,10 @@ def consumed_uncertain(self) -> defaultdict[str, list[nodes.NodeNG]]: return self._atomic.consumed_uncertain @property - def scope_type(self): + def scope_type(self) -> str: return self._atomic.scope_type - def mark_as_consumed(self, name, consumed_nodes): + def mark_as_consumed(self, name: str, consumed_nodes: list[nodes.NodeNG]) -> None: """Mark the given nodes as consumed for the name. If all of the nodes for the name were consumed, delete the name from @@ -613,6 +660,13 @@ 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 = [ @@ -658,6 +712,118 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: return found_nodes + @staticmethod + def _exhaustively_define_name_raise_or_return( + name: str, node: nodes.NodeNG + ) -> bool: + """Return True if there is a collectively exhaustive set of paths under + this `if_node` that define `name`, raise, or return. + """ + # Handle try and with + if isinstance(node, (nodes.TryExcept, nodes.TryFinally)): + # Allow either a path through try/else/finally OR a path through ALL except handlers + return ( + NamesConsumer._defines_name_raises_or_returns_recursive(name, node) + or isinstance(node, nodes.TryExcept) + and all( + NamesConsumer._defines_name_raises_or_returns_recursive( + name, handler + ) + for handler in node.handlers + ) + ) + if isinstance(node, nodes.With): + return NamesConsumer._defines_name_raises_or_returns_recursive(name, node) + + if not isinstance(node, nodes.If): + return False + + # Be permissive if there is a break + if any(node.nodes_of_class(nodes.Break)): + return True + + # Is there an assignment in this node itself, e.g. in named expression? + if NamesConsumer._defines_name_raises_or_returns(name, node): + return True + + # If there is no else, then there is no collectively exhaustive set of paths + if not node.orelse: + return False + + return NamesConsumer._branch_handles_name( + name, node.body + ) and NamesConsumer._branch_handles_name(name, node.orelse) + + @staticmethod + def _branch_handles_name(name: str, body: Iterable[nodes.NodeNG]) -> bool: + return any( + NamesConsumer._defines_name_raises_or_returns(name, if_body_stmt) + or isinstance( + if_body_stmt, + (nodes.If, nodes.TryExcept, nodes.TryFinally, nodes.With), + ) + and NamesConsumer._exhaustively_define_name_raise_or_return( + name, if_body_stmt + ) + for if_body_stmt in body + ) + + def _uncertain_nodes_in_false_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. + + Don't identify a node if there is a collectively exhaustive set of paths + that define the name, raise, or return (e.g. every if/else branch). + """ + uncertain_nodes = [] + for other_node in found_nodes: + if in_type_checking_block(other_node): + continue + + if not isinstance(other_node, nodes.AssignName): + continue + + closest_if = utils.get_node_first_ancestor_of_type(other_node, nodes.If) + if closest_if is None: + continue + if node.frame() is not closest_if.frame(): + continue + if closest_if is not None and closest_if.parent_of(node): + continue + + # Name defined in every if/else branch + if NamesConsumer._exhaustively_define_name_raise_or_return( + other_node.name, closest_if + ): + continue + + # Higher-level if already determined to be always false + if any( + if_node.parent_of(closest_if) + for if_node in self._if_nodes_deemed_uncertain + ): + uncertain_nodes.append(other_node) + continue + + # All inferred values must test false + if isinstance(closest_if.test, nodes.NamedExpr): + test = closest_if.test.value + else: + test = closest_if.test + all_inferred = utils.infer_all(test) + if not all_inferred or not all( + isinstance(inferred, nodes.Const) and not inferred.value + for inferred in all_inferred + ): + continue + + uncertain_nodes.append(other_node) + self._if_nodes_deemed_uncertain.add(closest_if) + + return uncertain_nodes + @staticmethod def _uncertain_nodes_in_except_blocks( found_nodes: list[nodes.NodeNG], @@ -689,7 +855,14 @@ def _uncertain_nodes_in_except_blocks( isinstance(else_statement, nodes.Return) for else_statement in closest_try_except.orelse ) - if try_block_returns or else_block_returns: + else_block_exits = any( + isinstance(else_statement, nodes.Expr) + and isinstance(else_statement.value, nodes.Call) + and utils.is_terminating_func(else_statement.value) + for else_statement in closest_try_except.orelse + ) + + if try_block_returns or else_block_returns or else_block_exits: # Exception: if this node is in the final block of the other_node_statement, # it will execute before returning. Assume the except statements are uncertain. if ( @@ -724,7 +897,7 @@ 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.Return)): + if isinstance(node, (nodes.Raise, nodes.Assert, nodes.Return)): return True if ( isinstance(node, nodes.AnnAssign) @@ -736,21 +909,16 @@ def _defines_name_raises_or_returns(name: str, node: nodes.NodeNG) -> bool: if isinstance(node, nodes.Assign): for target in node.targets: for elt in utils.get_all_elements(target): + if isinstance(elt, nodes.Starred): + elt = elt.value if isinstance(elt, nodes.AssignName) and elt.name == name: return True if isinstance(node, nodes.If): - # Check for assignments inside the test - if isinstance(node.test, nodes.NamedExpr) and node.test.target.name == name: + if any( + child_named_expr.target.name == name + for child_named_expr in node.nodes_of_class(nodes.NamedExpr) + ): return True - if isinstance(node.test, nodes.Call): - for arg_or_kwarg in node.test.args + [ - kw.value for kw in node.test.keywords - ]: - if ( - isinstance(arg_or_kwarg, nodes.NamedExpr) - and arg_or_kwarg.target.name == name - ): - return True return False @staticmethod @@ -1046,8 +1214,7 @@ class VariablesChecker(BaseChecker): "default": IGNORED_ARGUMENT_NAMES, "type": "regexp", "metavar": "", - "help": "Argument names that match this expression will be " - "ignored. Default to name with leading underscore.", + "help": "Argument names that match this expression will be ignored.", }, ), ( @@ -1070,17 +1237,51 @@ class VariablesChecker(BaseChecker): ), ) - def __init__(self, linter=None): + def __init__(self, linter: PyLinter) -> None: super().__init__(linter) self._to_consume: list[NamesConsumer] = [] - self._checking_mod_attr = None - self._type_annotation_names = [] + self._type_annotation_names: list[str] = [] self._except_handler_names_queue: list[ tuple[nodes.ExceptHandler, nodes.AssignName] ] = [] """This is a queue, last in first out.""" self._postponed_evaluation_enabled = False + @utils.only_required_for_messages( + "unbalanced-dict-unpacking", + ) + def visit_for(self, node: nodes.For) -> None: + if not isinstance(node.target, nodes.Tuple): + return + + targets = node.target.elts + + inferred = utils.safe_infer(node.iter) + if not isinstance(inferred, DICT_TYPES): + return + + values = self._nodes_to_unpack(inferred) + if not values: + # no dict items returned + return + + if isinstance(inferred, astroid.objects.DictItems): + # dict.items() is a bit special because values will be a tuple + # So as long as there are always 2 targets and values each are + # a tuple with two items, this will unpack correctly. + # Example: `for key, val in {1: 2, 3: 4}.items()` + if len(targets) == 2 and all(len(x.elts) == 2 for x in values): + return + + # Starred nodes indicate ambiguous unpacking + # if `dict.items()` is used so we won't flag them. + if any(isinstance(target, nodes.Starred) for target in targets): + return + + if len(targets) != len(values): + details = _get_unpacking_extra_info(node, inferred) + self._report_unbalanced_unpacking(node, inferred, targets, values, details) + def leave_for(self, node: nodes.For) -> None: self._store_type_annotation_names(node) @@ -1125,14 +1326,30 @@ def leave_module(self, node: nodes.Module) -> None: return self._check_imports(not_consumed) + self._type_annotation_names = [] def visit_classdef(self, node: nodes.ClassDef) -> None: """Visit class: update consumption analysis variable.""" self._to_consume.append(NamesConsumer(node, "class")) - def leave_classdef(self, _: nodes.ClassDef) -> None: + def leave_classdef(self, node: nodes.ClassDef) -> None: """Leave class: update consumption analysis variable.""" - # do not check for not used locals here (no sense) + # Check for hidden ancestor names + # e.g. "six" in: Class X(six.with_metaclass(ABCMeta, object)): + for name_node in node.nodes_of_class(nodes.Name): + if ( + isinstance(name_node.parent, nodes.Call) + and isinstance(name_node.parent.func, nodes.Attribute) + and isinstance(name_node.parent.func.expr, nodes.Name) + ): + hidden_name_node = name_node.parent.func.expr + for consumer in self._to_consume: + if hidden_name_node.name in consumer.to_consume: + consumer.mark_as_consumed( + hidden_name_node.name, + consumer.to_consume[hidden_name_node.name], + ) + break self._to_consume.pop() def visit_lambda(self, node: nodes.Lambda) -> None: @@ -1193,9 +1410,7 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: # Do not take in account redefined names for the purpose # of type checking.: if any( - isinstance(definition.parent, nodes.If) - and definition.parent.test.as_string() in TYPING_TYPE_CHECKS_GUARDS - for definition in globs[name] + in_type_checking_block(definition) for definition in globs[name] ): continue @@ -1242,13 +1457,12 @@ def leave_functiondef(self, node: nodes.FunctionDef) -> None: global_names = _flattened_scope_names(node.nodes_of_class(nodes.Global)) nonlocal_names = _flattened_scope_names(node.nodes_of_class(nodes.Nonlocal)) - comprehension_target_names: list[str] = [] + comprehension_target_names: set[str] = set() for comprehension_scope in node.nodes_of_class(nodes.ComprehensionScope): for generator in comprehension_scope.generators: - self._find_assigned_names_recursive( - generator.target, comprehension_target_names - ) + for name in utils.find_assigned_names_recursive(generator.target): + comprehension_target_names.add(name) for name, stmts in not_consumed.items(): self._check_is_unused( @@ -1288,7 +1502,8 @@ def visit_global(self, node: nodes.Global) -> None: assign_nodes = [] not_defined_locally_by_import = not any( - isinstance(local, nodes.Import) for local in locals_.get(name, ()) + isinstance(local, (nodes.Import, nodes.ImportFrom)) + for local in locals_.get(name, ()) ) if ( not utils.is_reassigned_after_current(node, name) @@ -1331,7 +1546,7 @@ def visit_assignname(self, node: nodes.AssignName) -> None: def visit_delname(self, node: nodes.DelName) -> None: self.visit_name(node) - def visit_name(self, node: nodes.Name) -> None: + def visit_name(self, node: nodes.Name | nodes.AssignName | nodes.DelName) -> None: """Don't add the 'utils.only_required_for_messages' decorator here! It's important that all 'Name' nodes are visited, otherwise the @@ -1455,28 +1670,14 @@ def _should_node_be_skipped( return False - def _find_assigned_names_recursive( - self, - target: nodes.AssignName | nodes.BaseContainer, - target_names: list[str], - ) -> None: - """Update `target_names` in place with the names of assignment - targets, recursively (to account for nested assignments). - """ - if isinstance(target, nodes.AssignName): - target_names.append(target.name) - elif isinstance(target, nodes.BaseContainer): - for elt in target.elts: - self._find_assigned_names_recursive(elt, target_names) - - # pylint: disable=too-many-return-statements + # pylint: disable = too-many-return-statements, too-many-branches def _check_consumer( self, node: nodes.Name, stmt: nodes.NodeNG, frame: nodes.LocalsDictNodeNG, current_consumer: NamesConsumer, - base_scope_type: Any, + base_scope_type: str, ) -> tuple[VariableVisitConsumerAction, list[nodes.NodeNG] | None]: """Checks a consumer for conditions that should trigger messages.""" # If the name has already been consumed, only check it's not a loop @@ -1518,7 +1719,7 @@ def _check_consumer( defframe = defstmt.frame(future=True) # The class reuses itself in the class scope. - is_recursive_klass = ( + is_recursive_klass: bool = ( frame is defframe and defframe.parent_of(node) and isinstance(defframe, nodes.ClassDef) @@ -1575,7 +1776,6 @@ def _check_consumer( and not utils.is_defined_before(node) and not astroid.are_exclusive(stmt, defstmt, ("NameError",)) ): - # Used and defined in the same place, e.g `x += 1` and `del x` defined_by_stmt = defstmt is stmt and isinstance( node, (nodes.DelName, nodes.AssignName) @@ -1587,7 +1787,6 @@ def _check_consumer( or isinstance(defstmt, nodes.Delete) ): if not utils.node_ignores_exception(node, NameError): - # Handle postponed evaluation of annotations if not ( self._postponed_evaluation_enabled @@ -1608,10 +1807,14 @@ def _check_consumer( elif base_scope_type != "lambda": # E0601 may *not* occurs in lambda scope. - # Handle postponed evaluation of annotations + # Skip postponed evaluation of annotations + # and unevaluated annotations inside a function body if not ( self._postponed_evaluation_enabled and isinstance(stmt, (nodes.AnnAssign, nodes.FunctionDef)) + ) and not ( + isinstance(stmt, nodes.AnnAssign) + and utils.get_node_first_ancestor_of_type(stmt, nodes.FunctionDef) ): self.add_message( "used-before-assignment", @@ -1718,7 +1921,10 @@ def visit_importfrom(self, node: nodes.ImportFrom) -> None: self._check_module_attrs(node, module, name.split(".")) @utils.only_required_for_messages( - "unbalanced-tuple-unpacking", "unpacking-non-sequence", "self-cls-assignment" + "unbalanced-tuple-unpacking", + "unpacking-non-sequence", + "self-cls-assignment", + "unbalanced_dict_unpacking", ) def visit_assign(self, node: nodes.Assign) -> None: """Check unbalanced tuple unpacking for assignments and unpacking @@ -1729,6 +1935,11 @@ def visit_assign(self, node: nodes.Assign) -> None: return targets = node.targets[0].itered() + + # Check if we have starred nodes. + if any(isinstance(target, nodes.Starred) for target in targets): + return + try: inferred = utils.safe_infer(node.value) if inferred is not None: @@ -1758,19 +1969,21 @@ def visit_arguments(self, node: nodes.Arguments) -> None: # Relying on other checker's options, which might not have been initialized yet. @cached_property - def _analyse_fallback_blocks(self): - return self.linter.config.analyse_fallback_blocks + def _analyse_fallback_blocks(self) -> bool: + return bool(self.linter.config.analyse_fallback_blocks) @cached_property - def _ignored_modules(self): - return self.linter.config.ignored_modules + def _ignored_modules(self) -> Iterable[str]: + return self.linter.config.ignored_modules # type: ignore[no-any-return] @cached_property - def _allow_global_unused_variables(self): - return self.linter.config.allow_global_unused_variables + def _allow_global_unused_variables(self) -> bool: + return bool(self.linter.config.allow_global_unused_variables) @staticmethod - def _defined_in_function_definition(node, frame): + def _defined_in_function_definition( + node: nodes.NodeNG, frame: nodes.NodeNG + ) -> bool: in_annotation_or_default_or_decorator = False if ( isinstance(frame, nodes.FunctionDef) @@ -1821,18 +2034,18 @@ def _in_lambda_or_comprehension_body( parent = parent.parent return False + # pylint: disable = too-many-branches @staticmethod def _is_variable_violation( node: nodes.Name, - defnode, + defnode: nodes.NodeNG, stmt: nodes.Statement, defstmt: nodes.Statement, - frame, # scope of statement of node - defframe, - base_scope_type, - is_recursive_klass, + frame: nodes.LocalsDictNodeNG, # scope of statement of node + defframe: nodes.LocalsDictNodeNG, + base_scope_type: str, + is_recursive_klass: bool, ) -> tuple[bool, bool, bool]: - # pylint: disable=too-many-nested-blocks maybe_before_assign = True annotation_return = False use_outer_definition = False @@ -1873,7 +2086,6 @@ def _is_variable_violation( and isinstance(frame, nodes.ClassDef) and node.name in frame.locals ): - # This rule verifies that if the definition node of the # checked name is an Arguments node and if the name # is used a default value in the arguments defaults @@ -1925,16 +2137,7 @@ def _is_variable_violation( # same line as the function definition maybe_before_assign = False elif ( - isinstance( - defstmt, - ( - nodes.Assign, - nodes.AnnAssign, - nodes.AugAssign, - nodes.Expr, - nodes.Return, - ), - ) + isinstance(defstmt, NODES_WITH_VALUE_ATTR) and VariablesChecker._maybe_used_and_assigned_at_once(defstmt) and frame is defframe and defframe.parent_of(node) @@ -1976,36 +2179,50 @@ def _is_variable_violation( ) ) ): - # Expressions, with assignment expressions - # Use only after assignment - # b = (c := 2) and c - maybe_before_assign = False + # Relation of a name to the same name in a named expression + # Could be used before assignment if self-referencing: + # (b := b) + # Otherwise, safe if used after assignment: + # (b := 2) and b + maybe_before_assign = defnode.value is node or any( + anc is defnode.value for anc in node.node_ancestors() + ) # Look for type checking definitions inside a type checking guard. - if isinstance(defstmt, (nodes.Import, nodes.ImportFrom)): + # Relevant for function annotations only, not variable annotations (AnnAssign) + if ( + isinstance(defstmt, (nodes.Import, nodes.ImportFrom)) + and isinstance(defstmt.parent, nodes.If) + and in_type_checking_block(defstmt) + and not in_type_checking_block(node) + ): defstmt_parent = defstmt.parent - if ( - isinstance(defstmt_parent, nodes.If) - and defstmt_parent.test.as_string() in TYPING_TYPE_CHECKS_GUARDS + maybe_annotation = utils.get_node_first_ancestor_of_type( + node, nodes.AnnAssign + ) + if not ( + maybe_annotation + and utils.get_node_first_ancestor_of_type( + maybe_annotation, nodes.FunctionDef + ) ): # Exempt those definitions that are used inside the type checking - # guard or that are defined in both type checking guard branches. + # guard or that are defined in any elif/else type checking guard branches. used_in_branch = defstmt_parent.parent_of(node) - defined_in_or_else = False - - for definition in defstmt_parent.orelse: - if isinstance(definition, nodes.Assign): + if not used_in_branch: + if defstmt_parent.has_elif_block(): + defined_in_or_else = utils.is_defined( + node.name, defstmt_parent.orelse[0] + ) + else: defined_in_or_else = any( - target.name == node.name - for target in definition.targets - if isinstance(target, nodes.AssignName) + utils.is_defined(node.name, content) + for content in defstmt_parent.orelse ) - if defined_in_or_else: - break - if not used_in_branch and not defined_in_or_else: - maybe_before_assign = True + if not defined_in_or_else: + maybe_before_assign = True return maybe_before_assign, annotation_return, use_outer_definition @@ -2014,20 +2231,36 @@ def _maybe_used_and_assigned_at_once(defstmt: nodes.Statement) -> bool: """Check if `defstmt` has the potential to use and assign a name in the same statement. """ - if isinstance(defstmt.value, nodes.BaseContainer) and defstmt.value.elts: - # The assignment must happen as part of the first element - # e.g. "assert (x:= True), x" - # NOT "assert x, (x:= True)" - value = defstmt.value.elts[0] - else: - value = defstmt.value + if isinstance(defstmt, nodes.Match): + return any(case.guard for case in defstmt.cases) + if isinstance(defstmt, nodes.IfExp): + return True + if isinstance(defstmt.value, nodes.BaseContainer): + return any( + VariablesChecker._maybe_used_and_assigned_at_once(elt) + for elt in defstmt.value.elts + if isinstance(elt, NODES_WITH_VALUE_ATTR + (nodes.IfExp, nodes.Match)) + ) + value = defstmt.value if isinstance(value, nodes.IfExp): return True if isinstance(value, nodes.Lambda) and isinstance(value.body, nodes.IfExp): return True - return isinstance(value, nodes.Call) and ( - any(isinstance(kwarg.value, nodes.IfExp) for kwarg in value.keywords) - or any(isinstance(arg, nodes.IfExp) for arg in value.args) + if isinstance(value, nodes.Dict) and any( + isinstance(item[0], nodes.IfExp) or isinstance(item[1], nodes.IfExp) + for item in value.items + ): + return True + if not isinstance(value, nodes.Call): + return False + return any( + any(isinstance(kwarg.value, nodes.IfExp) for kwarg in call.keywords) + or any(isinstance(arg, nodes.IfExp) for arg in call.args) + or ( + isinstance(call.func, nodes.Attribute) + and isinstance(call.func.expr, nodes.IfExp) + ) + for call in value.nodes_of_class(klass=nodes.Call) ) def _is_only_type_assignment( @@ -2079,6 +2312,14 @@ def _is_only_type_assignment( if ( not isinstance(ref_node.parent, nodes.AnnAssign) or ref_node.parent.value + ) and not ( + # EXCEPTION: will not have a value if a self-referencing named expression + # var: int + # if (var := var * var) <-- "var" still undefined + isinstance(ref_node.parent, nodes.NamedExpr) + and any( + anc is ref_node.parent.value for anc in node.node_ancestors() + ) ): return False parent = parent_scope.parent @@ -2162,6 +2403,7 @@ class D(Tp): and name in frame_locals ) + # pylint: disable = too-many-branches def _loopvar_name(self, node: astroid.Name) -> None: # filter variables according to node's scope astmts = [s for s in node.lookup(node.name)[1] if hasattr(s, "assign_type")] @@ -2184,9 +2426,8 @@ def _loopvar_name(self, node: astroid.Name) -> None: # scope lookup rules would need to be changed to return the initial # assignment (which does not exist in code per se) as well as any later # modifications. - # pylint: disable-next=too-many-boolean-expressions if ( - not astmts + not astmts # pylint: disable=too-many-boolean-expressions or ( astmts[0].parent == astmts[0].root() and astmts[0].parent.parent_of(node) @@ -2220,11 +2461,53 @@ def _loopvar_name(self, node: astroid.Name) -> None: if not isinstance(assign, nodes.For): self.add_message("undefined-loop-variable", args=node.name, node=node) return - if any( - isinstance(else_stmt, (nodes.Return, nodes.Raise)) - for else_stmt in assign.orelse - ): - return + for else_stmt in assign.orelse: + if isinstance( + else_stmt, (nodes.Return, nodes.Raise, nodes.Break, nodes.Continue) + ): + return + # TODO: 2.16: Consider using RefactoringChecker._is_function_def_never_returning + if isinstance(else_stmt, nodes.Expr) and isinstance( + else_stmt.value, nodes.Call + ): + inferred_func = utils.safe_infer(else_stmt.value.func) + if ( + isinstance(inferred_func, nodes.FunctionDef) + and inferred_func.returns + ): + inferred_return = utils.safe_infer(inferred_func.returns) + if isinstance( + inferred_return, nodes.FunctionDef + ) and inferred_return.qname() in { + *TYPING_NORETURN, + *TYPING_NEVER, + "typing._SpecialForm", + }: + return + # typing_extensions.NoReturn returns a _SpecialForm + if ( + isinstance(inferred_return, bases.Instance) + and inferred_return.qname() == "typing._SpecialForm" + ): + return + + maybe_walrus = utils.get_node_first_ancestor_of_type(node, nodes.NamedExpr) + if maybe_walrus: + maybe_comprehension = utils.get_node_first_ancestor_of_type( + maybe_walrus, nodes.Comprehension + ) + if maybe_comprehension: + comprehension_scope = utils.get_node_first_ancestor_of_type( + maybe_comprehension, nodes.ComprehensionScope + ) + if comprehension_scope is None: + # Should not be possible. + pass + elif ( + comprehension_scope.parent.scope() is scope + and node.name in comprehension_scope.locals + ): + return # For functions we can do more by inferring the length of the itered object try: @@ -2265,14 +2548,15 @@ def _loopvar_name(self, node: astroid.Name) -> None: if not elements: self.add_message("undefined-loop-variable", args=node.name, node=node) + # pylint: disable = too-many-branches def _check_is_unused( self, - name, - node, - stmt, - global_names, + name: str, + node: nodes.FunctionDef, + stmt: nodes.NodeNG, + global_names: set[str], nonlocal_names: Iterable[str], - comprehension_target_names: list[str], + comprehension_target_names: Iterable[str], ) -> None: # Ignore some special names specified by user configuration. if self._is_name_ignored(stmt, name): @@ -2296,13 +2580,17 @@ def _check_is_unused( if name in comprehension_target_names: return + # Ignore names in string literal type annotation. + if name in self._type_annotation_names: + return + argnames = node.argnames() # Care about functions with unknown argument (builtins) if name in argnames: self._check_unused_arguments(name, node, stmt, argnames, nonlocal_names) else: if stmt.parent and isinstance( - stmt.parent, (nodes.Assign, nodes.AnnAssign, nodes.Tuple) + stmt.parent, (nodes.Assign, nodes.AnnAssign, nodes.Tuple, nodes.For) ): if name in nonlocal_names: return @@ -2354,21 +2642,31 @@ def _check_is_unused( self.add_message(message_name, args=name, node=stmt) - def _is_name_ignored(self, stmt, name): + def _is_name_ignored( + self, stmt: nodes.NodeNG, name: str + ) -> re.Pattern[str] | re.Match[str] | None: authorized_rgx = self.linter.config.dummy_variables_rgx if ( isinstance(stmt, nodes.AssignName) and isinstance(stmt.parent, nodes.Arguments) or isinstance(stmt, nodes.Arguments) ): - regex = self.linter.config.ignored_argument_names + regex: re.Pattern[str] = self.linter.config.ignored_argument_names else: regex = authorized_rgx + # See https://stackoverflow.com/a/47007761/2519059 to + # understand what this function return. Please do NOT use + # this elsewhere, this is confusing for no benefit return regex and regex.match(name) def _check_unused_arguments( - self, name, node, stmt, argnames, nonlocal_names: Iterable[str] - ): + self, + name: str, + node: nodes.FunctionDef, + stmt: nodes.NodeNG, + argnames: list[str], + nonlocal_names: Iterable[str], + ) -> None: is_method = node.is_method() klass = node.parent.frame(future=True) if is_method and isinstance(klass, nodes.ClassDef): @@ -2464,12 +2762,12 @@ def _check_late_binding_closure(self, node: nodes.Name) -> None: ): self.add_message("cell-var-from-loop", node=node, args=node.name) - def _should_ignore_redefined_builtin(self, stmt): + def _should_ignore_redefined_builtin(self, stmt: nodes.NodeNG) -> bool: if not isinstance(stmt, nodes.ImportFrom): return False return stmt.modname in self.linter.config.redefining_builtins_modules - def _allowed_redefined_builtin(self, name): + def _allowed_redefined_builtin(self, name: str) -> bool: return name in self.linter.config.allowed_redefined_builtins @staticmethod @@ -2484,7 +2782,7 @@ def _comprehension_between_frame_and_node(node: nodes.Name) -> bool: future=True ).parent_of(closest_comprehension_scope) - def _store_type_annotation_node(self, type_annotation): + def _store_type_annotation_node(self, type_annotation: nodes.NodeNG) -> None: """Given a type annotation, store all the name nodes it refers to.""" if isinstance(type_annotation, nodes.Name): self._type_annotation_names.append(type_annotation.name) @@ -2509,7 +2807,9 @@ def _store_type_annotation_node(self, type_annotation): annotation.name for annotation in type_annotation.nodes_of_class(nodes.Name) ) - def _store_type_annotation_names(self, node): + def _store_type_annotation_names( + self, node: nodes.For | nodes.Assign | nodes.With + ) -> None: type_annotation = node.type_annotation if not type_annotation: return @@ -2545,7 +2845,9 @@ def _check_self_cls_assign(self, node: nodes.Assign) -> None: if self_cls_name in assign_names: self.add_message("self-cls-assignment", node=node, args=(self_cls_name,)) - def _check_unpacking(self, inferred, node, targets): + def _check_unpacking( + self, inferred: InferenceResult, node: nodes.Assign, targets: list[nodes.NodeNG] + ) -> None: """Check for unbalanced tuple unpacking and unpacking non sequences. """ @@ -2565,40 +2867,62 @@ def _check_unpacking(self, inferred, node, targets): # Attempt to check unpacking is properly balanced values = self._nodes_to_unpack(inferred) + details = _get_unpacking_extra_info(node, inferred) + if values is not None: if len(targets) != len(values): - # Check if we have starred nodes. - if any(isinstance(target, nodes.Starred) for target in targets): - return - self.add_message( - "unbalanced-tuple-unpacking", - node=node, - args=( - _get_unpacking_extra_info(node, inferred), - len(targets), - len(values), - ), + self._report_unbalanced_unpacking( + node, inferred, targets, values, details ) # attempt to check unpacking may be possible (i.e. RHS is iterable) elif not utils.is_iterable(inferred): - self.add_message( - "unpacking-non-sequence", - node=node, - args=(_get_unpacking_extra_info(node, inferred),), - ) + self._report_unpacking_non_sequence(node, details) @staticmethod def _nodes_to_unpack(node: nodes.NodeNG) -> list[nodes.NodeNG] | None: """Return the list of values of the `Assign` node.""" - if isinstance(node, (nodes.Tuple, nodes.List)): - return node.itered() + if isinstance(node, (nodes.Tuple, nodes.List) + DICT_TYPES): + return node.itered() # type: ignore[no-any-return] if isinstance(node, astroid.Instance) and any( ancestor.qname() == "typing.NamedTuple" for ancestor in node.ancestors() ): return [i for i in node.values() if isinstance(i, nodes.AssignName)] return None - def _check_module_attrs(self, node, module, module_names): + def _report_unbalanced_unpacking( + self, + node: nodes.NodeNG, + inferred: InferenceResult, + targets: list[nodes.NodeNG], + values: list[nodes.NodeNG], + details: str, + ) -> None: + args = ( + details, + len(targets), + "" if len(targets) == 1 else "s", + len(values), + "" if len(values) == 1 else "s", + ) + + symbol = ( + "unbalanced-dict-unpacking" + if isinstance(inferred, DICT_TYPES) + else "unbalanced-tuple-unpacking" + ) + self.add_message(symbol, node=node, args=args, confidence=INFERENCE) + + def _report_unpacking_non_sequence(self, node: nodes.NodeNG, details: str) -> None: + if details and not details.startswith(" "): + details = f" {details}" + self.add_message("unpacking-non-sequence", node=node, args=details) + + def _check_module_attrs( + self, + node: _base_nodes.ImportNode, + module: nodes.Module, + module_names: list[str], + ) -> nodes.Module | None: """Check that module_names (list of string) are accessible through the given module, if the latest access name corresponds to a module, return it. """ @@ -2609,7 +2933,7 @@ def _check_module_attrs(self, node, module, module_names): break try: module = next(module.getattr(name)[0].infer()) - if module is astroid.Uninferable: + if not isinstance(module, nodes.Module): return None except astroid.NotFoundError: if module.name in self._ignored_modules: @@ -2630,7 +2954,9 @@ def _check_module_attrs(self, node, module, module_names): return module return None - def _check_all(self, node: nodes.Module, not_consumed): + def _check_all( + self, node: nodes.Module, not_consumed: dict[str, list[nodes.NodeNG]] + ) -> None: assigned = next(node.igetattr("__all__")) if assigned is astroid.Uninferable: return @@ -2681,14 +3007,15 @@ def _check_all(self, node: nodes.Module, not_consumed): # when the file will be checked pass - def _check_globals(self, not_consumed): + def _check_globals(self, not_consumed: dict[str, nodes.NodeNG]) -> None: if self._allow_global_unused_variables: return for name, node_lst in not_consumed.items(): for node in node_lst: self.add_message("unused-variable", args=(name,), node=node) - def _check_imports(self, not_consumed): + # pylint: disable = too-many-branches + def _check_imports(self, not_consumed: dict[str, list[nodes.NodeNG]]) -> None: local_names = _fix_dot_imports(not_consumed) checked = set() unused_wildcard_imports: defaultdict[ @@ -2770,9 +3097,9 @@ def _check_imports(self, not_consumed): ) del self._to_consume - def _check_metaclasses(self, node): + def _check_metaclasses(self, node: nodes.Module | nodes.FunctionDef) -> None: """Update consumption analysis for metaclasses.""" - consumed = [] # [(scope_locals, consumed_key)] + consumed: list[tuple[dict[str, list[nodes.NodeNG]], str]] = [] for child_node in node.get_children(): if isinstance(child_node, nodes.ClassDef): @@ -2783,14 +3110,16 @@ def _check_metaclasses(self, node): for scope_locals, name in consumed: scope_locals.pop(name, None) - def _check_classdef_metaclasses(self, klass, parent_node): + def _check_classdef_metaclasses( + self, klass: nodes.ClassDef, parent_node: nodes.Module | nodes.FunctionDef + ) -> list[tuple[dict[str, list[nodes.NodeNG]], str]]: if not klass._metaclass: # Skip if this class doesn't use explicitly a metaclass, but inherits it from ancestors return [] - consumed = [] # [(scope_locals, consumed_key)] + consumed: list[tuple[dict[str, list[nodes.NodeNG]], str]] = [] metaclass = klass.metaclass() - name = None + name = "" if isinstance(klass._metaclass, nodes.Name): name = klass._metaclass.name elif isinstance(klass._metaclass, nodes.Attribute) and klass._metaclass.expr: @@ -2859,6 +3188,40 @@ def _check_potential_index_error( ) return + @utils.only_required_for_messages( + "unused-import", + "unused-variable", + ) + def visit_const(self, node: nodes.Const) -> None: + """Take note of names that appear inside string literal type annotations + unless the string is a parameter to `typing.Literal` or `typing.Annotation`. + """ + if node.pytype() != "builtins.str": + return + if not utils.is_node_in_type_annotation_context(node): + return + + # Check if parent's or grandparent's first child is typing.Literal + parent = node.parent + if isinstance(parent, nodes.Tuple): + parent = parent.parent + if isinstance(parent, nodes.Subscript): + origin = next(parent.get_children(), None) + if origin is not None and utils.is_typing_member( + origin, ("Annotated", "Literal") + ): + return + + try: + annotation = extract_node(node.value) + self._store_type_annotation_node(annotation) + except ValueError: + # e.g. node.value is white space + pass + except astroid.AstroidSyntaxError: + # e.g. "?" or ":" in typing.Literal["?", ":"] + pass + def register(linter: PyLinter) -> None: linter.register_checker(VariablesChecker(linter)) diff --git a/pylint/config/__init__.py b/pylint/config/__init__.py index 7dc96f0cf7..5f90bbae02 100644 --- a/pylint/config/__init__.py +++ b/pylint/config/__init__.py @@ -31,8 +31,10 @@ ) from pylint.config.option import Option from pylint.config.option_manager_mixin import OptionsManagerMixIn -from pylint.config.option_parser import OptionParser -from pylint.config.options_provider_mixin import OptionsProviderMixIn +from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] +from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] + OptionsProviderMixIn, +) from pylint.constants import PYLINT_HOME, USER_HOME from pylint.utils import LinterStats @@ -46,6 +48,7 @@ def load_results(base: str) -> LinterStats | None: "'pylint.config.load_results' is deprecated, please use " "'pylint.lint.load_results' instead. This will be removed in 3.0.", DeprecationWarning, + stacklevel=2, ) return _real_load_results(base, PYLINT_HOME) @@ -59,5 +62,6 @@ def save_results(results: LinterStats, base: str) -> None: "'pylint.config.save_results' is deprecated, please use " "'pylint.lint.save_results' instead. This will be removed in 3.0.", DeprecationWarning, + stacklevel=2, ) return _real_save_results(results, base, PYLINT_HOME) diff --git a/pylint/config/_pylint_config/__init__.py b/pylint/config/_pylint_config/__init__.py index d62400a0e0..622d0dfe36 100644 --- a/pylint/config/_pylint_config/__init__.py +++ b/pylint/config/_pylint_config/__init__.py @@ -7,5 +7,7 @@ Everything in this module is private. """ -from pylint.config._pylint_config.main import _handle_pylint_config_commands # noqa -from pylint.config._pylint_config.setup import _register_generate_config_options # noqa +from pylint.config._pylint_config.main import _handle_pylint_config_commands +from pylint.config._pylint_config.setup import _register_generate_config_options + +__all__ = ("_handle_pylint_config_commands", "_register_generate_config_options") diff --git a/pylint/config/_pylint_config/generate_command.py b/pylint/config/_pylint_config/generate_command.py index 325c713332..110069b901 100644 --- a/pylint/config/_pylint_config/generate_command.py +++ b/pylint/config/_pylint_config/generate_command.py @@ -22,10 +22,11 @@ def generate_interactive_config(linter: PyLinter) -> None: print("Starting interactive pylint configuration generation") format_type = utils.get_and_validate_format() + minimal = format_type == "toml" and utils.get_minimal_setting() to_file, output_file_name = utils.get_and_validate_output_file() if format_type == "toml": - config_string = linter._generate_config_file() + config_string = linter._generate_config_file(minimal=minimal) else: output_stream = StringIO() with warnings.catch_warnings(): diff --git a/pylint/config/_pylint_config/utils.py b/pylint/config/_pylint_config/utils.py index 0534340b1e..cd5f8affe0 100644 --- a/pylint/config/_pylint_config/utils.py +++ b/pylint/config/_pylint_config/utils.py @@ -9,6 +9,7 @@ import sys from collections.abc import Callable from pathlib import Path +from typing import TypeVar if sys.version_info >= (3, 8): from typing import Literal @@ -21,6 +22,7 @@ from typing_extensions import ParamSpec _P = ParamSpec("_P") +_ReturnValueT = TypeVar("_ReturnValueT", bool, str) SUPPORTED_FORMATS = {"t", "toml", "i", "ini"} YES_NO_ANSWERS = {"y", "yes", "n", "no"} @@ -36,11 +38,11 @@ def __init__(self, valid_input: str, input_value: str, *args: object) -> None: def should_retry_after_invalid_input( - func: Callable[_P, str | bool] -) -> Callable[_P, str | bool]: + func: Callable[_P, _ReturnValueT] +) -> Callable[_P, _ReturnValueT]: """Decorator that handles InvalidUserInput exceptions and retries.""" - def inner_function(*args: _P.args, **kwargs: _P.kwargs) -> str | bool: + def inner_function(*args: _P.args, **kwargs: _P.kwargs) -> _ReturnValueT: called_once = False while True: try: @@ -81,7 +83,7 @@ def validate_yes_no(question: str, default: Literal["yes", "no"] | None) -> bool # pylint: disable-next=bad-builtin answer = input(question).lower() - if answer == "" and default: + if not answer and default: answer = default if answer not in YES_NO_ANSWERS: @@ -90,6 +92,13 @@ def validate_yes_no(question: str, default: Literal["yes", "no"] | None) -> bool return answer.startswith("y") +def get_minimal_setting() -> bool: + """Ask the user if they want to use the minimal setting.""" + return validate_yes_no( + "Do you want a minimal configuration without comments or default values?", "no" + ) + + def get_and_validate_output_file() -> tuple[bool, Path]: """Make sure that the output file is correct.""" to_file = validate_yes_no("Do you want to write the output to a file?", "no") diff --git a/pylint/config/argument.py b/pylint/config/argument.py index 3c29515176..7a03d82b2e 100644 --- a/pylint/config/argument.py +++ b/pylint/config/argument.py @@ -99,11 +99,20 @@ def _py_version_transformer(value: str) -> tuple[int, ...]: return version +def _regex_transformer(value: str) -> Pattern[str]: + """Return `re.compile(value)`.""" + try: + return re.compile(value) + except re.error as e: + msg = f"Error in provided regular expression: {value} beginning at index {e.pos}: {e.msg}" + raise argparse.ArgumentTypeError(msg) + + def _regexp_csv_transfomer(value: str) -> Sequence[Pattern[str]]: """Transforms a comma separated list of regular expressions.""" patterns: list[Pattern[str]] = [] for pattern in _csv_transformer(value): - patterns.append(re.compile(pattern)) + patterns.append(_regex_transformer(pattern)) return patterns @@ -130,7 +139,7 @@ def _regexp_paths_csv_transfomer(value: str) -> Sequence[Pattern[str]]: "non_empty_string": _non_empty_string_transformer, "path": _path_transformer, "py_version": _py_version_transformer, - "regexp": re.compile, + "regexp": _regex_transformer, "regexp_csv": _regexp_csv_transfomer, "regexp_paths_csv": _regexp_paths_csv_transfomer, "string": pylint_utils._unquote, @@ -263,7 +272,7 @@ class _StoreTrueArgument(_BaseStoreArgument): https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument """ - # pylint: disable-next=useless-super-delegation # We narrow down the type of action + # pylint: disable-next=useless-parent-delegation # We narrow down the type of action def __init__( self, *, @@ -356,9 +365,9 @@ def __init__( ) -> None: # The extend action is included in the stdlib from 3.8+ if PY38_PLUS: - action_class = argparse._ExtendAction # type: ignore[attr-defined] + action_class = argparse._ExtendAction else: - action_class = _ExtendAction + action_class = _ExtendAction # type: ignore[assignment] self.dest = dest """The destination of the argument.""" diff --git a/pylint/config/arguments_manager.py b/pylint/config/arguments_manager.py index 301769e770..2151aefde4 100644 --- a/pylint/config/arguments_manager.py +++ b/pylint/config/arguments_manager.py @@ -38,11 +38,13 @@ ) from pylint.config.help_formatter import _HelpFormatter from pylint.config.option import Option -from pylint.config.option_parser import OptionParser -from pylint.config.options_provider_mixin import OptionsProviderMixIn +from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] +from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] + OptionsProviderMixIn, +) from pylint.config.utils import _convert_option_to_argument, _parse_rich_type_value from pylint.constants import MAIN_CHECKER_NAME -from pylint.typing import OptionDict +from pylint.typing import DirectoryNamespaceDict, OptionDict if sys.version_info >= (3, 11): import tomllib @@ -66,6 +68,14 @@ def __init__( self._config = argparse.Namespace() """Namespace for all options.""" + self._base_config = self._config + """Fall back Namespace object created during initialization. + + This is necessary for the per-directory configuration support. Whenever we + fail to match a file with a directory we fall back to the Namespace object + created during initialization. + """ + self._arg_parser = argparse.ArgumentParser( prog=prog, usage=usage or "%(prog)s [options]", @@ -82,6 +92,9 @@ def __init__( self._option_dicts: dict[str, OptionDict] = {} """All option dictionaries that have been registered.""" + self._directory_namespaces: DirectoryNamespaceDict = {} + """Mapping of directories and their respective namespace objects.""" + # TODO: 3.0: Remove deprecated attributes introduced to keep API # parity with optparse. Until '_maxlevel' with warnings.catch_warnings(): @@ -112,6 +125,7 @@ def options_providers(self) -> list[ConfigProvider]: warnings.warn( "options_providers has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) return self._options_providers @@ -120,6 +134,7 @@ def options_providers(self, value: list[ConfigProvider]) -> None: warnings.warn( "Setting options_providers has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) self._options_providers = value @@ -241,9 +256,12 @@ def _load_default_argument_values(self) -> None: def _parse_configuration_file(self, arguments: list[str]) -> None: """Parse the arguments found in a configuration file into the namespace.""" - self.config, parsed_args = self._arg_parser.parse_known_args( - arguments, self.config - ) + try: + self.config, parsed_args = self._arg_parser.parse_known_args( + arguments, self.config + ) + except SystemExit: + sys.exit(32) unrecognized_options: list[str] = [] for opt in parsed_args: if opt.startswith("--"): @@ -269,6 +287,7 @@ def reset_parsers(self, usage: str = "") -> None: # pragma: no cover "reset_parsers has been deprecated. Parsers should be instantiated " "once during initialization and do not need to be reset.", DeprecationWarning, + stacklevel=2, ) # configuration file parser self.cfgfile_parser = configparser.ConfigParser( @@ -276,7 +295,7 @@ def reset_parsers(self, usage: str = "") -> None: # pragma: no cover ) # command line parser self.cmdline_parser = OptionParser(Option, usage=usage) - self.cmdline_parser.options_manager = self # type: ignore[attr-defined] + self.cmdline_parser.options_manager = self self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) def register_options_provider( @@ -288,6 +307,7 @@ def register_options_provider( "arguments providers should be registered by initializing ArgumentsProvider. " "This automatically registers the provider on the ArgumentsManager.", DeprecationWarning, + stacklevel=2, ) self.options_providers.append(provider) non_group_spec_options = [ @@ -332,6 +352,7 @@ def add_option_group( "registered by initializing ArgumentsProvider. " "This automatically registers the group on the ArgumentsManager.", DeprecationWarning, + stacklevel=2, ) # add option group to the command line parser if group_name in self._mygroups: @@ -368,6 +389,7 @@ def add_optik_option( "add_optik_option has been deprecated. Options should be automatically " "added by initializing an ArgumentsProvider.", DeprecationWarning, + stacklevel=2, ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -386,6 +408,7 @@ def optik_option( "optik_option has been deprecated. Parsing of option dictionaries should be done " "automatically by initializing an ArgumentsProvider.", DeprecationWarning, + stacklevel=2, ) optdict = copy.copy(optdict) if "action" in optdict: @@ -423,10 +446,14 @@ def generate_config( warnings.warn( "generate_config has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) options_by_section = {} sections = [] - for group in self._arg_parser._action_groups: + for group in sorted( + self._arg_parser._action_groups, + key=lambda x: (x.title != "Main", x.title), + ): group_name = group.title assert group_name if group_name in skipsections: @@ -438,7 +465,7 @@ def generate_config( for i in group._group_actions if not isinstance(i, argparse._SubParsersAction) ] - for opt in option_actions: + for opt in sorted(option_actions, key=lambda x: x.option_strings[0][2:]): if "--help" in opt.option_strings: continue @@ -482,6 +509,7 @@ def load_provider_defaults(self) -> None: # pragma: no cover "load_provider_defaults has been deprecated. Parsing of option defaults should be done " "automatically by initializing an ArgumentsProvider.", DeprecationWarning, + stacklevel=2, ) for provider in self.options_providers: with warnings.catch_warnings(): @@ -499,6 +527,7 @@ def read_config_file( warnings.warn( "read_config_file has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) if not config_file: if verbose: @@ -570,6 +599,7 @@ def load_config_file(self) -> None: # pragma: no cover warnings.warn( "load_config_file has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) parser = self.cfgfile_parser for section in parser.sections(): @@ -584,6 +614,7 @@ def load_configuration(self, **kwargs: Any) -> None: # pragma: no cover warnings.warn( "load_configuration has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -595,6 +626,7 @@ def load_configuration_from_config( warnings.warn( "DEPRECATED: load_configuration_from_config has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) for opt, opt_value in config.items(): opt = opt.replace("_", "-") @@ -611,6 +643,7 @@ def load_command_line_configuration( warnings.warn( "load_command_line_configuration has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) args = sys.argv[1:] if args is None else list(args) (options, args) = self.cmdline_parser.parse_args(args=args) @@ -621,7 +654,7 @@ def load_command_line_configuration( if value is None: continue setattr(config, attr, value) - return args + return args # type: ignore[return-value] def help(self, level: int | None = None) -> str: """Return the usage string based on the available options.""" @@ -630,15 +663,19 @@ def help(self, level: int | None = None) -> str: "Supplying a 'level' argument to help() has been deprecated." "You can call help() without any arguments.", DeprecationWarning, + stacklevel=2, ) return self._arg_parser.format_help() - def cb_set_provider_option(self, option, opt, value, parser): # pragma: no cover + def cb_set_provider_option( # pragma: no cover + self, option: Any, opt: Any, value: Any, parser: Any + ) -> None: """DEPRECATED: Optik callback for option setting.""" # TODO: 3.0: Remove deprecated method. warnings.warn( "cb_set_provider_option has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) if opt.startswith("--"): # remove -- on long option @@ -658,10 +695,11 @@ def global_set_option(self, opt: str, value: Any) -> None: # pragma: no cover "global_set_option has been deprecated. You can use _arguments_manager.set_option " "or linter.set_option to set options on the global configuration object.", DeprecationWarning, + stacklevel=2, ) self.set_option(opt, value) - def _generate_config_file(self) -> str: + def _generate_config_file(self, *, minimal: bool = False) -> str: """Write a configuration file according to the current configuration into stdout. """ @@ -700,19 +738,21 @@ def _generate_config_file(self) -> str: continue # Add help comment - help_msg = optdict.get("help", "") - assert isinstance(help_msg, str) - help_text = textwrap.wrap(help_msg, width=79) - for line in help_text: - group_table.add(tomlkit.comment(line)) + if not minimal: + help_msg = optdict.get("help", "") + assert isinstance(help_msg, str) + help_text = textwrap.wrap(help_msg, width=79) + for line in help_text: + group_table.add(tomlkit.comment(line)) # Get current value of option value = getattr(self.config, optname.replace("-", "_")) # Create a comment if the option has no value if not value: - group_table.add(tomlkit.comment(f"{optname} =")) - group_table.add(tomlkit.nl()) + if not minimal: + group_table.add(tomlkit.comment(f"{optname} =")) + group_table.add(tomlkit.nl()) continue # Skip deprecated options @@ -733,19 +773,24 @@ def _generate_config_file(self) -> str: if optdict.get("type") == "py_version": value = ".".join(str(i) for i in value) + # Check if it is default value if we are in minimal mode + if minimal and value == optdict.get("default"): + continue + # Add to table group_table.add(optname, value) group_table.add(tomlkit.nl()) assert group.title - pylint_tool_table.add(group.title.lower(), group_table) + if group_table: + pylint_tool_table.add(group.title.lower(), group_table) toml_string = tomlkit.dumps(toml_doc) # Make sure the string we produce is valid toml and can be parsed tomllib.loads(toml_string) - return toml_string + return str(toml_string) def set_option( self, @@ -761,12 +806,14 @@ def set_option( "The 'action' argument has been deprecated. You can use set_option " "without the 'action' or 'optdict' arguments.", DeprecationWarning, + stacklevel=2, ) if optdict != "default_value": warnings.warn( "The 'optdict' argument has been deprecated. You can use set_option " "without the 'action' or 'optdict' arguments.", DeprecationWarning, + stacklevel=2, ) self.config = self._arg_parser.parse_known_args( diff --git a/pylint/config/arguments_provider.py b/pylint/config/arguments_provider.py index 2ab44b1614..ea229e1c33 100644 --- a/pylint/config/arguments_provider.py +++ b/pylint/config/arguments_provider.py @@ -24,6 +24,7 @@ def __init__(self, *args: object) -> None: warnings.warn( "UnsupportedAction has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) super().__init__(*args) @@ -55,6 +56,7 @@ def level(self) -> int: "The level attribute has been deprecated. It was used to display the checker in the help or not," " and everything is displayed in the help now. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) return self._level @@ -65,6 +67,7 @@ def level(self, value: int) -> None: "Setting the level attribute has been deprecated. It was used to display the checker in the help or not," " and everything is displayed in the help now. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) self._level = value @@ -75,6 +78,7 @@ def config(self) -> argparse.Namespace: "The checker-specific config attribute has been deprecated. Please use " "'linter.config' to access the global configuration object.", DeprecationWarning, + stacklevel=2, ) return self._arguments_manager.config @@ -85,6 +89,7 @@ def load_defaults(self) -> None: # pragma: no cover "registered by initializing an ArgumentsProvider. " "This automatically registers the group on the ArgumentsManager.", DeprecationWarning, + stacklevel=2, ) for opt, optdict in self.options: action = optdict.get("action") @@ -105,6 +110,7 @@ def option_attrname( "option_attrname has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) if optdict is None: with warnings.catch_warnings(): @@ -118,11 +124,17 @@ def option_value(self, opt: str) -> Any: # pragma: no cover "option_value has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) return getattr(self._arguments_manager.config, opt.replace("-", "_"), None) - # pylint: disable-next=unused-argument - def set_option(self, optname, value, action=None, optdict=None): # pragma: no cover + def set_option( # pragma: no cover + self, + optname: Any, + value: Any, + action: Any = None, # pylint: disable=unused-argument + optdict: Any = None, # pylint: disable=unused-argument + ) -> None: """DEPRECATED: Method called to set an option (registered in the options list). """ @@ -131,6 +143,7 @@ def set_option(self, optname, value, action=None, optdict=None): # pragma: no c "set_option has been deprecated. You can use _arguments_manager.set_option " "or linter.set_option to set options on the global configuration object.", DeprecationWarning, + stacklevel=2, ) self._arguments_manager.set_option(optname, value) @@ -143,6 +156,7 @@ def get_option_def(self, opt: str) -> OptionDict: # pragma: no cover "get_option_def has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) assert self.options for option in self.options: @@ -169,6 +183,7 @@ def options_by_section( "options_by_section has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) sections: dict[str, list[tuple[str, OptionDict, Any]]] = {} for optname, optdict in self.options: @@ -190,6 +205,7 @@ def options_and_values( "options_and_values has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) if options is None: options = self.options diff --git a/pylint/config/callback_actions.py b/pylint/config/callback_actions.py index 4526836fc8..a4c6334641 100644 --- a/pylint/config/callback_actions.py +++ b/pylint/config/callback_actions.py @@ -158,7 +158,11 @@ def __call__( option_string: str | None = "--help-msg", ) -> None: assert isinstance(values, (list, tuple)) - self.run.linter.msgs_store.help_message(values) + values_to_print: list[str] = [] + for msg in values: + assert isinstance(msg, str) + values_to_print += utils._check_csv(msg) + self.run.linter.msgs_store.help_message(values_to_print) sys.exit(0) @@ -261,7 +265,8 @@ def __call__( values: str | Sequence[Any] | None, option_string: str | None = "--generate-rcfile", ) -> None: - # TODO: 2.15: Deprecate this after discussion about this removal has been completed. + # TODO: 2.x: Deprecate this after the auto-upgrade functionality of + # pylint-config is sufficient. with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) self.run.linter.generate_config(skipsections=("Commands",)) @@ -373,7 +378,7 @@ def _call( xabling_function: Callable[[str], None], values: str | Sequence[Any] | None, option_string: str | None, - ): + ) -> None: assert isinstance(values, (tuple, list)) for msgid in utils._check_csv(values[0]): try: diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py index 7b6dfa5680..d26f0e8c58 100644 --- a/pylint/config/config_initialization.py +++ b/pylint/config/config_initialization.py @@ -72,17 +72,25 @@ def _config_initialization( # the configuration file parsed_args_list = linter._parse_command_line_configuration(args_list) + # Remove the positional arguments separator from the list of arguments if it exists + try: + parsed_args_list.remove("--") + except ValueError: + pass + # Check if there are any options that we do not recognize unrecognized_options: list[str] = [] for opt in parsed_args_list: if opt.startswith("--"): - if len(opt) > 2: - unrecognized_options.append(opt[2:]) + unrecognized_options.append(opt[2:]) elif opt.startswith("-"): unrecognized_options.append(opt[1:]) if unrecognized_options: msg = ", ".join(unrecognized_options) - linter._arg_parser.error(f"Unrecognized option found: {msg}") + try: + linter._arg_parser.error(f"Unrecognized option found: {msg}") + except SystemExit: + sys.exit(32) # Now that config file and command line options have been loaded # with all disables, it is safe to emit messages @@ -107,6 +115,9 @@ def _config_initialization( linter._parse_error_mode() + # Link the base Namespace object on the current directory + linter._directory_namespaces[Path(".").resolve()] = (linter.config, {}) + # parsed_args_list should now only be a list of files/directories to lint. # All other options have been removed from the list. return parsed_args_list diff --git a/pylint/config/configuration_mixin.py b/pylint/config/configuration_mixin.py index 7854ff7337..55857224ac 100644 --- a/pylint/config/configuration_mixin.py +++ b/pylint/config/configuration_mixin.py @@ -2,29 +2,35 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + import warnings +from typing import Any from pylint.config.option_manager_mixin import OptionsManagerMixIn -from pylint.config.options_provider_mixin import OptionsProviderMixIn +from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] + OptionsProviderMixIn, +) -class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): +class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): # type: ignore[misc] """Basic mixin for simple configurations which don't need the manager / providers model. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: # TODO: 3.0: Remove deprecated class warnings.warn( "ConfigurationMixIn has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) if not args: kwargs.setdefault("usage", "") OptionsManagerMixIn.__init__(self, *args, **kwargs) OptionsProviderMixIn.__init__(self) if not getattr(self, "option_groups", None): - self.option_groups = [] + self.option_groups: list[tuple[str, str]] = [] for _, optdict in self.options: try: gdef = (optdict["group"].upper(), "") diff --git a/pylint/config/deprecation_actions.py b/pylint/config/deprecation_actions.py index c7c3e9b171..ceef200a7e 100644 --- a/pylint/config/deprecation_actions.py +++ b/pylint/config/deprecation_actions.py @@ -52,7 +52,7 @@ def __call__( namespace: argparse.Namespace, values: str | Sequence[Any] | None, option_string: str | None = None, - ): + ) -> None: assert isinstance(values, list) setattr(namespace, self.dest, values[0]) for old_name in self.old_names: @@ -97,7 +97,7 @@ def __call__( namespace: argparse.Namespace, values: str | Sequence[Any] | None, option_string: str | None = None, - ): + ) -> None: assert isinstance(values, list) setattr(namespace, self.dest, values[0]) warnings.warn( diff --git a/pylint/config/find_default_config_files.py b/pylint/config/find_default_config_files.py index 36917a380c..43e682a589 100644 --- a/pylint/config/find_default_config_files.py +++ b/pylint/config/find_default_config_files.py @@ -39,17 +39,26 @@ def _cfg_has_config(path: Path | str) -> bool: return any(section.startswith("pylint.") for section in parser.sections()) -def find_default_config_files() -> Iterator[Path]: - """Find all possible config files.""" +def _yield_default_files() -> Iterator[Path]: + """Iterate over the default config file names and see if they exist.""" for config_name in CONFIG_NAMES: - if config_name.is_file(): - if config_name.suffix == ".toml" and not _toml_has_config(config_name): - continue - if config_name.suffix == ".cfg" and not _cfg_has_config(config_name): - continue + try: + if config_name.is_file(): + if config_name.suffix == ".toml" and not _toml_has_config(config_name): + continue + if config_name.suffix == ".cfg" and not _cfg_has_config(config_name): + continue + + yield config_name.resolve() + except OSError: + pass + - yield config_name.resolve() +def _find_project_config() -> Iterator[Path]: + """Traverse up the directory tree to find a config file. + Stop if no '__init__' is found and thus we are no longer in a package. + """ if Path("__init__.py").is_file(): curdir = Path(os.getcwd()).resolve() while (curdir / "__init__.py").is_file(): @@ -59,6 +68,9 @@ def find_default_config_files() -> Iterator[Path]: if rc_path.is_file(): yield rc_path.resolve() + +def _find_config_in_home_or_environment() -> Iterator[Path]: + """Find a config file in the specified environment var or the home directory.""" if "PYLINTRC" in os.environ and Path(os.environ["PYLINTRC"]).exists(): if Path(os.environ["PYLINTRC"]).is_file(): yield Path(os.environ["PYLINTRC"]).resolve() @@ -68,16 +80,36 @@ def find_default_config_files() -> Iterator[Path]: except RuntimeError: # If the home directory does not exist a RuntimeError will be raised user_home = None + if user_home is not None and str(user_home) not in ("~", "/root"): home_rc = user_home / ".pylintrc" if home_rc.is_file(): yield home_rc.resolve() + home_rc = user_home / ".config" / "pylintrc" if home_rc.is_file(): yield home_rc.resolve() - if os.path.isfile("/etc/pylintrc"): - yield Path("/etc/pylintrc").resolve() + +def find_default_config_files() -> Iterator[Path]: + """Find all possible config files.""" + yield from _yield_default_files() + + try: + yield from _find_project_config() + except OSError: + pass + + try: + yield from _find_config_in_home_or_environment() + except OSError: + pass + + try: + if os.path.isfile("/etc/pylintrc"): + yield Path("/etc/pylintrc").resolve() + except OSError: + pass def find_pylintrc() -> str | None: @@ -90,6 +122,7 @@ def find_pylintrc() -> str | None: "Use find_default_config_files if you want access to pylint's configuration file " "finding logic.", DeprecationWarning, + stacklevel=2, ) for config_file in find_default_config_files(): if str(config_file).endswith("pylintrc"): diff --git a/pylint/config/option.py b/pylint/config/option.py index 5043fe7659..95248d6b1e 100644 --- a/pylint/config/option.py +++ b/pylint/config/option.py @@ -9,30 +9,38 @@ import pathlib import re import warnings +from collections.abc import Callable, Sequence from re import Pattern +from typing import Any from pylint import utils # pylint: disable=unused-argument -def _csv_validator(_, name, value): +def _csv_validator( + _: Any, name: str, value: str | list[str] | tuple[str] +) -> Sequence[str]: return utils._check_csv(value) # pylint: disable=unused-argument -def _regexp_validator(_, name, value): +def _regexp_validator( + _: Any, name: str, value: str | re.Pattern[str] +) -> re.Pattern[str]: if hasattr(value, "pattern"): - return value + return value # type: ignore[return-value] return re.compile(value) # pylint: disable=unused-argument -def _regexp_csv_validator(_, name, value): +def _regexp_csv_validator( + _: Any, name: str, value: str | list[str] +) -> list[re.Pattern[str]]: return [_regexp_validator(_, name, val) for val in _csv_validator(_, name, value)] def _regexp_paths_csv_validator( - _, name: str, value: str | list[Pattern[str]] + _: Any, name: str, value: str | list[Pattern[str]] ) -> list[Pattern[str]]: if isinstance(value, list): return value @@ -48,14 +56,14 @@ def _regexp_paths_csv_validator( return patterns -def _choice_validator(choices, name, value): +def _choice_validator(choices: list[Any], name: str, value: Any) -> Any: if value not in choices: msg = "option %s: invalid value: %r, should be in %s" raise optparse.OptionValueError(msg % (name, value, choices)) return value -def _yn_validator(opt, _, value): +def _yn_validator(opt: str, _: str, value: Any) -> bool: if isinstance(value, int): return bool(value) if isinstance(value, str): @@ -68,7 +76,7 @@ def _yn_validator(opt, _, value): raise optparse.OptionValueError(msg % (opt, value)) -def _multiple_choice_validator(choices, name, value): +def _multiple_choice_validator(choices: list[Any], name: str, value: Any) -> Any: values = utils._check_csv(value) for csv_value in values: if csv_value not in choices: @@ -77,18 +85,24 @@ def _multiple_choice_validator(choices, name, value): return values -def _non_empty_string_validator(opt, _, value): # pragma: no cover # Unused +def _non_empty_string_validator( # pragma: no cover # Unused + opt: Any, _: str, value: str +) -> str: if not value: msg = "indent string can't be empty." raise optparse.OptionValueError(msg) return utils._unquote(value) -def _multiple_choices_validating_option(opt, name, value): # pragma: no cover # Unused - return _multiple_choice_validator(opt.choices, name, value) +def _multiple_choices_validating_option( # pragma: no cover # Unused + opt: optparse.Option, name: str, value: Any +) -> Any: + return _multiple_choice_validator( + opt.choices, name, value # type: ignore[attr-defined] + ) -def _py_version_validator(_, name, value): +def _py_version_validator(_: Any, name: str, value: Any) -> tuple[int, int, int]: if not isinstance(value, tuple): try: value = tuple(int(val) for val in value.split(".")) @@ -96,10 +110,10 @@ def _py_version_validator(_, name, value): raise optparse.OptionValueError( f"Invalid format for {name}, should be version string. E.g., '3.8'" ) from None - return value + return value # type: ignore[no-any-return] -VALIDATORS = { +VALIDATORS: dict[str, Callable[[Any, str, Any], Any] | Callable[[Any], Any]] = { "string": utils._unquote, "int": int, "float": float, @@ -120,21 +134,21 @@ def _py_version_validator(_, name, value): } -def _call_validator(opttype, optdict, option, value): +def _call_validator(opttype: str, optdict: Any, option: str, value: Any) -> Any: if opttype not in VALIDATORS: - raise Exception(f'Unsupported type "{opttype}"') + raise TypeError(f'Unsupported type "{opttype}"') try: - return VALIDATORS[opttype](optdict, option, value) + return VALIDATORS[opttype](optdict, option, value) # type: ignore[call-arg] except TypeError: try: - return VALIDATORS[opttype](value) + return VALIDATORS[opttype](value) # type: ignore[call-arg] except Exception as e: raise optparse.OptionValueError( f"{option} value ({value!r}) should be of type {opttype}" ) from e -def _validate(value, optdict, name=""): +def _validate(value: Any, optdict: Any, name: str = "") -> Any: """Return a validated value for an option according to its type. optional argument name is only used for error message formatting @@ -171,37 +185,41 @@ class Option(optparse.Option): TYPE_CHECKER["non_empty_string"] = _non_empty_string_validator TYPE_CHECKER["py_version"] = _py_version_validator - def __init__(self, *opts, **attrs): + def __init__(self, *opts: Any, **attrs: Any) -> None: # TODO: 3.0: Remove deprecated class warnings.warn( "Option has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) super().__init__(*opts, **attrs) if hasattr(self, "hide") and self.hide: self.help = optparse.SUPPRESS_HELP - def _check_choice(self): + def _check_choice(self) -> None: if self.type in {"choice", "multiple_choice", "confidence"}: - if self.choices is None: + if self.choices is None: # type: ignore[attr-defined] raise optparse.OptionError( "must supply a list of choices for type 'choice'", self ) - if not isinstance(self.choices, (tuple, list)): + if not isinstance(self.choices, (tuple, list)): # type: ignore[attr-defined] raise optparse.OptionError( # pylint: disable-next=consider-using-f-string "choices must be a list of strings ('%s' supplied)" - % str(type(self.choices)).split("'")[1], + % str(type(self.choices)).split("'")[1], # type: ignore[attr-defined] self, ) - elif self.choices is not None: + elif self.choices is not None: # type: ignore[attr-defined] raise optparse.OptionError( f"must not supply choices for type {self.type!r}", self ) optparse.Option.CHECK_METHODS[2] = _check_choice # type: ignore[index] - def process(self, opt, value, values, parser): # pragma: no cover # Argparse + def process( # pragma: no cover # Argparse + self, opt: Any, value: Any, values: Any, parser: Any + ) -> int: + assert isinstance(self.dest, str) if self.callback and self.callback.__module__ == "pylint.lint.run": return 1 # First, convert the value(s) to the right type. Howl if any diff --git a/pylint/config/option_manager_mixin.py b/pylint/config/option_manager_mixin.py index 2f0aac75fd..c468f494fc 100644 --- a/pylint/config/option_manager_mixin.py +++ b/pylint/config/option_manager_mixin.py @@ -2,6 +2,7 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + # pylint: disable=duplicate-code from __future__ import annotations @@ -14,31 +15,37 @@ import os import sys import warnings +from collections.abc import Iterator from pathlib import Path -from typing import Any, TextIO +from typing import TYPE_CHECKING, Any, TextIO from pylint import utils from pylint.config.option import Option -from pylint.config.option_parser import OptionParser +from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] from pylint.typing import OptionDict +if TYPE_CHECKING: + from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] + OptionsProviderMixin, + ) + if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib -def _expand_default(self, option): +def _expand_default(self: optparse.HelpFormatter, option: Option) -> str: """Patch OptionParser.expand_default with custom behaviour. This will handle defaults to avoid overriding values in the configuration file. """ if self.parser is None or not self.default_tag: - return option.help + return str(option.help) optname = option._long_opts[0][2:] try: - provider = self.parser.options_manager._all_options[optname] + provider = self.parser.options_manager._all_options[optname] # type: ignore[attr-defined] except KeyError: value = None else: @@ -48,41 +55,42 @@ def _expand_default(self, option): value = utils._format_option_value(optdict, value) if value is optparse.NO_DEFAULT or not value: value = self.NO_DEFAULT_VALUE - return option.help.replace(self.default_tag, str(value)) + return option.help.replace(self.default_tag, str(value)) # type: ignore[union-attr] @contextlib.contextmanager -def _patch_optparse(): +def _patch_optparse() -> Iterator[None]: # pylint: disable = redefined-variable-type orig_default = optparse.HelpFormatter try: - optparse.HelpFormatter.expand_default = _expand_default + optparse.HelpFormatter.expand_default = _expand_default # type: ignore[assignment] yield finally: - optparse.HelpFormatter.expand_default = orig_default + optparse.HelpFormatter.expand_default = orig_default # type: ignore[assignment] class OptionsManagerMixIn: """Handle configuration from both a configuration file and command line options.""" - def __init__(self, usage): + def __init__(self, usage: str) -> None: # TODO: 3.0: Remove deprecated class warnings.warn( "OptionsManagerMixIn has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) self.reset_parsers(usage) # list of registered options providers - self.options_providers = [] + self.options_providers: list[OptionsProviderMixin] = [] # dictionary associating option name to checker - self._all_options = collections.OrderedDict() - self._short_options = {} - self._nocallback_options = {} - self._mygroups = {} + self._all_options: collections.OrderedDict[Any, Any] = collections.OrderedDict() + self._short_options: dict[Any, Any] = {} + self._nocallback_options: dict[Any, Any] = {} + self._mygroups: dict[Any, Any] = {} # verbosity self._maxlevel = 0 - def reset_parsers(self, usage=""): + def reset_parsers(self, usage: str = "") -> None: # configuration file parser self.cfgfile_parser = configparser.ConfigParser( inline_comment_prefixes=("#", ";") @@ -92,7 +100,9 @@ def reset_parsers(self, usage=""): self.cmdline_parser.options_manager = self self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) - def register_options_provider(self, provider, own_group=True): + def register_options_provider( + self, provider: OptionsProviderMixin, own_group: bool = True + ) -> None: """Register an options provider.""" self.options_providers.append(provider) non_group_spec_options = [ @@ -118,7 +128,9 @@ def register_options_provider(self, provider, own_group=True): ] self.add_option_group(gname, gdoc, goptions, provider) - def add_option_group(self, group_name, _, options, provider): + def add_option_group( + self, group_name: str, _: Any, options: Any, provider: OptionsProviderMixin + ) -> None: # add option group to the command line parser if group_name in self._mygroups: group = self._mygroups[group_name] @@ -131,7 +143,7 @@ def add_option_group(self, group_name, _, options, provider): # add section to the config file if ( group_name != "DEFAULT" - and group_name not in self.cfgfile_parser._sections + and group_name not in self.cfgfile_parser._sections # type: ignore[attr-defined] ): self.cfgfile_parser.add_section(group_name) # add provider's specific options @@ -140,13 +152,21 @@ def add_option_group(self, group_name, _, options, provider): optdict["action"] = "callback" self.add_optik_option(provider, group, opt, optdict) - def add_optik_option(self, provider, optikcontainer, opt, optdict): + def add_optik_option( + self, + provider: OptionsProviderMixin, + optikcontainer: Any, + opt: str, + optdict: OptionDict, + ) -> None: args, optdict = self.optik_option(provider, opt, optdict) option = optikcontainer.add_option(*args, **optdict) self._all_options[opt] = provider self._maxlevel = max(self._maxlevel, option.level or 0) - def optik_option(self, provider, opt, optdict): + def optik_option( + self, provider: OptionsProviderMixin, opt: str, optdict: OptionDict + ) -> tuple[list[str], OptionDict]: """Get our personal option definition and return a suitable form for use with optik/optparse. """ @@ -164,12 +184,12 @@ def optik_option(self, provider, opt, optdict): and optdict.get("default") is not None and optdict["action"] not in ("store_true", "store_false") ): - optdict["help"] += " [current: %default]" + optdict["help"] += " [current: %default]" # type: ignore[operator] del optdict["default"] args = ["--" + str(opt)] if "short" in optdict: self._short_options[optdict["short"]] = opt - args.append("-" + optdict["short"]) + args.append("-" + optdict["short"]) # type: ignore[operator] del optdict["short"] # cleanup option definition dict before giving it to optik for key in list(optdict.keys()): @@ -177,7 +197,9 @@ def optik_option(self, provider, opt, optdict): optdict.pop(key) return args, optdict - def cb_set_provider_option(self, option, opt, value, parser): + def cb_set_provider_option( + self, option: Option, opt: str, value: Any, parser: Any + ) -> None: """Optik callback for option setting.""" if opt.startswith("--"): # remove -- on long option @@ -190,7 +212,7 @@ def cb_set_provider_option(self, option, opt, value, parser): value = 1 self.global_set_option(opt, value) - def global_set_option(self, opt, value): + def global_set_option(self, opt: str, value: Any) -> None: """Set option on the correct option provider.""" self._all_options[opt].set_option(opt, value) @@ -229,7 +251,7 @@ def generate_config( ) printed = True - def load_provider_defaults(self): + def load_provider_defaults(self) -> None: """Initialize configuration using default values.""" for provider in self.options_providers: provider.load_defaults() @@ -256,11 +278,11 @@ def read_config_file( with open(config_file, encoding="utf_8_sig") as fp: parser.read_file(fp) # normalize each section's title - for sect, values in list(parser._sections.items()): + for sect, values in list(parser._sections.items()): # type: ignore[attr-defined] if sect.startswith("pylint."): sect = sect[len("pylint.") :] if not sect.isupper() and values: - parser._sections[sect.upper()] = values + parser._sections[sect.upper()] = values # type: ignore[attr-defined] if not verbose: return @@ -302,7 +324,7 @@ def _parse_toml(self, config_file: Path, parser: configparser.ConfigParser) -> N parser.add_section(section_name) parser.set(section_name, option, value=value) - def load_config_file(self): + def load_config_file(self) -> None: """Dispatch values previously read from a configuration file to each option's provider. """ @@ -314,17 +336,19 @@ def load_config_file(self): except (KeyError, optparse.OptionError): continue - def load_configuration(self, **kwargs): + def load_configuration(self, **kwargs: Any) -> None: """Override configuration according to given parameters.""" return self.load_configuration_from_config(kwargs) - def load_configuration_from_config(self, config): + def load_configuration_from_config(self, config: dict[str, Any]) -> None: for opt, opt_value in config.items(): opt = opt.replace("_", "-") provider = self._all_options[opt] provider.set_option(opt, opt_value) - def load_command_line_configuration(self, args=None) -> list[str]: + def load_command_line_configuration( + self, args: list[str] | None = None + ) -> list[str]: """Override configuration according to command line parameters. return additional arguments @@ -339,10 +363,10 @@ def load_command_line_configuration(self, args=None) -> list[str]: if value is None: continue setattr(config, attr, value) - return args + return args # type: ignore[return-value] - def help(self, level=0): + def help(self, level: int = 0) -> str: """Return the usage string for available options.""" self.cmdline_parser.formatter.output_level = level with _patch_optparse(): - return self.cmdline_parser.format_help() + return str(self.cmdline_parser.format_help()) diff --git a/pylint/config/option_parser.py b/pylint/config/option_parser.py index b58fad3a44..c527c4f60f 100644 --- a/pylint/config/option_parser.py +++ b/pylint/config/option_parser.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# type: ignore # Deprecated module. + import optparse # pylint: disable=deprecated-module import warnings @@ -23,6 +25,7 @@ def __init__(self, option_class, *args, **kwargs): warnings.warn( "OptionParser has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) super().__init__(option_class=Option, *args, **kwargs) diff --git a/pylint/config/options_provider_mixin.py b/pylint/config/options_provider_mixin.py index 5b20a290fb..67f64ee0a5 100644 --- a/pylint/config/options_provider_mixin.py +++ b/pylint/config/options_provider_mixin.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# type: ignore # Deprecated module. + import optparse # pylint: disable=deprecated-module import warnings @@ -27,6 +29,7 @@ def __init__(self): warnings.warn( "OptionsProviderMixIn has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) self.config = optparse.Values() self.load_defaults() diff --git a/pylint/config/utils.py b/pylint/config/utils.py index a5d7b4d3d3..d7cbd7c075 100644 --- a/pylint/config/utils.py +++ b/pylint/config/utils.py @@ -42,7 +42,8 @@ def _convert_option_to_argument( if "level" in optdict and "hide" not in optdict: warnings.warn( "The 'level' key in optdicts has been deprecated. " - "Use 'hide' with a boolean to hide an option from the help message.", + "Use 'hide' with a boolean to hide an option from the help message. " + f"optdict={optdict}", DeprecationWarning, ) @@ -79,7 +80,8 @@ def _convert_option_to_argument( warnings.warn( "An option dictionary should have a 'default' key to specify " "the option's default value. This key will be required in pylint " - "3.0. It is not required for 'store_true' and callable actions.", + "3.0. It is not required for 'store_true' and callable actions. " + f"optdict={optdict}", DeprecationWarning, ) default = None @@ -151,7 +153,7 @@ def _parse_rich_type_value(value: Any) -> str: if isinstance(value, (list, tuple)): return ",".join(_parse_rich_type_value(i) for i in value) if isinstance(value, re.Pattern): - return value.pattern + return str(value.pattern) if isinstance(value, dict): return ",".join(f"{k}:{v}" for k, v in value.items()) return str(value) @@ -266,7 +268,7 @@ def _preprocess_options(run: Run, args: Sequence[str]) -> list[str]: raise ArgumentPreprocessingError(f"Option {option} expects a value") value = args[i] elif not takearg and value is not None: - raise ArgumentPreprocessingError(f"Option {option} doesn't expects a value") + raise ArgumentPreprocessingError(f"Option {option} doesn't expect a value") cb(run, value) i += 1 diff --git a/pylint/constants.py b/pylint/constants.py index a609f9cd6f..3bbda93d0f 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -99,9 +99,6 @@ class WarningScope: ) -TYPING_TYPE_CHECKS_GUARDS = frozenset({"typing.TYPE_CHECKING", "TYPE_CHECKING"}) - - def _warn_about_old_home(pylint_home: pathlib.Path) -> None: """Warn users about the old pylint home being deprecated. @@ -155,3 +152,146 @@ def _get_pylint_home() -> str: PYLINT_HOME = _get_pylint_home() + +TYPING_NORETURN = frozenset( + ( + "typing.NoReturn", + "typing_extensions.NoReturn", + ) +) +TYPING_NEVER = frozenset( + ( + "typing.Never", + "typing_extensions.Never", + ) +) + +DUNDER_METHODS: dict[tuple[int, int], dict[str, str]] = { + (0, 0): { + "__init__": "Instantiate class directly", + "__del__": "Use del keyword", + "__repr__": "Use repr built-in function", + "__str__": "Use str built-in function", + "__bytes__": "Use bytes built-in function", + "__format__": "Use format built-in function, format string method, or f-string", + "__lt__": "Use < operator", + "__le__": "Use <= operator", + "__eq__": "Use == operator", + "__ne__": "Use != operator", + "__gt__": "Use > operator", + "__ge__": "Use >= operator", + "__hash__": "Use hash built-in function", + "__bool__": "Use bool built-in function", + "__getattr__": "Access attribute directly or use getattr built-in function", + "__getattribute__": "Access attribute directly or use getattr built-in function", + "__setattr__": "Set attribute directly or use setattr built-in function", + "__delattr__": "Use del keyword", + "__dir__": "Use dir built-in function", + "__get__": "Use get method", + "__set__": "Use set method", + "__delete__": "Use del keyword", + "__instancecheck__": "Use isinstance built-in function", + "__subclasscheck__": "Use issubclass built-in function", + "__call__": "Invoke instance directly", + "__len__": "Use len built-in function", + "__length_hint__": "Use length_hint method", + "__getitem__": "Access item via subscript", + "__setitem__": "Set item via subscript", + "__delitem__": "Use del keyword", + "__iter__": "Use iter built-in function", + "__next__": "Use next built-in function", + "__reversed__": "Use reversed built-in function", + "__contains__": "Use in keyword", + "__add__": "Use + operator", + "__sub__": "Use - operator", + "__mul__": "Use * operator", + "__matmul__": "Use @ operator", + "__truediv__": "Use / operator", + "__floordiv__": "Use // operator", + "__mod__": "Use % operator", + "__divmod__": "Use divmod built-in function", + "__pow__": "Use ** operator or pow built-in function", + "__lshift__": "Use << operator", + "__rshift__": "Use >> operator", + "__and__": "Use & operator", + "__xor__": "Use ^ operator", + "__or__": "Use | operator", + "__radd__": "Use + operator", + "__rsub__": "Use - operator", + "__rmul__": "Use * operator", + "__rmatmul__": "Use @ operator", + "__rtruediv__": "Use / operator", + "__rfloordiv__": "Use // operator", + "__rmod__": "Use % operator", + "__rdivmod__": "Use divmod built-in function", + "__rpow__": "Use ** operator or pow built-in function", + "__rlshift__": "Use << operator", + "__rrshift__": "Use >> operator", + "__rand__": "Use & operator", + "__rxor__": "Use ^ operator", + "__ror__": "Use | operator", + "__iadd__": "Use += operator", + "__isub__": "Use -= operator", + "__imul__": "Use *= operator", + "__imatmul__": "Use @= operator", + "__itruediv__": "Use /= operator", + "__ifloordiv__": "Use //= operator", + "__imod__": "Use %= operator", + "__ipow__": "Use **= operator", + "__ilshift__": "Use <<= operator", + "__irshift__": "Use >>= operator", + "__iand__": "Use &= operator", + "__ixor__": "Use ^= operator", + "__ior__": "Use |= operator", + "__neg__": "Multiply by -1 instead", + "__pos__": "Multiply by +1 instead", + "__abs__": "Use abs built-in function", + "__invert__": "Use ~ operator", + "__complex__": "Use complex built-in function", + "__int__": "Use int built-in function", + "__float__": "Use float built-in function", + "__round__": "Use round built-in function", + "__trunc__": "Use math.trunc function", + "__floor__": "Use math.floor function", + "__ceil__": "Use math.ceil function", + "__enter__": "Invoke context manager directly", + "__aenter__": "Invoke context manager directly", + "__copy__": "Use copy.copy function", + "__deepcopy__": "Use copy.deepcopy function", + "__fspath__": "Use os.fspath function instead", + }, + (3, 10): { + "__aiter__": "Use aiter built-in function", + "__anext__": "Use anext built-in function", + }, +} + +EXTRA_DUNDER_METHODS = [ + "__new__", + "__subclasses__", + "__init_subclass__", + "__set_name__", + "__class_getitem__", + "__missing__", + "__exit__", + "__await__", + "__aexit__", + "__getnewargs_ex__", + "__getnewargs__", + "__getstate__", + "__setstate__", + "__reduce__", + "__reduce_ex__", + "__post_init__", # part of `dataclasses` module +] + +DUNDER_PROPERTIES = [ + "__class__", + "__dict__", + "__doc__", + "__format__", + "__module__", + "__sizeof__", + "__subclasshook__", + "__weakref__", +] diff --git a/pylint/epylint.py b/pylint/epylint.py index b6b6bf402a..dd23b450be 100755 --- a/pylint/epylint.py +++ b/pylint/epylint.py @@ -41,6 +41,7 @@ import os import shlex import sys +import warnings from collections.abc import Sequence from io import StringIO from subprocess import PIPE, Popen @@ -106,7 +107,6 @@ def lint(filename: str, options: Sequence[str] = ()) -> int: with Popen( cmd, stdout=PIPE, cwd=parent_path, env=_get_env(), universal_newlines=True ) as process: - for line in process.stdout: # type: ignore[union-attr] # remove pylintrc warning if line.startswith("No config file found"): @@ -168,6 +168,11 @@ def py_run( To silently run Pylint on a module, and get its standard output and error: >>> (pylint_stdout, pylint_stderr) = py_run( 'module_name.py', True) """ + warnings.warn( + "'epylint' will be removed in pylint 3.0, use https://github.com/emacsorphanage/pylint instead.", + DeprecationWarning, + stacklevel=2, + ) # Detect if we use Python as executable or not, else default to `python` executable = sys.executable if "python" in sys.executable else "python" @@ -198,6 +203,11 @@ def py_run( def Run(argv: Sequence[str] | None = None) -> NoReturn: + warnings.warn( + "'epylint' will be removed in pylint 3.0, use https://github.com/emacsorphanage/pylint instead.", + DeprecationWarning, + stacklevel=2, + ) if not argv and len(sys.argv) == 1: print(f"Usage: {sys.argv[0]} [options]") sys.exit(1) diff --git a/pylint/extensions/_check_docs_utils.py b/pylint/extensions/_check_docs_utils.py index d8bf22e228..811bd67b57 100644 --- a/pylint/extensions/_check_docs_utils.py +++ b/pylint/extensions/_check_docs_utils.py @@ -10,6 +10,7 @@ import astroid from astroid import nodes +from astroid.util import Uninferable from pylint.checkers import utils @@ -42,7 +43,7 @@ def get_setters_property_name(node: nodes.FunctionDef) -> str | None: and decorator.attrname == "setter" and isinstance(decorator.expr, nodes.Name) ): - return decorator.expr.name + return decorator.expr.name # type: ignore[no-any-return] return None @@ -88,7 +89,7 @@ def returns_something(return_node: nodes.Return) -> bool: return not (isinstance(returns, nodes.Const) and returns.value is None) -def _get_raise_target(node): +def _get_raise_target(node: nodes.NodeNG) -> nodes.NodeNG | Uninferable | None: if isinstance(node.exc, nodes.Call): func = node.exc.func if isinstance(func, (nodes.Name, nodes.Attribute)): @@ -246,7 +247,7 @@ class SphinxDocstring(Docstring): re_multiple_simple_type = r""" (?:{container_type}|{type}) - (?:(?:\s+(?:of|or)\s+|\s*,\s*)(?:{container_type}|{type}))* + (?:(?:\s+(?:of|or)\s+|\s*,\s*|\s+\|\s+)(?:{container_type}|{type}))* """.format( type=re_type, container_type=re_simple_container_type ) @@ -448,7 +449,7 @@ class GoogleDocstring(Docstring): re_multiple_type = r""" (?:{container_type}|{type}|{xref}) - (?:(?:\s+(?:of|or)\s+|\s*,\s*)(?:{container_type}|{type}|{xref}))* + (?:(?:\s+(?:of|or)\s+|\s*,\s*|\s+\|\s+)(?:{container_type}|{type}|{xref}))* """.format( type=re_type, xref=re_xref, container_type=re_container_type ) @@ -470,7 +471,7 @@ class GoogleDocstring(Docstring): re_param_line = re.compile( rf""" - \s* ((?:\\?\*{{0,2}})?\w+) # identifier potentially with asterisks + \s* ((?:\\?\*{{0,2}})?[\w\\]+) # identifier potentially with asterisks or escaped `\` \s* ( [(] {re_multiple_type} (?:,\s+optional)? @@ -727,13 +728,13 @@ class NumpyDocstring(GoogleDocstring): re.X | re.S | re.M, ) - re_default_value = r"""((['"]\w+\s*['"])|(True)|(False)|(None))""" + re_default_value = r"""((['"]\w+\s*['"])|(\d+)|(True)|(False)|(None))""" re_param_line = re.compile( rf""" - \s* (\*{{0,2}}\w+)(\s?(:|\n)) # identifier with potential asterisks + \s* (?P\*{{0,2}}\w+)(\s?(:|\n)) # identifier with potential asterisks \s* - ( + (?P ( ({GoogleDocstring.re_multiple_type}) # default type declaration (,\s+optional)? # optional 'optional' indication @@ -741,8 +742,11 @@ class NumpyDocstring(GoogleDocstring): ( {{({re_default_value},?\s*)+}} # set of default values )? - \n)? - \s* (.*) # optional description + (?:$|\n) + )? + ( + \s* (?P.*) # optional description + )? """, re.X | re.S, ) @@ -793,15 +797,26 @@ def match_param_docs(self) -> tuple[set[str], set[str]]: continue # check if parameter has description only - re_only_desc = re.match(r"\s* (\*{0,2}\w+)\s*:?\n", entry) + re_only_desc = re.match(r"\s*(\*{0,2}\w+)\s*:?\n\s*\w*$", entry) if re_only_desc: - param_name = match.group(1) - param_desc = match.group(2) + param_name = match.group("param_name") + param_desc = match.group("param_type") param_type = None else: - param_name = match.group(1) - param_type = match.group(3) - param_desc = match.group(4) + param_name = match.group("param_name") + param_type = match.group("param_type") + param_desc = match.group("param_desc") + # The re_param_line pattern needs to match multi-line which removes the ability + # to match a single line description like 'arg : a number type.' + # We are not trying to determine whether 'a number type' is correct typing + # but we do accept it as typing as it is in the place where typing + # should be + if param_type is None and re.match(r"\s*(\*{0,2}\w+)\s*:.+$", entry): + param_type = param_desc + # If the description is "" but we have a type description + # we consider the description to be the type + if not param_desc and param_type: + param_desc = param_type if param_type: params_with_type.add(param_name) diff --git a/pylint/extensions/bad_builtin.py b/pylint/extensions/bad_builtin.py index 7ffaf0f6c7..904b2a3943 100644 --- a/pylint/extensions/bad_builtin.py +++ b/pylint/extensions/bad_builtin.py @@ -18,12 +18,11 @@ BAD_FUNCTIONS = ["map", "filter"] # Some hints regarding the use of bad builtins. -BUILTIN_HINTS = {"map": "Using a list comprehension can be clearer."} -BUILTIN_HINTS["filter"] = BUILTIN_HINTS["map"] +LIST_COMP_MSG = "Using a list comprehension can be clearer." +BUILTIN_HINTS = {"map": LIST_COMP_MSG, "filter": LIST_COMP_MSG} class BadBuiltinChecker(BaseChecker): - name = "deprecated_builtins" msgs = { "W0141": ( diff --git a/pylint/extensions/broad_try_clause.py b/pylint/extensions/broad_try_clause.py index bfbb7c9e9f..2291d32d4e 100644 --- a/pylint/extensions/broad_try_clause.py +++ b/pylint/extensions/broad_try_clause.py @@ -46,7 +46,7 @@ class BroadTryClauseChecker(checkers.BaseChecker): ), ) - def _count_statements(self, try_node): + def _count_statements(self, try_node: nodes.TryExcept | nodes.TryFinally) -> int: statement_count = len(try_node.body) for body_node in try_node.body: @@ -58,13 +58,15 @@ def _count_statements(self, try_node): def visit_tryexcept(self, node: nodes.TryExcept | nodes.TryFinally) -> None: try_clause_statements = self._count_statements(node) if try_clause_statements > self.linter.config.max_try_statements: - msg = f"try clause contains {try_clause_statements} statements, expected at most {self.linter.config.max_try_statements}" + msg = ( + f"try clause contains {try_clause_statements} statements, expected at" + f" most {self.linter.config.max_try_statements}" + ) self.add_message( "too-many-try-statements", node.lineno, node=node, args=msg ) - def visit_tryfinally(self, node: nodes.TryFinally) -> None: - self.visit_tryexcept(node) + visit_tryfinally = visit_tryexcept def register(linter: PyLinter) -> None: diff --git a/pylint/extensions/check_elif.py b/pylint/extensions/check_elif.py index 53eec7f7e6..b584ea35ef 100644 --- a/pylint/extensions/check_elif.py +++ b/pylint/extensions/check_elif.py @@ -4,6 +4,7 @@ from __future__ import annotations +import tokenize from tokenize import TokenInfo from typing import TYPE_CHECKING @@ -31,12 +32,12 @@ class ElseifUsedChecker(BaseTokenChecker): ) } - def __init__(self, linter=None): + def __init__(self, linter: PyLinter) -> None: super().__init__(linter) self._init() - def _init(self): - self._elifs = {} + def _init(self) -> None: + self._elifs: dict[tokenize._Position, str] = {} def process_tokens(self, tokens: list[TokenInfo]) -> None: """Process tokens and look for 'if' or 'elif'.""" diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index ef8aaea785..262a7f0c4d 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -5,12 +5,13 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING, Tuple, Type, Union, cast +from typing import TYPE_CHECKING, Tuple, Type, cast from astroid import nodes from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import only_required_for_messages, safe_infer +from pylint.interfaces import INFERENCE if TYPE_CHECKING: from pylint.lint import PyLinter @@ -58,6 +59,16 @@ class CodeStyleChecker(BaseChecker): "both can be combined by using an assignment expression ``:=``. " "Requires Python 3.8 and ``py-version >= 3.8``.", ), + "R6104": ( + "Use '%s' to do an augmented assign directly", + "consider-using-augmented-assign", + "Emitted when an assignment is referring to the object that it is assigning " + "to. This can be changed to be an augmented assign.\n" + "Disabled by default!", + { + "default_enabled": False, + }, + ), } options = ( ( @@ -164,13 +175,11 @@ def _check_dict_consider_namedtuple_dataclass(self, node: nodes.Dict) -> None: if list_length == 0: return for _, dict_value in node.items[1:]: - dict_value = cast(Union[nodes.List, nodes.Tuple], dict_value) if len(dict_value.elts) != list_length: return # Make sure at least one list entry isn't a dict for _, dict_value in node.items: - dict_value = cast(Union[nodes.List, nodes.Tuple], dict_value) if all(isinstance(entry, nodes.Dict) for entry in dict_value.elts): return @@ -216,7 +225,6 @@ def _check_consider_using_assignment_expr(self, node: nodes.If) -> None: if CodeStyleChecker._check_prev_sibling_to_if_stmt( prev_sibling, node_name.name ): - # Check if match statement would be a better fit. # I.e. multiple ifs that test the same name. if CodeStyleChecker._check_ignore_assignment_expr_suggestion( @@ -303,6 +311,19 @@ def _check_ignore_assignment_expr_suggestion( return True return False + @only_required_for_messages("consider-using-augmented-assign") + def visit_assign(self, node: nodes.Assign) -> None: + is_aug, op = utils.is_augmented_assign(node) + if is_aug: + self.add_message( + "consider-using-augmented-assign", + args=f"{op}=", + node=node, + line=node.lineno, + col_offset=node.col_offset, + confidence=INFERENCE, + ) + def register(linter: PyLinter) -> None: linter.register_checker(CodeStyleChecker(linter)) diff --git a/pylint/extensions/comparetozero.py b/pylint/extensions/comparetozero.py index dd0f1949a1..116bf229a3 100644 --- a/pylint/extensions/comparetozero.py +++ b/pylint/extensions/comparetozero.py @@ -7,21 +7,25 @@ from __future__ import annotations import itertools -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import astroid from astroid import nodes from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter -def _is_constant_zero(node): - return isinstance(node, astroid.Const) and node.value == 0 +def _is_constant_zero(node: str | nodes.NodeNG) -> bool: + # We have to check that node.value is not False because node.value == 0 is True + # when node.value is False + return ( + isinstance(node, astroid.Const) and node.value == 0 and node.value is not False + ) class CompareToZeroChecker(checkers.BaseChecker): @@ -36,7 +40,7 @@ class CompareToZeroChecker(checkers.BaseChecker): name = "compare-to-zero" msgs = { "C2001": ( - "Avoid comparisons to zero", + '"%s" can be simplified to "%s" as 0 is falsey', "compare-to-zero", "Used when Pylint detects comparison to a 0 constant.", ) @@ -52,26 +56,39 @@ def visit_compare(self, node: nodes.Compare) -> None: # while the rest are a list of tuples in node.ops # the format of the tuple is ('compare operator sign', node) # here we squash everything into `ops` to make it easier for processing later - ops = [("", node.left)] + ops: list[tuple[str, nodes.NodeNG]] = [("", node.left)] ops.extend(node.ops) - iter_ops: Iterable[Any] = iter(ops) - ops = list(itertools.chain(*iter_ops)) + iter_ops = iter(ops) + all_ops = list(itertools.chain(*iter_ops)) - for ops_idx in range(len(ops) - 2): - op_1 = ops[ops_idx] - op_2 = ops[ops_idx + 1] - op_3 = ops[ops_idx + 2] + for ops_idx in range(len(all_ops) - 2): + op_1 = all_ops[ops_idx] + op_2 = all_ops[ops_idx + 1] + op_3 = all_ops[ops_idx + 2] error_detected = False # 0 ?? X if _is_constant_zero(op_1) and op_2 in _operators: error_detected = True + op = op_3 # X ?? 0 elif op_2 in _operators and _is_constant_zero(op_3): error_detected = True + op = op_1 if error_detected: - self.add_message("compare-to-zero", node=node) + original = f"{op_1.as_string()} {op_2} {op_3.as_string()}" + suggestion = ( + op.as_string() + if op_2 in {"!=", "is not"} + else f"not {op.as_string()}" + ) + self.add_message( + "compare-to-zero", + args=(original, suggestion), + node=node, + confidence=HIGH, + ) def register(linter: PyLinter) -> None: diff --git a/pylint/extensions/comparison_placement.py b/pylint/extensions/comparison_placement.py index 264b2402db..df7cc9890f 100644 --- a/pylint/extensions/comparison_placement.py +++ b/pylint/extensions/comparison_placement.py @@ -45,7 +45,7 @@ def _check_misplaced_constant( left: nodes.NodeNG, right: nodes.NodeNG, operator: str, - ): + ) -> None: if isinstance(right, nodes.Const): return operator = REVERSED_COMPS.get(operator, operator) diff --git a/pylint/extensions/confusing_elif.py b/pylint/extensions/confusing_elif.py index 8e70cab606..174f464aca 100644 --- a/pylint/extensions/confusing_elif.py +++ b/pylint/extensions/confusing_elif.py @@ -40,7 +40,7 @@ def visit_if(self, node: nodes.If) -> None: self.add_message("confusing-consecutive-elif", node=node.orelse[0]) @staticmethod - def _has_no_else_clause(node: nodes.If): + def _has_no_else_clause(node: nodes.If) -> bool: orelse = node.orelse while orelse and isinstance(orelse[0], nodes.If): orelse = orelse[0].orelse diff --git a/pylint/extensions/consider_refactoring_into_while_condition.py b/pylint/extensions/consider_refactoring_into_while_condition.py new file mode 100644 index 0000000000..b4b53d8fa9 --- /dev/null +++ b/pylint/extensions/consider_refactoring_into_while_condition.py @@ -0,0 +1,93 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Looks for try/except statements with too much code in the try clause.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint import checkers +from pylint.checkers import utils +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class ConsiderRefactorIntoWhileConditionChecker(checkers.BaseChecker): + """Checks for instances where while loops are implemented with a constant condition + which. + + always evaluates to truthy and the first statement(s) is/are if statements which, when + evaluated. + + to True, breaks out of the loop. + + The if statement(s) can be refactored into the while loop. + """ + + name = "consider_refactoring_into_while" + msgs = { + "R3501": ( + "Consider using 'while %s' instead of 'while %s:' an 'if', and a 'break'", + "consider-refactoring-into-while-condition", + "Emitted when `while True:` loop is used and the first statement is a break condition. " + "The ``if / break`` construct can be removed if the check is inverted and moved to " + "the ``while`` statement.", + ), + } + + @utils.only_required_for_messages("consider-refactoring-into-while-condition") + def visit_while(self, node: nodes.While) -> None: + self._check_breaking_after_while_true(node) + + def _check_breaking_after_while_true(self, node: nodes.While) -> None: + """Check that any loop with an ``if`` clause has a break statement.""" + if not isinstance(node.test, nodes.Const) or not node.test.bool_value(): + return + pri_candidates: list[nodes.If] = [] + for n in node.body: + if not isinstance(n, nodes.If): + break + pri_candidates.append(n) + candidates = [] + tainted = False + for c in pri_candidates: + if tainted or not isinstance(c.body[0], nodes.Break): + break + candidates.append(c) + orelse = c.orelse + while orelse: + orelse_node = orelse[0] + if not isinstance(orelse_node, nodes.If): + tainted = True + else: + candidates.append(orelse_node) + if not isinstance(orelse_node, nodes.If): + break + orelse = orelse_node.orelse + + candidates = [n for n in candidates if isinstance(n.body[0], nodes.Break)] + msg = " and ".join( + [f"({utils.not_condition_as_string(c.test)})" for c in candidates] + ) + if len(candidates) == 1: + msg = utils.not_condition_as_string(candidates[0].test) + if not msg: + return + + self.add_message( + "consider-refactoring-into-while-condition", + node=node, + line=node.lineno, + args=(msg, node.test.as_string()), + confidence=HIGH, + ) + + +def register(linter: PyLinter) -> None: + linter.register_checker(ConsiderRefactorIntoWhileConditionChecker(linter)) diff --git a/pylint/extensions/consider_ternary_expression.py b/pylint/extensions/consider_ternary_expression.py index c549b550ff..0e9444662a 100644 --- a/pylint/extensions/consider_ternary_expression.py +++ b/pylint/extensions/consider_ternary_expression.py @@ -17,7 +17,6 @@ class ConsiderTernaryExpressionChecker(BaseChecker): - name = "consider_ternary_expression" msgs = { "W0160": ( @@ -41,7 +40,7 @@ def visit_if(self, node: nodes.If) -> None: if not isinstance(bst, nodes.Assign) or not isinstance(ost, nodes.Assign): return - for (bname, oname) in zip(bst.targets, ost.targets): + for bname, oname in zip(bst.targets, ost.targets): if not isinstance(bname, nodes.AssignName) or not isinstance( oname, nodes.AssignName ): diff --git a/pylint/extensions/dict_init_mutate.py b/pylint/extensions/dict_init_mutate.py new file mode 100644 index 0000000000..fb4c83647d --- /dev/null +++ b/pylint/extensions/dict_init_mutate.py @@ -0,0 +1,66 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Check for use of dictionary mutation after initialization.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import only_required_for_messages +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint.pylinter import PyLinter + + +class DictInitMutateChecker(BaseChecker): + name = "dict-init-mutate" + msgs = { + "C3401": ( + "Declare all known key/values when initializing the dictionary.", + "dict-init-mutate", + "Dictionaries can be initialized with a single statement " + "using dictionary literal syntax.", + ) + } + + @only_required_for_messages("dict-init-mutate") + def visit_assign(self, node: nodes.Assign) -> None: + """ + Detect dictionary mutation immediately after initialization. + + At this time, detecting nested mutation is not supported. + """ + if not isinstance(node.value, nodes.Dict): + return + + dict_name = node.targets[0] + if len(node.targets) != 1 or not isinstance(dict_name, nodes.AssignName): + return + + first_sibling = node.next_sibling() + if ( + not first_sibling + or not isinstance(first_sibling, nodes.Assign) + or len(first_sibling.targets) != 1 + ): + return + + sibling_target = first_sibling.targets[0] + if not isinstance(sibling_target, nodes.Subscript): + return + + sibling_name = sibling_target.value + if not isinstance(sibling_name, nodes.Name): + return + + if sibling_name.name == dict_name.name: + self.add_message("dict-init-mutate", node=node, confidence=HIGH) + + +def register(linter: PyLinter) -> None: + linter.register_checker(DictInitMutateChecker(linter)) diff --git a/pylint/extensions/docparams.py b/pylint/extensions/docparams.py index 884b2afe61..0c2e4e9e35 100644 --- a/pylint/extensions/docparams.py +++ b/pylint/extensions/docparams.py @@ -16,6 +16,7 @@ from pylint.checkers import utils as checker_utils from pylint.extensions import _check_docs_utils as utils from pylint.extensions._check_docs_utils import Docstring +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter @@ -216,7 +217,9 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: visit_asyncfunctiondef = visit_functiondef - def check_functiondef_params(self, node, node_doc): + def check_functiondef_params( + self, node: nodes.FunctionDef, node_doc: Docstring + ) -> None: node_allow_no_param = None if node.name in self.constructor_names: class_node = checker_utils.node_frame_class(node) @@ -247,7 +250,9 @@ def check_functiondef_params(self, node, node_doc): node_doc, node.args, node, node_allow_no_param ) - def check_functiondef_returns(self, node, node_doc): + def check_functiondef_returns( + self, node: nodes.FunctionDef, node_doc: Docstring + ) -> None: if (not node_doc.supports_yields and node.is_generator()) or node.is_abstract(): return @@ -255,9 +260,11 @@ def check_functiondef_returns(self, node, node_doc): if (node_doc.has_returns() or node_doc.has_rtype()) and not any( utils.returns_something(ret_node) for ret_node in return_nodes ): - self.add_message("redundant-returns-doc", node=node) + self.add_message("redundant-returns-doc", node=node, confidence=HIGH) - def check_functiondef_yields(self, node, node_doc): + def check_functiondef_yields( + self, node: nodes.FunctionDef, node_doc: Docstring + ) -> None: if not node_doc.supports_yields or node.is_abstract(): return @@ -271,6 +278,11 @@ def visit_raise(self, node: nodes.Raise) -> None: if not isinstance(func_node, astroid.FunctionDef): return + # skip functions that match the 'no-docstring-rgx' config option + no_docstring_rgx = self.linter.config.no_docstring_rgx + if no_docstring_rgx and re.match(no_docstring_rgx, func_node.name): + return + expected_excs = utils.possible_exc_types(node) if not expected_excs: @@ -286,10 +298,14 @@ def visit_raise(self, node: nodes.Raise) -> None: doc = utils.docstringify( func_node.doc_node, self.linter.config.default_docstring_type ) + + if self.linter.config.accept_no_raise_doc and not doc.exceptions(): + return + if not doc.matching_sections(): if doc.doc: missing = {exc.name for exc in expected_excs} - self._handle_no_raise_doc(missing, func_node) + self._add_raise_message(missing, func_node) return found_excs_full_names = doc.exceptions() @@ -316,8 +332,11 @@ def visit_return(self, node: nodes.Return) -> None: if self.linter.config.accept_no_return_doc: return - func_node = node.frame(future=True) - if not isinstance(func_node, astroid.FunctionDef): + func_node: astroid.FunctionDef = node.frame(future=True) + + # skip functions that match the 'no-docstring-rgx' config option + no_docstring_rgx = self.linter.config.no_docstring_rgx + if no_docstring_rgx and re.match(no_docstring_rgx, func_node.name): return doc = utils.docstringify( @@ -327,20 +346,23 @@ def visit_return(self, node: nodes.Return) -> None: is_property = checker_utils.decorated_with_property(func_node) if not (doc.has_returns() or (doc.has_property_returns() and is_property)): - self.add_message("missing-return-doc", node=func_node) + self.add_message("missing-return-doc", node=func_node, confidence=HIGH) if func_node.returns: return if not (doc.has_rtype() or (doc.has_property_type() and is_property)): - self.add_message("missing-return-type-doc", node=func_node) + self.add_message("missing-return-type-doc", node=func_node, confidence=HIGH) - def visit_yield(self, node: nodes.Yield) -> None: + def visit_yield(self, node: nodes.Yield | nodes.YieldFrom) -> None: if self.linter.config.accept_no_yields_doc: return - func_node = node.frame(future=True) - if not isinstance(func_node, astroid.FunctionDef): + func_node: astroid.FunctionDef = node.frame(future=True) + + # skip functions that match the 'no-docstring-rgx' config option + no_docstring_rgx = self.linter.config.no_docstring_rgx + if no_docstring_rgx and re.match(no_docstring_rgx, func_node.name): return doc = utils.docstringify( @@ -355,13 +377,12 @@ def visit_yield(self, node: nodes.Yield) -> None: doc_has_yields_type = doc.has_rtype() if not doc_has_yields: - self.add_message("missing-yield-doc", node=func_node) + self.add_message("missing-yield-doc", node=func_node, confidence=HIGH) if not (doc_has_yields_type or func_node.returns): - self.add_message("missing-yield-type-doc", node=func_node) + self.add_message("missing-yield-type-doc", node=func_node, confidence=HIGH) - def visit_yieldfrom(self, node: nodes.YieldFrom) -> None: - self.visit_yield(node) + visit_yieldfrom = visit_yield def _compare_missing_args( self, @@ -400,6 +421,7 @@ def _compare_missing_args( message_id, args=(", ".join(sorted(missing_argument_names)),), node=warning_node, + confidence=HIGH, ) def _compare_different_args( @@ -442,29 +464,23 @@ def _compare_different_args( message_id, args=(", ".join(sorted(differing_argument_names)),), node=warning_node, + confidence=HIGH, ) - def _compare_ignored_args( + def _compare_ignored_args( # pylint: disable=useless-param-doc self, - found_argument_names, - message_id, - ignored_argument_names, - warning_node, - ): + found_argument_names: set[str], + message_id: str, + ignored_argument_names: set[str], + warning_node: nodes.NodeNG, + ) -> None: """Compare the found argument names with the ignored ones and generate a message if there are ignored arguments found. :param found_argument_names: argument names found in the docstring - :type found_argument_names: set - :param message_id: pylint message id - :type message_id: str - :param ignored_argument_names: Expected argument names - :type ignored_argument_names: set - :param warning_node: The node to be analyzed - :type warning_node: :class:`astroid.scoped_nodes.Node` """ existing_ignored_argument_names = ignored_argument_names & found_argument_names @@ -473,6 +489,7 @@ def _compare_ignored_args( message_id, args=(", ".join(sorted(existing_ignored_argument_names)),), node=warning_node, + confidence=HIGH, ) def check_arguments_in_docstring( @@ -481,7 +498,7 @@ def check_arguments_in_docstring( arguments_node: astroid.Arguments, warning_node: astroid.NodeNG, accept_no_param_doc: bool | None = None, - ): + ) -> None: """Check that all parameters are consistent with the parameters mentioned in the parameter documentation (e.g. the Sphinx tags 'param' and 'type'). @@ -581,6 +598,7 @@ class constructor. "missing-any-param-doc", args=(warning_node.name,), node=warning_node, + confidence=HIGH, ) else: self._compare_missing_args( @@ -620,39 +638,37 @@ class constructor. warning_node, ) - def check_single_constructor_params(self, class_doc, init_doc, class_node): + def check_single_constructor_params( + self, class_doc: Docstring, init_doc: Docstring, class_node: nodes.ClassDef + ) -> None: if class_doc.has_params() and init_doc.has_params(): self.add_message( - "multiple-constructor-doc", args=(class_node.name,), node=class_node + "multiple-constructor-doc", + args=(class_node.name,), + node=class_node, + confidence=HIGH, ) - def _handle_no_raise_doc(self, excs, node): - if self.linter.config.accept_no_raise_doc: - return - - self._add_raise_message(excs, node) - - def _add_raise_message(self, missing_excs, node): + def _add_raise_message( + self, missing_exceptions: set[str], node: nodes.FunctionDef + ) -> None: """Adds a message on :param:`node` for the missing exception type. - :param missing_excs: A list of missing exception types. - :type missing_excs: set(str) - + :param missing_exceptions: A list of missing exception types. :param node: The node show the message on. - :type node: nodes.NodeNG """ if node.is_abstract(): try: - missing_excs.remove("NotImplementedError") + missing_exceptions.remove("NotImplementedError") except KeyError: pass - - if not missing_excs: - return - - self.add_message( - "missing-raises-doc", args=(", ".join(sorted(missing_excs)),), node=node - ) + if missing_exceptions: + self.add_message( + "missing-raises-doc", + args=(", ".join(sorted(missing_exceptions)),), + node=node, + confidence=HIGH, + ) def register(linter: PyLinter) -> None: diff --git a/pylint/extensions/docstyle.py b/pylint/extensions/docstyle.py index 99b01563f8..1ca2885e94 100644 --- a/pylint/extensions/docstyle.py +++ b/pylint/extensions/docstyle.py @@ -48,7 +48,9 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: visit_asyncfunctiondef = visit_functiondef - def _check_docstring(self, node_type, node): + def _check_docstring( + self, node_type: str, node: nodes.Module | nodes.ClassDef | nodes.FunctionDef + ) -> None: docstring = node.doc_node.value if node.doc_node else None if docstring and docstring[0] == "\n": self.add_message( @@ -73,7 +75,7 @@ def _check_docstring(self, node_type, node): elif line and line[0] == "'": quotes = "'" else: - quotes = False + quotes = "" if quotes: self.add_message( "bad-docstring-quotes", diff --git a/pylint/extensions/dunder.py b/pylint/extensions/dunder.py new file mode 100644 index 0000000000..e0e9af316a --- /dev/null +++ b/pylint/extensions/dunder.py @@ -0,0 +1,77 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.constants import DUNDER_METHODS, DUNDER_PROPERTIES, EXTRA_DUNDER_METHODS +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class DunderChecker(BaseChecker): + """Checks related to dunder methods.""" + + name = "dunder" + priority = -1 + msgs = { + "W3201": ( + "Bad or misspelled dunder method name %s.", + "bad-dunder-name", + "Used when a dunder method is misspelled or defined with a name " + "not within the predefined list of dunder names.", + ), + } + options = ( + ( + "good-dunder-names", + { + "default": [], + "type": "csv", + "metavar": "", + "help": "Good dunder names which should always be accepted.", + }, + ), + ) + + def open(self) -> None: + self._dunder_methods = ( + EXTRA_DUNDER_METHODS + + DUNDER_PROPERTIES + + self.linter.config.good_dunder_names + ) + for since_vers, dunder_methods in DUNDER_METHODS.items(): + if since_vers <= self.linter.config.py_version: + self._dunder_methods.extend(list(dunder_methods.keys())) + + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + """Check if known dunder method is misspelled or dunder name is not one + of the pre-defined names. + """ + # ignore module-level functions + if not node.is_method(): + return + + # Detect something that could be a bad dunder method + if ( + node.name.startswith("_") + and node.name.endswith("_") + and node.name not in self._dunder_methods + ): + self.add_message( + "bad-dunder-name", + node=node, + args=(node.name), + confidence=HIGH, + ) + + +def register(linter: PyLinter) -> None: + linter.register_checker(DunderChecker(linter)) diff --git a/pylint/extensions/empty_comment.py b/pylint/extensions/empty_comment.py index 70d3f67d20..e8a914708d 100644 --- a/pylint/extensions/empty_comment.py +++ b/pylint/extensions/empty_comment.py @@ -14,7 +14,7 @@ from pylint.lint import PyLinter -def is_line_commented(line): +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"#") @@ -25,7 +25,7 @@ def is_line_commented(line): return True -def comment_part_of_string(line, comment_idx): +def comment_part_of_string(line: bytes, comment_idx: int) -> bool: """Checks if the symbol at comment_idx is part of a string.""" if ( @@ -40,8 +40,7 @@ def comment_part_of_string(line, comment_idx): class CommentChecker(BaseRawFileChecker): - - name = "refactoring" + name = "empty-comment" msgs = { "R2044": ( "Line with empty comment", @@ -55,7 +54,7 @@ class CommentChecker(BaseRawFileChecker): def process_module(self, node: nodes.Module) -> None: with node.stream() as stream: - for (line_num, line) in enumerate(stream): + for line_num, line in enumerate(stream): line = line.rstrip() if line.endswith(b"#"): if not is_line_commented(line[:-1]): diff --git a/pylint/extensions/emptystring.py b/pylint/extensions/emptystring.py index ec2839bdd0..f96a980f59 100644 --- a/pylint/extensions/emptystring.py +++ b/pylint/extensions/emptystring.py @@ -7,31 +7,23 @@ from __future__ import annotations import itertools -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from astroid import nodes from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter class CompareToEmptyStringChecker(checkers.BaseChecker): - """Checks for comparisons to empty string. - - Most of the time you should use the fact that empty strings are false. - An exception to this rule is when an empty string value is allowed in the program - and has a different meaning than None! - """ - - # configuration section name name = "compare-to-empty-string" msgs = { "C1901": ( - "Avoid comparisons to empty string", + '"%s" can be simplified to "%s" as an empty string is falsey', "compare-to-empty-string", "Used when Pylint detects comparison to an empty string constant.", ) @@ -41,31 +33,45 @@ class CompareToEmptyStringChecker(checkers.BaseChecker): @utils.only_required_for_messages("compare-to-empty-string") def visit_compare(self, node: nodes.Compare) -> None: - _operators = ["!=", "==", "is not", "is"] - # note: astroid.Compare has the left most operand in node.left - # while the rest are a list of tuples in node.ops - # the format of the tuple is ('compare operator sign', node) - # here we squash everything into `ops` to make it easier for processing later - ops = [("", node.left)] + """Checks for comparisons to empty string. + + Most of the time you should use the fact that empty strings are false. + An exception to this rule is when an empty string value is allowed in the program + and has a different meaning than None! + """ + _operators = {"!=", "==", "is not", "is"} + # note: astroid.Compare has the left most operand in node.left while the rest + # are a list of tuples in node.ops the format of the tuple is + # ('compare operator sign', node) here we squash everything into `ops` + # to make it easier for processing later + ops: list[tuple[str, nodes.NodeNG | None]] = [("", node.left)] ops.extend(node.ops) - iter_ops: Iterable[Any] = iter(ops) - ops = list(itertools.chain(*iter_ops)) - + iter_ops = iter(ops) + ops = list(itertools.chain(*iter_ops)) # type: ignore[arg-type] for ops_idx in range(len(ops) - 2): - op_1 = ops[ops_idx] - op_2 = ops[ops_idx + 1] - op_3 = ops[ops_idx + 2] + op_1: nodes.NodeNG | None = ops[ops_idx] + op_2: str = ops[ops_idx + 1] # type: ignore[assignment] + op_3: nodes.NodeNG | None = ops[ops_idx + 2] error_detected = False - + if op_1 is None or op_3 is None or op_2 not in _operators: + continue + node_name = "" # x ?? "" - if utils.is_empty_str_literal(op_1) and op_2 in _operators: + if utils.is_empty_str_literal(op_1): error_detected = True + node_name = op_3.as_string() # '' ?? X - elif op_2 in _operators and utils.is_empty_str_literal(op_3): + elif utils.is_empty_str_literal(op_3): error_detected = True - + node_name = op_1.as_string() if error_detected: - self.add_message("compare-to-empty-string", node=node) + suggestion = f"not {node_name}" if op_2 in {"==", "is"} else node_name + self.add_message( + "compare-to-empty-string", + args=(node.as_string(), suggestion), + node=node, + confidence=HIGH, + ) def register(linter: PyLinter) -> None: diff --git a/pylint/extensions/eq_without_hash.py b/pylint/extensions/eq_without_hash.py index bce2087bcd..b0d0f01bd0 100644 --- a/pylint/extensions/eq_without_hash.py +++ b/pylint/extensions/eq_without_hash.py @@ -17,7 +17,6 @@ class EqWithoutHash(checkers.BaseChecker): - name = "eq-without-hash" msgs = { diff --git a/pylint/extensions/for_any_all.py b/pylint/extensions/for_any_all.py index e6ab41c3fb..bc7dd9c488 100644 --- a/pylint/extensions/for_any_all.py +++ b/pylint/extensions/for_any_all.py @@ -11,14 +11,18 @@ from astroid import nodes from pylint.checkers import BaseChecker -from pylint.checkers.utils import only_required_for_messages, returns_bool +from pylint.checkers.utils import ( + assigned_bool, + only_required_for_messages, + returns_bool, +) +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter class ConsiderUsingAnyOrAllChecker(BaseChecker): - name = "consider-using-any-or-all" msgs = { "C0501": ( @@ -36,19 +40,100 @@ def visit_for(self, node: nodes.For) -> None: return if_children = list(node.body[0].get_children()) - if not len(if_children) == 2: # The If node has only a comparison and return - return - if not returns_bool(if_children[1]): + if any(isinstance(child, nodes.If) for child in if_children): + # an if node within the if-children indicates an elif clause, + # suggesting complex logic. return - # Check for terminating boolean return right after the loop node_after_loop = node.next_sibling() - if returns_bool(node_after_loop): + + if self._assigned_reassigned_returned(node, if_children, node_after_loop): + final_return_bool = node_after_loop.value.name + suggested_string = self._build_suggested_string(node, final_return_bool) + self.add_message( + "consider-using-any-or-all", + node=node, + args=suggested_string, + confidence=HIGH, + ) + return + + if self._if_statement_returns_bool(if_children, node_after_loop): final_return_bool = node_after_loop.value.value suggested_string = self._build_suggested_string(node, final_return_bool) self.add_message( - "consider-using-any-or-all", node=node, args=suggested_string + "consider-using-any-or-all", + node=node, + args=suggested_string, + confidence=HIGH, ) + return + + @staticmethod + def _if_statement_returns_bool( + if_children: list[nodes.NodeNG], node_after_loop: nodes.NodeNG + ) -> bool: + """Detect for-loop, if-statement, return pattern: + + Ex: + def any_uneven(items): + for item in items: + if not item % 2 == 0: + return True + return False + """ + if not len(if_children) == 2: + # The If node has only a comparison and return + return False + if not returns_bool(if_children[1]): + return False + + # Check for terminating boolean return right after the loop + return returns_bool(node_after_loop) + + @staticmethod + def _assigned_reassigned_returned( + node: nodes.For, if_children: list[nodes.NodeNG], node_after_loop: nodes.NodeNG + ) -> bool: + """Detect boolean-assign, for-loop, re-assign, return pattern: + + Ex: + def check_lines(lines, max_chars): + long_line = False + for line in lines: + if len(line) > max_chars: + long_line = True + # no elif / else statement + return long_line + """ + node_before_loop = node.previous_sibling() + + if not assigned_bool(node_before_loop): + # node before loop isn't assigning to boolean + return False + + assign_children = [x for x in if_children if isinstance(x, nodes.Assign)] + if not assign_children: + # if-nodes inside loop aren't assignments + return False + + # We only care for the first assign node of the if-children. Otherwise it breaks the pattern. + first_target = assign_children[0].targets[0] + target_before_loop = node_before_loop.targets[0] + + if not ( + isinstance(first_target, nodes.AssignName) + and isinstance(target_before_loop, nodes.AssignName) + ): + return False + + node_before_loop_name = node_before_loop.targets[0].name + return ( + first_target.name == node_before_loop_name + and isinstance(node_after_loop, nodes.Return) + and isinstance(node_after_loop.value, nodes.Name) + and node_after_loop.value.name == node_before_loop_name + ) @staticmethod def _build_suggested_string(node: nodes.For, final_return_bool: bool) -> str: diff --git a/pylint/extensions/magic_value.py b/pylint/extensions/magic_value.py new file mode 100644 index 0000000000..7cfb410ae4 --- /dev/null +++ b/pylint/extensions/magic_value.py @@ -0,0 +1,119 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Checks for magic values instead of literals.""" + +from __future__ import annotations + +from re import match as regex_match +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker, utils +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class MagicValueChecker(BaseChecker): + """Checks for constants in comparisons.""" + + name = "magic-value" + msgs = { + "R2004": ( + "Consider using a named constant or an enum instead of '%s'.", + "magic-value-comparison", + "Using named constants instead of magic values helps improve readability and maintainability of your" + " code, try to avoid them in comparisons.", + ) + } + + options = ( + ( + "valid-magic-values", + { + "default": (0, -1, 1, "", "__main__"), + "type": "csv", + "metavar": "", + "help": "List of valid magic values that `magic-value-compare` will not detect. " + "Supports integers, floats, negative numbers, for empty string enter ``''``," + " for backslash values just use one backslash e.g \\n.", + }, + ), + ) + + def __init__(self, linter: PyLinter) -> None: + """Initialize checker instance.""" + super().__init__(linter=linter) + self.valid_magic_vals: tuple[float | str, ...] = () + + def open(self) -> None: + # Extra manipulation is needed in case of using external configuration like an rcfile + if self._magic_vals_ext_configured(): + self.valid_magic_vals = tuple( + self._parse_rcfile_magic_numbers(value) + for value in self.linter.config.valid_magic_values + ) + else: + self.valid_magic_vals = self.linter.config.valid_magic_values + + def _magic_vals_ext_configured(self) -> bool: + return not isinstance(self.linter.config.valid_magic_values, tuple) + + def _check_constants_comparison(self, node: nodes.Compare) -> None: + """ + Magic values in any side of the comparison should be avoided, + Detects comparisons that `comparison-of-constants` core checker cannot detect. + """ + const_operands = [] + LEFT_OPERAND = 0 + RIGHT_OPERAND = 1 + + left_operand = node.left + const_operands.append(isinstance(left_operand, nodes.Const)) + + right_operand = node.ops[0][1] + const_operands.append(isinstance(right_operand, nodes.Const)) + + if all(const_operands): + # `comparison-of-constants` avoided + return + + operand_value = None + if const_operands[LEFT_OPERAND] and self._is_magic_value(left_operand): + operand_value = left_operand.value + elif const_operands[RIGHT_OPERAND] and self._is_magic_value(right_operand): + operand_value = right_operand.value + if operand_value is not None: + self.add_message( + "magic-value-comparison", + node=node, + args=(operand_value), + confidence=HIGH, + ) + + def _is_magic_value(self, node: nodes.Const) -> bool: + return (not utils.is_singleton_const(node)) and ( + node.value not in (self.valid_magic_vals) + ) + + @staticmethod + def _parse_rcfile_magic_numbers(parsed_val: str) -> float | str: + parsed_val = parsed_val.encode().decode("unicode_escape") + + if parsed_val.startswith("'") and parsed_val.endswith("'"): + return parsed_val[1:-1] + + is_number = regex_match(r"[-+]?\d+(\.0*)?$", parsed_val) + return float(parsed_val) if is_number else parsed_val + + @utils.only_required_for_messages("magic-comparison") + def visit_compare(self, node: nodes.Compare) -> None: + self._check_constants_comparison(node) + + +def register(linter: PyLinter) -> None: + linter.register_checker(MagicValueChecker(linter)) diff --git a/pylint/extensions/mccabe.py b/pylint/extensions/mccabe.py index 2b731f239f..ea64d2ebfa 100644 --- a/pylint/extensions/mccabe.py +++ b/pylint/extensions/mccabe.py @@ -6,7 +6,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, TypeVar, Union from astroid import nodes from mccabe import PathGraph as Mccabe_PathGraph @@ -19,23 +20,48 @@ if TYPE_CHECKING: from pylint.lint import PyLinter - -class PathGraph(Mccabe_PathGraph): - def __init__(self, node): +_StatementNodes = Union[ + nodes.Assert, + nodes.Assign, + nodes.AugAssign, + nodes.Delete, + nodes.Raise, + nodes.Yield, + nodes.Import, + nodes.Call, + nodes.Subscript, + nodes.Pass, + nodes.Continue, + nodes.Break, + nodes.Global, + nodes.Return, + nodes.Expr, + nodes.Await, +] + +_SubGraphNodes = Union[nodes.If, nodes.TryExcept, nodes.For, nodes.While] +_AppendableNodeT = TypeVar( + "_AppendableNodeT", bound=Union[_StatementNodes, nodes.While, nodes.FunctionDef] +) + + +class PathGraph(Mccabe_PathGraph): # type: ignore[misc] + def __init__(self, node: _SubGraphNodes | nodes.FunctionDef): super().__init__(name="", entity="", lineno=1) self.root = node -class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor): - def __init__(self): +class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor): # type: ignore[misc] + def __init__(self) -> None: super().__init__() self._bottom_counter = 0 + self.graph: PathGraph | None = None - def default(self, node, *args): + def default(self, node: nodes.NodeNG, *args: Any) -> None: for child in node.get_children(): self.dispatch(child, *args) - def dispatch(self, node, *args): + def dispatch(self, node: nodes.NodeNG, *args: Any) -> Any: self.node = node klass = node.__class__ meth = self._cache.get(klass) @@ -45,7 +71,7 @@ def dispatch(self, node, *args): self._cache[klass] = meth return meth(node, *args) - def visitFunctionDef(self, node): + def visitFunctionDef(self, node: nodes.FunctionDef) -> None: if self.graph is not None: # closure pathnode = self._append_node(node) @@ -65,7 +91,7 @@ def visitFunctionDef(self, node): visitAsyncFunctionDef = visitFunctionDef - def visitSimpleStatement(self, node): + def visitSimpleStatement(self, node: _StatementNodes) -> None: self._append_node(node) visitAssert = ( @@ -74,8 +100,6 @@ def visitSimpleStatement(self, node): visitAugAssign ) = ( visitDelete - ) = ( - visitPrint ) = ( visitRaise ) = ( @@ -94,20 +118,25 @@ def visitSimpleStatement(self, node): visitBreak ) = visitGlobal = visitReturn = visitExpr = visitAwait = visitSimpleStatement - def visitWith(self, node): + def visitWith(self, node: nodes.With) -> None: self._append_node(node) self.dispatch_list(node.body) visitAsyncWith = visitWith - def _append_node(self, node): - if not self.tail: + def _append_node(self, node: _AppendableNodeT) -> _AppendableNodeT | None: + if not self.tail or not self.graph: return None self.graph.connect(self.tail, node) self.tail = node return node - def _subgraph(self, node, name, extra_blocks=()): + def _subgraph( + self, + node: _SubGraphNodes, + name: str, + extra_blocks: Sequence[nodes.ExceptHandler] = (), + ) -> None: """Create the subgraphs representing any `if` and `for` statements.""" if self.graph is None: # global loop @@ -119,7 +148,12 @@ def _subgraph(self, node, name, extra_blocks=()): self._append_node(node) self._subgraph_parse(node, node, extra_blocks) - def _subgraph_parse(self, node, pathnode, extra_blocks): + def _subgraph_parse( + self, + node: _SubGraphNodes, + pathnode: _SubGraphNodes, + extra_blocks: Sequence[nodes.ExceptHandler], + ) -> None: """Parse the body and any `else` block of `if` and `for` statements.""" loose_ends = [] self.tail = node @@ -135,7 +169,7 @@ def _subgraph_parse(self, node, pathnode, extra_blocks): loose_ends.append(self.tail) else: loose_ends.append(node) - if node: + if node and self.graph: bottom = f"{self._bottom_counter}" self._bottom_counter += 1 for end in loose_ends: diff --git a/pylint/extensions/no_self_use.py b/pylint/extensions/no_self_use.py index 4a20873c84..0fd38877fc 100644 --- a/pylint/extensions/no_self_use.py +++ b/pylint/extensions/no_self_use.py @@ -23,7 +23,6 @@ class NoSelfUseChecker(BaseChecker): - name = "no_self_use" msgs = { "R6301": ( diff --git a/pylint/extensions/private_import.py b/pylint/extensions/private_import.py index 53e285ac34..fb4458e54d 100644 --- a/pylint/extensions/private_import.py +++ b/pylint/extensions/private_import.py @@ -19,7 +19,6 @@ class PrivateImportChecker(BaseChecker): - name = "import-private-name" msgs = { "C2701": ( @@ -195,7 +194,7 @@ def _populate_type_annotations_annotation( """ if isinstance(node, nodes.Name) and node.name not in all_used_type_annotations: all_used_type_annotations[node.name] = True - return node.name + return node.name # type: ignore[no-any-return] if isinstance(node, nodes.Subscript): # e.g. Optional[List[str]] # slice is the next nested type self._populate_type_annotations_annotation( diff --git a/pylint/extensions/redefined_loop_name.py b/pylint/extensions/redefined_loop_name.py index e6308cb114..df333fab9b 100644 --- a/pylint/extensions/redefined_loop_name.py +++ b/pylint/extensions/redefined_loop_name.py @@ -15,7 +15,6 @@ class RedefinedLoopNameChecker(checkers.BaseChecker): - name = "redefined-loop-name" msgs = { diff --git a/pylint/extensions/redefined_variable_type.py b/pylint/extensions/redefined_variable_type.py index 994d422785..8d88d856ed 100644 --- a/pylint/extensions/redefined_variable_type.py +++ b/pylint/extensions/redefined_variable_type.py @@ -46,13 +46,13 @@ def visit_classdef(self, _: nodes.ClassDef) -> None: def leave_classdef(self, _: nodes.ClassDef) -> None: self._check_and_add_messages() - visit_functiondef = visit_classdef - leave_functiondef = leave_module = leave_classdef + visit_functiondef = visit_asyncfunctiondef = visit_classdef + leave_functiondef = leave_asyncfunctiondef = leave_module = leave_classdef def visit_module(self, _: nodes.Module) -> None: - self._assigns: list[dict] = [{}] + self._assigns: list[dict[str, list[tuple[nodes.Assign, str]]]] = [{}] - def _check_and_add_messages(self): + def _check_and_add_messages(self) -> None: assigns = self._assigns.pop() for name, args in assigns.items(): if len(args) <= 1: diff --git a/pylint/extensions/set_membership.py b/pylint/extensions/set_membership.py index d04ba9ae77..f267e046ff 100644 --- a/pylint/extensions/set_membership.py +++ b/pylint/extensions/set_membership.py @@ -16,7 +16,6 @@ class SetMembershipChecker(BaseChecker): - name = "set_membership" msgs = { "R6201": ( diff --git a/pylint/extensions/typing.py b/pylint/extensions/typing.py index 5f83e56040..1cf7ec10cc 100644 --- a/pylint/extensions/typing.py +++ b/pylint/extensions/typing.py @@ -17,7 +17,8 @@ only_required_for_messages, safe_infer, ) -from pylint.interfaces import INFERENCE +from pylint.constants import TYPING_NORETURN +from pylint.interfaces import HIGH, INFERENCE if TYPE_CHECKING: from pylint.lint import PyLinter @@ -75,12 +76,6 @@ class TypingAlias(NamedTuple): ALIAS_NAMES = frozenset(key.split(".")[1] for key in DEPRECATED_TYPING_ALIASES) UNION_NAMES = ("Optional", "Union") -TYPING_NORETURN = frozenset( - ( - "typing.NoReturn", - "typing_extensions.NoReturn", - ) -) class DeprecatedTypingAliasMsg(NamedTuple): @@ -129,6 +124,11 @@ class TypingChecker(BaseChecker): "Python 3.9.0 and 3.9.1. Use ``typing.Callable`` for these cases instead. " "https://bugs.python.org/issue42965", ), + "R6006": ( + "Type `%s` is used more than once in union type annotation. Remove redundant typehints.", + "redundant-typehint-argument", + "Duplicated type arguments will be skipped by `mypy` tool, therefore should be removed to avoid confusion.", + ), } options = ( ( @@ -142,7 +142,7 @@ class TypingChecker(BaseChecker): "support runtime introspection of type annotations. " "If you use type annotations **exclusively** for type checking " "of an application, you're probably fine. For libraries, " - "evaluate if some users what to access the type hints " + "evaluate if some users want to access the type hints " "at runtime first, e.g., through ``typing.get_type_hints``. " "Applies to Python versions 3.7 - 3.9" ), @@ -224,6 +224,79 @@ def visit_attribute(self, node: nodes.Attribute) -> None: if self._should_check_callable and node.attrname == "Callable": self._check_broken_callable(node) + @only_required_for_messages("redundant-typehint-argument") + def visit_annassign(self, node: nodes.AnnAssign) -> None: + annotation = node.annotation + if self._is_deprecated_union_annotation(annotation, "Optional"): + if self._is_optional_none_annotation(annotation): + self.add_message( + "redundant-typehint-argument", + node=annotation, + args="None", + confidence=HIGH, + ) + return + if self._is_deprecated_union_annotation(annotation, "Union") and isinstance( + annotation.slice, nodes.Tuple + ): + types = annotation.slice.elts + elif self._is_binop_union_annotation(annotation): + types = self._parse_binops_typehints(annotation) + else: + return + + self._check_union_types(types, node) + + @staticmethod + def _is_deprecated_union_annotation( + annotation: nodes.NodeNG, union_name: str + ) -> bool: + return ( + isinstance(annotation, nodes.Subscript) + and isinstance(annotation.value, nodes.Name) + and annotation.value.name == union_name + ) + + def _is_binop_union_annotation(self, annotation: nodes.NodeNG) -> bool: + return self._should_check_alternative_union_syntax and isinstance( + annotation, nodes.BinOp + ) + + @staticmethod + def _is_optional_none_annotation(annotation: nodes.Subscript) -> bool: + return ( + isinstance(annotation.slice, nodes.Const) and annotation.slice.value is None + ) + + def _parse_binops_typehints( + self, binop_node: nodes.BinOp, typehints_list: list[nodes.NodeNG] | None = None + ) -> list[nodes.NodeNG]: + typehints_list = typehints_list or [] + if isinstance(binop_node.left, nodes.BinOp): + typehints_list.extend( + self._parse_binops_typehints(binop_node.left, typehints_list) + ) + else: + typehints_list.append(binop_node.left) + typehints_list.append(binop_node.right) + return typehints_list + + def _check_union_types( + self, types: list[nodes.NodeNG], annotation: nodes.NodeNG + ) -> None: + types_set = set() + for typehint in types: + typehint_str = typehint.as_string() + if typehint_str in types_set: + self.add_message( + "redundant-typehint-argument", + node=annotation, + args=(typehint_str), + confidence=HIGH, + ) + else: + types_set.add(typehint_str) + def _check_for_alternative_union_syntax( self, node: nodes.Name | nodes.Attribute, diff --git a/pylint/extensions/while_used.py b/pylint/extensions/while_used.py index a65092df0f..6f96121961 100644 --- a/pylint/extensions/while_used.py +++ b/pylint/extensions/while_used.py @@ -18,13 +18,13 @@ class WhileChecker(BaseChecker): - name = "while_used" msgs = { "W0149": ( "Used `while` loop", "while-used", - "Unbounded `while` loops can often be rewritten as bounded `for` loops.", + "Unbounded `while` loops can often be rewritten as bounded `for` loops. " + "Exceptions can be made for cases such as event loops, listeners, etc.", ) } diff --git a/pylint/graph.py b/pylint/graph.py index 21b5b2fee8..5cffca6150 100644 --- a/pylint/graph.py +++ b/pylint/graph.py @@ -13,7 +13,6 @@ import os import shutil import subprocess -import sys import tempfile from collections.abc import Sequence from typing import Any @@ -113,9 +112,8 @@ def generate( "executable not found. Install graphviz, or specify a `.gv` " "outputfile to produce the DOT source code." ) - use_shell = sys.platform == "win32" if mapfile: - subprocess.call( + subprocess.run( [ self.renderer, "-Tcmapx", @@ -127,12 +125,12 @@ def generate( "-o", outputfile, ], - shell=use_shell, + check=True, ) else: - subprocess.call( + subprocess.run( [self.renderer, "-T", target, dot_sourcepath, "-o", outputfile], - shell=use_shell, + check=True, ) os.unlink(dot_sourcepath) return outputfile diff --git a/pylint/interfaces.py b/pylint/interfaces.py index a4d1288d80..221084fab2 100644 --- a/pylint/interfaces.py +++ b/pylint/interfaces.py @@ -7,9 +7,8 @@ from __future__ import annotations import warnings -from collections import namedtuple from tokenize import TokenInfo -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NamedTuple from astroid import nodes @@ -33,7 +32,12 @@ "CONFIDENCE_LEVEL_NAMES", ) -Confidence = namedtuple("Confidence", ["name", "description"]) + +class Confidence(NamedTuple): + name: str + description: str + + # Warning Certainties HIGH = Confidence("HIGH", "Warning that is not based on inference result.") CONTROL_FLOW = Confidence( @@ -57,6 +61,7 @@ def __init__(self) -> None: "Interface and all of its subclasses have been deprecated " "and will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) @classmethod @@ -78,6 +83,7 @@ def implements( "implements has been deprecated in favour of using basic " "inheritance patterns without using __implements__.", DeprecationWarning, + stacklevel=2, ) implements_ = getattr(obj, "__implements__", ()) if not isinstance(implements_, (list, tuple)): diff --git a/pylint/lint/base_options.py b/pylint/lint/base_options.py index d909e39c59..1c37eac2fb 100644 --- a/pylint/lint/base_options.py +++ b/pylint/lint/base_options.py @@ -56,7 +56,7 @@ def _make_linter_options(linter: PyLinter) -> Options: "metavar": "[,...]", "dest": "black_list_re", "default": (re.compile(r"^\.#"),), - "help": "Files or directories matching the regex patterns are" + "help": "Files or directories matching the regular expression patterns are" " skipped. The regex matches against base names, not paths. The default value " "ignores Emacs file locks", }, @@ -67,9 +67,10 @@ def _make_linter_options(linter: PyLinter) -> Options: "type": "regexp_paths_csv", "metavar": "[,...]", "default": [], - "help": "Add files or directories matching the regex patterns to the " + "help": "Add files or directories matching the regular expressions patterns to the " "ignore-list. The regex matches against paths and can be in " - "Posix or Windows format.", + "Posix or Windows format. Because '\\\\' represents the directory delimiter " + "on Windows systems, it can't be used as an escape character.", }, ), ( @@ -154,7 +155,7 @@ def _make_linter_options(linter: PyLinter) -> Options: "default": 10, "type": "float", "metavar": "", - "help": "Specify a score threshold to be exceeded before program exits with error.", + "help": "Specify a score threshold under which the program will exit with error.", }, ), ( @@ -390,6 +391,16 @@ def _make_linter_options(linter: PyLinter) -> Options: "positives when analysed.", }, ), + ( + "clear-cache-post-run", + { + "default": False, + "type": "yn", + "metavar": "", + "help": "Clear in-memory caches upon conclusion of linting. " + "Useful if running pylint in a server-like mode.", + }, + ), ) diff --git a/pylint/lint/caching.py b/pylint/lint/caching.py index 573b976288..8ea8a22368 100644 --- a/pylint/lint/caching.py +++ b/pylint/lint/caching.py @@ -12,12 +12,14 @@ from pylint.constants import PYLINT_HOME from pylint.utils import LinterStats +PYLINT_HOME_AS_PATH = Path(PYLINT_HOME) + def _get_pdata_path( - base_name: Path, recurs: int, pylint_home: Path = Path(PYLINT_HOME) + base_name: Path, recurs: int, pylint_home: Path = PYLINT_HOME_AS_PATH ) -> Path: - # We strip all characters that can't be used in a filename - # Also strip '/' and '\\' because we want to create a single file, not sub-directories + # We strip all characters that can't be used in a filename. Also strip '/' and + # '\\' because we want to create a single file, not sub-directories. underscored_name = "_".join( str(p.replace(":", "_").replace("/", "_").replace("\\", "_")) for p in base_name.parts diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index 289e1afce5..8259e25ada 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -18,7 +18,7 @@ def _modpath_from_file(filename: str, is_namespace: bool, path: list[str]) -> li def _is_package_cb(inner_path: str, parts: list[str]) -> bool: return modutils.check_modpath_has_init(inner_path, parts) or is_namespace - return modutils.modpath_from_file_with_callback( + return modutils.modpath_from_file_with_callback( # type: ignore[no-any-return] filename, path=path, is_package_cb=_is_package_cb ) @@ -61,16 +61,17 @@ def _is_ignored_file( ) +# pylint: disable = too-many-locals, too-many-statements def expand_modules( files_or_modules: Sequence[str], ignore_list: list[str], ignore_list_re: list[Pattern[str]], ignore_list_paths_re: list[Pattern[str]], -) -> tuple[list[ModuleDescriptionDict], list[ErrorDescriptionDict]]: +) -> tuple[dict[str, ModuleDescriptionDict], list[ErrorDescriptionDict]]: """Take a list of files/modules/packages and return the list of tuple (file, module name) which have to be actually checked. """ - result: list[ModuleDescriptionDict] = [] + result: dict[str, ModuleDescriptionDict] = {} errors: list[ErrorDescriptionDict] = [] path = sys.path.copy() @@ -103,9 +104,7 @@ def expand_modules( ) if filepath is None: continue - except (ImportError, SyntaxError) as ex: - # The SyntaxError is a Python bug and should be - # removed once we move away from imp.find_module: https://bugs.python.org/issue10588 + except ImportError as ex: errors.append({"key": "fatal", "mod": modname, "ex": ex}) continue filepath = os.path.normpath(filepath) @@ -122,15 +121,17 @@ def expand_modules( is_namespace = modutils.is_namespace(spec) is_directory = modutils.is_directory(spec) if not is_namespace: - result.append( - { + if filepath in result: + # Always set arg flag if module explicitly given. + result[filepath]["isarg"] = True + else: + result[filepath] = { "path": filepath, "name": modname, "isarg": True, "basepath": filepath, "basename": modname, } - ) has_init = ( not (modname.endswith(".__init__") or modname == "__init__") and os.path.basename(filepath) == "__init__.py" @@ -150,13 +151,13 @@ def expand_modules( subfilepath, is_namespace, path=additional_search_path ) submodname = ".".join(modpath) - result.append( - { - "path": subfilepath, - "name": submodname, - "isarg": False, - "basepath": filepath, - "basename": modname, - } - ) + # Preserve arg flag if module is also explicitly given. + isarg = subfilepath in result and result[subfilepath]["isarg"] + result[subfilepath] = { + "path": subfilepath, + "name": submodname, + "isarg": isarg, + "basepath": filepath, + "basename": modname, + } return result, errors diff --git a/pylint/lint/message_state_handler.py b/pylint/lint/message_state_handler.py index 8415c88544..ddeeaa7bca 100644 --- a/pylint/lint/message_state_handler.py +++ b/pylint/lint/message_state_handler.py @@ -70,10 +70,10 @@ def _set_one_msg_status( self, scope: str, msg: MessageDefinition, line: int | None, enable: bool ) -> None: """Set the status of an individual message.""" - if scope == "module": + if scope in {"module", "line"}: assert isinstance(line, int) # should always be int inside module scope - self.linter.file_state.set_msg_status(msg, line, enable) + self.linter.file_state.set_msg_status(msg, line, enable, scope) if not enable and msg.symbol != "locally-disabled": self.linter.add_message( "locally-disabled", line=line, args=(msg.symbol, msg.msgid) @@ -143,7 +143,7 @@ def _set_msg_status( ignore_unknown: bool = False, ) -> None: """Do some tests and then iterate over message definitions to set state.""" - assert scope in {"package", "module"} + assert scope in {"package", "module", "line"} message_definitions = self._get_messages_to_set(msgid, enable, ignore_unknown) @@ -197,7 +197,7 @@ def disable( def disable_next( self, msgid: str, - scope: str = "package", + _: str = "package", line: int | None = None, ignore_unknown: bool = False, ) -> None: @@ -207,7 +207,7 @@ def disable_next( self._set_msg_status( msgid, enable=False, - scope=scope, + scope="line", line=line + 1, ignore_unknown=ignore_unknown, ) @@ -347,7 +347,7 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: prev_line = None saw_newline = True seen_newline = True - for (tok_type, content, start, _, _) in tokens: + for tok_type, content, start, _, _ in tokens: if prev_line and prev_line != start[0]: saw_newline = seen_newline seen_newline = False @@ -361,7 +361,7 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: match = OPTION_PO.search(content) if match is None: continue - try: + try: # pylint: disable = too-many-try-statements for pragma_repr in parse_pragma(match.group(2)): if pragma_repr.action in {"disable-all", "skip-file"}: if pragma_repr.action == "disable-all": diff --git a/pylint/lint/parallel.py b/pylint/lint/parallel.py index 646d269943..9730914b75 100644 --- a/pylint/lint/parallel.py +++ b/pylint/lint/parallel.py @@ -23,10 +23,15 @@ except ImportError: multiprocessing = None # type: ignore[assignment] +try: + from concurrent.futures import ProcessPoolExecutor +except ImportError: + ProcessPoolExecutor = None # type: ignore[assignment,misc] + if TYPE_CHECKING: from pylint.lint import PyLinter -# PyLinter object used by worker processes when checking files using multiprocessing +# PyLinter object used by worker processes when checking files using parallel mode # should only be used by the worker processes _worker_linter: PyLinter | None = None @@ -34,8 +39,7 @@ def _worker_initialize( linter: bytes, arguments: None | str | Sequence[str] = None ) -> None: - """Function called to initialize a worker for a Process within a multiprocessing - Pool. + """Function called to initialize a worker for a Process within a concurrent Pool. :param linter: A linter-class (PyLinter) instance pickled with dill :param arguments: File or module name(s) to lint and to be added to sys.path @@ -67,7 +71,7 @@ def _worker_check_single_file( defaultdict[str, list[Any]], ]: if not _worker_linter: - raise Exception("Worker linter not yet initialised") + raise RuntimeError("Worker linter not yet initialised") _worker_linter.open() _worker_linter.check_single_file_item(file_item) mapreduce_data = defaultdict(list) @@ -137,9 +141,9 @@ def check_parallel( # is identical to the linter object here. This is required so that # a custom PyLinter object can be used. initializer = functools.partial(_worker_initialize, arguments=arguments) - with multiprocessing.Pool( - jobs, initializer=initializer, initargs=[dill.dumps(linter)] - ) as pool: + with ProcessPoolExecutor( + max_workers=jobs, initializer=initializer, initargs=(dill.dumps(linter),) + ) as executor: linter.open() all_stats = [] all_mapreduce_data: defaultdict[ @@ -158,7 +162,7 @@ def check_parallel( stats, msg_status, mapreduce_data, - ) in pool.imap_unordered(_worker_check_single_file, files): + ) in executor.map(_worker_check_single_file, files): linter.file_state.base_name = base_name linter.file_state._is_base_filestate = False linter.set_current_module(module, file_path) @@ -168,8 +172,5 @@ def check_parallel( all_mapreduce_data[worker_idx].append(mapreduce_data) linter.msg_status |= msg_status - pool.close() - pool.join() - _merge_mapreduce_data(linter, all_mapreduce_data) linter.stats = merge_stats([linter.stats] + all_stats) diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index f530b2959b..4d1bede9a6 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -4,6 +4,7 @@ from __future__ import annotations +import argparse import collections import contextlib import functools @@ -13,12 +14,15 @@ import traceback import warnings from collections import defaultdict -from collections.abc import Callable, Iterable, Iterator, Sequence +from collections.abc import Callable, Iterator, Sequence from io import TextIOWrapper +from pathlib import Path +from re import Pattern +from types import ModuleType from typing import Any import astroid -from astroid import AstroidError, nodes +from astroid import nodes from pylint import checkers, exceptions, interfaces, reporters from pylint.checkers.base_checker import BaseChecker @@ -41,6 +45,7 @@ report_total_messages_stats, ) from pylint.lint.utils import ( + _is_relative_to, fix_import_path, get_fatal_error_message, prepare_crash_report, @@ -50,6 +55,7 @@ from pylint.reporters.text import TextReporter from pylint.reporters.ureports import nodes as report_nodes from pylint.typing import ( + DirectoryNamespaceDict, FileItem, ManagedMessage, MessageDefinitionTuple, @@ -89,7 +95,7 @@ def _load_reporter_by_class(reporter_class: str) -> type[BaseReporter]: class_name = qname.split(".")[-1] klass = getattr(module, class_name) assert issubclass(klass, BaseReporter), f"{klass} is not a BaseReporter" - return klass + return klass # type: ignore[no-any-return] # Python Linter class ######################################################### @@ -245,7 +251,7 @@ class PyLinter( * handle some basic but necessary stats' data (number of classes, methods...) IDE plugin developers: you may have to call - `astroid.builder.MANAGER.astroid_cache.clear()` across runs if you want + `astroid.MANAGER.clear_cache()` across runs if you want to ensure the latest code version is actually checked. This class needs to support pickling for parallel linting to work. The exception @@ -255,7 +261,7 @@ class PyLinter( name = MAIN_CHECKER_NAME msgs = MSGS # Will be used like this : datetime.now().strftime(crash_file_path) - crash_file_path: str = "pylint-crash-%Y-%m-%d-%H.txt" + crash_file_path: str = "pylint-crash-%Y-%m-%d-%H-%M-%S.txt" option_groups_descs = { "Messages control": "Options controlling analysis messages", @@ -290,20 +296,9 @@ def __init__( str, list[checkers.BaseChecker] ] = collections.defaultdict(list) """Dictionary of registered and initialized checkers.""" - self._dynamic_plugins: set[str] = set() + self._dynamic_plugins: dict[str, ModuleType | ModuleNotFoundError | bool] = {} """Set of loaded plugin names.""" - # Attributes related to registering messages and their handling - self.msgs_store = MessageDefinitionStore() - self.msg_status = 0 - self._by_id_managed_msgs: list[ManagedMessage] = [] - - # Attributes related to visiting files - self.file_state = FileState("", self.msgs_store, is_base_filestate=True) - self.current_name: str | None = None - self.current_file: str | None = None - self._ignore_file = False - # Attributes related to stats self.stats = LinterStats() @@ -331,6 +326,19 @@ def __init__( ), ("RP0003", "Messages", report_messages_stats), ) + + # Attributes related to registering messages and their handling + self.msgs_store = MessageDefinitionStore(self.config.py_version) + self.msg_status = 0 + self._by_id_managed_msgs: list[ManagedMessage] = [] + + # Attributes related to visiting files + self.file_state = FileState("", self.msgs_store, is_base_filestate=True) + self.current_name: str | None = None + self.current_file: str | None = None + self._ignore_file = False + self._ignore_paths: list[Pattern[str]] = [] + self.register_checker(self) @property @@ -339,6 +347,7 @@ def option_groups(self) -> tuple[tuple[str, str], ...]: warnings.warn( "The option_groups attribute has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) return self._option_groups @@ -347,6 +356,7 @@ def option_groups(self, value: tuple[tuple[str, str], ...]) -> None: warnings.warn( "The option_groups attribute has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) self._option_groups = value @@ -355,16 +365,21 @@ def load_default_plugins(self) -> None: reporters.initialize(self) def load_plugin_modules(self, modnames: list[str]) -> None: - """Check a list pylint plugins modules, load and register them.""" + """Check a list of pylint plugins modules, load and register them. + + If a module cannot be loaded, never try to load it again and instead + store the error message for later use in ``load_plugin_configuration`` + below. + """ for modname in modnames: if modname in self._dynamic_plugins: continue - self._dynamic_plugins.add(modname) try: module = astroid.modutils.load_module_from_name(modname) module.register(self) - except ModuleNotFoundError: - pass + self._dynamic_plugins[modname] = module + except ModuleNotFoundError as mnf_e: + self._dynamic_plugins[modname] = mnf_e def load_plugin_configuration(self) -> None: """Call the configuration hook for plugins. @@ -372,14 +387,31 @@ def load_plugin_configuration(self) -> None: This walks through the list of plugins, grabs the "load_configuration" hook, if exposed, and calls it to allow plugins to configure specific settings. + + The result of attempting to load the plugin of the given name + is stored in the dynamic plugins dictionary in ``load_plugin_modules`` above. + + ..note:: + This function previously always tried to load modules again, which + led to some confusion and silent failure conditions as described + in GitHub issue #7264. Making it use the stored result is more efficient, and + means that we avoid the ``init-hook`` problems from before. """ - for modname in self._dynamic_plugins: - try: - module = astroid.modutils.load_module_from_name(modname) - if hasattr(module, "load_configuration"): - module.load_configuration(self) - except ModuleNotFoundError as e: - self.add_message("bad-plugin-value", args=(modname, e), line=0) + for modname, module_or_error in self._dynamic_plugins.items(): + if isinstance(module_or_error, ModuleNotFoundError): + self.add_message( + "bad-plugin-value", args=(modname, module_or_error), line=0 + ) + elif hasattr(module_or_error, "load_configuration"): + module_or_error.load_configuration(self) + + # We re-set all the dictionary values to True here to make sure the dict + # is pickle-able. This is only a problem in multiprocessing/parallel mode. + # (e.g. invoking pylint -j 2) + self._dynamic_plugins = { + modname: not isinstance(val, ModuleNotFoundError) + for modname, val in self._dynamic_plugins.items() + } def _load_reporters(self, reporter_names: str) -> None: """Load the reporters if they are available on _reporters.""" @@ -422,8 +454,8 @@ def _load_reporter_by_name(self, reporter_name: str) -> reporters.BaseReporter: reporter_class = _load_reporter_by_class(reporter_name) except (ImportError, AttributeError, AssertionError) as e: raise exceptions.InvalidReporterError(name) from e - else: - return reporter_class() + + return reporter_class() def set_reporter( self, reporter: reporters.BaseReporter | reporters.MultiReporter @@ -457,6 +489,9 @@ def register_checker(self, checker: checkers.BaseChecker) -> None: self.register_report(r_id, r_title, r_cb, checker) if hasattr(checker, "msgs"): self.msgs_store.register_messages_from_checker(checker) + for message in checker.messages: + if not message.default_enabled: + self.disable(message.msgid) # Register the checker, but disable all of its messages. if not getattr(checker, "enabled", True): self.disable(checker.name) @@ -572,10 +607,11 @@ def initialize(self) -> None: This method is called before any linting is done. """ + self._ignore_paths = self.config.ignore_paths # initialize msgs_state now that all messages have been registered into # the store for msg in self.msgs_store.messages: - if not msg.may_be_emitted(): + if not msg.may_be_emitted(self.config.py_version): self._msgs_state[msg.msgid] = False def _discover_files(self, files_or_modules: Sequence[str]) -> Iterator[str]: @@ -619,12 +655,16 @@ def check(self, files_or_modules: Sequence[str] | str) -> None: files_or_modules is either a string or list of strings presenting modules to check. """ + # 1) Initialize self.initialize() + + # 2) Gather all files if not isinstance(files_or_modules, (list, tuple)): # TODO: 3.0: Remove deprecated typing and update docstring warnings.warn( "In pylint 3.0, the checkers check function will only accept sequence of string", DeprecationWarning, + stacklevel=2, ) files_or_modules = (files_or_modules,) # type: ignore[assignment] if self.config.recursive: @@ -635,30 +675,68 @@ def check(self, files_or_modules: Sequence[str] | str) -> None: "Missing filename required for --from-stdin" ) - filepath = files_or_modules[0] - with fix_import_path(files_or_modules): - self._check_files( - functools.partial(self.get_ast, data=_read_stdin()), - [self._get_file_descr_from_stdin(filepath)], - ) - elif self.config.jobs == 1: - with fix_import_path(files_or_modules): - self._check_files( - self.get_ast, self._iterate_file_descrs(files_or_modules) - ) - else: + # TODO: Move the parallel invocation into step 5 of the checking process + if not self.config.from_stdin and self.config.jobs > 1: + original_sys_path = sys.path[:] check_parallel( self, self.config.jobs, self._iterate_file_descrs(files_or_modules), - files_or_modules, + files_or_modules, # this argument patches sys.path ) + sys.path = original_sys_path + return + + # 3) Get all FileItems + with fix_import_path(files_or_modules): + if self.config.from_stdin: + fileitems = self._get_file_descr_from_stdin(files_or_modules[0]) + data: str | None = _read_stdin() + else: + fileitems = self._iterate_file_descrs(files_or_modules) + data = None + + # The contextmanager also opens all checkers and sets up the PyLinter class + with fix_import_path(files_or_modules): + with self._astroid_module_checker() as check_astroid_module: + # 4) Get the AST for each FileItem + ast_per_fileitem = self._get_asts(fileitems, data) + + # 5) Lint each ast + self._lint_files(ast_per_fileitem, check_astroid_module) + + def _get_asts( + self, fileitems: Iterator[FileItem], data: str | None + ) -> dict[FileItem, nodes.Module | None]: + """Get the AST for all given FileItems.""" + ast_per_fileitem: dict[FileItem, nodes.Module | None] = {} + + for fileitem in fileitems: + self.set_current_module(fileitem.name, fileitem.filepath) + + try: + ast_per_fileitem[fileitem] = self.get_ast( + fileitem.filepath, fileitem.name, data + ) + except astroid.AstroidBuildingError as ex: + template_path = prepare_crash_report( + ex, fileitem.filepath, self.crash_file_path + ) + msg = get_fatal_error_message(fileitem.filepath, template_path) + self.add_message( + "astroid-error", + args=(fileitem.filepath, msg), + confidence=HIGH, + ) + + return ast_per_fileitem def check_single_file(self, name: str, filepath: str, modname: str) -> None: warnings.warn( "In pylint 3.0, the checkers check_single_file function will be removed. " "Use check_single_file_item instead.", DeprecationWarning, + stacklevel=2, ) self.check_single_file_item(FileItem(name, filepath, modname)) @@ -672,27 +750,61 @@ def check_single_file_item(self, file: FileItem) -> None: with self._astroid_module_checker() as check_astroid_module: self._check_file(self.get_ast, check_astroid_module, file) - def _check_files( + def _lint_files( self, - get_ast: GetAstProtocol, - file_descrs: Iterable[FileItem], + ast_mapping: dict[FileItem, nodes.Module | None], + check_astroid_module: Callable[[nodes.Module], bool | None], ) -> None: - """Check all files from file_descrs.""" - with self._astroid_module_checker() as check_astroid_module: - for file in file_descrs: - try: - self._check_file(get_ast, check_astroid_module, file) - except Exception as ex: # pylint: disable=broad-except - template_path = prepare_crash_report( - ex, file.filepath, self.crash_file_path + """Lint all AST modules from a mapping..""" + for fileitem, module in ast_mapping.items(): + if module is None: + continue + try: + self._lint_file(fileitem, module, check_astroid_module) + except Exception as ex: # pylint: disable=broad-except + template_path = prepare_crash_report( + ex, fileitem.filepath, self.crash_file_path + ) + msg = get_fatal_error_message(fileitem.filepath, template_path) + if isinstance(ex, astroid.AstroidError): + self.add_message( + "astroid-error", args=(fileitem.filepath, msg), confidence=HIGH ) - msg = get_fatal_error_message(file.filepath, template_path) - if isinstance(ex, AstroidError): - symbol = "astroid-error" - self.add_message(symbol, args=(file.filepath, msg)) - else: - symbol = "fatal" - self.add_message(symbol, args=msg) + else: + self.add_message("fatal", args=msg, confidence=HIGH) + + def _lint_file( + self, + file: FileItem, + module: nodes.Module, + check_astroid_module: Callable[[nodes.Module], bool | None], + ) -> None: + """Lint a file using the passed utility function check_astroid_module). + + :param FileItem file: data about the file + :param nodes.Module module: the ast module to lint + :param Callable check_astroid_module: callable checking an AST taking the following arguments + - ast: AST of the module + :raises AstroidError: for any failures stemming from astroid + """ + self.set_current_module(file.name, file.filepath) + self._ignore_file = False + self.file_state = FileState(file.modpath, self.msgs_store, module) + # fix the current file (if the source file was not available or + # if it's actually a c extension) + self.current_file = module.file + + try: + check_astroid_module(module) + except Exception as e: + raise astroid.AstroidError from e + + # warn about spurious inline messages handling + spurious_messages = self.file_state.iter_spurious_suppression_messages( + self.msgs_store + ) + for msgid, line, args in spurious_messages: + self.add_message(msgid, line, None, args) def _check_file( self, @@ -734,14 +846,21 @@ def _check_file( for msgid, line, args in spurious_messages: self.add_message(msgid, line, None, args) - @staticmethod - def _get_file_descr_from_stdin(filepath: str) -> FileItem: + def _get_file_descr_from_stdin(self, filepath: str) -> Iterator[FileItem]: """Return file description (tuple of module name, file path, base name) from given file path. This method is used for creating suitable file description for _check_files when the source is standard input. """ + if _is_ignored_file( + filepath, + self.config.ignore, + self.config.ignore_patterns, + self.config.ignore_paths, + ): + return + try: # Note that this function does not really perform an # __import__ but may raise an ImportError exception, which @@ -750,7 +869,7 @@ def _get_file_descr_from_stdin(filepath: str) -> FileItem: except ImportError: modname = os.path.splitext(os.path.basename(filepath))[0] - return FileItem(modname, filepath, filepath) + yield FileItem(modname, filepath, filepath) def _iterate_file_descrs( self, files_or_modules: Sequence[str] @@ -760,12 +879,12 @@ def _iterate_file_descrs( The returned generator yield one item for each Python module that should be linted. """ - for descr in self._expand_files(files_or_modules): + for descr in self._expand_files(files_or_modules).values(): name, filepath, is_arg = descr["name"], descr["path"], descr["isarg"] if self.should_analyze_file(name, filepath, is_argument=is_arg): yield FileItem(name, filepath, descr["basename"]) - def _expand_files(self, modules: Sequence[str]) -> list[ModuleDescriptionDict]: + def _expand_files(self, modules: Sequence[str]) -> dict[str, ModuleDescriptionDict]: """Get modules and errors from a list of modules and handle errors.""" result, errors = expand_modules( modules, @@ -800,11 +919,32 @@ def set_current_module( "If unknown it should be initialized as an empty string." ), DeprecationWarning, + stacklevel=2, ) self.current_name = modname self.current_file = filepath or modname self.stats.init_single_module(modname or "") + # If there is an actual filepath we might need to update the config attribute + if filepath: + namespace = self._get_namespace_for_file( + Path(filepath), self._directory_namespaces + ) + if namespace: + self.config = namespace or self._base_config + + def _get_namespace_for_file( + self, filepath: Path, namespaces: DirectoryNamespaceDict + ) -> argparse.Namespace | None: + for directory in namespaces: + if _is_relative_to(filepath, directory): + namespace = self._get_namespace_for_file( + filepath, namespaces[directory][1] + ) + if namespace is None: + return namespaces[directory][0] + return None + @contextlib.contextmanager def _astroid_module_checker( self, @@ -816,9 +956,7 @@ def _astroid_module_checker( walker = ASTWalker(self) _checkers = self.prepare_checkers() tokencheckers = [ - c - for c in _checkers - if isinstance(c, checkers.BaseTokenChecker) and c is not self + c for c in _checkers if isinstance(c, checkers.BaseTokenChecker) ] # TODO: 3.0: Remove deprecated for-loop for c in _checkers: @@ -873,10 +1011,10 @@ def _astroid_module_checker( def get_ast( self, filepath: str, modname: str, data: str | None = None - ) -> nodes.Module: + ) -> nodes.Module | None: """Return an ast(roid) representation of a module or a string. - :param str filepath: path to checked file. + :param filepath: path to checked file. :param str modname: The name of the module to be checked. :param str data: optional contents of the checked file. :returns: the AST @@ -890,12 +1028,15 @@ def get_ast( data, modname, filepath ) except astroid.AstroidSyntaxError as ex: - # pylint: disable=no-member + line = getattr(ex.error, "lineno", None) + if line is None: + line = 0 self.add_message( "syntax-error", - line=getattr(ex.error, "lineno", 0), + line=line, col_offset=getattr(ex.error, "offset", None), - args=str(ex.error), + args=f"Parsing failed: '{ex.error}'", + confidence=HIGH, ) except astroid.AstroidBuildingError as ex: self.add_message("parse-error", args=ex) @@ -987,7 +1128,6 @@ def open(self) -> None: self.config.extension_pkg_whitelist ) self.stats.reset_message_count() - self._ignore_paths = self.linter.config.ignore_paths def generate_reports(self) -> int | None: """Close the whole package /module, it's time to make reports ! @@ -1112,8 +1252,9 @@ def _add_one_message( msg_cat = MSG_TYPES[message_definition.msgid[0]] self.msg_status |= MSG_TYPES_STATUS[message_definition.msgid[0]] self.stats.increase_single_message_count(msg_cat, 1) + # TODO: 3.0 Should be removable after https://github.com/PyCQA/pylint/pull/5580 self.stats.increase_single_module_message_count( - self.current_name, # type: ignore[arg-type] # Should be removable after https://github.com/PyCQA/pylint/pull/5580 + self.current_name, # type: ignore[arg-type] msg_cat, 1, ) diff --git a/pylint/lint/run.py b/pylint/lint/run.py index b8923aac5f..a753f7868c 100644 --- a/pylint/lint/run.py +++ b/pylint/lint/run.py @@ -21,7 +21,7 @@ from pylint.config.utils import _preprocess_options from pylint.constants import full_version from pylint.lint.base_options import _make_run_options -from pylint.lint.pylinter import PyLinter +from pylint.lint.pylinter import MANAGER, PyLinter from pylint.reporters.base_reporter import BaseReporter try: @@ -30,6 +30,11 @@ except ImportError: multiprocessing = None # type: ignore[assignment] +try: + from concurrent.futures import ProcessPoolExecutor +except ImportError: + ProcessPoolExecutor = None # type: ignore[assignment,misc] + def _query_cpu() -> int | None: """Try to determine number of CPUs allotted in a docker container. @@ -110,6 +115,7 @@ class Run: Used by _PylintConfigRun to make the 'pylint-config' command work. """ + # pylint: disable = too-many-statements, too-many-branches def __init__( self, args: Sequence[str], @@ -185,9 +191,9 @@ def __init__( ) sys.exit(32) if linter.config.jobs > 1 or linter.config.jobs == 0: - if multiprocessing is None: + if ProcessPoolExecutor is None: print( - "Multiprocessing library is missing, fallback to single process", + "concurrent.futures module is missing, fallback to single process", file=sys.stderr, ) linter.set_option("jobs", 1) @@ -214,6 +220,9 @@ def __init__( ) exit = do_exit + if linter.config.clear_cache_post_run: + MANAGER.clear_cache() + if exit: if linter.config.exit_zero: sys.exit(0) diff --git a/pylint/lint/utils.py b/pylint/lint/utils.py index 30694c25cf..d4ad131f32 100644 --- a/pylint/lint/utils.py +++ b/pylint/lint/utils.py @@ -58,7 +58,8 @@ def prepare_crash_report(ex: Exception, filepath: str, crash_file_path: str) -> except Exception as exc: # pylint: disable=broad-except print( f"Can't write the issue template for the crash in {issue_template_path} " - f"because of: '{exc}'\nHere's the content anyway:\n{template}." + f"because of: '{exc}'\nHere's the content anyway:\n{template}.", + file=sys.stderr, ) return issue_template_path @@ -99,3 +100,16 @@ def fix_import_path(args: Sequence[str]) -> Iterator[None]: yield finally: sys.path[:] = original + + +def _is_relative_to(self: Path, *other: Path) -> bool: + """Checks if self is relative to other. + + Backport of pathlib.Path.is_relative_to for Python <3.9 + TODO: py39: Remove this backport and use stdlib function. + """ + try: + self.relative_to(*other) + return True + except ValueError: + return False diff --git a/pylint/message/message.py b/pylint/message/message.py index 4efa3f1244..23dd6c082d 100644 --- a/pylint/message/message.py +++ b/pylint/message/message.py @@ -43,6 +43,7 @@ def __init__( warn( "In pylint 3.0, Messages will only accept a MessageLocationTuple as location parameter", DeprecationWarning, + stacklevel=2, ) location = MessageLocationTuple( location[0], @@ -77,3 +78,16 @@ def format(self, template: str) -> str: cf. https://docs.python.org/2/library/string.html#formatstrings """ return template.format(**asdict(self)) + + @property + def location(self) -> MessageLocationTuple: + return MessageLocationTuple( + self.abspath, + self.path, + self.module, + self.obj, + self.line, + self.column, + self.end_line, + self.end_column, + ) diff --git a/pylint/message/message_definition.py b/pylint/message/message_definition.py index 714ce7efac..25aa87d92b 100644 --- a/pylint/message/message_definition.py +++ b/pylint/message/message_definition.py @@ -5,6 +5,7 @@ from __future__ import annotations import sys +import warnings from typing import TYPE_CHECKING, Any from astroid import nodes @@ -18,6 +19,7 @@ class MessageDefinition: + # pylint: disable-next=too-many-arguments def __init__( self, checker: BaseChecker, @@ -29,6 +31,8 @@ def __init__( minversion: tuple[int, int] | None = None, maxversion: tuple[int, int] | None = None, old_names: list[tuple[str, str]] | None = None, + shared: bool = False, + default_enabled: bool = True, ) -> None: self.checker_name = checker.name self.check_msgid(msgid) @@ -39,6 +43,8 @@ def __init__( self.scope = scope self.minversion = minversion self.maxversion = maxversion + self.shared = shared + self.default_enabled = default_enabled self.old_names: list[tuple[str, str]] = [] if old_names: for old_msgid, old_symbol in old_names: @@ -67,11 +73,24 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"{repr(self)}:\n{self.msg} {self.description}" - def may_be_emitted(self) -> bool: - """Return True if message may be emitted using the current interpreter.""" - if self.minversion is not None and self.minversion > sys.version_info: + def may_be_emitted( + self, + py_version: tuple[int, ...] | sys._version_info | None = None, + ) -> bool: + """Return True if message may be emitted using the configured py_version.""" + if py_version is None: + py_version = sys.version_info + warnings.warn( + "'py_version' will be a required parameter of " + "'MessageDefinition.may_be_emitted' in pylint 3.0. The most likely " + "solution is to use 'linter.config.py_version' if you need to keep " + "using this function, or to use 'MessageDefinition.is_message_enabled'" + " instead.", + DeprecationWarning, + ) + if self.minversion is not None and self.minversion > py_version: return False - if self.maxversion is not None and self.maxversion <= sys.version_info: + if self.maxversion is not None and self.maxversion <= py_version: return False return True diff --git a/pylint/message/message_definition_store.py b/pylint/message/message_definition_store.py index ef26d648df..7bbc70a517 100644 --- a/pylint/message/message_definition_store.py +++ b/pylint/message/message_definition_store.py @@ -6,6 +6,7 @@ import collections import functools +import sys from collections.abc import Sequence, ValuesView from typing import TYPE_CHECKING @@ -23,7 +24,9 @@ class MessageDefinitionStore: has no particular state during analysis. """ - def __init__(self) -> None: + def __init__( + self, py_version: tuple[int, ...] | sys._version_info = sys.version_info + ) -> None: self.message_id_store: MessageIdStore = MessageIdStore() # Primary registry for all active messages definitions. # It contains the 1:1 mapping from msgid to MessageDefinition. @@ -31,6 +34,7 @@ def __init__(self) -> None: self._messages_definitions: dict[str, MessageDefinition] = {} # MessageDefinition kept by category self._msgs_by_category: dict[str, list[str]] = collections.defaultdict(list) + self.py_version = py_version @property def messages(self) -> ValuesView[MessageDefinition]: @@ -52,10 +56,12 @@ def register_message(self, message: MessageDefinition) -> None: self._msgs_by_category[message.msgid[0]].append(message.msgid) # Since MessageDefinitionStore is only initialized once - # and the arguments are relatively small in size we do not run the + # and the arguments are relatively small we do not run the # risk of creating a large memory leak. # See discussion in: https://github.com/PyCQA/pylint/pull/5673 - @functools.lru_cache(maxsize=None) # pylint: disable=method-cache-max-size-none + @functools.lru_cache( # pylint: disable=method-cache-max-size-none # noqa: B019 + maxsize=None + ) def get_message_definitions(self, msgid_or_symbol: str) -> list[MessageDefinition]: """Returns the Message definition for either a numeric or symbolic id. @@ -108,7 +114,7 @@ def find_emittable_messages( emittable = [] non_emittable = [] for message in messages: - if message.may_be_emitted(): + if message.may_be_emitted(self.py_version): emittable.append(message) else: non_emittable.append(message) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index c6939e2a1a..85b23052e1 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -36,7 +36,7 @@ def get_title(self, node: nodes.ClassDef) -> str: title = node.name if self.module_names: title = f"{node.root().name}.{title}" - return title + return title # type: ignore[no-any-return] def _set_option(self, option: bool | None) -> bool: """Activate some options if not explicitly deactivated.""" @@ -70,7 +70,7 @@ def show_node(self, node: nodes.ClassDef) -> bool: """True if builtins and not show_builtins.""" if self.config.show_builtin: return True - return node.root().name != "builtins" + return node.root().name != "builtins" # type: ignore[no-any-return] def add_class(self, node: nodes.ClassDef) -> None: """Visit one class and add it to diagram.""" diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py index 7b15bf8163..c3bf8e0f01 100644 --- a/pylint/pyreverse/diagrams.py +++ b/pylint/pyreverse/diagrams.py @@ -143,7 +143,7 @@ def get_methods(self, node: nodes.ClassDef) -> list[nodes.FunctionDef]: and not decorated_with_property(m) and self.show_attr(m.name) ] - return sorted(methods, key=lambda n: n.name) + return sorted(methods, key=lambda n: n.name) # type: ignore[no-any-return] def add_object(self, title: str, node: nodes.ClassDef) -> None: """Create a diagram object.""" @@ -214,20 +214,34 @@ def extract_relationships(self) -> None: self.add_relationship(obj, impl_obj, "implements") except KeyError: continue - # associations link - for name, values in list(node.instance_attrs_type.items()) + list( + + # associations & aggregations links + for name, values in list(node.aggregations_type.items()): + for value in values: + self.assign_association_relationship( + value, obj, name, "aggregation" + ) + + for name, values in list(node.associations_type.items()) + list( node.locals_type.items() ): for value in values: - if value is astroid.Uninferable: - continue - if isinstance(value, astroid.Instance): - value = value._proxied - try: - associated_obj = self.object_from_node(value) - self.add_relationship(associated_obj, obj, "association", name) - except KeyError: - continue + self.assign_association_relationship( + value, obj, name, "association" + ) + + def assign_association_relationship( + self, value: astroid.NodeNG, obj: ClassEntity, name: str, type_relationship: str + ) -> None: + if value is astroid.Uninferable: + return + if isinstance(value, astroid.Instance): + value = value._proxied + try: + associated_obj = self.object_from_node(value) + self.add_relationship(associated_obj, obj, type_relationship, name) + except KeyError: + return class PackageDiagram(ClassDiagram): diff --git a/pylint/pyreverse/dot_printer.py b/pylint/pyreverse/dot_printer.py index 8836827049..077e0552da 100644 --- a/pylint/pyreverse/dot_printer.py +++ b/pylint/pyreverse/dot_printer.py @@ -8,8 +8,8 @@ import os import subprocess -import sys import tempfile +from enum import Enum from pathlib import Path from astroid import nodes @@ -17,19 +17,34 @@ from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer from pylint.pyreverse.utils import get_annotation_label + +class HTMLLabels(Enum): + LINEBREAK_LEFT = '
' + + ALLOWED_CHARSETS: frozenset[str] = frozenset(("utf-8", "iso-8859-1", "latin1")) SHAPES: dict[NodeType, str] = { NodeType.PACKAGE: "box", NodeType.INTERFACE: "record", NodeType.CLASS: "record", } +# pylint: disable-next=consider-using-namedtuple-or-dataclass ARROWS: dict[EdgeType, dict[str, str]] = { - EdgeType.INHERITS: dict(arrowtail="none", arrowhead="empty"), - EdgeType.IMPLEMENTS: dict(arrowtail="node", arrowhead="empty", style="dashed"), - EdgeType.ASSOCIATION: dict( - fontcolor="green", arrowtail="none", arrowhead="diamond", style="solid" - ), - EdgeType.USES: dict(arrowtail="none", arrowhead="open"), + EdgeType.INHERITS: {"arrowtail": "none", "arrowhead": "empty"}, + EdgeType.IMPLEMENTS: {"arrowtail": "node", "arrowhead": "empty", "style": "dashed"}, + EdgeType.ASSOCIATION: { + "fontcolor": "green", + "arrowtail": "none", + "arrowhead": "diamond", + "style": "solid", + }, + EdgeType.AGGREGATION: { + "fontcolor": "green", + "arrowtail": "none", + "arrowhead": "odiamond", + "style": "solid", + }, + EdgeType.USES: {"arrowtail": "none", "arrowhead": "open"}, } @@ -73,7 +88,7 @@ def emit_node( color = properties.color if properties.color is not None else self.DEFAULT_COLOR style = "filled" if color != self.DEFAULT_COLOR else "solid" label = self._build_label_for_node(properties) - label_part = f', label="{label}"' if label else "" + label_part = f", label=<{label}>" if label else "" fontcolor_part = ( f', fontcolor="{properties.fontcolor}"' if properties.fontcolor else "" ) @@ -92,17 +107,22 @@ def _build_label_for_node(self, properties: NodeProperties) -> str: # Add class attributes attrs: list[str] = properties.attrs or [] - attrs_string = r"\l".join(attr.replace("|", r"\|") for attr in attrs) - label = rf"{{{label}|{attrs_string}\l|" + attrs_string = rf"{HTMLLabels.LINEBREAK_LEFT.value}".join( + attr.replace("|", r"\|") for attr in attrs + ) + label = rf"{{{label}|{attrs_string}{HTMLLabels.LINEBREAK_LEFT.value}|" # Add class methods methods: list[nodes.FunctionDef] = properties.methods or [] for func in methods: args = self._get_method_arguments(func) - label += rf"{func.name}({', '.join(args)})" + method_name = ( + f"{func.name}" if func.is_abstract() else f"{func.name}" + ) + label += rf"{method_name}({', '.join(args)})" if func.returns: label += ": " + get_annotation_label(func.returns) - label += r"\l" + label += rf"{HTMLLabels.LINEBREAK_LEFT.value}" label += "}" return label @@ -143,10 +163,8 @@ def generate(self, outputfile: str) -> None: with open(dot_sourcepath, "w", encoding="utf8") as outfile: outfile.writelines(self.lines) if target not in graphviz_extensions: - use_shell = sys.platform == "win32" - subprocess.call( - ["dot", "-T", target, dot_sourcepath, "-o", outputfile], - shell=use_shell, + subprocess.run( + ["dot", "-T", target, dot_sourcepath, "-o", outputfile], check=True ) os.unlink(dot_sourcepath) diff --git a/pylint/pyreverse/inspector.py b/pylint/pyreverse/inspector.py index 042d3845e1..9150bbef3c 100644 --- a/pylint/pyreverse/inspector.py +++ b/pylint/pyreverse/inspector.py @@ -13,6 +13,7 @@ import os import traceback import warnings +from abc import ABC, abstractmethod from collections.abc import Generator from typing import Any, Callable, Optional @@ -123,6 +124,12 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor): * instance_attrs_type as locals_type but for klass member attributes (only on astroid.Class) + * associations_type + as instance_attrs_type but for association relationships + + * aggregations_type + as instance_attrs_type but for aggregations relationships + * implements, list of implemented interface _objects_ (only on astroid.Class nodes) """ @@ -134,6 +141,8 @@ def __init__(self, project: Project, tag: bool = False) -> None: self.tag = tag # visited project self.project = project + self.associations_handler = AggregationsHandler() + self.associations_handler.set_next(OtherAssociationsHandler()) def visit_project(self, node: Project) -> None: """Visit a pyreverse.utils.Project node. @@ -178,9 +187,12 @@ def visit_classdef(self, node: nodes.ClassDef) -> None: baseobj.specializations = specializations # resolve instance attributes node.instance_attrs_type = collections.defaultdict(list) + node.aggregations_type = collections.defaultdict(list) + node.associations_type = collections.defaultdict(list) for assignattrs in tuple(node.instance_attrs.values()): for assignattr in assignattrs: if not isinstance(assignattr, nodes.Unknown): + self.associations_handler.handle(assignattr, node) self.handle_assignattr_type(assignattr, node) # resolve implemented interface try: @@ -190,9 +202,10 @@ def visit_classdef(self, node: nodes.ClassDef) -> None: if node.implements: # TODO: 3.0: Remove support for __implements__ warnings.warn( - "pyreverse will drop support for resolving and displaying implemented interfaces in pylint 3.0. " - "The implementation relies on the '__implements__' attribute proposed in PEP 245, which was rejected " - "in 2006.", + "pyreverse will drop support for resolving and displaying " + "implemented interfaces in pylint 3.0. The implementation " + "relies on the '__implements__' attribute proposed in PEP 245" + ", which was rejected in 2006.", DeprecationWarning, ) else: @@ -313,6 +326,63 @@ def _imported_module( mod_paths.append(mod_path) +class AssociationHandlerInterface(ABC): + @abstractmethod + def set_next( + self, handler: AssociationHandlerInterface + ) -> AssociationHandlerInterface: + pass + + @abstractmethod + def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: + pass + + +class AbstractAssociationHandler(AssociationHandlerInterface): + """ + Chain of Responsibility for handling types of association, useful + to expand in the future if we want to add more distinct associations. + + Every link of the chain checks if it's a certain type of association. + If no association is found it's set as a generic association in `associations_type`. + + The default chaining behavior is implemented inside the base handler + class. + """ + + _next_handler: AssociationHandlerInterface + + def set_next( + self, handler: AssociationHandlerInterface + ) -> AssociationHandlerInterface: + self._next_handler = handler + return handler + + @abstractmethod + def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: + if self._next_handler: + self._next_handler.handle(node, parent) + + +class AggregationsHandler(AbstractAssociationHandler): + def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: + if isinstance(node.parent, (nodes.AnnAssign, nodes.Assign)) and isinstance( + node.parent.value, astroid.node_classes.Name + ): + current = set(parent.aggregations_type[node.attrname]) + parent.aggregations_type[node.attrname] = list( + current | utils.infer_node(node) + ) + else: + super().handle(node, parent) + + +class OtherAssociationsHandler(AbstractAssociationHandler): + def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: + current = set(parent.associations_type[node.attrname]) + parent.associations_type[node.attrname] = list(current | utils.infer_node(node)) + + def project_from_files( files: list[str], func_wrapper: _WrapperFuncT = _astroid_wrapper, diff --git a/pylint/pyreverse/main.py b/pylint/pyreverse/main.py index 043e2a3f3a..7a02755eda 100644 --- a/pylint/pyreverse/main.py +++ b/pylint/pyreverse/main.py @@ -36,14 +36,14 @@ OPTIONS: Options = ( ( "filter-mode", - dict( - short="f", - default="PUB_ONLY", - dest="mode", - type="string", - action="store", - metavar="", - help="""filter attributes and functions according to + { + "short": "f", + "default": "PUB_ONLY", + "dest": "mode", + "type": "string", + "action": "store", + "metavar": "", + "help": """filter attributes and functions according to . Correct modes are : 'PUB_ONLY' filter all non public attributes [DEFAULT], equivalent to PRIVATE+SPECIAL_A @@ -52,154 +52,156 @@ except constructor 'OTHER' filter protected and private attributes""", - ), + }, ), ( "class", - dict( - short="c", - action="extend", - metavar="", - type="csv", - dest="classes", - default=None, - help="create a class diagram with all classes related to ;\ + { + "short": "c", + "action": "extend", + "metavar": "", + "type": "csv", + "dest": "classes", + "default": None, + "help": "create a class diagram with all classes related to ;\ this uses by default the options -ASmy", - ), + }, ), ( "show-ancestors", - dict( - short="a", - action="store", - metavar="", - type="int", - default=None, - help="show generations of ancestor classes not in ", - ), + { + "short": "a", + "action": "store", + "metavar": "", + "type": "int", + "default": None, + "help": "show generations of ancestor classes not in ", + }, ), ( "all-ancestors", - dict( - short="A", - default=None, - action="store_true", - help="show all ancestors off all classes in ", - ), + { + "short": "A", + "default": None, + "action": "store_true", + "help": "show all ancestors off all classes in ", + }, ), ( "show-associated", - dict( - short="s", - action="store", - metavar="", - type="int", - default=None, - help="show levels of associated classes not in ", - ), + { + "short": "s", + "action": "store", + "metavar": "", + "type": "int", + "default": None, + "help": "show levels of associated classes not in ", + }, ), ( "all-associated", - dict( - short="S", - default=None, - action="store_true", - help="show recursively all associated off all associated classes", - ), + { + "short": "S", + "default": None, + "action": "store_true", + "help": "show recursively all associated off all associated classes", + }, ), ( "show-builtin", - dict( - short="b", - action="store_true", - default=False, - help="include builtin objects in representation of classes", - ), + { + "short": "b", + "action": "store_true", + "default": False, + "help": "include builtin objects in representation of classes", + }, ), ( "module-names", - dict( - short="m", - default=None, - type="yn", - metavar="", - help="include module name in representation of classes", - ), + { + "short": "m", + "default": None, + "type": "yn", + "metavar": "", + "help": "include module name in representation of classes", + }, ), ( "only-classnames", - dict( - short="k", - action="store_true", - default=False, - help="don't show attributes and methods in the class boxes; this disables -f values", - ), + { + "short": "k", + "action": "store_true", + "default": False, + "help": "don't show attributes and methods in the class boxes; this disables -f values", + }, ), ( "output", - dict( - short="o", - dest="output_format", - action="store", - default="dot", - metavar="", - type="string", - help=( - f"create a *. output file if format is available. Available formats are: {', '.join(DIRECTLY_SUPPORTED_FORMATS)}. " - f"Any other format will be tried to create by means of the 'dot' command line tool, which requires a graphviz installation." + { + "short": "o", + "dest": "output_format", + "action": "store", + "default": "dot", + "metavar": "", + "type": "string", + "help": ( + "create a *. output file if format is available. Available " + f"formats are: {', '.join(DIRECTLY_SUPPORTED_FORMATS)}. Any other " + f"format will be tried to create by means of the 'dot' command line " + f"tool, which requires a graphviz installation." ), - ), + }, ), ( "colorized", - dict( - dest="colorized", - action="store_true", - default=False, - help="Use colored output. Classes/modules of the same package get the same color.", - ), + { + "dest": "colorized", + "action": "store_true", + "default": False, + "help": "Use colored output. Classes/modules of the same package get the same color.", + }, ), ( "max-color-depth", - dict( - dest="max_color_depth", - action="store", - default=2, - metavar="", - type="int", - help="Use separate colors up to package depth of ", - ), + { + "dest": "max_color_depth", + "action": "store", + "default": 2, + "metavar": "", + "type": "int", + "help": "Use separate colors up to package depth of ", + }, ), ( "ignore", - dict( - type="csv", - metavar="", - dest="ignore_list", - default=constants.DEFAULT_IGNORE_LIST, - help="Files or directories to be skipped. They should be base names, not paths.", - ), + { + "type": "csv", + "metavar": "", + "dest": "ignore_list", + "default": constants.DEFAULT_IGNORE_LIST, + "help": "Files or directories to be skipped. They should be base names, not paths.", + }, ), ( "project", - dict( - default="", - type="string", - short="p", - metavar="", - help="set the project name.", - ), + { + "default": "", + "type": "string", + "short": "p", + "metavar": "", + "help": "set the project name.", + }, ), ( "output-directory", - dict( - default="", - type="path", - short="d", - action="store", - metavar="", - help="set the output directory path.", - ), + { + "default": "", + "type": "path", + "short": "d", + "action": "store", + "metavar": "", + "help": "set the output directory path.", + }, ), ) @@ -210,8 +212,13 @@ class Run(_ArgumentsManager, _ArgumentsProvider): options = OPTIONS name = "pyreverse" - # For mypy issue, see https://github.com/python/mypy/issues/10342 - def __init__(self, args: Sequence[str]) -> NoReturn: # type: ignore[misc] + def __init__(self, args: Sequence[str]) -> NoReturn: + # Immediately exit if user asks for version + if "--version" in args: + print("pyreverse is included in pylint:") + print(constants.full_version) + sys.exit(0) + _ArgumentsManager.__init__(self, prog="pyreverse", description=__doc__) _ArgumentsProvider.__init__(self, self) @@ -222,7 +229,8 @@ def __init__(self, args: Sequence[str]) -> NoReturn: # type: ignore[misc] if self.config.output_format not in DIRECTLY_SUPPORTED_FORMATS: check_graphviz_availability() print( - f"Format {self.config.output_format} is not supported natively. Pyreverse will try to generate it using Graphviz..." + f"Format {self.config.output_format} is not supported natively." + " Pyreverse will try to generate it using Graphviz..." ) check_if_graphviz_supports_format(self.config.output_format) diff --git a/pylint/pyreverse/mermaidjs_printer.py b/pylint/pyreverse/mermaidjs_printer.py index 9a2309a748..a8f3c576b7 100644 --- a/pylint/pyreverse/mermaidjs_printer.py +++ b/pylint/pyreverse/mermaidjs_printer.py @@ -24,6 +24,7 @@ class MermaidJSPrinter(Printer): EdgeType.INHERITS: "--|>", EdgeType.IMPLEMENTS: "..|>", EdgeType.ASSOCIATION: "--*", + EdgeType.AGGREGATION: "--o", EdgeType.USES: "-->", } @@ -54,6 +55,7 @@ def emit_node( for func in properties.methods: args = self._get_method_arguments(func) line = f"{func.name}({', '.join(args)})" + line += "*" if func.is_abstract() else "" if func.returns: line += f" {get_annotation_label(func.returns)}" body.append(line) diff --git a/pylint/pyreverse/plantuml_printer.py b/pylint/pyreverse/plantuml_printer.py index 45106152d0..de3f983b7a 100644 --- a/pylint/pyreverse/plantuml_printer.py +++ b/pylint/pyreverse/plantuml_printer.py @@ -24,6 +24,7 @@ class PlantUmlPrinter(Printer): EdgeType.INHERITS: "--|>", EdgeType.IMPLEMENTS: "..|>", EdgeType.ASSOCIATION: "--*", + EdgeType.AGGREGATION: "--o", EdgeType.USES: "-->", } @@ -39,7 +40,8 @@ def _open_graph(self) -> None: self.emit("top to bottom direction") else: raise ValueError( - f"Unsupported layout {self.layout}. PlantUmlPrinter only supports left to right and top to bottom layout." + f"Unsupported layout {self.layout}. PlantUmlPrinter only " + "supports left to right and top to bottom layout." ) def emit_node( @@ -66,7 +68,8 @@ def emit_node( if properties.methods: for func in properties.methods: args = self._get_method_arguments(func) - line = f"{func.name}({', '.join(args)})" + line = "{abstract}" if func.is_abstract() else "" + line += f"{func.name}({', '.join(args)})" if func.returns: line += " -> " + get_annotation_label(func.returns) body.append(line) diff --git a/pylint/pyreverse/printer.py b/pylint/pyreverse/printer.py index 55ce2c8b16..cdbf7e3c8b 100644 --- a/pylint/pyreverse/printer.py +++ b/pylint/pyreverse/printer.py @@ -25,6 +25,7 @@ class EdgeType(Enum): INHERITS = "inherits" IMPLEMENTS = "implements" ASSOCIATION = "association" + AGGREGATION = "aggregation" USES = "uses" diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py index b1be195e37..078bc1b7e5 100644 --- a/pylint/pyreverse/utils.py +++ b/pylint/pyreverse/utils.py @@ -15,6 +15,7 @@ import astroid from astroid import nodes +from astroid.typing import InferenceResult if TYPE_CHECKING: from pylint.pyreverse.diagrams import ClassDiagram, PackageDiagram @@ -73,12 +74,12 @@ def get_visibility(name: str) -> str: def is_interface(node: nodes.ClassDef) -> bool: # bw compatibility - return node.type == "interface" + return node.type == "interface" # type: ignore[no-any-return] def is_exception(node: nodes.ClassDef) -> bool: # bw compatibility - return node.type == "exception" + return node.type == "exception" # type: ignore[no-any-return] # Helpers ##################################################################### @@ -169,9 +170,9 @@ def visit(self, node: nodes.NodeNG) -> Any: def get_annotation_label(ann: nodes.Name | nodes.NodeNG) -> str: if isinstance(ann, nodes.Name) and ann.name is not None: - return ann.name + return ann.name # type: ignore[no-any-return] if isinstance(ann, nodes.NodeNG): - return ann.as_string() + return ann.as_string() # type: ignore[no-any-return] return "" @@ -210,7 +211,7 @@ def get_annotation( return ann -def infer_node(node: nodes.AssignAttr | nodes.AssignName) -> set[Any]: +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. """ diff --git a/pylint/pyreverse/vcg_printer.py b/pylint/pyreverse/vcg_printer.py index ec7152baa0..b9e2e94f38 100644 --- a/pylint/pyreverse/vcg_printer.py +++ b/pylint/pyreverse/vcg_printer.py @@ -154,20 +154,34 @@ NodeType.CLASS: "box", NodeType.INTERFACE: "ellipse", } -ARROWS: dict[EdgeType, dict] = { - EdgeType.USES: dict(arrowstyle="solid", backarrowstyle="none", backarrowsize=0), - EdgeType.INHERITS: dict( - arrowstyle="solid", backarrowstyle="none", backarrowsize=10 - ), - EdgeType.IMPLEMENTS: dict( - arrowstyle="solid", - backarrowstyle="none", - linestyle="dotted", - backarrowsize=10, - ), - EdgeType.ASSOCIATION: dict( - arrowstyle="solid", backarrowstyle="none", textcolor="green" - ), +# pylint: disable-next=consider-using-namedtuple-or-dataclass +ARROWS: dict[EdgeType, dict[str, str | int]] = { + EdgeType.USES: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "backarrowsize": 0, + }, + EdgeType.INHERITS: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "backarrowsize": 10, + }, + EdgeType.IMPLEMENTS: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "linestyle": "dotted", + "backarrowsize": 10, + }, + EdgeType.ASSOCIATION: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "textcolor": "green", + }, + EdgeType.AGGREGATION: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "textcolor": "green", + }, } ORIENTATION: dict[Layout, str] = { Layout.LEFT_TO_RIGHT: "left_to_right", @@ -265,13 +279,15 @@ def emit_edge( ) self.emit("}") - def _write_attributes(self, attributes_dict: Mapping[str, Any], **args) -> None: + def _write_attributes( + self, attributes_dict: Mapping[str, Any], **args: Any + ) -> None: """Write graph, node or edge attributes.""" for key, value in args.items(): try: _type = attributes_dict[key] except KeyError as e: - raise Exception( + raise AttributeError( f"no such attribute {key}\npossible attributes are {attributes_dict.keys()}" ) from e @@ -282,6 +298,6 @@ def _write_attributes(self, attributes_dict: Mapping[str, Any], **args) -> None: elif value in _type: self.emit(f"{key}:{value}\n") else: - raise Exception( + raise ValueError( f"value {value} isn't correct for attribute {key} correct values are {type}" ) diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py index 12a76df9bf..68a49eea19 100644 --- a/pylint/pyreverse/writer.py +++ b/pylint/pyreverse/writer.py @@ -92,7 +92,7 @@ def write_packages(self, diagram: PackageDiagram) -> None: def write_classes(self, diagram: ClassDiagram) -> None: """Write a class diagram.""" # sorted to get predictable (hence testable) results - for obj in sorted(diagram.objects, key=lambda x: x.title): + for obj in sorted(diagram.objects, key=lambda x: x.title): # type: ignore[no-any-return] obj.fig_id = obj.node.qname() type_ = NodeType.INTERFACE if obj.shape == "interface" else NodeType.CLASS self.printer.emit_node( @@ -120,6 +120,14 @@ def write_classes(self, diagram: ClassDiagram) -> None: label=rel.name, type_=EdgeType.ASSOCIATION, ) + # generate aggregations + for rel in diagram.get_relationships("aggregation"): + self.printer.emit_edge( + rel.from_object.fig_id, + rel.to_object.fig_id, + label=rel.name, + type_=EdgeType.AGGREGATION, + ) def set_printer(self, file_name: str, basename: str) -> None: """Set printer.""" diff --git a/pylint/reporters/base_reporter.py b/pylint/reporters/base_reporter.py index 0b4507e5c1..3df970d80b 100644 --- a/pylint/reporters/base_reporter.py +++ b/pylint/reporters/base_reporter.py @@ -36,6 +36,7 @@ def __init__(self, output: TextIO | None = None) -> None: "Using the __implements__ inheritance pattern for BaseReporter is no " "longer supported. Child classes should only inherit BaseReporter", DeprecationWarning, + stacklevel=2, ) self.linter: PyLinter self.section = 0 @@ -54,6 +55,7 @@ def set_output(self, output: TextIO | None = None) -> None: warn( "'set_output' will be removed in 3.0, please use 'reporter.out = stream' instead", DeprecationWarning, + stacklevel=2, ) self.out = output or sys.stdout diff --git a/pylint/reporters/json_reporter.py b/pylint/reporters/json_reporter.py index 8adb783027..29df6ba076 100644 --- a/pylint/reporters/json_reporter.py +++ b/pylint/reporters/json_reporter.py @@ -7,16 +7,43 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING +import sys +from typing import TYPE_CHECKING, Optional +from pylint.interfaces import UNDEFINED +from pylint.message import Message from pylint.reporters.base_reporter import BaseReporter +from pylint.typing import MessageLocationTuple + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter from pylint.reporters.ureports.nodes import Section +# Since message-id is an invalid name we need to use the alternative syntax +OldJsonExport = TypedDict( + "OldJsonExport", + { + "type": str, + "module": str, + "obj": str, + "line": int, + "column": int, + "endLine": Optional[int], + "endColumn": Optional[int], + "path": str, + "symbol": str, + "message": str, + "message-id": str, + }, +) + -class JSONReporter(BaseReporter): +class BaseJSONReporter(BaseReporter): """Report messages and layouts in JSON.""" name = "json" @@ -24,22 +51,7 @@ class JSONReporter(BaseReporter): def display_messages(self, layout: Section | None) -> None: """Launch layouts display.""" - json_dumpable = [ - { - "type": msg.category, - "module": msg.module, - "obj": msg.obj, - "line": msg.line, - "column": msg.column, - "endLine": msg.end_line, - "endColumn": msg.end_column, - "path": msg.path, - "symbol": msg.symbol, - "message": msg.msg or "", - "message-id": msg.msg_id, - } - for msg in self.messages - ] + json_dumpable = [self.serialize(message) for message in self.messages] print(json.dumps(json_dumpable, indent=4), file=self.out) def display_reports(self, layout: Section) -> None: @@ -48,6 +60,62 @@ def display_reports(self, layout: Section) -> None: def _display(self, layout: Section) -> None: """Do nothing.""" + @staticmethod + def serialize(message: Message) -> OldJsonExport: + raise NotImplementedError + + @staticmethod + def deserialize(message_as_json: OldJsonExport) -> Message: + raise NotImplementedError + + +class JSONReporter(BaseJSONReporter): + + """ + TODO: 3.0: Remove this JSONReporter in favor of the new one handling abs-path + and confidence. + + TODO: 2.16: Add a new JSONReporter handling abs-path, confidence and scores. + (Ultimately all other breaking change related to json for 3.0). + """ + + @staticmethod + def serialize(message: Message) -> OldJsonExport: + return { + "type": message.category, + "module": message.module, + "obj": message.obj, + "line": message.line, + "column": message.column, + "endLine": message.end_line, + "endColumn": message.end_column, + "path": message.path, + "symbol": message.symbol, + "message": message.msg or "", + "message-id": message.msg_id, + } + + @staticmethod + def deserialize(message_as_json: OldJsonExport) -> Message: + return Message( + msg_id=message_as_json["message-id"], + symbol=message_as_json["symbol"], + msg=message_as_json["message"], + location=MessageLocationTuple( + # TODO: 3.0: Add abs-path and confidence in a new JSONReporter + abspath=message_as_json["path"], + path=message_as_json["path"], + module=message_as_json["module"], + obj=message_as_json["obj"], + line=message_as_json["line"], + column=message_as_json["column"], + end_line=message_as_json["endLine"], + end_column=message_as_json["endColumn"], + ), + # TODO: 3.0: Make confidence available in a new JSONReporter + confidence=UNDEFINED, + ) + def register(linter: PyLinter) -> None: linter.register_reporter(JSONReporter) diff --git a/pylint/reporters/multi_reporter.py b/pylint/reporters/multi_reporter.py index 45d3574456..8bf0828c4e 100644 --- a/pylint/reporters/multi_reporter.py +++ b/pylint/reporters/multi_reporter.py @@ -6,6 +6,7 @@ import os from collections.abc import Callable +from copy import copy from typing import TYPE_CHECKING, TextIO from pylint.message import Message @@ -77,7 +78,8 @@ def linter(self, value: PyLinter) -> None: def handle_message(self, msg: Message) -> None: """Handle a new message triggered on the current file.""" for rep in self._sub_reporters: - rep.handle_message(msg) + # We provide a copy so reporters can't modify message for others. + rep.handle_message(copy(msg)) def writeln(self, string: str = "") -> None: """Write a line in the output buffer.""" diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index 29bd46798f..bc11ac4faf 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -135,6 +135,7 @@ def colorize_ansi( warnings.warn( "In pylint 3.0, the colorize_ansi function of Text reporters will only accept a MessageStyle parameter", DeprecationWarning, + stacklevel=2, ) color = kwargs.get("color") style_attrs = tuple(_splitstrip(style)) @@ -149,6 +150,10 @@ def colorize_ansi( return msg +def make_header(msg: Message) -> str: + return f"************* Module {msg.module}" + + class TextReporter(BaseReporter): """Reports messages and layouts in plain text.""" @@ -175,7 +180,7 @@ def on_set_current_module(self, module: str, filepath: str | None) -> None: self._template = template # Check to see if all parameters in the template are attributes of the Message - arguments = re.findall(r"\{(.+?)(:.*)?\}", template) + arguments = re.findall(r"\{(\w+?)(:.*)?\}", template) for argument in arguments: if argument[0] not in MESSAGE_FIELDS: warnings.warn( @@ -198,11 +203,8 @@ def write_message(self, msg: Message) -> None: def handle_message(self, msg: Message) -> None: """Manage message of different type and in the context of path.""" if msg.module not in self._modules: - if msg.module: - self.writeln(f"************* Module {msg.module}") - self._modules.add(msg.module) - else: - self.writeln("************* ") + self.writeln(make_header(msg)) + self._modules.add(msg.module) self.write_message(msg) def _display(self, layout: Section) -> None: @@ -211,6 +213,18 @@ def _display(self, layout: Section) -> None: TextWriter().format(layout, self.out) +class NoHeaderReporter(TextReporter): + """Reports messages and layouts in plain text without a module header.""" + + name = "no-header" + + def handle_message(self, msg: Message) -> None: + """Write message(s) without module header.""" + if msg.module not in self._modules: + self._modules.add(msg.module) + self.write_message(msg) + + class ParseableTextReporter(TextReporter): """A reporter very similar to TextReporter, but display messages in a form recognized by most text editors : @@ -225,6 +239,7 @@ def __init__(self, output: TextIO | None = None) -> None: warnings.warn( f"{self.name} output format is deprecated. This is equivalent to --msg-template={self.line_format}", DeprecationWarning, + stacklevel=2, ) super().__init__(output) @@ -265,6 +280,7 @@ def __init__( warnings.warn( "In pylint 3.0, the ColorizedTextReporter will only accept ColorMappingDict as color_mapping parameter", DeprecationWarning, + stacklevel=2, ) temp_color_mapping: ColorMappingDict = {} for key, value in color_mapping.items(): @@ -293,10 +309,7 @@ def handle_message(self, msg: Message) -> None: """ if msg.module not in self._modules: msg_style = self._get_decoration("S") - if msg.module: - modsep = colorize_ansi(f"************* Module {msg.module}", msg_style) - else: - modsep = colorize_ansi(f"************* {msg.module}", msg_style) + modsep = colorize_ansi(make_header(msg), msg_style) self.writeln(modsep) self._modules.add(msg.module) msg_style = self._get_decoration(msg.C) @@ -310,6 +323,7 @@ def handle_message(self, msg: Message) -> None: 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) diff --git a/pylint/testutils/_primer/__init__.py b/pylint/testutils/_primer/__init__.py new file mode 100644 index 0000000000..17c854572e --- /dev/null +++ b/pylint/testutils/_primer/__init__.py @@ -0,0 +1,10 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +__all__ = ["PackageToLint", "PRIMER_DIRECTORY_PATH"] + +from pylint.testutils._primer.package_to_lint import ( + PRIMER_DIRECTORY_PATH, + PackageToLint, +) diff --git a/pylint/testutils/primer.py b/pylint/testutils/_primer/package_to_lint.py similarity index 76% rename from pylint/testutils/primer.py rename to pylint/testutils/_primer/package_to_lint.py index 0492c3a8bc..09ecb44560 100644 --- a/pylint/testutils/primer.py +++ b/pylint/testutils/_primer/package_to_lint.py @@ -5,9 +5,16 @@ from __future__ import annotations import logging +import sys from pathlib import Path -import git +from git.cmd import Git +from git.repo import Repo + +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal PRIMER_DIRECTORY_PATH = Path("tests") / ".pylint_primer_tests" @@ -33,6 +40,9 @@ class PackageToLint: pylintrc_relpath: str | None """Path relative to project's main directory to the pylintrc if it exists.""" + minimum_python: str | None + """Minimum python version supported by the package.""" + def __init__( self, url: str, @@ -41,6 +51,7 @@ def __init__( commit: str | None = None, pylint_additional_args: list[str] | None = None, pylintrc_relpath: str | None = None, + minimum_python: str | None = None, ) -> None: self.url = url self.branch = branch @@ -48,11 +59,13 @@ def __init__( self.commit = commit self.pylint_additional_args = pylint_additional_args or [] self.pylintrc_relpath = pylintrc_relpath + self.minimum_python = minimum_python @property - def pylintrc(self) -> Path | None: + def pylintrc(self) -> Path | Literal[""]: if self.pylintrc_relpath is None: - return None + # Fall back to "" to ensure pylint's own pylintrc is not discovered + return "" return self.clone_directory / self.pylintrc_relpath @property @@ -69,9 +82,8 @@ def paths_to_lint(self) -> list[str]: @property def pylint_args(self) -> list[str]: options: list[str] = [] - if self.pylintrc is not None: - # There is an error if rcfile is given but does not exist - options += [f"--rcfile={self.pylintrc}"] + # There is an error if rcfile is given but does not exist + options += [f"--rcfile={self.pylintrc}"] return self.paths_to_lint + options + self.pylint_additional_args def lazy_clone(self) -> str: # pragma: no cover @@ -92,22 +104,22 @@ def lazy_clone(self) -> str: # pragma: no cover "depth": 1, } logging.info("Directory does not exists, cloning: %s", options) - repo = git.Repo.clone_from(**options) - return repo.head.object.hexsha + repo = Repo.clone_from( + url=self.url, to_path=self.clone_directory, branch=self.branch, depth=1 + ) + return str(repo.head.object.hexsha) - remote_sha1_commit = ( - git.cmd.Git().ls_remote(self.url, self.branch).split("\t")[0] - ) - local_sha1_commit = git.Repo(self.clone_directory).head.object.hexsha + remote_sha1_commit = Git().ls_remote(self.url, self.branch).split("\t")[0] + local_sha1_commit = Repo(self.clone_directory).head.object.hexsha if remote_sha1_commit != local_sha1_commit: logging.info( "Remote sha is '%s' while local sha is '%s': pulling new commits", remote_sha1_commit, local_sha1_commit, ) - repo = git.Repo(self.clone_directory) + repo = Repo(self.clone_directory) origin = repo.remotes.origin origin.pull() else: logging.info("Repository already up to date.") - return remote_sha1_commit + return str(remote_sha1_commit) diff --git a/pylint/testutils/_primer/primer.py b/pylint/testutils/_primer/primer.py new file mode 100644 index 0000000000..7d08f1df5f --- /dev/null +++ b/pylint/testutils/_primer/primer.py @@ -0,0 +1,110 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +from pylint.testutils._primer import PackageToLint +from pylint.testutils._primer.primer_command import PrimerCommand +from pylint.testutils._primer.primer_compare_command import CompareCommand +from pylint.testutils._primer.primer_prepare_command import PrepareCommand +from pylint.testutils._primer.primer_run_command import RunCommand + + +class Primer: + """Main class to handle priming of packages.""" + + def __init__(self, primer_directory: Path, json_path: Path) -> None: + # Preparing arguments + self.primer_directory = primer_directory + self._argument_parser = argparse.ArgumentParser(prog="Pylint Primer") + self._subparsers = self._argument_parser.add_subparsers( + dest="command", required=True + ) + + # All arguments for the prepare parser + prepare_parser = self._subparsers.add_parser("prepare") + prepare_parser.add_argument( + "--clone", help="Clone all packages.", action="store_true", default=False + ) + prepare_parser.add_argument( + "--check", + help="Check consistencies and commits of all packages.", + action="store_true", + default=False, + ) + prepare_parser.add_argument( + "--make-commit-string", + help="Get latest commit string.", + action="store_true", + default=False, + ) + prepare_parser.add_argument( + "--read-commit-string", + help="Print latest commit string.", + action="store_true", + default=False, + ) + + # All arguments for the run parser + run_parser = self._subparsers.add_parser("run") + run_parser.add_argument( + "--type", choices=["main", "pr"], required=True, help="Type of primer run." + ) + + # All arguments for the compare parser + compare_parser = self._subparsers.add_parser("compare") + compare_parser.add_argument( + "--base-file", + required=True, + help="Location of output file of the base run.", + ) + compare_parser.add_argument( + "--new-file", + required=True, + help="Location of output file of the new run.", + ) + compare_parser.add_argument( + "--commit", + required=True, + help="Commit hash of the PR commit being checked.", + ) + + # Storing arguments + self.config = self._argument_parser.parse_args() + + self.packages = self._get_packages_to_lint_from_json(json_path) + """All packages to prime.""" + + if self.config.command == "prepare": + command_class: type[PrimerCommand] = PrepareCommand + elif self.config.command == "run": + command_class = RunCommand + elif self.config.command == "compare": + command_class = CompareCommand + self.command = command_class(self.primer_directory, self.packages, self.config) + + def run(self) -> None: + self.command.run() + + @staticmethod + def _minimum_python_supported(package_data: dict[str, str]) -> bool: + min_python_str = package_data.get("minimum_python", None) + if not min_python_str: + return True + min_python_tuple = tuple(int(n) for n in min_python_str.split(".")) + return min_python_tuple <= sys.version_info[:2] + + @staticmethod + def _get_packages_to_lint_from_json(json_path: Path) -> dict[str, PackageToLint]: + with open(json_path, encoding="utf8") as f: + return { + name: PackageToLint(**package_data) + for name, package_data in json.load(f).items() + if Primer._minimum_python_supported(package_data) + } diff --git a/pylint/testutils/_primer/primer_command.py b/pylint/testutils/_primer/primer_command.py new file mode 100644 index 0000000000..bbc930913d --- /dev/null +++ b/pylint/testutils/_primer/primer_command.py @@ -0,0 +1,45 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +import abc +import argparse +import sys +from pathlib import Path +from typing import Dict + +from pylint.reporters.json_reporter import OldJsonExport +from pylint.testutils._primer import PackageToLint + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + + +class PackageData(TypedDict): + commit: str + messages: list[OldJsonExport] + + +PackageMessages = Dict[str, PackageData] + + +class PrimerCommand: + """Generic primer action with required arguments.""" + + def __init__( + self, + primer_directory: Path, + packages: dict[str, PackageToLint], + config: argparse.Namespace, + ) -> None: + self.primer_directory = primer_directory + self.packages = packages + self.config = config + + @abc.abstractmethod + def run(self) -> None: + pass diff --git a/pylint/testutils/_primer/primer_compare_command.py b/pylint/testutils/_primer/primer_compare_command.py new file mode 100644 index 0000000000..442ffa227a --- /dev/null +++ b/pylint/testutils/_primer/primer_compare_command.py @@ -0,0 +1,155 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + +import json +from pathlib import Path, PurePosixPath + +from pylint.reporters.json_reporter import OldJsonExport +from pylint.testutils._primer.primer_command import ( + PackageData, + PackageMessages, + PrimerCommand, +) + +MAX_GITHUB_COMMENT_LENGTH = 65536 + + +class CompareCommand(PrimerCommand): + def run(self) -> None: + main_data = self._load_json(self.config.base_file) + pr_data = self._load_json(self.config.new_file) + missing_messages_data, new_messages_data = self._cross_reference( + main_data, pr_data + ) + comment = self._create_comment(missing_messages_data, new_messages_data) + with open(self.primer_directory / "comment.txt", "w", encoding="utf-8") as f: + f.write(comment) + + @staticmethod + def _cross_reference( + main_data: PackageMessages, pr_data: PackageMessages + ) -> tuple[PackageMessages, PackageMessages]: + missing_messages_data: PackageMessages = {} + for package, data in main_data.items(): + package_missing_messages: list[OldJsonExport] = [] + for message in data["messages"]: + try: + pr_data[package]["messages"].remove(message) + except ValueError: + package_missing_messages.append(message) + missing_messages_data[package] = PackageData( + commit=pr_data[package]["commit"], messages=package_missing_messages + ) + return missing_messages_data, pr_data + + @staticmethod + def _load_json(file_path: Path | str) -> PackageMessages: + with open(file_path, encoding="utf-8") as f: + result: PackageMessages = json.load(f) + return result + + def _create_comment( + self, all_missing_messages: PackageMessages, all_new_messages: PackageMessages + ) -> str: + comment = "" + for package, missing_messages in all_missing_messages.items(): + if len(comment) >= MAX_GITHUB_COMMENT_LENGTH: + break + new_messages = all_new_messages[package] + if not missing_messages["messages"] and not new_messages["messages"]: + continue + comment += self._create_comment_for_package( + package, new_messages, missing_messages + ) + comment = ( + f"🤖 **Effect of this PR on checked open source code:** 🤖\n\n{comment}" + if comment + else ( + "🤖 According to the primer, this change has **no effect** on the" + " checked open source code. 🤖🎉\n\n" + ) + ) + return self._truncate_comment(comment) + + def _create_comment_for_package( + self, package: str, new_messages: PackageData, missing_messages: PackageData + ) -> str: + comment = f"\n\n**Effect on [{package}]({self.packages[package].url}):**\n" + # Create comment for new messages + count = 1 + astroid_errors = 0 + new_non_astroid_messages = "" + if new_messages["messages"]: + print("Now emitted:") + for message in new_messages["messages"]: + filepath = str( + PurePosixPath(message["path"]).relative_to( + self.packages[package].clone_directory + ) + ) + # Existing astroid errors may still show up as "new" because the timestamp + # in the message is slightly different. + if message["symbol"] == "astroid-error": + astroid_errors += 1 + else: + new_non_astroid_messages += ( + f"{count}) {message['symbol']}:\n*{message['message']}*\n" + f"{self.packages[package].url}/blob/{new_messages['commit']}/{filepath}#L{message['line']}\n" + ) + print(message) + count += 1 + + if astroid_errors: + comment += ( + f'{astroid_errors} "astroid error(s)" were found. ' + "Please open the GitHub Actions log to see what failed or crashed.\n\n" + ) + if new_non_astroid_messages: + comment += ( + "The following messages are now emitted:\n\n
\n\n" + + new_non_astroid_messages + + "\n
\n\n" + ) + + # Create comment for missing messages + count = 1 + if missing_messages["messages"]: + comment += "The following messages are no longer emitted:\n\n
\n\n" + print("No longer emitted:") + for message in missing_messages["messages"]: + comment += f"{count}) {message['symbol']}:\n*{message['message']}*\n" + filepath = str( + PurePosixPath(message["path"]).relative_to( + self.packages[package].clone_directory + ) + ) + assert not self.packages[package].url.endswith( + ".git" + ), "You don't need the .git at the end of the github url." + comment += f"{self.packages[package].url}/blob/{new_messages['commit']}/{filepath}#L{message['line']}\n" + count += 1 + print(message) + if missing_messages: + comment += "\n
\n\n" + return comment + + def _truncate_comment(self, comment: str) -> str: + """GitHub allows only a set number of characters in a comment.""" + hash_information = ( + f"*This comment was generated for commit {self.config.commit}*" + ) + if len(comment) + len(hash_information) >= MAX_GITHUB_COMMENT_LENGTH: + truncation_information = ( + f"*This comment was truncated because GitHub allows only" + f" {MAX_GITHUB_COMMENT_LENGTH} characters in a comment.*" + ) + max_len = ( + MAX_GITHUB_COMMENT_LENGTH + - len(hash_information) + - len(truncation_information) + ) + comment = f"{comment[:max_len - 10]}...\n\n{truncation_information}\n\n" + comment += hash_information + return comment diff --git a/pylint/testutils/_primer/primer_prepare_command.py b/pylint/testutils/_primer/primer_prepare_command.py new file mode 100644 index 0000000000..e69e55b955 --- /dev/null +++ b/pylint/testutils/_primer/primer_prepare_command.py @@ -0,0 +1,47 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + +import sys + +from git.cmd import Git +from git.repo import Repo + +from pylint.testutils._primer.primer_command import PrimerCommand + + +class PrepareCommand(PrimerCommand): + def run(self) -> None: + commit_string = "" + version_string = ".".join(str(x) for x in sys.version_info[:2]) + if self.config.clone: + for package, data in self.packages.items(): + local_commit = data.lazy_clone() + print(f"Cloned '{package}' at commit '{local_commit}'.") + commit_string += local_commit + "_" + elif self.config.check: + for package, data in self.packages.items(): + local_commit = Repo(data.clone_directory).head.object.hexsha + print(f"Found '{package}' at commit '{local_commit}'.") + commit_string += local_commit + "_" + elif self.config.make_commit_string: + for package, data in self.packages.items(): + remote_sha1_commit = ( + Git().ls_remote(data.url, data.branch).split("\t")[0] + ) + print(f"'{package}' remote is at commit '{remote_sha1_commit}'.") + commit_string += remote_sha1_commit + "_" + elif self.config.read_commit_string: + with open( + self.primer_directory / f"commit_string_{version_string}.txt", + encoding="utf-8", + ) as f: + print(f.read()) + if commit_string: + with open( + self.primer_directory / f"commit_string_{version_string}.txt", + "w", + encoding="utf-8", + ) as f: + f.write(commit_string) diff --git a/pylint/testutils/_primer/primer_run_command.py b/pylint/testutils/_primer/primer_run_command.py new file mode 100644 index 0000000000..cd17d6b1d8 --- /dev/null +++ b/pylint/testutils/_primer/primer_run_command.py @@ -0,0 +1,101 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +import json +import sys +import warnings +from io import StringIO + +from git.repo import Repo + +from pylint.lint import Run +from pylint.message import Message +from pylint.reporters import JSONReporter +from pylint.reporters.json_reporter import OldJsonExport +from pylint.testutils._primer.package_to_lint import PackageToLint +from pylint.testutils._primer.primer_command import ( + PackageData, + PackageMessages, + PrimerCommand, +) + +GITHUB_CRASH_TEMPLATE_LOCATION = "/home/runner/.cache" +CRASH_TEMPLATE_INTRO = "There is a pre-filled template" + + +class RunCommand(PrimerCommand): + def run(self) -> None: + packages: PackageMessages = {} + fatal_msgs: list[Message] = [] + for package, data in self.packages.items(): + messages, p_fatal_msgs = self._lint_package(package, data) + fatal_msgs += p_fatal_msgs + local_commit = Repo(data.clone_directory).head.object.hexsha + packages[package] = PackageData(commit=local_commit, messages=messages) + path = ( + self.primer_directory + / f"output_{'.'.join(str(i) for i in sys.version_info[:3])}_{self.config.type}.txt" + ) + print(f"Writing result in {path}") + with open(path, "w", encoding="utf-8") as f: + json.dump(packages, f) + # Assert that a PR run does not introduce new fatal errors + if self.config.type == "pr": + plural = "s" if len(fatal_msgs) > 1 else "" + assert ( + not fatal_msgs + ), f"We encountered {len(fatal_msgs)} fatal error message{plural} (see log)." + + @staticmethod + def _filter_fatal_errors( + messages: list[OldJsonExport], + ) -> list[Message]: + """Separate fatal errors so we can report them independently.""" + fatal_msgs: list[Message] = [] + for raw_message in messages: + message = JSONReporter.deserialize(raw_message) + if message.category == "fatal": + if GITHUB_CRASH_TEMPLATE_LOCATION in message.msg: + # Remove the crash template location if we're running on GitHub. + # We were falsely getting "new" errors when the timestamp changed. + message.msg = message.msg.rsplit(CRASH_TEMPLATE_INTRO)[0] + fatal_msgs.append(message) + return fatal_msgs + + @staticmethod + def _print_msgs(msgs: list[Message]) -> str: + return "\n".join(f"- {JSONReporter.serialize(m)}" for m in msgs) + + def _lint_package( + self, package_name: str, data: PackageToLint + ) -> tuple[list[OldJsonExport], list[Message]]: + # We want to test all the code we can + enables = ["--enable-all-extensions", "--enable=all"] + # Duplicate code takes too long and is relatively safe + # TODO: Find a way to allow cyclic-import and compare output correctly + disables = ["--disable=duplicate-code,cyclic-import"] + arguments = data.pylint_args + enables + disables + output = StringIO() + reporter = JSONReporter(output) + print(f"Running 'pylint {', '.join(arguments)}'") + pylint_exit_code = -1 + try: + Run(arguments, reporter=reporter) + except SystemExit as e: + pylint_exit_code = int(e.code) # type: ignore[arg-type] + readable_messages: str = output.getvalue() + messages: list[OldJsonExport] = json.loads(readable_messages) + fatal_msgs: list[Message] = [] + if pylint_exit_code % 2 == 0: + print(f"Successfully primed {package_name}.") + else: + fatal_msgs = self._filter_fatal_errors(messages) + if fatal_msgs: + warnings.warn( + f"Encountered fatal errors while priming {package_name} !\n" + f"{self._print_msgs(fatal_msgs)}\n\n" + ) + return messages, fatal_msgs diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index 0c24648b33..291f520023 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -85,6 +85,7 @@ def assertAddsMessages( f"the expected value in {expected_msg}. In pylint 3.0 correct end_line " "attributes will be required for MessageTest.", DeprecationWarning, + stacklevel=2, ) if not expected_msg.end_col_offset == gotten_msg.end_col_offset: warnings.warn( # pragma: no cover @@ -92,6 +93,7 @@ def assertAddsMessages( f"the expected value in {expected_msg}. In pylint 3.0 correct end_col_offset " "attributes will be required for MessageTest.", DeprecationWarning, + stacklevel=2, ) def walk(self, node: nodes.NodeNG) -> None: diff --git a/pylint/testutils/functional/find_functional_tests.py b/pylint/testutils/functional/find_functional_tests.py index 7b86ee642c..200cee7ec0 100644 --- a/pylint/testutils/functional/find_functional_tests.py +++ b/pylint/testutils/functional/find_functional_tests.py @@ -38,9 +38,11 @@ def get_functional_test_files_from_directory( _check_functional_tests_structure(Path(input_dir)) - for dirpath, _, filenames in os.walk(input_dir): + for dirpath, dirnames, filenames in os.walk(input_dir): if dirpath.endswith("__pycache__"): continue + dirnames.sort() + filenames.sort() for filename in filenames: if filename != "__init__.py" and filename.endswith(".py"): suite.append(FunctionalTestFile(dirpath, filename)) diff --git a/pylint/testutils/functional/test_file.py b/pylint/testutils/functional/test_file.py index cfa2b3f1d5..85a72daa9b 100644 --- a/pylint/testutils/functional/test_file.py +++ b/pylint/testutils/functional/test_file.py @@ -62,7 +62,8 @@ class FunctionalTestFile: def __init__(self, directory: str, filename: str) -> None: self._directory = directory self.base = filename.replace(".py", "") - # TODO: 2.15: Deprecate FunctionalTestFile.options and related code + # TODO: 2.x: Deprecate FunctionalTestFile.options and related code + # We should just parse these options like a normal configuration file. self.options: TestFileOptions = { "min_pyver": (2, 5), "max_pyver": (4, 0), @@ -90,7 +91,7 @@ def _parse_options(self) -> None: assert ( name in POSSIBLE_TEST_OPTIONS - ), f"[testoptions]' can only contains one of {POSSIBLE_TEST_OPTIONS}" + ), f"[testoptions]' can only contains one of {POSSIBLE_TEST_OPTIONS} and had '{name}'" self.options[name] = conv(value) # type: ignore[literal-required] @property diff --git a/pylint/testutils/functional_test_file.py b/pylint/testutils/functional_test_file.py index fc1cdcbb14..e2bd7f59bd 100644 --- a/pylint/testutils/functional_test_file.py +++ b/pylint/testutils/functional_test_file.py @@ -20,4 +20,5 @@ "'pylint.testutils.functional_test_file' will be accessible from" " the 'pylint.testutils.functional' namespace in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) diff --git a/pylint/testutils/lint_module_test.py b/pylint/testutils/lint_module_test.py index 0d3dbb0bf4..fec0074d54 100644 --- a/pylint/testutils/lint_module_test.py +++ b/pylint/testutils/lint_module_test.py @@ -145,7 +145,7 @@ def runTest(self) -> None: self._runTest() def _should_be_skipped_due_to_version(self) -> bool: - return ( + return ( # type: ignore[no-any-return] sys.version_info < self._linter.config.min_pyver or sys.version_info > self._linter.config.max_pyver ) @@ -287,12 +287,7 @@ def error_msg_for_unequal_output( ) -> str: missing = set(expected_lines) - set(received_lines) unexpected = set(received_lines) - set(expected_lines) - error_msg = ( - f"Wrong output for '{self._test_file.base}.txt':\n" - "You can update the expected output automatically with: '" - f"python tests/test_functional.py {UPDATE_OPTION} -k " - f'"test_functional[{self._test_file.base}]"\'\n\n' - ) + error_msg = f"Wrong output for '{self._test_file.base}.txt':" sort_by_line_number = operator.attrgetter("lineno") if missing: error_msg += "\n- Missing lines:\n" @@ -302,6 +297,17 @@ def error_msg_for_unequal_output( error_msg += "\n- Unexpected lines:\n" for line in sorted(unexpected, key=sort_by_line_number): error_msg += f"{line}\n" + error_msg += ( + "\nYou can update the expected output automatically with:\n'" + f"python tests/test_functional.py {UPDATE_OPTION} -k " + f'"test_functional[{self._test_file.base}]"\'\n\n' + "Here's the update text in case you can't:\n" + ) + expected_csv = StringIO() + writer = csv.writer(expected_csv, dialect="test") + for line in sorted(received_lines, key=sort_by_line_number): + writer.writerow(line.to_csv()) + error_msg += expected_csv.getvalue() return error_msg def _check_output_text( diff --git a/pylint/testutils/output_line.py b/pylint/testutils/output_line.py index 2ccf92ffbc..7465fce9d1 100644 --- a/pylint/testutils/output_line.py +++ b/pylint/testutils/output_line.py @@ -33,44 +33,6 @@ class MessageTest(NamedTuple): """ -class MalformedOutputLineException(Exception): - def __init__( - self, - row: Sequence[str] | str, - exception: Exception, - ) -> None: - example = "msg-symbolic-name:42:27:MyClass.my_function:The message" - other_example = "msg-symbolic-name:7:42::The message" - expected = [ - "symbol", - "line", - "column", - "end_line", - "end_column", - "MyClass.myFunction, (or '')", - "Message", - "confidence", - ] - reconstructed_row = "" - i = 0 - try: - for i, column in enumerate(row): - reconstructed_row += f"\t{expected[i]}='{column}' ?\n" - for missing in expected[i + 1 :]: - reconstructed_row += f"\t{missing}= Nothing provided !\n" - except IndexError: - pass - raw = ":".join(row) - msg = f"""\ -{exception} - -Expected '{example}' or '{other_example}' but we got '{raw}': -{reconstructed_row} - -Try updating it with: 'python tests/test_functional.py {UPDATE_OPTION}'""" - super().__init__(msg) - - class OutputLine(NamedTuple): symbol: str lineno: int @@ -127,6 +89,8 @@ def from_csv( """ if isinstance(row, str): row = row.split(",") + # noinspection PyBroadException + # pylint: disable = too-many-try-statements try: column = cls._get_column(row[2]) if len(row) == 5: @@ -135,6 +99,7 @@ def from_csv( "expected confidence level, expected end_line and expected end_column. " "An OutputLine should thus have a length of 8.", DeprecationWarning, + stacklevel=2, ) return cls( row[0], @@ -152,6 +117,7 @@ def from_csv( "expected end_line and expected end_column. An OutputLine should thus have " "a length of 8.", DeprecationWarning, + stacklevel=2, ) return cls( row[0], int(row[1]), column, None, None, row[3], row[4], row[5] @@ -170,8 +136,14 @@ def from_csv( row[7], ) raise IndexError - except Exception as e: - raise MalformedOutputLineException(row, e) from e + except Exception: # pylint: disable=broad-except + warnings.warn( + "Expected 'msg-symbolic-name:42:27:MyClass.my_function:The message:" + f"CONFIDENCE' but we got '{':'.join(row)}'. Try updating the expected" + f" output with:\npython tests/test_functional.py {UPDATE_OPTION}", + UserWarning, + ) + return cls("", 0, 0, None, None, "", "", "") def to_csv(self) -> tuple[str, str, str, str, str, str, str, str]: """Convert an OutputLine to a tuple of string to be written by a diff --git a/pylint/testutils/unittest_linter.py b/pylint/testutils/unittest_linter.py index 2fc7cef08c..a519680f12 100644 --- a/pylint/testutils/unittest_linter.py +++ b/pylint/testutils/unittest_linter.py @@ -24,8 +24,6 @@ class UnittestLinter(PyLinter): """A fake linter class to capture checker messages.""" - # pylint: disable=unused-argument - def __init__(self) -> None: self._messages: list[MessageTest] = [] super().__init__() diff --git a/pylint/testutils/utils.py b/pylint/testutils/utils.py index 36771ea391..292e991c25 100644 --- a/pylint/testutils/utils.py +++ b/pylint/testutils/utils.py @@ -7,7 +7,9 @@ import contextlib import os import sys -from collections.abc import Iterator +from collections.abc import Generator, Iterator +from copy import copy +from pathlib import Path from typing import TextIO @@ -22,6 +24,51 @@ def _patch_streams(out: TextIO) -> Iterator[None]: sys.stdout = sys.__stdout__ +@contextlib.contextmanager +def _test_sys_path( + replacement_sys_path: list[str] | None = None, +) -> Generator[None, None, None]: + original_path = sys.path + try: + if replacement_sys_path is not None: + sys.path = copy(replacement_sys_path) + yield + finally: + sys.path = original_path + + +@contextlib.contextmanager +def _test_cwd( + current_working_directory: str | Path | None = None, +) -> Generator[None, None, None]: + original_dir = os.getcwd() + try: + if current_working_directory is not None: + os.chdir(current_working_directory) + yield + finally: + os.chdir(original_dir) + + +@contextlib.contextmanager +def _test_environ_pythonpath( + new_pythonpath: str | None = None, +) -> Generator[None, None, None]: + original_pythonpath = os.environ.get("PYTHONPATH") + if new_pythonpath: + os.environ["PYTHONPATH"] = new_pythonpath + elif new_pythonpath is None and original_pythonpath is not None: + # If new_pythonpath is None, make sure to delete PYTHONPATH if present + del os.environ["PYTHONPATH"] + try: + yield + finally: + if original_pythonpath is not None: + os.environ["PYTHONPATH"] = original_pythonpath + elif "PYTHONPATH" in os.environ: + del os.environ["PYTHONPATH"] + + def create_files(paths: list[str], chroot: str = ".") -> None: """Creates directories and files found in . @@ -46,7 +93,7 @@ def create_files(paths: list[str], chroot: str = ".") -> None: path = os.path.join(chroot, path) filename = os.path.basename(path) # path is a directory path - if filename == "": + if not filename: dirs.add(path) # path is a filename path else: diff --git a/pylint/typing.py b/pylint/typing.py index f5650c1e78..d62618605c 100644 --- a/pylint/typing.py +++ b/pylint/typing.py @@ -6,7 +6,9 @@ from __future__ import annotations +import argparse import sys +from pathlib import Path from typing import ( TYPE_CHECKING, Any, @@ -22,12 +24,13 @@ ) if sys.version_info >= (3, 8): - from typing import Literal, TypedDict + from typing import Literal, Protocol, TypedDict else: - from typing_extensions import Literal, TypedDict + from typing_extensions import Literal, Protocol, TypedDict if TYPE_CHECKING: from pylint.config.callback_actions import _CallbackAction + from pylint.pyreverse.inspector import Project from pylint.reporters.ureports.nodes import Section from pylint.utils import LinterStats @@ -121,9 +124,17 @@ class ExtraMessageOptions(TypedDict, total=False): old_names: list[tuple[str, str]] maxversion: tuple[int, int] minversion: tuple[int, int] + shared: bool + default_enabled: bool MessageDefinitionTuple = Union[ Tuple[str, str, str], Tuple[str, str, str, ExtraMessageOptions], ] +DirectoryNamespaceDict = Dict[Path, Tuple[argparse.Namespace, "DirectoryNamespaceDict"]] + + +class GetProjectCallable(Protocol): + def __call__(self, module: str, name: str | None = "No Name") -> Project: + ... # pragma: no cover diff --git a/pylint/utils/ast_walker.py b/pylint/utils/ast_walker.py index cc387d8600..cf0f13fd12 100644 --- a/pylint/utils/ast_walker.py +++ b/pylint/utils/ast_walker.py @@ -37,7 +37,7 @@ def __init__(self, linter: PyLinter) -> None: def _is_method_enabled(self, method: AstCallback) -> bool: if not hasattr(method, "checks_msgs"): return True - return any(self.linter.is_message_enabled(m) for m in method.checks_msgs) # type: ignore[attr-defined] + return any(self.linter.is_message_enabled(m) for m in method.checks_msgs) def add_checker(self, checker: BaseChecker) -> None: """Walk to the checker's dir and collect visit and leave methods.""" @@ -82,6 +82,7 @@ def walk(self, astroid: nodes.NodeNG) -> None: visit_events: Sequence[AstCallback] = self.visit_events.get(cid, ()) leave_events: Sequence[AstCallback] = self.leave_events.get(cid, ()) + # pylint: disable = too-many-try-statements try: if astroid.is_statement: self.nbstatements += 1 diff --git a/pylint/utils/docs.py b/pylint/utils/docs.py index 0995d5a68f..ebd7cc8c3e 100644 --- a/pylint/utils/docs.py +++ b/pylint/utils/docs.py @@ -64,9 +64,13 @@ def _get_global_options_documentation(linter: PyLinter) -> str: return result -def _get_checkers_documentation(linter: PyLinter) -> str: +def _get_checkers_documentation(linter: PyLinter, show_options: bool = True) -> str: """Get documentation for individual checkers.""" - result = _get_global_options_documentation(linter) + if show_options: + result = _get_global_options_documentation(linter) + else: + result = "" + result += get_rst_title("Pylint checkers' options and switches", "-") result += """\ @@ -84,10 +88,16 @@ def _get_checkers_documentation(linter: PyLinter) -> str: information = by_checker[checker_name] checker = information["checker"] del information["checker"] - result += checker.get_full_documentation(**information) + result += checker.get_full_documentation( + **information, show_options=show_options + ) return result -def print_full_documentation(linter: PyLinter, stream: TextIO = sys.stdout) -> None: +def print_full_documentation( + linter: PyLinter, stream: TextIO = sys.stdout, show_options: bool = True +) -> None: """Output a full documentation in ReST format.""" - print(_get_checkers_documentation(linter)[:-3], file=stream) + print( + _get_checkers_documentation(linter, show_options=show_options)[:-3], file=stream + ) diff --git a/pylint/utils/file_state.py b/pylint/utils/file_state.py index acd59d6488..19122b3739 100644 --- a/pylint/utils/file_state.py +++ b/pylint/utils/file_state.py @@ -47,12 +47,14 @@ def __init__( "FileState needs a string as modname argument. " "This argument will be required in pylint 3.0", DeprecationWarning, + stacklevel=2, ) if msg_store is None: warnings.warn( "FileState needs a 'MessageDefinitionStore' as msg_store argument. " "This argument will be required in pylint 3.0", DeprecationWarning, + stacklevel=2, ) self.base_name = modname self._module_msgs_state: MessageStateDict = {} @@ -79,6 +81,7 @@ def collect_block_lines( warnings.warn( "'collect_block_lines' has been deprecated and will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) for msg, lines in self._module_msgs_state.items(): self._raw_module_msgs_state[msg] = lines.copy() @@ -194,30 +197,50 @@ def _set_message_state_in_block( state = lines[line] original_lineno = line - # Update suppression mapping - if not state: - self._suppression_mapping[(msg.msgid, line)] = original_lineno - else: - self._suppression_mapping.pop((msg.msgid, line), None) + self._set_message_state_on_line(msg, line, state, original_lineno) - # Update message state for respective line - try: - self._module_msgs_state[msg.msgid][line] = state - except KeyError: - self._module_msgs_state[msg.msgid] = {line: state} del lines[lineno] - def set_msg_status(self, msg: MessageDefinition, line: int, status: bool) -> None: + def _set_message_state_on_line( + self, + msg: MessageDefinition, + line: int, + state: bool, + original_lineno: int, + ) -> None: + """Set the state of a message on a line.""" + # Update suppression mapping + if not state: + self._suppression_mapping[(msg.msgid, line)] = original_lineno + else: + self._suppression_mapping.pop((msg.msgid, line), None) + + # Update message state for respective line + try: + self._module_msgs_state[msg.msgid][line] = state + except KeyError: + self._module_msgs_state[msg.msgid] = {line: state} + + def set_msg_status( + self, + msg: MessageDefinition, + line: int, + status: bool, + scope: str = "package", + ) -> None: """Set status (enabled/disable) for a given message at a given line.""" assert line > 0 assert self._module # TODO: 3.0: Remove unnecessary assertion assert self._msgs_store - # Expand the status to cover all relevant block lines - self._set_state_on_block_lines( - self._msgs_store, self._module, msg, {line: status} - ) + if scope != "line": + # Expand the status to cover all relevant block lines + self._set_state_on_block_lines( + self._msgs_store, self._module, msg, {line: status} + ) + else: + self._set_message_state_on_line(msg, line, status, line) # Store the raw value try: @@ -272,4 +295,4 @@ def iter_spurious_suppression_messages( ) def get_effective_max_line_number(self) -> int | None: - return self._effective_max_line_number + return self._effective_max_line_number # type: ignore[no-any-return] diff --git a/pylint/utils/pragma_parser.py b/pylint/utils/pragma_parser.py index 8e34fa693f..df36273806 100644 --- a/pylint/utils/pragma_parser.py +++ b/pylint/utils/pragma_parser.py @@ -5,8 +5,8 @@ from __future__ import annotations import re -from collections import namedtuple from collections.abc import Generator +from typing import NamedTuple # Allow stopping after the first semicolon/hash encountered, # so that an option can be continued with the reasons @@ -27,7 +27,9 @@ OPTION_PO = re.compile(OPTION_RGX, re.VERBOSE) -PragmaRepresenter = namedtuple("PragmaRepresenter", "action messages") +class PragmaRepresenter(NamedTuple): + action: str + messages: list[str] ATOMIC_KEYWORDS = frozenset(("disable-all", "skip-file")) diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 6a4277642b..054d307bc0 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -6,6 +6,7 @@ try: import isort.api + import isort.settings HAS_ISORT_5 = True except ImportError: # isort < 5 @@ -280,6 +281,7 @@ def get_global_option( "get_global_option has been deprecated. You can use " "checker.linter.config to get all global options instead.", DeprecationWarning, + stacklevel=2, ) return getattr(checker.linter.config, option.replace("-", "_")) @@ -366,6 +368,7 @@ def format_section( warnings.warn( "format_section has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) if doc: print(_comment(doc), file=stream) @@ -380,6 +383,7 @@ def _ini_format(stream: TextIO, options: list[tuple[str, OptionDict, Any]]) -> N warnings.warn( "_ini_format has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) for optname, optdict, value in options: # Skip deprecated option @@ -413,7 +417,7 @@ class IsortDriver: def __init__(self, config: argparse.Namespace) -> None: if HAS_ISORT_5: - self.isort5_config = isort.api.Config( + self.isort5_config = isort.settings.Config( # There is no typo here. EXTRA_standard_library is # what most users want. The option has been named # KNOWN_standard_library for ages in pylint, and we @@ -423,7 +427,7 @@ def __init__(self, config: argparse.Namespace) -> None: ) else: # pylint: disable-next=no-member - self.isort4_obj = isort.SortImports( + self.isort4_obj = isort.SortImports( # type: ignore[attr-defined] file_contents="", known_standard_library=config.known_standard_library, known_third_party=config.known_third_party, @@ -432,4 +436,4 @@ def __init__(self, config: argparse.Namespace) -> None: def place_module(self, package: str) -> str: if HAS_ISORT_5: return isort.api.place_module(package, self.isort5_config) - return self.isort4_obj.place_module(package) + return self.isort4_obj.place_module(package) # type: ignore[no-any-return] diff --git a/pylintrc b/pylintrc index 8314c21be6..791b164475 100644 --- a/pylintrc +++ b/pylintrc @@ -1,8 +1,5 @@ [MAIN] -# Specify a configuration file. -#rcfile= - # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= @@ -35,6 +32,9 @@ load-plugins= pylint.extensions.typing, pylint.extensions.redefined_variable_type, pylint.extensions.comparison_placement, + pylint.extensions.broad_try_clause, + pylint.extensions.dict_init_mutate, + pylint.extensions.consider_refactoring_into_while_condition, # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. @@ -61,7 +61,7 @@ py-version = 3.7.2 # complex, nested conditions. limit-inference-results=100 -# Specify a score threshold to be exceeded before program exits with error. +# Specify a score threshold under which the program will exit with error. fail-under=10.0 # Return non-zero exit code if any of these messages/categories are detected, @@ -69,6 +69,10 @@ fail-under=10.0 # specified are enabled, while categories only check already-enabled messages. fail-on= +# Clear in-memory caches upon conclusion of linting. Useful if running pylint in +# a server-like mode. +clear-cache-post-run=no + [MESSAGES CONTROL] @@ -104,7 +108,6 @@ disable= format, # We anticipate #3512 where it will become optional fixme, - cyclic-import, [REPORTS] @@ -175,10 +178,6 @@ ignore-signatures=yes # Tells whether we should check for unused import in __init__ files. init-import=no -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= @@ -193,10 +192,6 @@ allow-global-unused-variables=yes # List of names allowed to shadow builtins allowed-redefined-builtins= -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.* - # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io @@ -324,7 +319,7 @@ method-naming-style=snake_case # Regular expression matching correct method names method-rgx=[a-z_][a-z0-9_]{2,}$ -# Regular expression which can overwrite the naming style set by typevar-naming-style. +# Regular expression matching correct type variable names #typevar-rgx= # Regular expression which should only match function or class names that do @@ -416,41 +411,25 @@ max-spelling-suggestions=2 [DESIGN] # Maximum number of arguments for function / method -max-args=10 +max-args = 9 # Maximum number of locals for function / method body -max-locals=25 +max-locals = 19 # Maximum number of return / yield for function / method body max-returns=11 # Maximum number of branch for function / method body -max-branches=27 +max-branches = 20 # Maximum number of statements in function / method body -max-statements=100 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# List of qualified class names to ignore when counting class parents (see R0901). -ignored-parents= +max-statements = 50 # Maximum number of attributes for a class (see R0902). max-attributes=11 -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=25 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# List of regular expressions of class ancestor names to -# ignore when counting public methods (see R0903). -exclude-too-few-public-methods= +# Maximum number of statements in a try-block +max-try-statements = 7 [CLASSES] @@ -479,6 +458,9 @@ allow-any-import-level= # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. @@ -514,7 +496,7 @@ preferred-modules= # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception [TYPING] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..9f724e7e9e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,132 @@ +[build-system] +requires = ["setuptools~=62.6", "wheel~=0.37.1"] +build-backend = "setuptools.build_meta" + +[project] +name = "pylint" +license = {text = "GPL-2.0-or-later"} +description = "python code static checker" +readme = "README.rst" +authors = [ + {name = "Python Code Quality Authority", email = "code-quality@python.org"} +] +keywords = ["static code analysis", "linter", "python", "lint"] +classifiers = [ + "Development Status :: 6 - Mature", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Debuggers", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", +] +requires-python = ">=3.7.2" +dependencies = [ + "dill>=0.2;python_version<'3.11'", + "dill>=0.3.6;python_version>='3.11'", + "platformdirs>=2.2.0", + # 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/PyCQA/astroid/issues/1341 + "astroid>=2.14.2,<=2.16.0-dev0", + "isort>=4.2.5,<6", + "mccabe>=0.6,<0.8", + "tomli>=1.1.0;python_version<'3.11'", + "tomlkit>=0.10.1", + "colorama>=0.4.5;sys_platform=='win32'", + "typing-extensions>=3.10.0;python_version<'3.10'", +] +dynamic = ["version"] + +[project.optional-dependencies] +testutils = ["gitpython>3"] +spelling = ["pyenchant~=3.2"] + +[project.urls] +"Docs: User Guide" = "https://pylint.pycqa.org/en/latest/" +"Source Code" = "https://github.com/PyCQA/pylint" +"homepage" = "https://github.com/PyCQA/pylint" +"What's New" = "https://pylint.pycqa.org/en/latest/whatsnew/2/" +"Bug Tracker" = "https://github.com/PyCQA/pylint/issues" +"Discord Server" = "https://discord.com/invite/Egy6P8AMB5" +"Docs: Contributor Guide" = "https://pylint.pycqa.org/en/latest/development_guide/contributor_guide/index.html" + +[project.scripts] +pylint = "pylint:run_pylint" +pylint-config = "pylint:_run_pylint_config" +epylint = "pylint:run_epylint" +pyreverse = "pylint:run_pyreverse" +symilar = "pylint:run_symilar" + +[tool.setuptools] +license-files = ["LICENSE", "CONTRIBUTORS.txt"] # Keep in sync with setup.cfg + +[tool.setuptools.packages.find] +include = ["pylint*"] + +[tool.setuptools.package-data] +pylint = ["testutils/testing_pylintrc"] + +[tool.setuptools.dynamic] +version = {attr = "pylint.__pkginfo__.__version__"} + +[tool.aliases] +test = "pytest" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["*test_*.py"] +addopts = "--strict-markers" +filterwarnings = "error" +markers = [ + "primer_stdlib: Checks for crashes and errors when running pylint on stdlib", + "primer_external_batch_one: Checks for crashes and errors when running pylint on external libs (batch one)", + "benchmark: Baseline of pylint performance, if this regress something serious happened", + "timeout: Marks from pytest-timeout.", + "needs_two_cores: Checks that need 2 or more cores to be meaningful", +] + +[tool.isort] +profile = "black" +known_third_party = ["platformdirs", "astroid", "sphinx", "isort", "pytest", "mccabe", "six", "toml"] +skip_glob = ["tests/functional/**", "tests/input/**", "tests/extensions/data/**", "tests/regrtest_data/**", "tests/data/**", "astroid/**", "venv/**"] +src_paths = ["pylint"] + +[tool.mypy] +scripts_are_modules = true +warn_unused_ignores = true +show_error_codes = true +enable_error_code = "ignore-without-code" +strict = true +# TODO: Remove this once pytest has annotations +disallow_untyped_decorators = false + +[[tool.mypy.overrides]] +ignore_missing_imports = true +module = [ + "_pytest.*", + "_string", + "astroid.*", + # `colorama` ignore is needed for Windows environment + "colorama", + "contributors_txt", + "coverage", + "dill", + "enchant.*", + "git.*", + "mccabe", + "pytest_benchmark.*", + "pytest", + "sphinx.*", +] diff --git a/requirements_test.txt b/requirements_test.txt index 997960d005..ad44a0cb9d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,11 @@ -r requirements_test_pre_commit.txt -r requirements_test_min.txt -coveralls~=3.3 -coverage~=6.4 -pre-commit~=2.19 +coverage~=7.1 tbump~=6.9.0 -contributors-txt>=0.7.3 -pytest-cov~=3.0 +contributors-txt>=1.0.0 +pytest-cov~=4.0 pytest-profiling~=1.7 -pytest-xdist~=2.5 +pytest-xdist~=3.1 # Type packages for mypy types-pkg_resources==0.1.3 tox>=3 diff --git a/requirements_test_min.txt b/requirements_test_min.txt index 386f2605cb..a4e6c307f4 100644 --- a/requirements_test_min.txt +++ b/requirements_test_min.txt @@ -1,7 +1,10 @@ -e .[testutils,spelling] -# astroid dependency is also defined in setup.cfg -astroid==2.11.6 # Pinned to a specific version for tests -typing-extensions~=4.2 -pytest~=7.1 -pytest-benchmark~=3.4 +# astroid dependency is also defined in pyproject.toml +astroid==2.14.2 # Pinned to a specific version for tests +typing-extensions~=4.4 +py~=1.11.0 +pytest~=7.2 +pytest-benchmark~=4.0 pytest-timeout~=2.1 +towncrier~=22.12 +requests diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 56a2bae211..eb2300734d 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,10 @@ # Everything in this file should reflect the pre-commit configuration # in .pre-commit-config.yaml -black==22.3.0 -flake8==4.0.1 -flake8-typing-imports==1.12.0 -isort==5.10.1 -mypy==0.960 +pre-commit~=3.0;python_version>='3.8' +bandit==1.7.4 +black==23.1a1 +flake8==6.0.0;python_version>='3.8' +flake8-bugbear==23.1.20;python_version>='3.8' +flake8-typing-imports==1.14.0;python_version>='3.8' +isort==5.12.0;python_version>='3.8' +mypy==0.991 diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index 85892ce0ae..601aa1c088 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -1,4 +1,12 @@ { + "112832187+clavedeluna@users.noreply.github.com": { + "mails": [ + "112832187+clavedeluna@users.noreply.github.com", + "danalitovsky+git@gmail.com" + ], + "name": "Dani Alcala", + "team": "Maintainers" + }, "13665637+DanielNoord@users.noreply.github.com": { "mails": ["13665637+DanielNoord@users.noreply.github.com"], "name": "Daniël van Noord", @@ -13,6 +21,7 @@ "name": "Victor Jiajunsu" }, "17108752+mattlbeck@users.noreply.github.com": { + "comment": " (mattlbeck)", "mails": ["17108752+mattlbeck@users.noreply.github.com"], "name": "Matthew Beckers" }, @@ -31,9 +40,11 @@ }, "31762852+mbyrnepr2@users.noreply.github.com": { "mails": ["31762852+mbyrnepr2@users.noreply.github.com", "mbyrnepr2@gmail.com"], - "name": "Mark Byrne" + "name": "Mark Byrne", + "team": "Maintainers" }, "31987769+sushobhit27@users.noreply.github.com": { + "comment": " (sushobhit27)\n * Added new check 'comparison-with-itself'.\n * Added new check 'useless-import-alias'.\n * Added support of annotations in missing-type-doc and missing-return-type-doc.\n * Added new check 'comparison-with-callable'.\n * Removed six package dependency.\n * Added new check 'chained-comparison'.\n * Added new check 'useless-object-inheritance'.", "mails": [ "sushobhitsolanki@gmail.com", "31987769+sushobhit27@users.noreply.github.com" @@ -70,6 +81,10 @@ "mails": ["46202743+lorena-b@users.noreply.github.com"], "name": "Lorena Buciu" }, + "53538590+zenlyj@users.noreply.github.com": { + "mails": ["53538590+zenlyj@users.noreply.github.com"], + "name": "Zen Lee" + }, "53625739+dbrookman@users.noreply.github.com": { "mails": ["53625739+dbrookman@users.noreply.github.com"], "name": "Daniel Brookman" @@ -82,10 +97,17 @@ "name": "Tiago Honorato" }, "62866982+SupImDos@users.noreply.github.com": { + "comment": "\n * Fixed \"no-self-use\" for async methods\n * Fixed \"docparams\" extension for async functions and methods", "mails": ["62866982+SupImDos@users.noreply.github.com"], "name": "Hayden Richards" }, + "74228962+tanvimoharir@users.noreply.github.com": { + "comment": ": Fix for invalid toml config", + "mails": ["74228962+tanvimoharir@users.noreply.github.com"], + "name": "Tanvi Moharir" + }, "80432516+jpy-git@users.noreply.github.com": { + "comment": " (jpy-git)", "mails": ["80432516+jpy-git@users.noreply.github.com"], "name": "Joseph Young" }, @@ -94,6 +116,7 @@ "name": "Sam Vermeiren" }, "95424144+allanc65@users.noreply.github.com": { + "comment": " (allanc65)\n * Fixed issue 5452, false positive missing-param-doc for multi-line Google-style params", "mails": ["95424144+allanc65@users.noreply.github.com"], "name": "Allan Chandler" }, @@ -105,15 +128,23 @@ "mails": ["Github@pheanex.de"], "name": "Konstantin" }, + "Humetsky@gmail.com": { + "comment": " (mhumetskyi)\n * Fixed ignored empty functions by similarities checker with \"ignore-signatures\" option enabled\n * Ignore function decorators signatures as well by similarities checker with \"ignore-signatures\" option enabled\n * Ignore class methods and nested functions signatures as well by similarities checker with \"ignore-signatures\" option enabled", + "mails": ["Humetsky@gmail.com"], + "name": "Maksym Humetskyi" + }, "Mariatta@users.noreply.github.com": { + "comment": "\n * Added new check `logging-fstring-interpolation`\n * Documentation typo fixes", "mails": ["Mariatta@users.noreply.github.com", "mariatta@python.org"], "name": "Mariatta Wijaya" }, "MartinBasti@users.noreply.github.com": { + "comment": "\n * Added new check for shallow copy of os.environ\n * Added new check for useless `with threading.Lock():` statement", "mails": ["MartinBasti@users.noreply.github.com"], "name": "Martin Bašti" }, "Pablogsal@gmail.com": { + "comment": "\n * Fix false positive 'Non-iterable value' with async comprehensions.", "mails": ["Pablogsal@gmail.com"], "name": "Pablo Galindo Salgado" }, @@ -121,14 +152,35 @@ "mails": ["renvoisepaul@gmail.com", "PaulRenvoise@users.noreply.github.com"], "name": "Paul Renvoisé" }, + "adityagupta1089@users.noreply.github.com": { + "comment": " (adityagupta1089)\n * Added ignore_signatures to duplicate checker", + "mails": ["adityagupta1089@users.noreply.github.com"], + "name": "Aditya Gupta" + }, + "afoglia@users.noreply.github.com": { + "comment": " (Google): Added simple string slots check.", + "mails": ["afoglia@users.noreply.github.com"], + "name": "Anthony Foglia" + }, "ahirnish@gmail.com": { + "comment": ": 'keyword-arg-before-var-arg' check", "mails": ["ahirnish@gmail.com"], "name": "Ahirnish Pareek" }, "alexandre.fayolle@logilab.fr": { + "comment": " (Logilab): TkInter gui, documentation, debian support", "mails": ["alexandre.fayolle@logilab.fr", "afayolle.ml@free.fr"], "name": "Alexandre Fayolle" }, + "alvarofriasgaray@gmail.com": { + "mails": ["alvaro.frias@eclypsium.com", "alvarofriasgaray@gmail.com"], + "name": "Alvaro Frias" + }, + "andreas.freimuth@united-bits.de": { + "comment": ": fix indentation checking with tabs", + "mails": ["andreas.freimuth@united-bits.de"], + "name": "Andreas Freimuth" + }, "anjsimmo@gmail.com": { "mails": ["anjsimmo@gmail.com", "a.simmons@deakin.edu.au"], "name": "Andrew J. Simmons" @@ -143,6 +195,7 @@ "team": "Maintainers" }, "arusahni@gmail.com": { + "comment": ": Git ignoring, regex-based ignores", "mails": ["arusahni@gmail.com", "aru@thehumangeo.com"], "name": "Aru Sahni" }, @@ -157,30 +210,73 @@ "name": "Ashley Whetter", "team": "Maintainers" }, + "athoscr@fedoraproject.org": { + "comment": ": Fixed dict-keys-not-iterating false positive for inverse containment checks", + "mails": ["athoscr@fedoraproject.org"], + "name": "Athos Ribeiro" + }, + "atodorov@otb.bg": { + "comment": ":\n * added new error conditions to 'bad-super-call',\n * Added new check for incorrect len(SEQUENCE) usage,\n * Added new extension for comparison against empty string constants,\n * Added new extension which detects comparing integers to zero,\n * Added new useless-return checker,\n * Added new try-except-raise checker", + "mails": ["atodorov@otb.bg"], + "name": "Alexander Todorov" + }, + "awilliam@redhat.com": { + "mails": ["awilliam@redhat.com", "adam@blueradius.ca"], + "name": "Adam Williamson" + }, "balint.mihai@gmail.com": { "mails": ["balint.mihai@gmail.com", "mihai@cs.upt.ro"], "name": "Mihai Balint" }, + "balparda@google.com": { + "comment": " (Google): GPyLint maintainer (Google's pylint variant)", + "mails": ["balparda@google.com"], + "name": "Daniel Balparda" + }, + "baltazar.bz@gmail.com": { + "comment": ": Added epytext support to docparams extension.", + "mails": ["baltazar.bz@gmail.com"], + "name": "Yuri Bochkarev" + }, "bastien.vallet@gmail.com": { + "comment": " (Djailla)", "mails": ["bastien.vallet@gmail.com"], "name": "Bastien Vallet" }, + "benjamin.drung@profitbricks.com": { + "comment": ": contributing Debian Developer", + "mails": ["benjamin.drung@profitbricks.com"], + "name": "Benjamin Drung" + }, "benny.mueller91@gmail.com": { "mails": ["benny.mueller91@gmail.com"], "name": "Benny Mueller" }, "bitbucket@carlcrowder.com": { + "comment": ": don't evaluate the value of arguments for 'dangerous-default-value'", "mails": ["bitbucket@carlcrowder.com"], "name": "Carl Crowder" }, "bot@noreply.github.com": { "mails": [ "66853113+pre-commit-ci[bot]@users.noreply.github.com", - "49699333+dependabot[bot]@users.noreply.github.com" + "49699333+dependabot[bot]@users.noreply.github.com", + "41898282+github-actions[bot]@users.noreply.github.com" ], "name": "bot" }, + "brett@python.org": { + "comment": ":\n * Port source code to be Python 2/3 compatible\n * Python 3 checker", + "mails": ["brett@python.org"], + "name": "Brett Cannon" + }, + "brian.shaginaw@warbyparker.com": { + "comment": ": prevent error on exception check for functions", + "mails": ["brian.shaginaw@warbyparker.com"], + "name": "Brian Shaginaw" + }, "bruno.daniel@blue-yonder.com": { + "comment": ": check_docs extension.", "mails": ["Bruno.Daniel@blue-yonder.com", "bruno.daniel@blue-yonder.com"], "name": "Bruno Daniel" }, @@ -189,15 +285,26 @@ "name": "Bryce Guinta", "team": "Maintainers" }, - "buck@yelp.com": { - "mails": ["buck@yelp.com", "buck.2019@gmail.com"], - "name": "Buck (Yelp)" + "buck.2019@gmail.com": { + "mails": [ + "buck.2019@gmail.com", + "buck@yelp.com", + "workitharder@gmail.com", + "bukzor@google.com" + ], + "name": "Buck Evan" }, "calen.pennington@gmail.com": { "mails": ["cale@edx.org", "calen.pennington@gmail.com"], "name": "Calen Pennington" }, + "carey@cmetcalfe.ca": { + "comment": ": demoted `try-except-raise` from error to warning", + "mails": ["carey@cmetcalfe.ca"], + "name": "Carey Metcalfe" + }, "carli.freudenberg@energymeteo.de": { + "comment": " (CarliJoy)\n * Fixed issue 5281, added Unicode checker\n * Improve non-ascii-name checker", "mails": ["carli.freudenberg@energymeteo.de"], "name": "Carli Freudenberg" }, @@ -207,21 +314,33 @@ "team": "Maintainers" }, "cezar.elnazli2@gmail.com": { + "comment": ": deprecated-method", "mails": ["celnazli@bitdefender.com", "cezar.elnazli2@gmail.com"], "name": "Cezar Elnazli" }, "cmin@ropython.org": { + "comment": ": unichr-builtin and improvements to bad-open-mode.", "mails": ["cmin@ropython.org"], "name": "Cosmin Poieană" }, + "code@rebertia.com": { + "comment": ": unidiomatic-typecheck.", + "mails": ["code@rebertia.com"], + "name": "Chris Rebert" + }, "contact@ionelmc.ro": { "mails": ["contact@ionelmc.ro"], "name": "Ionel Maries Cristian" }, "dan.r.neal@gmail.com": { + "comment": " (danrneal)", "mails": ["dan.r.neal@gmail.com"], "name": "Daniel R. Neal" }, + "daniel.werner@scalableminds.com": { + "mails": ["daniel.werner@scalableminds.com"], + "name": "Daniel Werner" + }, "david.douard@sdfa3.org": { "mails": ["david.douard@sdfa3.org", "david.douard@logilab.fr"], "name": "David Douard" @@ -230,7 +349,13 @@ "mails": ["david.pursehouse@gmail.com", "david.pursehouse@sonymobile.com"], "name": "David Pursehouse" }, + "david@cs.toronto.edu": { + "comment": " (david-yz-liu)", + "mails": ["david@cs.toronto.edu"], + "name": "David Liu" + }, "ddandd@gmail.com": { + "comment": " (doranid)", "mails": ["ddandd@gmail.com"], "name": "Daniel Dorani" }, @@ -239,19 +364,36 @@ "name": "Daniel Harding" }, "djgoldsmith@googlemail.com": { + "comment": ": support for msg-template in HTML reporter.", "mails": ["djgoldsmith@googlemail.com"], "name": "Dan Goldsmith" }, + "dlindquist@google.com": { + "comment": ": logging-format-interpolation warning.", + "mails": ["dlindquist@google.com"], + "name": "David Lindquist" + }, "dmand@yandex.ru": { + "comment": "\n * multiple-imports, not-iterable, not-a-mapping, various patches.", "mails": ["dmand@yandex.ru"], "name": "Dimitri Prybysh", "team": "Maintainers" }, + "dmrtzn@gmail.com": { + "mails": ["dmrtzn@gmail.com"], + "name": "Daniel Mouritzen" + }, "drewrisinger@users.noreply.github.com": { "mails": ["drewrisinger@users.noreply.github.com"], "name": "Drew Risinger" }, + "dshea@redhat.com": { + "comment": ": invalid sequence and slice index", + "mails": ["dshea@redhat.com"], + "name": "David Shea" + }, "ejfine@gmail.com": { + "comment": " (eli88fine): Fixed false positive duplicate code warning for lines with symbols only", "mails": [ "ubuntu@ip-172-31-89-59.ec2.internal", "eli88fine@gmail.com", @@ -259,11 +401,17 @@ ], "name": "Eli Fine" }, + "eliasdorneles@gmail.com": { + "comment": ": minor adjust to config defaults and docs", + "mails": ["eliasdorneles@gmail.com"], + "name": "Elias Dorneles" + }, "email@holger-peters.de": { "mails": ["email@holger-peters.de", "holger.peters@blue-yonder.com"], "name": "Holger Peters" }, "emile.anclin@logilab.fr": { + "comment": " (Logilab): python 3 support", "mails": ["emile.anclin@logilab.fr", ""], "name": "Emile Anclin" }, @@ -275,7 +423,13 @@ "mails": ["ethanleba5@gmail.com"], "name": "Ethan Leba" }, + "fantix@uchicago.edu": { + "comment": " (UChicago)", + "mails": ["fantix@uchicago.edu"], + "name": "Fantix King" + }, "flying-sheep@web.de": { + "comment": " (pylbrecht)", "mails": ["flying-sheep@web.de", "palbrecht@mailbox.org"], "name": "Philipp Albrecht" }, @@ -284,6 +438,7 @@ "name": "Marco Forte" }, "frank@doublethefish.com": { + "comment": " (doublethefish)", "mails": ["frank@doublethefish.com", "doublethefish@gmail.com"], "name": "Frank Harrison" }, @@ -296,9 +451,15 @@ "name": "Frost Ming" }, "g@briel.dev": { + "comment": ": Fixed \"exception-escape\" false positive with generators", "mails": ["g@briel.dev", "gabriel@sezefredo.com.br"], "name": "Gabriel R. Sezefredo" }, + "gagern@google.com": { + "comment": " (Google): Added 'raising-format-tuple' warning.", + "mails": ["gagern@google.com"], + "name": "Martin von Gagern" + }, "gergely.kalmar@logikal.jp": { "mails": ["gergely.kalmar@logikal.jp"], "name": "Gergely Kalmár" @@ -308,13 +469,24 @@ "name": "David Euresti" }, "github@hornwitser.no": { + "comment": ": fix import graph", "mails": ["github@hornwitser.no"], "name": "Hornwitser" }, "glenn@e-dad.net": { + "comment": ":\n * autogenerated documentation for optional extensions,\n * bug fixes and enhancements for docparams (née check_docs) extension", "mails": ["glenn@e-dad.net", "glmatthe@cisco.com"], "name": "Glenn Matthews" }, + "godfryd@gmail.com": { + "comment": ":\n * wrong-spelling-in-comment\n * wrong-spelling-in-docstring\n * parallel execution on multiple CPUs", + "mails": ["godfryd@gmail.com"], + "name": "Michal Nowikowski" + }, + "grizzly.nyo@gmail.com": { + "mails": ["grizzly.nyo@gmail.com"], + "name": "Grizzly Nyo" + }, "guillaume.peillex@gmail.com": { "mails": ["guillaume.peillex@gmail.com", "guillaume.peillex@gmail.col"], "name": "Hippo91", @@ -325,9 +497,14 @@ "name": "Gunung Pambudi W." }, "hg@stevenmyint.com": { + "comment": ": duplicate-except.", "mails": ["hg@stevenmyint.com", "git@stevenmyint.com"], "name": "Steven Myint" }, + "hofrob@protonmail.com": { + "mails": ["hofrob@protonmail.com"], + "name": "Robert Hofer" + }, "hugovk@users.noreply.github.com": { "mails": ["hugovk@users.noreply.github.com"], "name": "Hugo van Kemenade" @@ -340,15 +517,50 @@ "mails": ["iilei@users.noreply.github.com"], "name": "Jochen Preusche" }, + "ikraduya@gmail.com": { + "comment": ": Added new checks 'consider-using-generator' and 'use-a-generator'.", + "mails": ["ikraduya@gmail.com"], + "name": "Ikraduya Edian" + }, + "io.paraskev@gmail.com": { + "comment": ": add 'differing-param-doc' and 'differing-type-doc'", + "mails": ["io.paraskev@gmail.com"], + "name": "John Paraskevopoulos" + }, + "ioana.tagirta@gmail.com": { + "comment": ": fix bad thread instantiation check", + "mails": ["ioana.tagirta@gmail.com"], + "name": "Ioana Tagirta" + }, + "jacebrowning@gmail.com": { + "comment": ": updated default report format with clickable paths", + "mails": ["jacebrowning@gmail.com"], + "name": "Jace Browning" + }, "jacob@bogdanov.dev": { "mails": ["jacob@bogdanov.dev", "jbogdanov@128technology.com"], "name": "Jacob Bogdanov" }, "jacobtylerwalls@gmail.com": { - "mails": ["jacobtylerwalls@gmail.com"], + "mails": ["jacobtylerwalls@gmail.com", "jwalls@azavea.com"], "name": "Jacob Walls", "team": "Maintainers" }, + "jaehoonhwang@users.noreply.github.com": { + "comment": " (jaehoonhwang)", + "mails": ["jaehoonhwang@users.noreply.github.com"], + "name": "Jaehoon Hwang" + }, + "james.morgensen@gmail.com": { + "comment": ": ignored-modules option applies to import errors.", + "mails": ["james.morgensen@gmail.com"], + "name": "James Morgensen" + }, + "jeroenseegers@users.noreply.github.com": { + "comment": ":\n * Fixed `toml` dependency issue", + "mails": ["jeroenseegers@users.noreply.github.com"], + "name": "Jeroen Seegers" + }, "joffrey.mander+pro@gmail.com": { "mails": ["joffrey.mander+pro@gmail.com"], "name": "Joffrey Mander" @@ -365,6 +577,11 @@ "mails": ["jpkotta@gmail.com", "jpkotta@shannon"], "name": "jpkotta" }, + "julien.cristau@logilab.fr": { + "comment": " (Logilab): python 3 support", + "mails": ["julien.cristau@logilab.fr"], + "name": "Julien Cristau" + }, "justinnhli@gmail.com": { "mails": ["justinnhli@gmail.com", "justinnhli@users.noreply.github.com"], "name": "Justin Li" @@ -386,10 +603,22 @@ "name": "laike9m" }, "laura.medioni@logilab.fr": { + "comment": " (Logilab, on behalf of the CNES):\n * misplaced-comparison-constant\n * no-classmethod-decorator\n * no-staticmethod-decorator\n * too-many-nested-blocks,\n * too-many-boolean-expressions\n * unneeded-not\n * wrong-import-order\n * ungrouped-imports,\n * wrong-import-position\n * redefined-variable-type", "mails": ["laura.medioni@logilab.fr", "lmedioni@logilab.fr"], "name": "Laura Médioni" }, + "leinardi@gmail.com": { + "comment": ": PyCharm plugin maintainer", + "mails": ["leinardi@gmail.com"], + "name": "Roberto Leinardi" + }, + "lescobar@vauxoo.com": { + "comment": " (Vauxoo): Add bad-docstring-quotes and docstring-first-line-empty", + "mails": ["lescobar@vauxoo.com"], + "name": "Luis Escobar" + }, "liyt@ios.ac.cn": { + "comment": " (yetingli)", "mails": ["liyt@ios.ac.cn"], "name": "Yeting Li" }, @@ -398,13 +627,23 @@ "name": "Boris Feld" }, "lucristofolini@gmail.com": { + "comment": " (luigibertaco)", "mails": ["luigi.cristofolini@q-ctrl.com", "lucristofolini@gmail.com"], "name": "Luigi Bertaco Cristofolini" }, + "ludal@logilab.fr": { + "mails": ["ludal@logilab.fr"], + "name": "Ludovic Aubry" + }, "m.fesenko@corp.vk.com": { "mails": ["m.fesenko@corp.vk.com", "proggga@gmail.com"], "name": "Mikhail Fesenko" }, + "marcogorelli@protonmail.com": { + "comment": ": Documented Jupyter integration", + "mails": ["marcogorelli@protonmail.com"], + "name": "Marco Edward Gorelli" + }, "mariocj89@gmail.com": { "mails": ["mcorcherojim@bloomberg.net", "mariocj89@gmail.com"], "name": "Mario Corchero" @@ -422,6 +661,15 @@ "name": "Matus Valo", "team": "Maintainers" }, + "mbp@google.com": { + "comment": " (Google):\n * warnings for anomalous backslashes\n * symbolic names for messages (like 'unused')\n * etc.", + "mails": ["mbp@google.com"], + "name": "Martin Pool" + }, + "me@daogilvie.com": { + "mails": ["me@daogilvie.com", "drum.ogilvie@ovo.com"], + "name": "Drum Ogilvie" + }, "me@the-compiler.org": { "mails": [ "me@the-compiler.org", @@ -431,26 +679,57 @@ "name": "Florian Bruhin", "team": "Maintainers" }, + "miketheman@gmail.com": { + "comment": " (miketheman)", + "mails": ["miketheman@gmail.com"], + "name": "Mike Fiedler" + }, "mitchelly@gmail.com": { + "comment": ": minor adjustment to docparams", "mails": ["mitchelly@gmail.com"], "name": "Mitchell Young" }, "molobrakos@users.noreply.github.com": { + "comment": ": Added overlapping-except error check.", "mails": ["molobrakos@users.noreply.github.com", "erik.eriksson@yahoo.com"], "name": "Erik Eriksson" }, "moylop260@vauxoo.com": { + "comment": " (Vauxoo):\n * Support for deprecated-modules in modules not installed,\n * Refactor wrong-import-order to integrate it with `isort` library\n * Add check too-complex with mccabe for cyclomatic complexity\n * Refactor wrong-import-position to skip try-import and nested cases\n * Add consider-merging-isinstance, superfluous-else-return\n * Fix consider-using-ternary for 'True and True and True or True' case\n * Add bad-docstring-quotes and docstring-first-line-empty\n * Add missing-timeout", "mails": ["moylop260@vauxoo.com"], "name": "Moisés López" }, + "mpolatoglou@bloomberg.net": { + "comment": ": minor contribution for wildcard import check", + "mails": ["mpolatoglou@bloomberg.net"], + "name": "Marianna Polatoglou" + }, "mtmiller@users.noreply.github.com": { + "comment": ": fix inline defs in too-many-statements", "mails": ["725mrm@gmail.com", "mtmiller@users.noreply.github.com"], "name": "Mark Roman Miller" }, + "naslundx@gmail.com": { + "comment": " (naslundx)", + "mails": ["naslundx@gmail.com"], + "name": "Marcus Näslund" + }, + "nathaniel@google.com": { + "comment": ": suspicious lambda checking", + "mails": ["nathaniel@google.com"], + "name": "Nathaniel Manista" + }, "nelfin@gmail.com": { + "comment": " (nelfin)", "mails": ["nelfin@gmail.com", "hello@nelf.in"], "name": "Andrew Haigh" }, + "nicholasdrozd@gmail.com": { + "comment": ": performance improvements to astroid", + "mails": ["nicholasdrozd@gmail.com"], + "name": "Nick Drozd", + "team": "Maintainers" + }, "nickpesce22@gmail.com": { "mails": ["nickpesce22@gmail.com", "npesce@terpmail.umd.edu"], "name": "Nick Pesce" @@ -468,10 +747,25 @@ "name": "Claudiu Popa", "team": "Ex-maintainers" }, + "pedro@algarvio.me": { + "comment": " (s0undt3ch)", + "mails": ["pedro@algarvio.me"], + "name": "Pedro Algarvio" + }, "peter.kolbus@gmail.com": { + "comment": " (Garmin)", "mails": ["peter.kolbus@gmail.com", "peter.kolbus@garmin.com"], "name": "Peter Kolbus" }, + "petrpulc@gmail.com": { + "comment": ": require whitespace around annotations", + "mails": ["petrpulc@gmail.com"], + "name": "Petr Pulc" + }, + "pierre-yves.david@logilab.fr": { + "mails": ["pyves@crater.logilab.fr", "pierre-yves.david@logilab.fr"], + "name": "Pierre-Yves David" + }, "pierre.sassoulas@gmail.com": { "mails": [ "pierre.sassoulas@gmail.com", @@ -485,10 +779,30 @@ "mails": ["pnlbagan@gmail.com"], "name": "Sasha Bagan" }, + "radu@devrandom.ro": { + "comment": ": not-context-manager and confusing-with-statement warnings.", + "mails": ["radu@devrandom.ro"], + "name": "Radu Ciorba" + }, + "ram@rachum.com": { + "comment": " (cool-RR)", + "mails": ["ram@rachum.com"], + "name": "Ram Rachum" + }, + "ramiroleal050@gmail.com": { + "comment": " (ramiro050): Fixed bug preventing pylint from working with Emacs tramp", + "mails": ["ramiroleal050@gmail.com"], + "name": "Ramiro Leal-Cavazos" + }, "raphael@makeleaps.com": { "mails": ["raphael@rtpg.co", "raphael@makeleaps.com"], "name": "Raphael Gaschignard" }, + "rbt@sent.as": { + "comment": " (9999years)", + "mails": ["rbt@sent.as"], + "name": "Rebecca Turner" + }, "reverbc@users.noreply.github.com": { "mails": ["reverbc@users.noreply.github.com"], "name": "Reverb Chu" @@ -503,18 +817,34 @@ "team": "Maintainers" }, "roy.williams.iii@gmail.com": { + "comment": " (Lyft)\n * added check for implementing __eq__ without implementing __hash__,\n * Added Python 3 check for accessing Exception.message.\n * Added Python 3 check for calling encode/decode with invalid codecs.\n * Added Python 3 check for accessing sys.maxint.\n * Added Python 3 check for bad import statements.\n * Added Python 3 check for accessing deprecated methods on the 'string' module,\n various patches.", "mails": ["roy.williams.iii@gmail.com", "rwilliams@lyft.com"], "name": "Roy Williams", "team": "Maintainers" }, + "rr-@sakuya.pl": { + "comment": " (rr-)", + "mails": ["rr-@sakuya.pl"], + "name": "Marcin Kurczewski" + }, "ruro.ruro@ya.ru": { "mails": ["ruro.ruro@ya.ru"], "name": "Ruro" }, + "sandro.tosi@gmail.com": { + "comment": ": Debian packaging", + "mails": ["sandro.tosi@gmail.com", "sandrotosi@users.noreply.github.com"], + "name": "Sandro Tosi" + }, "sergeykosarchuk@gmail.com": { "mails": ["sergeykosarchuk@gmail.com"], "name": "Kosarchuk Sergey" }, + "sfreilich@google.com": { + "comment": " (sfreilich)", + "mails": ["sfreilich@google.com"], + "name": "Samuel Freilich" + }, "shlomme@gmail.com": { "mails": ["shlomme@gmail.com", "tmarek@google.com"], "name": "Torsten Marek", @@ -533,6 +863,7 @@ "name": "Stefan Scherfke" }, "stephane@wirtel.be": { + "comment": ": nonlocal-without-binding", "mails": ["stephane@wirtel.be"], "name": "Stéphane Wirtel" }, @@ -544,12 +875,24 @@ "mails": ["tanant@users.noreply.github.com"], "name": "Anthony Tan" }, + "tbekolay@gmail.com": { + "comment": "\n * Added --list-msgs-enabled command", + "mails": ["tbekolay@gmail.com"], + "name": "Trevor Bekolay" + }, "thenault@gmail.com": { + "comment": " : main author / maintainer", "mails": ["thenault@gmail.com", "sylvain.thenault@logilab.fr"], "name": "Sylvain Thénault", "team": "Ex-maintainers" }, + "tomer.chachamu@gmail.com": { + "comment": ": simplifiable-if-expression", + "mails": ["tomer.chachamu@gmail.com"], + "name": "Tomer Chachamu" + }, "tushar.sadhwani000@gmail.com": { + "comment": " (tusharsadhwani)", "mails": [ "tushar.sadhwani000@gmail.com", "tushar@deepsource.io", @@ -566,11 +909,27 @@ "name": "Ville Skyttä" }, "viorels@gmail.com": { + "comment": ": intern-builtin warning.", "mails": ["viorels@gmail.com"], "name": "Viorel Știrbu" }, "vladtemian@gmail.com": { + "comment": ": redundant-unittest-assert and the JSON reporter.", "mails": ["vladtemian@gmail.com"], "name": "Vlad Temian" + }, + "westurner@google.com": { + "comment": " (Google): added new check 'inconsistent-quotes'", + "mails": ["westurner@google.com"], + "name": "Wes Turner" + }, + "yileiyang@google.com": { + "mails": ["yileiyang@google.com"], + "name": "Yilei \"Dolee\" Yang" + }, + "zebedee.nicholls@climate-energy-college.org": { + "comment": "\n * Made W9011 compatible with 'of' syntax in return types", + "mails": ["zebedee.nicholls@climate-energy-college.org"], + "name": "Zeb Nicholls" } } diff --git a/script/bump_changelog.py b/script/bump_changelog.py index f27195bdcd..0963c7c604 100644 --- a/script/bump_changelog.py +++ b/script/bump_changelog.py @@ -2,167 +2,122 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt -# ORIGINAL here: https://github.com/PyCQA/astroid/blob/main/script/bump_changelog.py -# DO NOT MODIFY DIRECTLY - -"""This script permits to upgrade the changelog in astroid or pylint when releasing a version.""" -# pylint: disable=logging-fstring-interpolation - +"""This script updates towncrier.toml and creates a new newsfile and intermediate +folders if necessary. +""" from __future__ import annotations import argparse -import enum -import logging -from datetime import datetime +import re from pathlib import Path +from subprocess import check_call + +NEWSFILE_PATTERN = re.compile(r"doc/whatsnew/\d/\d.\d+/index\.rst") +NEWSFILE_PATH = "doc/whatsnew/{major}/{major}.{minor}/index.rst" +TOWNCRIER_CONFIG_FILE = Path("towncrier.toml") +TOWNCRIER_VERSION_PATTERN = re.compile(r"version = \"(\d+\.\d+\.\d+)\"") + +NEWSFILE_CONTENT_TEMPLATE = """ +*************************** + What's New in Pylint {major}.{minor} +*************************** -# TODO: 2.15.0 Modify the way we handle the patch version -# release notes -DEFAULT_CHANGELOG_PATH = Path("doc/whatsnew/2/2.14/full.rst") +.. toctree:: + :maxdepth: 2 -RELEASE_DATE_TEXT = "Release date: TBA" -WHATS_NEW_TEXT = "What's New in Pylint" -TODAY = datetime.now() -FULL_WHATS_NEW_TEXT = WHATS_NEW_TEXT + " {version}?" -NEW_RELEASE_DATE_MESSAGE = f"Release date: {TODAY.strftime('%Y-%m-%d')}" +:Release:{major}.{minor} +:Date: TBA + +Summary -- Release highlights +============================= + + +.. towncrier release notes start +""" def main() -> None: - parser = argparse.ArgumentParser(__doc__) - parser.add_argument("version", help="The version we want to release") + parser = argparse.ArgumentParser() + parser.add_argument("version", help="The new version to set") parser.add_argument( - "-v", "--verbose", action="store_true", default=False, help="Logging or not" + "--dry-run", + action="store_true", + help="Just show what would be done, don't write anything", ) args = parser.parse_args() - if args.verbose: - logging.basicConfig(level=logging.DEBUG) - logging.debug(f"Launching bump_changelog with args: {args}") + if "dev" in args.version: - return - with open(DEFAULT_CHANGELOG_PATH, encoding="utf-8") as f: - content = f.read() - content = transform_content(content, args.version) - with open(DEFAULT_CHANGELOG_PATH, "w", encoding="utf-8") as f: - f.write(content) - - -class VersionType(enum.Enum): - MAJOR = 0 - MINOR = 1 - PATCH = 2 - - -def get_next_version(version: str, version_type: VersionType) -> str: - new_version = version.split(".") - part_to_increase = new_version[version_type.value] - if "-" in part_to_increase: - part_to_increase = part_to_increase.split("-")[0] - for i in range(version_type.value, 3): - new_version[i] = "0" - new_version[version_type.value] = str(int(part_to_increase) + 1) - return ".".join(new_version) - - -def get_next_versions(version: str, version_type: VersionType) -> list[str]: - - if version_type == VersionType.PATCH: - # "2.6.1" => ["2.6.2"] - return [get_next_version(version, VersionType.PATCH)] - if version_type == VersionType.MINOR: - # "2.6.0" => ["2.7.0", "2.6.1"] - assert version.endswith(".0"), f"{version} does not look like a minor version" - else: - # "3.0.0" => ["3.1.0", "3.0.1"] - assert version.endswith(".0.0"), f"{version} does not look like a major version" - next_minor_version = get_next_version(version, VersionType.MINOR) - next_patch_version = get_next_version(version, VersionType.PATCH) - logging.debug(f"Getting the new version for {version} - {version_type.name}") - return [next_minor_version, next_patch_version] - - -def get_version_type(version: str) -> VersionType: - if version.endswith(".0.0"): - version_type = VersionType.MAJOR - elif version.endswith(".0"): - version_type = VersionType.MINOR - else: - version_type = VersionType.PATCH - return version_type - - -def get_whats_new( - version: str, add_date: bool = False, change_date: bool = False -) -> str: - whats_new_text = FULL_WHATS_NEW_TEXT.format(version=version) - result = [whats_new_text, "-" * len(whats_new_text)] - if add_date and change_date: - result += [NEW_RELEASE_DATE_MESSAGE] - elif add_date: - result += [RELEASE_DATE_TEXT] - elif change_date: - raise ValueError("Can't use change_date=True with add_date=False") - logging.debug( - f"version='{version}', add_date='{add_date}', change_date='{change_date}': {result}" - ) - return "\n".join(result) - - -def get_all_whats_new(version: str, version_type: VersionType) -> str: - result = "" - for version_ in get_next_versions(version, version_type=version_type): - result += get_whats_new(version_, add_date=True) + "\n" * 4 - return result - - -def transform_content(content: str, version: str) -> str: - version_type = get_version_type(version) - next_version = get_next_version(version, version_type) - old_date = get_whats_new(version, add_date=True) - new_date = get_whats_new(version, add_date=True, change_date=True) - next_version_with_date = get_all_whats_new(version, version_type) - do_checks(content, next_version, version, version_type) - index = content.find(old_date) - logging.debug(f"Replacing\n'{old_date}'\nby\n'{new_date}'\n") - content = content.replace(old_date, new_date) - end_content = content[index:] - content = content[:index] - logging.debug(f"Adding:\n'{next_version_with_date}'\n") - content += next_version_with_date + end_content - return content - - -def do_checks(content, next_version, version, version_type): - err = "in the changelog, fix that first!" - NEW_VERSION_ERROR_MSG = ( - # pylint: disable-next=consider-using-f-string - "The text for this version '{version}' did not exists %s" - % err + print("'-devXY' will be cut from version in towncrier.toml") + match = re.match( + r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-?\w+\d*)*", args.version ) - NEXT_VERSION_ERROR_MSG = ( - # pylint: disable-next=consider-using-f-string - "The text for the next version '{version}' already exists %s" - % err - ) - wn_next_version = get_whats_new(next_version) - wn_this_version = get_whats_new(version) - # There is only one field where the release date is TBA - if version_type in [VersionType.MAJOR, VersionType.MINOR]: - assert ( - content.count(RELEASE_DATE_TEXT) <= 1 - ), f"There should be only one release date 'TBA' ({version}) {err}" - else: - next_minor_version = get_next_version(version, VersionType.MINOR) - assert ( - content.count(RELEASE_DATE_TEXT) <= 2 - ), f"There should be only two release dates 'TBA' ({version} and {next_minor_version}) {err}" - # There is already a release note for the version we want to release - assert content.count(wn_this_version) == 1, NEW_VERSION_ERROR_MSG.format( - version=version + if not match: + print( + "Fatal error - new version did not match the " + "expected format (major.minor.patch[.*]). Abort!" + ) + return + major, minor, patch, suffix = match.groups() + new_version = f"{major}.{minor}.{patch}" + + new_newsfile = NEWSFILE_PATH.format(major=major, minor=minor) + create_new_newsfile_if_necessary(new_newsfile, major, minor, args.dry_run) + patch_towncrier_toml(new_newsfile, new_version, args.dry_run) + build_changelog(suffix, args.dry_run) + + +def create_new_newsfile_if_necessary( + new_newsfile: str, major: str, minor: str, dry_run: bool +) -> None: + new_newsfile_path = Path(new_newsfile) + if new_newsfile_path.exists(): + return + + # create new file and add boiler plate content + if dry_run: + print( + f"Dry run enabled - would create file {new_newsfile} " + "and intermediate folders" + ) + return + + print("Creating new newsfile:", new_newsfile) + new_newsfile_path.parent.mkdir(parents=True, exist_ok=True) + new_newsfile_path.touch() + new_newsfile_path.write_text( + NEWSFILE_CONTENT_TEMPLATE.format(major=major, minor=minor), + encoding="utf8", ) - # There is no release notes for the next version - assert content.count(wn_next_version) == 0, NEXT_VERSION_ERROR_MSG.format( - version=next_version + + # tbump does not add and commit new files, so we add it ourselves + print("Adding new newsfile to git") + check_call(["git", "add", new_newsfile]) + + +def patch_towncrier_toml(new_newsfile: str, version: str, dry_run: bool) -> None: + file_content = TOWNCRIER_CONFIG_FILE.read_text(encoding="utf-8") + patched_newsfile_path = NEWSFILE_PATTERN.sub(new_newsfile, file_content) + new_file_content = TOWNCRIER_VERSION_PATTERN.sub( + f'version = "{version}"', patched_newsfile_path ) + if dry_run: + print("Dry run enabled - this is what I would write:\n") + print(new_file_content) + return + TOWNCRIER_CONFIG_FILE.write_text(new_file_content, encoding="utf-8") + + +def build_changelog(suffix: str | None, dry_run: bool) -> None: + if suffix: + print("Not a release version, skipping changelog generation") + return + + if dry_run: + print("Dry run enabled - not building changelog") + return + + print("Building changelog") + check_call(["towncrier", "build", "--yes"]) if __name__ == "__main__": diff --git a/script/check_changelog.py b/script/check_changelog.py deleted file mode 100644 index 77f2d61b41..0000000000 --- a/script/check_changelog.py +++ /dev/null @@ -1,145 +0,0 @@ -# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -"""Small script to check the changelog. Used by 'changelog.yml' and pre-commit. - -If no issue number is provided we only check that proper formatting is respected.""" - -from __future__ import annotations - -import argparse -import re -import sys -from collections.abc import Iterator -from pathlib import Path -from re import Pattern - -VALID_ISSUES_KEYWORDS = ["Refs", "Closes", "Follow-up in", "Fixes part of"] -ISSUE_NUMBER_PATTERN = r"#\d{1,5}" -VALID_ISSUE_NUMBER_PATTERN = r"\*[\S\s]*?" + ISSUE_NUMBER_PATTERN -ISSUES_KEYWORDS = "|".join(VALID_ISSUES_KEYWORDS) -PREFIX_CHANGELOG_PATTERN = ( - rf"(\*\s[\S[\n ]+?]*\n\n\s\s({ISSUES_KEYWORDS})) (PyCQA/astroid)?" -) -VALID_CHANGELOG_PATTERN = PREFIX_CHANGELOG_PATTERN + ISSUE_NUMBER_PATTERN - -ISSUE_NUMBER_COMPILED_PATTERN = re.compile(ISSUE_NUMBER_PATTERN) -VALID_CHANGELOG_COMPILED_PATTERN: Pattern[str] = re.compile(VALID_CHANGELOG_PATTERN) -VALID_ISSUE_NUMBER_COMPILED_PATTERN: Pattern[str] = re.compile( - VALID_ISSUE_NUMBER_PATTERN -) - -DOC_PATH = (Path(__file__).parent / "../doc/").resolve() -PATH_TO_WHATSNEW = DOC_PATH / "whatsnew" -UNCHECKED_VERSION = [ - # Not checking version prior to 1.0.0 because the issues referenced are a mix - # between Logilab internal issue and Bitbucket. It's hard to tell, it's - # inaccessible for Logilab and often dead links for Bitbucket anyway. - # Not very useful generally, unless you're an open source historian. - "0.x", - # Too much Bitbucket issues in this one : - "1.0", - "1.1", - "1.2", -] - -NO_CHECK_REQUIRED_FILES = { - "index.rst", - "full_changelog_explanation.rst", - "summary_explanation.rst", -} - - -def sorted_whatsnew(verbose: bool) -> Iterator[Path]: - """Return the whats-new in the 'right' numerical order ('9' before '10')""" - numeric_whatsnew = {} - for file in PATH_TO_WHATSNEW.glob("**/*"): - relpath_file = file.relative_to(DOC_PATH) - if file.is_dir(): - if verbose: - print(f"I don't care about '{relpath_file}', it's a directory : 🤖🤷") - continue - if file.name in NO_CHECK_REQUIRED_FILES: - if verbose: - print( - f"I don't care about '{relpath_file}' it's in 'NO_CHECK_REQUIRED_FILES' : 🤖🤷" - ) - continue - version = ( - file.parents[0].name if file.stem in {"summary", "full"} else file.stem - ) - if any(version == x for x in UNCHECKED_VERSION): - if verbose: - print( - f"I don't care about '{relpath_file}' {version} is in UNCHECKED_VERSION : 🤖🤷" - ) - continue - if verbose: - print(f"I'm going to check '{relpath_file}' 🤖") - num = tuple(int(x) for x in (version.split("."))) - numeric_whatsnew[num] = file - for num in sorted(numeric_whatsnew): - yield numeric_whatsnew[num] - - -def main(argv: list[str] | None = None) -> int: - argv = argv or sys.argv[1:] - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--issue-number", - type=int, - default=0, - help="The issue we expect to find in the changelog.", - ) - parser.add_argument("--verbose", "-v", action="count", default=0) - args = parser.parse_args(argv) - verbose = args.verbose - is_valid = True - for file in sorted_whatsnew(verbose): - if not check_file(file, verbose): - is_valid = False - return 0 if is_valid else 1 - - -def check_file(file: Path, verbose: bool) -> bool: - """Check that a file contain valid change-log's entries.""" - with open(file, encoding="utf8") as f: - content = f.read() - valid_full_descriptions = VALID_CHANGELOG_COMPILED_PATTERN.findall(content) - result = len(valid_full_descriptions) - contain_issue_number_descriptions = VALID_ISSUE_NUMBER_COMPILED_PATTERN.findall( - content - ) - expected = len(contain_issue_number_descriptions) - if result != expected: - return create_detailed_fail_message( - file, contain_issue_number_descriptions, valid_full_descriptions - ) - if verbose: - relpath_file = file.relative_to(DOC_PATH) - print(f"Checked '{relpath_file}' : LGTM 🤖👍") - return True - - -def create_detailed_fail_message( - file_name: Path, - contain_issue_number_descriptions: list, - valid_full_descriptions: list, -) -> bool: - is_valid = True - for issue_number_description in contain_issue_number_descriptions: - if not any(v[0] in issue_number_description for v in valid_full_descriptions): - is_valid = False - issue_number = ISSUE_NUMBER_COMPILED_PATTERN.findall( - issue_number_description - )[0] - print( - f"{file_name}: {issue_number}'s description is not on one line, or " - "does not respect the standard format 🤖👎" - ) - return is_valid - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/script/check_newsfragments.py b/script/check_newsfragments.py new file mode 100644 index 0000000000..62560c9da4 --- /dev/null +++ b/script/check_newsfragments.py @@ -0,0 +1,120 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Small script to check the formatting of news fragments for towncrier. +Used by pre-commit. +""" + +from __future__ import annotations + +import argparse +import difflib +import re +import sys +from pathlib import Path +from re import Pattern + +VALID_ISSUES_KEYWORDS = [ + "Refs", + "Closes", + "Follow-up in", + "Fixes part of", +] +VALID_FILE_TYPE = frozenset( + [ + "breaking", + "user_action", + "feature", + "new_check", + "removed_check", + "extension", + "false_positive", + "false_negative", + "bugfix", + "other", + "internal", + ] +) +ISSUES_KEYWORDS = "|".join(VALID_ISSUES_KEYWORDS) +VALID_CHANGELOG_PATTERN = rf"(?P(.*\n)*(.*\.\n))\n(?P({ISSUES_KEYWORDS}) (PyCQA/astroid)?#(?P\d+))" +VALID_CHANGELOG_COMPILED_PATTERN: Pattern[str] = re.compile( + VALID_CHANGELOG_PATTERN, flags=re.MULTILINE +) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "filenames", + nargs="*", + metavar="FILES", + help="File names to check", + ) + parser.add_argument("--verbose", "-v", action="count", default=0) + args = parser.parse_args(argv) + is_valid = True + for filename in args.filenames: + is_valid &= check_file(Path(filename), args.verbose) + return 0 if is_valid else 1 + + +def check_file(file: Path, verbose: bool) -> bool: + """Check that a file contains a valid changelog entry.""" + with open(file, encoding="utf8") as f: + content = f.read() + match = VALID_CHANGELOG_COMPILED_PATTERN.match(content) + if match: + issue = match.group("issue") + if file.stem != issue: + echo( + f"{file} must be named '{issue}.', after the issue it references." + ) + return False + if not any(file.suffix.endswith(t) for t in VALID_FILE_TYPE): + suggestions = difflib.get_close_matches(file.suffix, VALID_FILE_TYPE) + if suggestions: + multiple_suggestions = "', '".join(f"{issue}.{s}" for s in suggestions) + suggestion = f"should probably be named '{multiple_suggestions}'" + else: + multiple_suggestions = "', '".join( + f"{issue}.{s}" for s in VALID_FILE_TYPE + ) + suggestion = f"must be named one of '{multiple_suggestions}'" + echo(f"{file} {suggestion} instead.") + return False + if verbose: + echo(f"Checked '{file}': LGTM 🤖👍") + return True + echo( + f"""\ +{file}: does not respect the standard format 🤖👎 + +The standard format is: + + + + # + +Where can be one of: {', '.join(VALID_ISSUES_KEYWORDS)} + +The regex used is '{VALID_CHANGELOG_COMPILED_PATTERN}'. + +For example: + +``pylint.x.y`` is now a private API. + +Refs #1234 +""" + ) + return False + + +def echo(msg: str) -> None: + # To support non-UTF-8 environments like Windows, we need + # to explicitly encode the message instead of using plain print() + sys.stdout.buffer.write(f"{msg}\n".encode()) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/create_contributor_list.py b/script/create_contributor_list.py index 9b336a01b1..4502f824d3 100644 --- a/script/create_contributor_list.py +++ b/script/create_contributor_list.py @@ -6,12 +6,13 @@ from contributors_txt import create_contributors_txt -BASE_DIRECTORY = Path(__file__).parent.parent -ALIASES_FILE = BASE_DIRECTORY / "script/.contributors_aliases.json" -DEFAULT_CONTRIBUTOR_PATH = BASE_DIRECTORY / "CONTRIBUTORS.txt" +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) -def main(): +def main() -> None: create_contributors_txt( aliases_file=ALIASES_FILE, output=DEFAULT_CONTRIBUTOR_PATH, verbose=True ) diff --git a/script/fix_documentation.py b/script/fix_documentation.py index 35eed9aff4..e8def2f737 100644 --- a/script/fix_documentation.py +++ b/script/fix_documentation.py @@ -14,7 +14,7 @@ r"(?<=\s`)([\w\-\.\(\)\=]+\s{0,1}[\w\-\.\(\)\=]*)(?=`[,\.]{0,1}\s|$)" ) -# TODO: 2.14.0: Upgrade script for change in changelog +# TODO: 2.16.0: Upgrade script for change in changelog DEFAULT_CHANGELOG = "ChangeLog" DEFAULT_SUBTITLE_PREFIX = "What's New in" @@ -36,12 +36,7 @@ def changelog_insert_empty_lines(file_content: str, subtitle_text: str) -> str: for i, line in enumerate(lines): if line.startswith(subtitle_text): subtitle_count += 1 - if ( - subtitle_count == 1 - or i < 2 - or lines[i - 1] == "" - and lines[i - 2] == "" - ): + if subtitle_count == 1 or i < 2 or not lines[i - 1] and not lines[i - 2]: continue lines.insert(i, "") return "\n".join(lines) diff --git a/setup.cfg b/setup.cfg index 7b2c954fd7..e4c696c4c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,151 +1,25 @@ +# Setuptools v62.6 doesn't support editable installs with just 'pyproject.toml' (PEP 660). +# Keep this file until it does! + [metadata] -name = pylint -version = attr: pylint.__pkginfo__.__version__ -description = python code static checker -long_description = file: README.rst -long_description_content_type = text/x-rst -author = Python Code Quality Authority -author_email = code-quality@python.org -license = GPL-2.0-or-later +# wheel doesn't yet read license_files from pyproject.toml - tools.setuptools +# Keep it here until it does! license_files = LICENSE CONTRIBUTORS.txt -classifiers = - Development Status :: 6 - Mature - Environment :: Console - Intended Audience :: Developers - License :: OSI Approved :: GNU General Public License v2 (GPLv2) - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python :: Implementation :: PyPy - Topic :: Software Development :: Debuggers - Topic :: Software Development :: Quality Assurance - Topic :: Software Development :: Testing -keywords = static code analysis linter python lint -project_urls = - Homepage = https://www.pylint.org/ - Source Code = https://github.com/PyCQA/pylint - What's New = https://pylint.pycqa.org/en/latest/whatsnew/ - Bug Tracker = https://github.com/PyCQA/pylint/issues - Discord Server = https://discord.com/invite/Egy6P8AMB5 - Docs: User Guide = https://pylint.pycqa.org/en/latest/ - Docs: Contributing = https://pylint.pycqa.org/en/latest/development_guide/contribute.html - Docs: Technical Reference = https://pylint.pycqa.org/en/latest/technical_reference/index.html - -[options] -packages = find: -install_requires = - dill>=0.2 - platformdirs>=2.2.0 - # Also upgrade requirements_test_min.txt if you are bumping astroid. - # Pinned to dev of next minor update to allow editable installs, - # see https://github.com/PyCQA/astroid/issues/1341 - astroid>=2.11.6,<=2.12.0-dev0 - isort>=4.2.5,<6 - mccabe>=0.6,<0.8 - tomli>=1.1.0;python_version<"3.11" - tomlkit>=0.10.1 - colorama>=0.4.5;sys_platform=="win32" - typing-extensions>=3.10.0;python_version<"3.10" -python_requires = >=3.7.2 - -[options.extras_require] -testutils=gitpython>3 -spelling=pyenchant~=3.2 - -[options.packages.find] -include = - pylint* - -[options.entry_points] -console_scripts = - pylint = pylint:run_pylint - pylint-config = pylint:_run_pylint_config - epylint = pylint:run_epylint - pyreverse = pylint:run_pyreverse - symilar = pylint:run_symilar - -[options.package_data] -pylint = testutils/testing_pylintrc - -[aliases] -test = pytest - -[tool:pytest] -testpaths = tests -python_files = *test_*.py -addopts = --strict-markers -markers = - primer_stdlib: Checks for crashes and errors when running pylint on stdlib - primer_external_batch_one: Checks for crashes and errors when running pylint on external libs (batch one) - benchmark: Baseline of pylint performance, if this regress something serious happened - timeout: Marks from pytest-timeout. - needs_two_cores: Checks that need 2 or more cores to be meaningful - -[isort] -profile = black -known_third_party = platformdirs, astroid, sphinx, isort, pytest, mccabe, six, toml -skip_glob = tests/functional/**,tests/input/**,tests/extensions/data/**,tests/regrtest_data/**,tests/data/**,astroid/**,venv/** -src_paths = pylint - -[mypy] -no_implicit_optional = True -scripts_are_modules = True -warn_unused_ignores = True -show_error_codes = True -enable_error_code = ignore-without-code - -[mypy-astroid.*] -ignore_missing_imports = True - -[mypy-tests.*] -ignore_missing_imports = True - -[mypy-contributors_txt] -ignore_missing_imports = True - -[mypy-coverage] -ignore_missing_imports = True - -[mypy-enchant.*] -ignore_missing_imports = True - -[mypy-isort.*] -ignore_missing_imports = True - -[mypy-mccabe] -ignore_missing_imports = True - -[mypy-pytest] -ignore_missing_imports = True - -[mypy-_pytest.*] -ignore_missing_imports = True - -[mypy-setuptools] -ignore_missing_imports = True - -[mypy-_string] -ignore_missing_imports = True - -[mypy-git.*] -ignore_missing_imports = True - -[mypy-tomlkit] -ignore_missing_imports = True - -[mypy-sphinx.*] -ignore_missing_imports = True - -[mypy-dill] -ignore_missing_imports = True -[mypy-colorama] -ignore_missing_imports = True +[flake8] +# Incompatible with black see https://github.com/ambv/black/issues/315 +# E203: Whitespace before ':' +# W503: Line break occurred before a binary operator +# B028: consider using the `!r` conversion flag +ignore = + E203, + W503, + B028, +# Flake8 is less lenient than pylint and does not make any exceptions +# (for docstrings, strings and comments in particular). +max-line-length=125 +# Required for flake8-typing-imports (v1.12.0) +# The plugin doesn't yet read the value from pyproject.toml +min_python_version = 3.7.2 diff --git a/setup.py b/setup.py deleted file mode 100644 index 606849326a..0000000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() diff --git a/tbump.toml b/tbump.toml index 474fff9797..c4d04acf24 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,14 +1,14 @@ github_url = "https://github.com/PyCQA/pylint" [version] -current = "2.14.5" +current = "2.16.3" regex = ''' ^(?P0|[1-9]\d*) \. (?P0|[1-9]\d*) \. (?P0|[1-9]\d*) -(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ +(?:-?(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ ''' [git] @@ -52,6 +52,10 @@ cmd = "python3 script/create_contributor_list.py" name = "Apply pre-commit" cmd = "pre-commit run --all-files||echo 'Hack so this command does not fail'" +[[before_commit]] +name = "Confirm changes" +cmd = "read -p 'Continue (y)? ' -n 1 -r; echo; [[ ! $REPLY =~ ^[Yy]$ ]] && exit 1 || exit 0" + # Or run some commands after the git tag and the branch # have been pushed: # [[after_push]] diff --git a/tests/benchmark/test_baseline_benchmarks.py b/tests/benchmark/test_baseline_benchmarks.py index 6fb1cdf18b..42521b5932 100644 --- a/tests/benchmark/test_baseline_benchmarks.py +++ b/tests/benchmark/test_baseline_benchmarks.py @@ -13,6 +13,7 @@ import pytest from astroid import nodes +from pytest_benchmark.fixture import BenchmarkFixture from pylint.checkers import BaseRawFileChecker from pylint.lint import PyLinter, check_parallel @@ -22,7 +23,7 @@ from pylint.utils import register_plugins -def _empty_filepath(): +def _empty_filepath() -> str: return os.path.abspath( os.path.join( os.path.dirname(__file__), "..", "input", "benchmark_minimal_file.py" @@ -114,7 +115,7 @@ class TestEstablishBaselineBenchmarks: ) lot_of_files = 500 - def test_baseline_benchmark_j1(self, benchmark): + def test_baseline_benchmark_j1(self, benchmark: BenchmarkFixture) -> None: """Establish a baseline of pylint performance with no work. We will add extra Checkers in other benchmarks. @@ -131,7 +132,7 @@ def test_baseline_benchmark_j1(self, benchmark): ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_benchmark_j2(self, benchmark): + def test_baseline_benchmark_j2(self, benchmark: BenchmarkFixture) -> None: """Establish a baseline of pylint performance with no work across threads. Same as `test_baseline_benchmark_j1` but we use -j2 with 2 fake files to @@ -154,7 +155,9 @@ def test_baseline_benchmark_j2(self, benchmark): ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_benchmark_check_parallel_j2(self, benchmark): + def test_baseline_benchmark_check_parallel_j2( + self, benchmark: BenchmarkFixture + ) -> None: """Should demonstrate times very close to `test_baseline_benchmark_j2`.""" linter = PyLinter(reporter=Reporter()) @@ -167,7 +170,7 @@ def test_baseline_benchmark_check_parallel_j2(self, benchmark): linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" - def test_baseline_lots_of_files_j1(self, benchmark): + def test_baseline_lots_of_files_j1(self, benchmark: BenchmarkFixture) -> None: """Establish a baseline with only 'main' checker being run in -j1. We do not register any checkers except the default 'main', so the cost is just @@ -187,7 +190,7 @@ def test_baseline_lots_of_files_j1(self, benchmark): ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_lots_of_files_j2(self, benchmark): + def test_baseline_lots_of_files_j2(self, benchmark: BenchmarkFixture) -> None: """Establish a baseline with only 'main' checker being run in -j2. As with the -j1 variant above `test_baseline_lots_of_files_j1`, we do not @@ -207,7 +210,9 @@ def test_baseline_lots_of_files_j2(self, benchmark): linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" - def test_baseline_lots_of_files_j1_empty_checker(self, benchmark): + def test_baseline_lots_of_files_j1_empty_checker( + self, benchmark: BenchmarkFixture + ) -> None: """Baselines pylint for a single extra checker being run in -j1, for N-files. We use a checker that does no work, so the cost is just that of the system at @@ -228,7 +233,9 @@ def test_baseline_lots_of_files_j1_empty_checker(self, benchmark): ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_lots_of_files_j2_empty_checker(self, benchmark): + def test_baseline_lots_of_files_j2_empty_checker( + self, benchmark: BenchmarkFixture + ) -> None: """Baselines pylint for a single extra checker being run in -j2, for N-files. We use a checker that does no work, so the cost is just that of the system at @@ -248,7 +255,9 @@ def test_baseline_lots_of_files_j2_empty_checker(self, benchmark): linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" - def test_baseline_benchmark_j1_single_working_checker(self, benchmark): + def test_baseline_benchmark_j1_single_working_checker( + self, benchmark: BenchmarkFixture + ) -> None: """Establish a baseline of single-worker performance for PyLinter. Here we mimic a single Checker that does some work so that we can see the @@ -275,7 +284,9 @@ def test_baseline_benchmark_j1_single_working_checker(self, benchmark): ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_benchmark_j2_single_working_checker(self, benchmark): + def test_baseline_benchmark_j2_single_working_checker( + self, benchmark: BenchmarkFixture + ) -> None: """Establishes baseline of multi-worker performance for PyLinter/check_parallel. We expect this benchmark to take less time that test_baseline_benchmark_j1, @@ -302,7 +313,9 @@ def test_baseline_benchmark_j2_single_working_checker(self, benchmark): linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" - def test_baseline_benchmark_j1_all_checks_single_file(self, benchmark): + def test_baseline_benchmark_j1_all_checks_single_file( + self, benchmark: BenchmarkFixture + ) -> None: """Runs a single file, with -j1, against all checkers/Extensions.""" args = [self.empty_filepath, "--enable=all", "--enable-all-extensions"] runner = benchmark(Run, args, reporter=Reporter(), exit=False) @@ -314,7 +327,9 @@ def test_baseline_benchmark_j1_all_checks_single_file(self, benchmark): runner.linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(runner.linter.reporter.messages)}" - def test_baseline_benchmark_j1_all_checks_lots_of_files(self, benchmark): + def test_baseline_benchmark_j1_all_checks_lots_of_files( + self, benchmark: BenchmarkFixture + ) -> None: """Runs lots of files, with -j1, against all plug-ins. ... that's the intent at least. diff --git a/tests/checkers/__init__.py b/tests/checkers/__init__.py index e8a8ff79fd..e69de29bb2 100644 --- a/tests/checkers/__init__.py +++ b/tests/checkers/__init__.py @@ -1,3 +0,0 @@ -# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt diff --git a/tests/checkers/base/unittest_base.py b/tests/checkers/base/unittest_base.py index 05a7271cda..99a8f659ee 100644 --- a/tests/checkers/base/unittest_base.py +++ b/tests/checkers/base/unittest_base.py @@ -9,7 +9,7 @@ class TestNoSix(unittest.TestCase): @unittest.skip("too many dependencies need six :(") - def test_no_six(self): + def test_no_six(self) -> None: try: has_six = True except ImportError: diff --git a/tests/checkers/base/unittest_multi_naming_style.py b/tests/checkers/base/unittest_multi_naming_style.py index b9215a1e0f..4579131493 100644 --- a/tests/checkers/base/unittest_multi_naming_style.py +++ b/tests/checkers/base/unittest_multi_naming_style.py @@ -105,7 +105,7 @@ class CLASSC(object): #@ function_rgx=MULTI_STYLE_RE, name_group=("function:method",), ) - def test_multi_name_detection_group(self): + def test_multi_name_detection_group(self) -> None: function_defs = astroid.extract_node( """ class First(object): diff --git a/tests/checkers/unittest_base_checker.py b/tests/checkers/unittest_base_checker.py index 1fcdcfc9da..81a9c73d07 100644 --- a/tests/checkers/unittest_base_checker.py +++ b/tests/checkers/unittest_base_checker.py @@ -4,10 +4,12 @@ """Unittest for the BaseChecker class.""" +import pytest from pylint.checkers import BaseChecker from pylint.checkers.imports import ImportsChecker from pylint.checkers.typecheck import TypeChecker +from pylint.exceptions import InvalidMessageError from pylint.extensions.while_used import WhileChecker from pylint.lint.pylinter import PyLinter @@ -26,6 +28,11 @@ def __init__(self) -> None: } +class MissingFieldsChecker(BaseChecker): + name = "basic" + msgs = {"W0001": ("msg-name",)} # type: ignore[dict-item] + + class LessBasicChecker(OtherBasicChecker): options = ( ( @@ -121,3 +128,20 @@ def test_base_checker_ordering() -> None: assert fake_checker_1 > fake_checker_3 assert fake_checker_2 > fake_checker_3 assert fake_checker_1 == fake_checker_2 + + +def test_base_checker_invalid_message() -> None: + linter = PyLinter() + + with pytest.raises(InvalidMessageError): + linter.register_checker(MissingFieldsChecker(linter)) + + +def test_get_message_definition() -> None: + checker = LessBasicChecker() + with pytest.warns(DeprecationWarning): + with pytest.raises(InvalidMessageError): + checker.get_message_definition("W123") + + with pytest.warns(DeprecationWarning): + assert checker.get_message_definition("W0001") diff --git a/tests/checkers/unittest_deprecated.py b/tests/checkers/unittest_deprecated.py index a90e64ed8a..d34150247d 100644 --- a/tests/checkers/unittest_deprecated.py +++ b/tests/checkers/unittest_deprecated.py @@ -25,7 +25,7 @@ def deprecated_classes(self, module: str) -> list[str]: def deprecated_arguments( self, method: str - ) -> (tuple[tuple[int | None, str], ...] | tuple[tuple[int, str], tuple[int, str]]): + ) -> tuple[tuple[int | None, str], ...] | tuple[tuple[int, str], tuple[int, str]]: if method == "myfunction1": # def myfunction1(arg1, deprecated_arg1='spam') return ((1, "deprecated_arg1"),) @@ -53,6 +53,7 @@ def deprecated_decorators(self) -> set[str]: return {".deprecated_decorator"} +# pylint: disable-next = too-many-public-methods class TestDeprecatedChecker(CheckerTestCase): CHECKER_CLASS = _DeprecatedChecker @@ -108,33 +109,49 @@ def deprecated_method(): def test_deprecated_method_alias(self) -> None: # Tests detecting deprecated method defined as alias - # to existing method node = astroid.extract_node( """ class Deprecated: - def _deprecated_method(self): + def deprecated_method(self): pass - deprecated_method = _deprecated_method + new_name = deprecated_method d = Deprecated() - d.deprecated_method() + d.new_name() """ ) with self.assertAddsMessages( MessageTest( msg_id="deprecated-method", - args=("deprecated_method",), + args=("new_name",), node=node, confidence=UNDEFINED, line=9, col_offset=0, end_line=9, - end_col_offset=21, + end_col_offset=12, ) ): self.checker.visit_call(node) + def test_not_deprecated(self) -> None: + # Tests detecting method is NOT deprecated when alias name is a deprecated name + node = astroid.extract_node( + """ + class Deprecated: + def not_deprecated(self): + pass + + deprecated_method = not_deprecated + + d = Deprecated() + d.deprecated_method() + """ + ) + with self.assertNoMessages(): + self.checker.visit_call(node) + def test_no_message(self) -> None: # Tests not raising error when no deprecated functions/methods are present. node = astroid.extract_node( @@ -469,7 +486,6 @@ def mymethod2(self, arg1, deprecated_arg1, arg2='foo', deprecated_arg2='spam'): self.checker.visit_call(node) def test_class_deprecated_arguments(self) -> None: - node = astroid.extract_node( """ class MyClass: diff --git a/tests/checkers/unittest_design.py b/tests/checkers/unittest_design.py index e81a68a92e..f2ea09d2d2 100644 --- a/tests/checkers/unittest_design.py +++ b/tests/checkers/unittest_design.py @@ -9,7 +9,6 @@ class TestDesignChecker(CheckerTestCase): - CHECKER_CLASS = design_analysis.MisdesignChecker @set_config( diff --git a/tests/checkers/unittest_format.py b/tests/checkers/unittest_format.py index 9a34429ee4..fd0699daab 100644 --- a/tests/checkers/unittest_format.py +++ b/tests/checkers/unittest_format.py @@ -165,6 +165,7 @@ def test_disable_global_option_end_of_line() -> None: 1 """ ) + # pylint: disable = too-many-try-statements try: linter = lint.PyLinter() checker = BasicChecker(linter) diff --git a/tests/checkers/unittest_imports.py b/tests/checkers/unittest_imports.py index ac36f784fb..40e504c49b 100644 --- a/tests/checkers/unittest_imports.py +++ b/tests/checkers/unittest_imports.py @@ -7,17 +7,17 @@ import os import astroid +from pytest import CaptureFixture -from pylint import epylint as lint from pylint.checkers import imports from pylint.interfaces import UNDEFINED from pylint.testutils import CheckerTestCase, MessageTest +from pylint.testutils._run import _Run as Run REGR_DATA = os.path.join(os.path.dirname(__file__), "..", "regrtest_data", "") class TestImportsChecker(CheckerTestCase): - CHECKER_CLASS = imports.ImportsChecker def test_relative_beyond_top_level(self) -> None: @@ -40,39 +40,56 @@ def test_relative_beyond_top_level(self) -> None: self.checker.visit_importfrom(module.body[2].body[0]) @staticmethod - def test_relative_beyond_top_level_two() -> None: - output, errors = lint.py_run( - f"{os.path.join(REGR_DATA, 'beyond_top_two')} -d all -e relative-beyond-top-level", - return_std=True, + def test_relative_beyond_top_level_two(capsys: CaptureFixture[str]) -> None: + Run( + [ + f"{os.path.join(REGR_DATA, 'beyond_top_two')}", + "-d all", + "-e relative-beyond-top-level", + ], + exit=False, ) + output, errors = capsys.readouterr() + top_level_function = os.path.join( REGR_DATA, "beyond_top_two/namespace_package/top_level_function.py" ) - output2, errors2 = lint.py_run( - f"{top_level_function} -d all -e relative-beyond-top-level", - return_std=True, + Run( + [top_level_function, "-d all", "-e relative-beyond-top-level"], + exit=False, ) + output2, errors2 = capsys.readouterr() - assert len(output.readlines()) == len(output2.readlines()) - assert errors.readlines() == errors2.readlines() + assert len(output.split("\n")) == 5 + assert len(output2.split("\n")) == 5 + assert errors == errors2 @staticmethod - def test_relative_beyond_top_level_three() -> None: - output, errors = lint.py_run( - f"{os.path.join(REGR_DATA, 'beyond_top_three/a.py')} -d all -e relative-beyond-top-level", - return_std=True, + def test_relative_beyond_top_level_three(capsys: CaptureFixture[str]) -> None: + Run( + [ + f"{os.path.join(REGR_DATA, 'beyond_top_three/a.py')}", + "-d all", + "-e relative-beyond-top-level", + ], + exit=False, ) - assert len(output.readlines()) == 5 - assert errors.readlines() == [] + output, errors = capsys.readouterr() + assert len(output.split("\n")) == 5 + assert errors == "" @staticmethod - def test_relative_beyond_top_level_four() -> None: - output, errors = lint.py_run( - f"{os.path.join(REGR_DATA, 'beyond_top_four/module')} -d missing-docstring,unused-import", - return_std=True, + def test_relative_beyond_top_level_four(capsys: CaptureFixture[str]) -> None: + Run( + [ + f"{os.path.join(REGR_DATA, 'beyond_top_four/module')}", + "-d missing-docstring,unused-import", + ], + exit=False, ) - assert len(output.readlines()) == 5 - assert errors.readlines() == [] + output, errors = capsys.readouterr() + assert len(output.split("\n")) == 5 + assert errors == "" def test_wildcard_import_init(self) -> None: module = astroid.MANAGER.ast_from_module_name("init_wildcard", REGR_DATA) @@ -97,3 +114,70 @@ def test_wildcard_import_non_init(self) -> None: ) with self.assertAddsMessages(msg): self.checker.visit_importfrom(import_from) + + @staticmethod + def test_preferred_module(capsys: CaptureFixture[str]) -> None: + """ + Tests preferred-module configuration option + """ + # test preferred-modules case with base module import + Run( + [ + f"{os.path.join(REGR_DATA, 'preferred_module')}", + "-d all", + "-e preferred-module", + # prefer sys instead of os (for triggering test) + "--preferred-modules=os:sys", + ], + exit=False, + ) + output, errors = capsys.readouterr() + + # assert that we saw preferred-modules triggered + assert "Prefer importing 'sys' instead of 'os'" in output + # assert there were no errors + assert len(errors) == 0 + + @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( + [ + f"{os.path.join(REGR_DATA, 'allow_reexport')}", + "--allow-reexport-from-package=no", + "-sn", + ], + exit=False, + ) + output, errors = capsys.readouterr() + assert len(output.split("\n")) == 7, f"Expected 7 line breaks in:{output}" + assert ( + "__init__.py:1:0: C0414: Import alias does not rename original package (useless-import-alias)" + in output + ) + assert ( + "file.py:2:0: C0414: Import alias does not rename original package (useless-import-alias)" + in output + ) + assert len(errors) == 0 + + # Option enabled - useless-import-alias should only be emitted for 'file.py' + Run( + [ + f"{os.path.join(REGR_DATA, 'allow_reexport')}", + "--allow-reexport-from-package=yes", + "--disable=missing-module-docstring", + "-sn", + ], + exit=False, + ) + output, errors = capsys.readouterr() + assert len(output.split("\n")) == 3 + assert "__init__.py" not in output + assert ( + "file.py:2:0: C0414: Import alias does not rename original package (useless-import-alias)" + in output + ) + assert len(errors) == 0 diff --git a/tests/checkers/unittest_non_ascii_name.py b/tests/checkers/unittest_non_ascii_name.py index 1830dd7ec8..4f854dddc6 100644 --- a/tests/checkers/unittest_non_ascii_name.py +++ b/tests/checkers/unittest_non_ascii_name.py @@ -23,7 +23,7 @@ class TestNonAsciiChecker(pylint.testutils.CheckerTestCase): @pytest.mark.skipif( sys.version_info < (3, 8), reason="requires python3.8 or higher" ) - def test_kwargs_and_position_only(self): + def test_kwargs_and_position_only(self) -> None: """Even the new position only and keyword only should be found.""" node = astroid.extract_node( """ @@ -136,7 +136,7 @@ def test_assignname( self, code: str, assign_type: str, - ): + ) -> None: """Variables defined no matter where, should be checked for non ascii.""" assign_node = astroid.extract_node(code) @@ -261,7 +261,7 @@ def test_assignname( ), ], ) - def test_check_import(self, import_statement: str, wrong_name: str | None): + def test_check_import(self, import_statement: str, wrong_name: str | None) -> None: """We expect that for everything that user can change there is a message.""" node = astroid.extract_node(f"{import_statement} #@") diff --git a/tests/checkers/unittest_spelling.py b/tests/checkers/unittest_spelling.py index 3554255967..aa6a2e195a 100644 --- a/tests/checkers/unittest_spelling.py +++ b/tests/checkers/unittest_spelling.py @@ -35,13 +35,13 @@ class TestSpellingChecker(CheckerTestCase): # pylint:disable=too-many-public-me reason="missing python-enchant package or missing spelling dictionaries", ) - def _get_msg_suggestions(self, word, count=4): + def _get_msg_suggestions(self, word: str, count: int = 4) -> str: suggestions = "' or '".join(self.checker.spelling_dict.suggest(word)[:count]) return f"'{suggestions}'" @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_check_bad_coment(self): + def test_check_bad_coment(self) -> None: with self.assertAddsMessages( MessageTest( "wrong-spelling-in-comment", @@ -59,7 +59,7 @@ def test_check_bad_coment(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) @set_config(max_spelling_suggestions=2) - def test_check_bad_comment_custom_suggestion_count(self): + def test_check_bad_comment_custom_suggestion_count(self) -> None: with self.assertAddsMessages( MessageTest( "wrong-spelling-in-comment", @@ -76,7 +76,7 @@ def test_check_bad_comment_custom_suggestion_count(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_check_bad_docstring(self): + def test_check_bad_docstring(self) -> None: stmt = astroid.extract_node('def fff():\n """bad coment"""\n pass') with self.assertAddsMessages( MessageTest( @@ -109,13 +109,13 @@ def test_check_bad_docstring(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_shebangs(self): + def test_skip_shebangs(self) -> None: self.checker.process_tokens(_tokenize_str("#!/usr/bin/env python")) assert not self.linter.release_messages() @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_python_coding_comments(self): + def test_skip_python_coding_comments(self) -> None: self.checker.process_tokens(_tokenize_str("# -*- coding: utf-8 -*-")) assert not self.linter.release_messages() self.checker.process_tokens(_tokenize_str("# coding=utf-8")) @@ -138,7 +138,7 @@ def test_skip_python_coding_comments(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_top_level_pylint_enable_disable_comments(self): + def test_skip_top_level_pylint_enable_disable_comments(self) -> None: self.checker.process_tokens( _tokenize_str("# Line 1\n Line 2\n# pylint: disable=ungrouped-imports") ) @@ -146,13 +146,13 @@ def test_skip_top_level_pylint_enable_disable_comments(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_words_with_numbers(self): + def test_skip_words_with_numbers(self) -> None: self.checker.process_tokens(_tokenize_str("\n# 0ne\n# Thr33\n# Sh3ll")) assert not self.linter.release_messages() @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_wiki_words(self): + def test_skip_wiki_words(self) -> None: stmt = astroid.extract_node( 'class ComentAbc(object):\n """ComentAbc with a bad coment"""\n pass' ) @@ -172,7 +172,7 @@ def test_skip_wiki_words(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_camel_cased_words(self): + def test_skip_camel_cased_words(self) -> None: stmt = astroid.extract_node( 'class ComentAbc(object):\n """comentAbc with a bad coment"""\n pass' ) @@ -224,7 +224,7 @@ def test_skip_camel_cased_words(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_words_with_underscores(self): + def test_skip_words_with_underscores(self) -> None: stmt = astroid.extract_node( 'def fff(param_name):\n """test param_name"""\n pass' ) @@ -233,19 +233,40 @@ def test_skip_words_with_underscores(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_email_address(self): + def test_skip_email_address(self) -> None: self.checker.process_tokens(_tokenize_str("# uname@domain.tld")) assert not self.linter.release_messages() @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_urls(self): + def test_skip_urls(self) -> None: self.checker.process_tokens(_tokenize_str("# https://github.com/rfk/pyenchant")) assert not self.linter.release_messages() @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_sphinx_directives(self): + @pytest.mark.parametrize( + "type_comment", + [ + "# type: (NotAWord) -> NotAWord", + "# type: List[NotAWord] -> List[NotAWord]", + "# type: Dict[NotAWord] -> Dict[NotAWord]", + "# type: NotAWord", + "# type: List[NotAWord]", + "# type: Dict[NotAWord]", + "# type: ImmutableList[Manager]", + # will result in error: Invalid "type: ignore" comment [syntax] + # when analyzed with mypy 1.02 + "# type: ignore[attr-defined] NotAWord", + ], + ) + def test_skip_type_comments(self, type_comment: str) -> None: + self.checker.process_tokens(_tokenize_str(type_comment)) + assert not self.linter.release_messages() + + @skip_on_missing_package_or_dict + @set_config(spelling_dict=spell_dict) + def test_skip_sphinx_directives(self) -> None: stmt = astroid.extract_node( 'class ComentAbc(object):\n """This is :class:`ComentAbc` with a bad coment"""\n pass' ) @@ -265,7 +286,7 @@ def test_skip_sphinx_directives(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_sphinx_directives_2(self): + def test_skip_sphinx_directives_2(self) -> None: stmt = astroid.extract_node( 'class ComentAbc(object):\n """This is :py:attr:`ComentAbc` with a bad coment"""\n pass' ) @@ -286,50 +307,35 @@ def test_skip_sphinx_directives_2(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) @pytest.mark.parametrize( - ",".join( - ( - "misspelled_portion_of_directive", - "second_portion_of_directive", - "description", - ) - ), + "prefix,suffix", ( - ("fmt", ": on", "black directive to turn on formatting"), - ("fmt", ": off", "black directive to turn off formatting"), - ("noqa", "", "pycharm directive"), - ("noqa", ":", "flake8 / zimports directive"), - ("nosec", "", "bandit directive"), - ("isort", ":skip", "isort directive"), - ("mypy", ":", "mypy top of file directive"), + pytest.param("fmt", ": on", id="black directive to turn on formatting"), + pytest.param("fmt", ": off", id="black directive to turn off formatting"), + pytest.param("noqa", "", id="pycharm directive"), + pytest.param("noqa", ":", id="flake8 / zimports directive"), + pytest.param("nosec", "", id="bandit directive"), + pytest.param("isort", ":skip", id="isort directive"), + pytest.param("mypy", ":", id="mypy top of file directive"), ), ) - def test_skip_tool_directives_at_beginning_of_comments_but_still_raise_error_if_directive_appears_later_in_comment( # pylint:disable=unused-argument - # Having the extra description parameter allows the description - # to show up in the pytest output as part of the test name - # when running parameterized tests. - self, - misspelled_portion_of_directive, - second_portion_of_directive, - description, - ): - full_comment = f"# {misspelled_portion_of_directive}{second_portion_of_directive} {misspelled_portion_of_directive}" + 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.""" + full_comment = f"# {prefix}{suffix} {prefix}" + args = ( + prefix, + full_comment, + f" {'^' * len(prefix)}", + self._get_msg_suggestions(prefix), + ) with self.assertAddsMessages( - MessageTest( - "wrong-spelling-in-comment", - line=1, - args=( - misspelled_portion_of_directive, - full_comment, - f" {'^'*len(misspelled_portion_of_directive)}", - self._get_msg_suggestions(misspelled_portion_of_directive), - ), - ) + MessageTest("wrong-spelling-in-comment", line=1, args=args) ): self.checker.process_tokens(_tokenize_str(full_comment)) @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_code_flanked_in_double_backticks(self): + def test_skip_code_flanked_in_double_backticks(self) -> None: full_comment = "# The function ``.qsize()`` .qsize()" with self.assertAddsMessages( MessageTest( @@ -347,7 +353,7 @@ def test_skip_code_flanked_in_double_backticks(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_skip_code_flanked_in_single_backticks(self): + def test_skip_code_flanked_in_single_backticks(self) -> None: full_comment = "# The function `.qsize()` .qsize()" with self.assertAddsMessages( MessageTest( @@ -363,30 +369,12 @@ def test_skip_code_flanked_in_single_backticks(self): ): self.checker.process_tokens(_tokenize_str(full_comment)) - @skip_on_missing_package_or_dict - @set_config(spelling_dict=spell_dict) - def test_skip_mypy_ignore_directives(self): - full_comment = "# type: ignore[attr-defined] attr" - with self.assertAddsMessages( - MessageTest( - "wrong-spelling-in-comment", - line=1, - args=( - "attr", - full_comment, - " ^^^^", - self._get_msg_suggestions("attr"), - ), - ) - ): - self.checker.process_tokens(_tokenize_str(full_comment)) - @skip_on_missing_package_or_dict @set_config( spelling_dict=spell_dict, spelling_ignore_comment_directives="newdirective:,noqa", ) - def test_skip_directives_specified_in_pylintrc(self): + def test_skip_directives_specified_in_pylintrc(self) -> None: full_comment = "# newdirective: do this newdirective" with self.assertAddsMessages( MessageTest( @@ -404,7 +392,7 @@ def test_skip_directives_specified_in_pylintrc(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_handle_words_joined_by_forward_slash(self): + def test_handle_words_joined_by_forward_slash(self) -> None: stmt = astroid.extract_node( ''' class ComentAbc(object): @@ -428,7 +416,7 @@ class ComentAbc(object): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_more_than_one_error_in_same_line_for_same_word_on_docstring(self): + def test_more_than_one_error_in_same_line_for_same_word_on_docstring(self) -> None: stmt = astroid.extract_node( 'class ComentAbc(object):\n """Check teh dummy comment teh"""\n pass' ) @@ -458,7 +446,7 @@ def test_more_than_one_error_in_same_line_for_same_word_on_docstring(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_more_than_one_error_in_same_line_for_same_word_on_comment(self): + def test_more_than_one_error_in_same_line_for_same_word_on_comment(self) -> None: with self.assertAddsMessages( MessageTest( "wrong-spelling-in-comment", @@ -485,7 +473,7 @@ def test_more_than_one_error_in_same_line_for_same_word_on_comment(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_docstring_lines_that_look_like_comments_1(self): + def test_docstring_lines_that_look_like_comments_1(self) -> None: stmt = astroid.extract_node( '''def f(): """ @@ -508,7 +496,7 @@ def test_docstring_lines_that_look_like_comments_1(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_docstring_lines_that_look_like_comments_2(self): + def test_docstring_lines_that_look_like_comments_2(self) -> None: stmt = astroid.extract_node( '''def f(): """# msitake"""''' @@ -529,7 +517,7 @@ def test_docstring_lines_that_look_like_comments_2(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_docstring_lines_that_look_like_comments_3(self): + def test_docstring_lines_that_look_like_comments_3(self) -> None: stmt = astroid.extract_node( '''def f(): """ @@ -552,7 +540,7 @@ def test_docstring_lines_that_look_like_comments_3(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_docstring_lines_that_look_like_comments_4(self): + def test_docstring_lines_that_look_like_comments_4(self) -> None: stmt = astroid.extract_node( '''def f(): """ @@ -564,7 +552,7 @@ def test_docstring_lines_that_look_like_comments_4(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_docstring_lines_that_look_like_comments_5(self): + def test_docstring_lines_that_look_like_comments_5(self) -> None: stmt = astroid.extract_node( '''def f(): """ @@ -587,7 +575,7 @@ def test_docstring_lines_that_look_like_comments_5(self): @skip_on_missing_package_or_dict @set_config(spelling_dict=spell_dict) - def test_docstring_lines_that_look_like_comments_6(self): + def test_docstring_lines_that_look_like_comments_6(self) -> None: stmt = astroid.extract_node( '''def f(): """ diff --git a/tests/checkers/unittest_stdlib.py b/tests/checkers/unittest_stdlib.py index f8de2f00df..66747deb9d 100644 --- a/tests/checkers/unittest_stdlib.py +++ b/tests/checkers/unittest_stdlib.py @@ -6,24 +6,26 @@ import contextlib from collections.abc import Callable, Iterator -from typing import Any +from typing import Any, Type import astroid from astroid import nodes +from astroid.context import InferenceContext from astroid.manager import AstroidManager -from astroid.nodes.node_classes import AssignAttr, Name from pylint.checkers import stdlib from pylint.testutils import CheckerTestCase +_NodeNGT = Type[nodes.NodeNG] + @contextlib.contextmanager def _add_transform( manager: AstroidManager, - node: type, - transform: Callable, + node: _NodeNGT, + transform: Callable[[_NodeNGT], _NodeNGT], predicate: Any | None = None, -) -> Iterator: +) -> Iterator[None]: manager.register_transform(node, transform, predicate) try: yield @@ -43,9 +45,10 @@ def test_deprecated_no_qname_on_unexpected_nodes(self) -> None: """ def infer_func( - node: Name, context: Any | None = None # pylint: disable=unused-argument - ) -> Iterator[Iterator | Iterator[AssignAttr]]: - new_node = nodes.AssignAttr(attrname="alpha", parent=node) + inner_node: nodes.Name, + context: InferenceContext | None = None, # pylint: disable=unused-argument + ) -> Iterator[nodes.AssignAttr]: + new_node = nodes.AssignAttr(attrname="alpha", parent=inner_node) yield new_node manager = astroid.MANAGER diff --git a/tests/checkers/unittest_unicode/__init__.py b/tests/checkers/unittest_unicode/__init__.py index df1e5bf3dc..3f516f0323 100644 --- a/tests/checkers/unittest_unicode/__init__.py +++ b/tests/checkers/unittest_unicode/__init__.py @@ -82,5 +82,5 @@ class FakeNode: def __init__(self, content: bytes): self.content = io.BytesIO(content) - def stream(self): + def stream(self) -> io.BytesIO: return self.content diff --git a/tests/checkers/unittest_unicode/unittest_bad_chars.py b/tests/checkers/unittest_unicode/unittest_bad_chars.py index 7933597f7d..7746ce4ae7 100644 --- a/tests/checkers/unittest_unicode/unittest_bad_chars.py +++ b/tests/checkers/unittest_unicode/unittest_bad_chars.py @@ -29,7 +29,7 @@ def bad_char_file_generator(tmp_path: Path) -> Callable[[str, bool, str], Path]: The generator also ensures that file generated is correct """ - def encode_without_bom(string, encoding): + def encode_without_bom(string: str, encoding: str) -> bytes: return pylint.checkers.unicode._encode_without_bom(string, encoding) # All lines contain a not extra checked invalid character @@ -41,7 +41,9 @@ def encode_without_bom(string, encoding): "# Invalid char esc: \x1B", ) - def _bad_char_file_generator(codec: str, add_invalid_bytes: bool, line_ending: str): + def _bad_char_file_generator( + codec: str, add_invalid_bytes: bool, line_ending: str + ) -> Path: byte_suffix = b"" if add_invalid_bytes: if codec == "utf-8": @@ -70,7 +72,7 @@ def _bad_char_file_generator(codec: str, add_invalid_bytes: bool, line_ending: s byte_line.decode(codec, "strict") except UnicodeDecodeError as e: raise ValueError( - f"Line {lineno} did raise unexpected error: {byte_line}\n{e}" + f"Line {lineno} did raise unexpected error: {byte_line!r}\n{e}" ) from e else: try: @@ -81,7 +83,7 @@ def _bad_char_file_generator(codec: str, add_invalid_bytes: bool, line_ending: s ... else: raise ValueError( - f"Line {lineno} did not raise decode error: {byte_line}" + f"Line {lineno} did not raise decode error: {byte_line!r}" ) file = tmp_path / "bad_chars.py" @@ -120,7 +122,7 @@ def test_find_bad_chars( codec_and_msg: tuple[str, tuple[pylint.testutils.MessageTest]], line_ending: str, add_invalid_bytes: bool, - ): + ) -> None: """All combinations of bad characters that are accepted by Python at the moment are tested in all possible combinations of - line ending @@ -215,7 +217,7 @@ def test_bad_chars_that_would_currently_crash_python( char: str, msg_id: str, 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) """ diff --git a/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py b/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py index 68134fcc98..6b11dcfef6 100644 --- a/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py +++ b/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py @@ -25,7 +25,7 @@ class TestBidirectionalUnicodeChecker(pylint.testutils.CheckerTestCase): checker: pylint.checkers.unicode.UnicodeChecker - def test_finds_bidirectional_unicode_that_currently_not_parsed(self): + 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 @@ -78,7 +78,7 @@ def test_finds_bidirectional_unicode_that_currently_not_parsed(self): ) ], ) - def test_find_bidi_string(self, bad_string: str, codec: str): + def test_find_bidi_string(self, bad_string: str, codec: str) -> None: """Ensure that all Bidirectional strings are detected. Tests also UTF-16 and UTF-32. diff --git a/tests/checkers/unittest_unicode/unittest_functions.py b/tests/checkers/unittest_unicode/unittest_functions.py index c2fef93574..0c809ccdcb 100644 --- a/tests/checkers/unittest_unicode/unittest_functions.py +++ b/tests/checkers/unittest_unicode/unittest_functions.py @@ -105,8 +105,10 @@ def test_map_positions_to_result( line: pylint.checkers.unicode._StrLike, expected: dict[int, pylint.checkers.unicode._BadChar], - search_dict, -): + search_dict: dict[ + pylint.checkers.unicode._StrLike, pylint.checkers.unicode._BadChar + ], +) -> None: """Test all possible outcomes for map position function in UTF-8 and ASCII.""" if isinstance(line, bytes): newline = b"\n" @@ -133,7 +135,7 @@ def test_map_positions_to_result( pytest.param(b"12345678\n\r", id="wrong_order_byte"), ], ) -def test_line_length(line: pylint.checkers.unicode._StrLike): +def test_line_length(line: pylint.checkers.unicode._StrLike) -> None: assert pylint.checkers.unicode._line_length(line, "utf-8") == 10 @@ -146,7 +148,7 @@ def test_line_length(line: pylint.checkers.unicode._StrLike): pytest.param("12345678\n\r", id="wrong_order"), ], ) -def test_line_length_utf16(line: str): +def test_line_length_utf16(line: str) -> None: assert pylint.checkers.unicode._line_length(line.encode("utf-16"), "utf-16") == 10 @@ -159,7 +161,7 @@ def test_line_length_utf16(line: str): pytest.param("12345678\n\r", id="wrong_order"), ], ) -def test_line_length_utf32(line: str): +def test_line_length_utf32(line: str) -> None: assert pylint.checkers.unicode._line_length(line.encode("utf-32"), "utf-32") == 10 @@ -186,7 +188,7 @@ def test_line_length_utf32(line: str): ("ASCII", "ascii"), ], ) -def test__normalize_codec_name(codec: str, expected: str): +def test__normalize_codec_name(codec: str, expected: str) -> None: assert pylint.checkers.unicode._normalize_codec_name(codec) == expected @@ -216,7 +218,7 @@ def test__normalize_codec_name(codec: str, expected: str): ) def test___fix_utf16_32_line_stream( tmp_path: Path, codec: str, line_ending: str, final_new_line: bool -): +) -> None: """Content of stream should be the same as should be the length.""" def decode_line(line: bytes, codec: str) -> str: @@ -260,5 +262,5 @@ def decode_line(line: bytes, codec: str) -> str: ("ascii", 1), ], ) -def test__byte_to_str_length(codec: str, expected: int): +def test__byte_to_str_length(codec: str, expected: int) -> None: assert pylint.checkers.unicode._byte_to_str_length(codec) == expected diff --git a/tests/checkers/unittest_unicode/unittest_invalid_encoding.py b/tests/checkers/unittest_unicode/unittest_invalid_encoding.py index ac12af8565..e8695a74f6 100644 --- a/tests/checkers/unittest_unicode/unittest_invalid_encoding.py +++ b/tests/checkers/unittest_unicode/unittest_invalid_encoding.py @@ -52,7 +52,9 @@ class TestInvalidEncoding(pylint.testutils.CheckerTestCase): ("pep_bidirectional_utf_32_bom.txt", 1), ], ) - def test_invalid_unicode_files(self, tmp_path: Path, test_file: str, line_no: int): + def test_invalid_unicode_files( + self, tmp_path: Path, test_file: str, line_no: int + ) -> None: test_file_path = UNICODE_TESTS / test_file target = shutil.copy( test_file_path, tmp_path / test_file.replace(".txt", ".py") @@ -126,11 +128,11 @@ def test_invalid_unicode_files(self, tmp_path: Path, test_file: str, line_no: in ), ], ) - def test__determine_codec(self, content: bytes, codec: str, line: int): + def test__determine_codec(self, content: bytes, codec: str, line: int) -> None: """The codec determined should be exact no matter what we throw at it.""" assert self.checker._determine_codec(io.BytesIO(content)) == (codec, line) - def test__determine_codec_raises_syntax_error_on_invalid_input(self): + def test__determine_codec_raises_syntax_error_on_invalid_input(self) -> None: """Invalid input should lead to a SyntaxError.""" with pytest.raises(SyntaxError): self.checker._determine_codec(io.BytesIO(b"\x80abc")) @@ -139,6 +141,8 @@ def test__determine_codec_raises_syntax_error_on_invalid_input(self): "codec, msg", (pytest.param(codec, msg, id=codec) for codec, msg in CODEC_AND_MSG), ) - def test___check_codec(self, codec: str, msg: tuple[pylint.testutils.MessageTest]): + def test___check_codec( + self, codec: str, msg: tuple[pylint.testutils.MessageTest] + ) -> None: with self.assertAddsMessages(*msg): self.checker._check_codec(codec, 1) diff --git a/tests/checkers/unittest_utils.py b/tests/checkers/unittest_utils.py index 8b91898920..08b9c188df 100644 --- a/tests/checkers/unittest_utils.py +++ b/tests/checkers/unittest_utils.py @@ -25,7 +25,7 @@ ("mybuiltin", False), ], ) -def testIsBuiltin(name, expected): +def testIsBuiltin(name: str, expected: bool) -> None: assert utils.is_builtin(name) == expected @@ -489,3 +489,29 @@ def visit_assname(self, node: nodes.NodeNG) -> None: records[0].message.args[0] == "utils.check_messages will be removed in favour of calling utils.only_required_for_messages in pylint 3.0" ) + + +def test_is_typing_member() -> None: + code = astroid.extract_node( + """ + from typing import Literal as Lit, Set as Literal + import typing as t + + Literal #@ + Lit #@ + t.Literal #@ + """ + ) + + assert not utils.is_typing_member(code[0], ("Literal",)) + assert utils.is_typing_member(code[1], ("Literal",)) + assert utils.is_typing_member(code[2], ("Literal",)) + + code = astroid.extract_node( + """ + Literal #@ + typing.Literal #@ + """ + ) + assert not utils.is_typing_member(code[0], ("Literal",)) + assert not utils.is_typing_member(code[1], ("Literal",)) diff --git a/tests/checkers/unittest_variables.py b/tests/checkers/unittest_variables.py index d810163d2d..e3f0132269 100644 --- a/tests/checkers/unittest_variables.py +++ b/tests/checkers/unittest_variables.py @@ -18,7 +18,6 @@ class TestVariablesChecker(CheckerTestCase): - CHECKER_CLASS = variables.VariablesChecker def test_all_elements_without_parent(self) -> None: @@ -31,7 +30,6 @@ def test_all_elements_without_parent(self) -> None: class TestVariablesCheckerWithTearDown(CheckerTestCase): - CHECKER_CLASS = variables.VariablesChecker def setup_method(self) -> None: @@ -209,7 +207,6 @@ class TestMissingSubmodule(CheckerTestCase): @staticmethod def test_package_all() -> None: - sys.path.insert(0, REGR_DATA_DIR) try: linter.check([os.path.join(REGR_DATA_DIR, "package_all")]) diff --git a/tests/config/pylint_config/test_pylint_config_generate.py b/tests/config/pylint_config/test_pylint_config_generate.py index 4650ab1fbf..adf7129a59 100644 --- a/tests/config/pylint_config/test_pylint_config_generate.py +++ b/tests/config/pylint_config/test_pylint_config_generate.py @@ -22,6 +22,9 @@ def test_generate_interactive_exitcode(monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr( "pylint.config._pylint_config.utils.get_and_validate_format", lambda: "toml" ) + monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_minimal_setting", lambda: False + ) monkeypatch.setattr( "pylint.config._pylint_config.utils.get_and_validate_output_file", lambda: (False, Path()), @@ -41,6 +44,9 @@ def test_format_of_output( ) -> None: """Check that we output the correct format.""" # Monkeypatch everything we don't want to check in this test + monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_minimal_setting", lambda: False + ) monkeypatch.setattr( "pylint.config._pylint_config.utils.get_and_validate_output_file", lambda: (False, Path()), @@ -90,6 +96,9 @@ def test_writing_to_output_file( monkeypatch.setattr( "pylint.config._pylint_config.utils.get_and_validate_format", lambda: "toml" ) + monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_minimal_setting", lambda: False + ) # Set up a temporary file to write to tempfile_name = Path(tempfile.gettempdir()) / "CONFIG" @@ -150,3 +159,42 @@ def test_writing_to_output_file( Run(["generate", "--interactive"], exit=False) captured = capsys.readouterr() assert last_modified != tempfile_name.stat().st_mtime + + +def test_writing_minimal_file( + monkeypatch: MonkeyPatch, capsys: CaptureFixture[str] +) -> None: + """Check that we can write a minimal file.""" + # Monkeypatch everything we don't want to check in this test + monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_and_validate_format", lambda: "toml" + ) + monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_and_validate_output_file", + lambda: (False, Path()), + ) + + # Set the answers needed for the input() calls + answers = iter(["no", "yes"]) + monkeypatch.setattr("builtins.input", lambda x: next(answers)) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="NOTE:.*", category=UserWarning) + # Check not minimal has comments + Run(["generate", "--interactive"], exit=False) + captured = capsys.readouterr() + assert any(line.startswith("#") for line in captured.out.splitlines()) + + # Check minimal doesn't have comments and no default values + Run( + [ + "--load-plugins=pylint.extensions.docparams", + "--accept-no-return-doc=y", + "generate", + "--interactive", + ], + exit=False, + ) + captured = capsys.readouterr() + assert not any(i.startswith("#") for i in captured.out.split("\n")) + assert "accept-no-return-doc" not in captured.out diff --git a/tests/config/pylint_config/test_run_pylint_config.py b/tests/config/pylint_config/test_run_pylint_config.py index 17a9509f1f..9795e21838 100644 --- a/tests/config/pylint_config/test_run_pylint_config.py +++ b/tests/config/pylint_config/test_run_pylint_config.py @@ -18,7 +18,7 @@ def test_invocation_of_pylint_config(capsys: CaptureFixture[str]) -> None: with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="NOTE:.*", category=UserWarning) with pytest.raises(SystemExit) as ex: - _run_pylint_config() + _run_pylint_config([""]) captured = capsys.readouterr() assert captured.err.startswith("usage: pylint-config [options]") assert ex.value.code == 2 diff --git a/tests/config/test_argparse_config.py b/tests/config/test_argparse_config.py index 3bad5e8fa4..a9d7f70c2c 100644 --- a/tests/config/test_argparse_config.py +++ b/tests/config/test_argparse_config.py @@ -43,7 +43,10 @@ def test_logger_commandline() -> None: def test_logger_rcfile() -> None: """Check that we parse the rcfile for the logging checker correctly.""" with pytest.raises(SystemExit) as ex: - Run([LOGGING_TEST, f"--rcfile={LOGGING_TEST.replace('.py', '.rc')}"]) + # replace only the last .py in the string with .rc + # we do so by inverting the string and replace the first occurrence (of the inverted tokens!) + _rcfile = LOGGING_TEST[::-1].replace("yp.", "cr.", 1)[::-1] + Run([LOGGING_TEST, f"--rcfile={_rcfile}"]) assert ex.value.code == 0 diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 47891aee25..be28d324bf 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -6,12 +6,14 @@ import os from pathlib import Path +from tempfile import TemporaryDirectory import pytest from pytest import CaptureFixture from pylint.interfaces import CONFIDENCE_LEVEL_NAMES from pylint.lint import Run as LintRun +from pylint.testutils import create_files from pylint.testutils._run import _Run as Run from pylint.testutils.configuration_test import run_using_a_configuration_file @@ -111,6 +113,36 @@ def test_unknown_py_version(capsys: CaptureFixture) -> None: assert "the-newest has an invalid format, should be a version string." in output.err +def test_regex_error(capsys: CaptureFixture) -> None: + """Check that we correctly error when an an option is passed whose value is an invalid regular expression.""" + with pytest.raises(SystemExit): + Run( + [str(EMPTY_MODULE), r"--function-rgx=[\p{Han}a-z_][\p{Han}a-z0-9_]{2,30}$"], + exit=False, + ) + output = capsys.readouterr() + assert ( + r"Error in provided regular expression: [\p{Han}a-z_][\p{Han}a-z0-9_]{2,30}$ beginning at index 1: bad escape \p" + in output.err + ) + + +def test_csv_regex_error(capsys: CaptureFixture) -> None: + """Check that we correctly error when an option is passed and one + of its comma-separated regular expressions values is an invalid regular expression. + """ + with pytest.raises(SystemExit): + Run( + [str(EMPTY_MODULE), r"--bad-names-rgx=(foo{1,3})"], + exit=False, + ) + output = capsys.readouterr() + assert ( + r"Error in provided regular expression: (foo{1 beginning at index 0: missing ), unterminated subpattern" + in output.err + ) + + def test_short_verbose(capsys: CaptureFixture) -> None: """Check that we correctly handle the -v flag.""" Run([str(EMPTY_MODULE), "-v"], exit=False) @@ -118,11 +150,26 @@ def test_short_verbose(capsys: CaptureFixture) -> None: assert "Using config file" in output.err -def test_argument_separator(capsys: CaptureFixture) -> None: +def test_argument_separator() -> None: """Check that we support using '--' to separate argument types. Reported in https://github.com/PyCQA/pylint/issues/7003. """ - Run(["--", str(EMPTY_MODULE)], exit=False) - output = capsys.readouterr() - assert not output.err + runner = Run(["--", str(EMPTY_MODULE)], exit=False) + assert not runner.linter.stats.by_msg + + +def test_clear_cache_post_run() -> None: + modname = "changing.py" + with TemporaryDirectory() as tmp_dir: + create_files([modname], tmp_dir) + module = tmp_dir + os.sep + modname + # Run class does not produce the wanted failure + # must use LintRun to get pylint.lint.Run + run_before_edit = LintRun([module, "--clear-cache-post-run=y"], exit=False) + with open(module, mode="a", encoding="utf-8") as f: + f.write("undefined\n") + run_after_edit = LintRun([module, "--clear-cache-post-run=y"], exit=False) + + assert not run_before_edit.linter.stats.by_msg + assert run_after_edit.linter.stats.by_msg diff --git a/tests/config/test_find_default_config_files.py b/tests/config/test_find_default_config_files.py index 9addb6db2e..2fd66544d7 100644 --- a/tests/config/test_find_default_config_files.py +++ b/tests/config/test_find_default_config_files.py @@ -233,7 +233,7 @@ def test_toml_has_config(content: str, expected: bool, tmp_path: Path) -> None: ], ], ) -def test_cfg_has_config(content: str, expected: str, tmp_path: Path) -> None: +def test_cfg_has_config(content: str, expected: bool, tmp_path: Path) -> None: """Test that a cfg file has a pylint config.""" fake_cfg = tmp_path / "fake.cfg" with open(fake_cfg, "w", encoding="utf8") as f: @@ -253,3 +253,12 @@ def test_non_existent_home() -> None: assert not list(config.find_default_config_files()) os.chdir(current_dir) + + +def test_permission_error() -> None: + """Test that we handle PermissionError correctly in find_default_config_files. + + Reported in https://github.com/PyCQA/pylint/issues/7169. + """ + with mock.patch("pathlib.Path.is_file", side_effect=PermissionError): + list(config.find_default_config_files()) diff --git a/tests/config/test_functional_config_loading.py b/tests/config/test_functional_config_loading.py index 64227df792..432bdc1a1a 100644 --- a/tests/config/test_functional_config_loading.py +++ b/tests/config/test_functional_config_loading.py @@ -64,9 +64,9 @@ def test_functional_config_loading( configuration_path: str, default_configuration: PylintConfiguration, file_to_lint_path: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], caplog: LogCaptureFixture, -): +) -> None: """Functional tests for configurations.""" # logging is helpful to see what's expected and why. The output of the # program is checked during the test so printing messes with the result. diff --git a/tests/config/test_per_directory_config.py b/tests/config/test_per_directory_config.py new file mode 100644 index 0000000000..85d918a211 --- /dev/null +++ b/tests/config/test_per_directory_config.py @@ -0,0 +1,23 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from pathlib import Path + +from pylint.testutils._run import _Run as Run + + +def test_fall_back_on_base_config(tmp_path: Path) -> None: + """Test that we correctly fall back on the base config.""" + # A file under the current dir should fall back to the highest level + # For pylint this is ./pylintrc + test_file = tmp_path / "test.py" + runner = Run([__name__], exit=False) + assert id(runner.linter.config) == id(runner.linter._base_config) + + # When the file is a directory that does not have any of its parents in + # linter._directory_namespaces it should default to the base config + with open(test_file, "w", encoding="utf-8") as f: + f.write("1") + Run([str(test_file)], exit=False) + assert id(runner.linter.config) == id(runner.linter._base_config) diff --git a/tests/config/unittest_config.py b/tests/config/unittest_config.py index 8c668aa529..3436636022 100644 --- a/tests/config/unittest_config.py +++ b/tests/config/unittest_config.py @@ -7,7 +7,6 @@ from __future__ import annotations import re -import sre_constants import pytest @@ -18,19 +17,19 @@ def test__regexp_validator_valid() -> None: - result = config.option._regexp_validator(None, None, "test_.*") + result = config.option._regexp_validator(None, "", "test_.*") assert isinstance(result, re.Pattern) assert result.pattern == "test_.*" def test__regexp_validator_invalid() -> None: - with pytest.raises(sre_constants.error): - config.option._regexp_validator(None, None, "test_)") + with pytest.raises(re.error): + config.option._regexp_validator(None, "", "test_)") def test__csv_validator_no_spaces() -> None: values = ["One", "Two", "Three"] - result = config.option._csv_validator(None, None, ",".join(values)) + result = config.option._csv_validator(None, "", ",".join(values)) assert isinstance(result, list) assert len(result) == 3 for i, value in enumerate(values): @@ -39,7 +38,7 @@ def test__csv_validator_no_spaces() -> None: def test__csv_validator_spaces() -> None: values = ["One", "Two", "Three"] - result = config.option._csv_validator(None, None, ", ".join(values)) + result = config.option._csv_validator(None, "", ", ".join(values)) assert isinstance(result, list) assert len(result) == 3 for i, value in enumerate(values): @@ -48,7 +47,7 @@ def test__csv_validator_spaces() -> None: def test__regexp_csv_validator_valid() -> None: pattern_strings = ["test_.*", "foo\\.bar", "^baz$"] - result = config.option._regexp_csv_validator(None, None, ",".join(pattern_strings)) + result = config.option._regexp_csv_validator(None, "", ",".join(pattern_strings)) for i, regex in enumerate(result): assert isinstance(regex, re.Pattern) assert regex.pattern == pattern_strings[i] @@ -56,8 +55,8 @@ def test__regexp_csv_validator_valid() -> None: def test__regexp_csv_validator_invalid() -> None: pattern_strings = ["test_.*", "foo\\.bar", "^baz)$"] - with pytest.raises(sre_constants.error): - config.option._regexp_csv_validator(None, None, ",".join(pattern_strings)) + with pytest.raises(re.error): + config.option._regexp_csv_validator(None, "", ",".join(pattern_strings)) class TestPyLinterOptionSetters(CheckerTestCase): diff --git a/tests/conftest.py b/tests/conftest.py index 1ebe62e976..a35e5cc147 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,13 +7,16 @@ from __future__ import annotations import os +from collections.abc import Callable from pathlib import Path import pytest from pylint import checkers +from pylint.checkers import BaseChecker from pylint.lint import PyLinter from pylint.lint.run import _cpu_count +from pylint.reporters import BaseReporter from pylint.testutils import MinimalTestReporter HERE = Path(__file__).parent @@ -25,7 +28,13 @@ def tests_directory() -> Path: @pytest.fixture -def linter(checker, register, enable, disable, reporter): +def linter( + checker: type[BaseChecker] | None, + register: Callable[[PyLinter], None] | None, + enable: str | None, + disable: str | None, + reporter: type[BaseReporter], +) -> PyLinter: _linter = PyLinter() _linter.set_reporter(reporter()) checkers.initialize(_linter) @@ -44,31 +53,31 @@ def linter(checker, register, enable, disable, reporter): @pytest.fixture(scope="module") -def checker(): +def checker() -> None: return None @pytest.fixture(scope="module") -def register(): +def register() -> None: return None @pytest.fixture(scope="module") -def enable(): +def enable() -> None: return None @pytest.fixture(scope="module") -def disable(): +def disable() -> None: return None @pytest.fixture(scope="module") -def reporter(): +def reporter() -> type[MinimalTestReporter]: return MinimalTestReporter -def pytest_addoption(parser) -> None: +def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--primer-stdlib", action="store_true", diff --git a/tests/extensions/test_check_docs_utils.py b/tests/extensions/test_check_docs_utils.py index 3e70ffbfde..692c148595 100644 --- a/tests/extensions/test_check_docs_utils.py +++ b/tests/extensions/test_check_docs_utils.py @@ -3,8 +3,12 @@ # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt """Unit tests for utils functions in :mod:`pylint.extensions._check_docs_utils`.""" + +from __future__ import annotations + import astroid import pytest +from astroid import nodes from pylint.extensions import _check_docs_utils as utils @@ -134,7 +138,7 @@ def my_func(): ), ], ) -def test_exception(raise_node, expected): +def test_exception(raise_node: nodes.NodeNG, expected: set[str]) -> None: found_nodes = utils.possible_exc_types(raise_node) for node in found_nodes: assert isinstance(node, astroid.nodes.ClassDef) diff --git a/tests/extensions/test_private_import.py b/tests/extensions/test_private_import.py index d2d79947fb..c82f51a429 100644 --- a/tests/extensions/test_private_import.py +++ b/tests/extensions/test_private_import.py @@ -4,7 +4,7 @@ """Tests the local module directory comparison logic which requires mocking file directories""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import astroid @@ -19,7 +19,7 @@ class TestPrivateImport(CheckerTestCase): CHECKER_CLASS = private_import.PrivateImportChecker @patch("pathlib.Path.parent") - def test_internal_module(self, parent) -> None: + def test_internal_module(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "module") import_from = astroid.extract_node("""from module import _file""") @@ -27,7 +27,7 @@ def test_internal_module(self, parent) -> None: self.checker.visit_importfrom(import_from) @patch("pathlib.Path.parent") - def test_external_module_nested(self, parent) -> None: + def test_external_module_nested(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "module", "module_files", "util") import_from = astroid.extract_node("""from module import _file""") @@ -36,7 +36,7 @@ def test_external_module_nested(self, parent) -> None: self.checker.visit_importfrom(import_from) @patch("pathlib.Path.parent") - def test_external_module_dot_import(self, parent) -> None: + def test_external_module_dot_import(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "outer", "inner", "module_files", "util") import_from = astroid.extract_node("""from outer.inner import _file""") @@ -45,7 +45,7 @@ def test_external_module_dot_import(self, parent) -> None: self.checker.visit_importfrom(import_from) @patch("pathlib.Path.parent") - def test_external_module_dot_import_outer_only(self, parent) -> None: + def test_external_module_dot_import_outer_only(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "outer", "extensions") import_from = astroid.extract_node("""from outer.inner import _file""") @@ -54,7 +54,7 @@ def test_external_module_dot_import_outer_only(self, parent) -> None: self.checker.visit_importfrom(import_from) @patch("pathlib.Path.parent") - def test_external_module(self, parent) -> None: + def test_external_module(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "other") import_from = astroid.extract_node("""from module import _file""") diff --git a/tests/functional/a/abstract/abstract_abc_methods.py b/tests/functional/a/abstract/abstract_abc_methods.py index d63389c505..4d4af2cdbe 100644 --- a/tests/functional/a/abstract/abstract_abc_methods.py +++ b/tests/functional/a/abstract/abstract_abc_methods.py @@ -1,9 +1,9 @@ """ This should not warn about `prop` being abstract in Child """ -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods import abc -class Parent(object): +class Parent: """Abstract Base Class """ __metaclass__ = abc.ABCMeta diff --git a/tests/functional/a/abstract/abstract_class_instantiated.py b/tests/functional/a/abstract/abstract_class_instantiated.py index bf1666a854..289870c9d4 100644 --- a/tests/functional/a/abstract/abstract_class_instantiated.py +++ b/tests/functional/a/abstract/abstract_class_instantiated.py @@ -4,31 +4,31 @@ """ # pylint: disable=too-few-public-methods, missing-docstring -# pylint: disable=abstract-method, import-error, useless-object-inheritance +# pylint: disable=abstract-method, import-error import abc import weakref from lala import Bala -class GoodClass(object, metaclass=abc.ABCMeta): +class GoodClass(metaclass=abc.ABCMeta): pass -class SecondGoodClass(object, metaclass=abc.ABCMeta): +class SecondGoodClass(metaclass=abc.ABCMeta): def test(self): """ do nothing. """ -class ThirdGoodClass(object, metaclass=abc.ABCMeta): +class ThirdGoodClass(metaclass=abc.ABCMeta): """ This should not raise the warning. """ def test(self): raise NotImplementedError() -class BadClass(object, metaclass=abc.ABCMeta): +class BadClass(metaclass=abc.ABCMeta): @abc.abstractmethod def test(self): """ do nothing. """ -class SecondBadClass(object, metaclass=abc.ABCMeta): +class SecondBadClass(metaclass=abc.ABCMeta): @property @abc.abstractmethod def test(self): @@ -38,7 +38,7 @@ class ThirdBadClass(SecondBadClass): pass -class Structure(object, metaclass=abc.ABCMeta): +class Structure(metaclass=abc.ABCMeta): @abc.abstractmethod def __iter__(self): pass @@ -112,12 +112,12 @@ def main(): if 1: # pylint: disable=using-constant-test - class FourthBadClass(object, metaclass=abc.ABCMeta): + class FourthBadClass(metaclass=abc.ABCMeta): def test(self): pass else: - class FourthBadClass(object, metaclass=abc.ABCMeta): + class FourthBadClass(metaclass=abc.ABCMeta): @abc.abstractmethod def test(self): diff --git a/tests/functional/a/abstract/abstract_class_instantiated_in_class.py b/tests/functional/a/abstract/abstract_class_instantiated_in_class.py index daaa3b689c..e31f9470dd 100644 --- a/tests/functional/a/abstract/abstract_class_instantiated_in_class.py +++ b/tests/functional/a/abstract/abstract_class_instantiated_in_class.py @@ -1,11 +1,11 @@ """Don't warn if the class is instantiated in its own body.""" -# pylint: disable=missing-docstring, useless-object-inheritance +# pylint: disable=missing-docstring import abc -class Ala(object, metaclass=abc.ABCMeta): +class Ala(metaclass=abc.ABCMeta): @abc.abstractmethod def bala(self): diff --git a/tests/functional/a/abstract/abstract_method.py b/tests/functional/a/abstract/abstract_method.py index 2ea7511411..75ffda2209 100644 --- a/tests/functional/a/abstract/abstract_method.py +++ b/tests/functional/a/abstract/abstract_method.py @@ -1,12 +1,12 @@ """Test abstract-method warning.""" -from __future__ import print_function + # pylint: disable=missing-docstring -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods import abc -class Abstract(object): +class Abstract: def aaaa(self): """should be overridden in concrete class""" raise NotImplementedError() @@ -51,7 +51,7 @@ def aaaa(self): """overridden form Abstract""" -class Structure(object, metaclass=abc.ABCMeta): +class Structure(metaclass=abc.ABCMeta): @abc.abstractmethod def __iter__(self): pass diff --git a/tests/functional/a/abstract/abstract_method.txt b/tests/functional/a/abstract/abstract_method.txt index 2b4ea9a2eb..f2b2b6c74f 100644 --- a/tests/functional/a/abstract/abstract_method.txt +++ b/tests/functional/a/abstract/abstract_method.txt @@ -1,16 +1,16 @@ -abstract-method:47:0:47:14:Concrete:Method 'bbbb' is abstract in class 'Abstract' but is not overridden:UNDEFINED -abstract-method:70:0:70:15:Container:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:70:0:70:15:Container:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:70:0:70:15:Container:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:76:0:76:13:Sizable:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:76:0:76:13:Sizable:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:76:0:76:13:Sizable:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:82:0:82:14:Hashable:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:82:0:82:14:Hashable:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:82:0:82:14:Hashable:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:87:0:87:14:Iterator:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:87:0:87:14:Iterator:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:87:0:87:14:Iterator:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:106:0:106:19:BadComplexMro:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:106:0:106:19:BadComplexMro:Method '__len__' is abstract in class 'AbstractSizable' but is not overridden:UNDEFINED -abstract-method:106:0:106:19:BadComplexMro:Method 'length' is abstract in class 'AbstractSizable' but is not overridden:UNDEFINED +abstract-method:47:0:47:14:Concrete:Method 'bbbb' is abstract in class 'Abstract' but is not overridden in child class 'Concrete':INFERENCE +abstract-method:70:0:70:15:Container:Method '__hash__' is abstract in class 'Structure' but is not overridden in child class 'Container':INFERENCE +abstract-method:70:0:70:15:Container:Method '__iter__' is abstract in class 'Structure' but is not overridden in child class 'Container':INFERENCE +abstract-method:70:0:70:15:Container:Method '__len__' is abstract in class 'Structure' but is not overridden in child class 'Container':INFERENCE +abstract-method:76:0:76:13:Sizable:Method '__contains__' is abstract in class 'Structure' but is not overridden in child class 'Sizable':INFERENCE +abstract-method:76:0:76:13:Sizable:Method '__hash__' is abstract in class 'Structure' but is not overridden in child class 'Sizable':INFERENCE +abstract-method:76:0:76:13:Sizable:Method '__iter__' is abstract in class 'Structure' but is not overridden in child class 'Sizable':INFERENCE +abstract-method:82:0:82:14:Hashable:Method '__contains__' is abstract in class 'Structure' but is not overridden in child class 'Hashable':INFERENCE +abstract-method:82:0:82:14:Hashable:Method '__iter__' is abstract in class 'Structure' but is not overridden in child class 'Hashable':INFERENCE +abstract-method:82:0:82:14:Hashable:Method '__len__' is abstract in class 'Structure' but is not overridden in child class 'Hashable':INFERENCE +abstract-method:87:0:87:14:Iterator:Method '__contains__' is abstract in class 'Structure' but is not overridden in child class 'Iterator':INFERENCE +abstract-method:87:0:87:14:Iterator:Method '__hash__' is abstract in class 'Structure' but is not overridden in child class 'Iterator':INFERENCE +abstract-method:87:0:87:14:Iterator:Method '__len__' is abstract in class 'Structure' but is not overridden in child class 'Iterator':INFERENCE +abstract-method:106:0:106:19:BadComplexMro:Method '__hash__' is abstract in class 'Structure' but is not overridden in child class 'BadComplexMro':INFERENCE +abstract-method:106:0:106:19:BadComplexMro:Method '__len__' is abstract in class 'AbstractSizable' but is not overridden in child class 'BadComplexMro':INFERENCE +abstract-method:106:0:106:19:BadComplexMro:Method 'length' is abstract in class 'AbstractSizable' but is not overridden in child class 'BadComplexMro':INFERENCE 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 cb978c84c2..ebdb76c6af 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 @@ -1,11 +1,10 @@ # pylint: disable=invalid-name,too-many-public-methods,attribute-defined-outside-init -# pylint: disable=useless-object-inheritance,too-few-public-methods +# pylint: disable=too-few-public-methods,deprecated-module """This module demonstrates a possible problem of pyLint with calling __init__ s from inherited classes. Initializations done there are not considered, which results in Error E0203 for self.cookedq.""" -from __future__ import print_function import telnetlib @@ -37,7 +36,7 @@ def readUntilArray(self, matches, _=None): if len(match) > maxLength: maxLength = len(match) -class Base(object): +class Base: """bla bla""" dougloup_papa = None @@ -66,7 +65,7 @@ def Work(self): self.dougloup_moi = True -class QoSALConnection(object): +class QoSALConnection: """blabla""" _the_instance = None @@ -79,7 +78,7 @@ def __new__(cls): def __init__(self): pass -class DefinedOutsideInit(object): +class DefinedOutsideInit: """use_attr is seen as the method defining attr because its in first position """ diff --git a/tests/functional/a/access/access_member_before_definition.py b/tests/functional/a/access/access_member_before_definition.py index 224e6e43ea..6ffebdd03f 100644 --- a/tests/functional/a/access/access_member_before_definition.py +++ b/tests/functional/a/access/access_member_before_definition.py @@ -1,8 +1,8 @@ # pylint: disable=missing-docstring,too-few-public-methods,invalid-name -# pylint: disable=attribute-defined-outside-init, useless-object-inheritance +# pylint: disable=attribute-defined-outside-init -class Aaaa(object): +class Aaaa: """class with attributes defined in wrong order""" def __init__(self): @@ -11,7 +11,7 @@ def __init__(self): self._var3 = var1 -class Bbbb(object): +class Bbbb: A = 23 B = A @@ -31,7 +31,7 @@ def catchme(self, attr): return attr -class Mixin(object): +class Mixin: def test_mixin(self): """Don't emit access-member-before-definition for mixin classes.""" if self.already_defined: diff --git a/tests/functional/a/access/access_to__name__.py b/tests/functional/a/access/access_to__name__.py index 9f16d3a7a2..18445f7b45 100644 --- a/tests/functional/a/access/access_to__name__.py +++ b/tests/functional/a/access/access_to__name__.py @@ -1,8 +1,8 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods """test access to __name__ gives undefined member on new/old class instances but not on new/old class object """ -from __future__ import print_function + class Aaaa: """old class""" @@ -10,7 +10,7 @@ def __init__(self): print(self.__name__) # [no-member] print(self.__class__.__name__) -class NewClass(object): +class NewClass: """new class""" def __new__(cls, *args, **kwargs): diff --git a/tests/functional/a/access/access_to_protected_members.py b/tests/functional/a/access/access_to_protected_members.py index a521a6467a..9ca221628e 100644 --- a/tests/functional/a/access/access_to_protected_members.py +++ b/tests/functional/a/access/access_to_protected_members.py @@ -1,9 +1,8 @@ # pylint: disable=too-few-public-methods, super-init-not-called -# pylint: disable=no-classmethod-decorator,useless-object-inheritance +# pylint: disable=no-classmethod-decorator """Test external access to protected class members.""" -from __future__ import print_function -class MyClass(object): +class MyClass: """Class with protected members.""" _cls_protected = 5 @@ -44,7 +43,7 @@ def __init__(self): print(INST._cls_protected) # [protected-access] -class Issue1031(object): +class Issue1031: """Test for GitHub issue 1031""" _attr = 1 @@ -59,7 +58,7 @@ def incorrect_access(self): return None -class Issue1802(object): +class Issue1802: """Test for GitHub issue 1802""" def __init__(self, value): self._foo = value @@ -101,7 +100,7 @@ def __fake_special__(self, other): return False -class Issue1159OtherClass(object): +class Issue1159OtherClass: """Test for GitHub issue 1159""" _foo = 0 @@ -110,7 +109,7 @@ def __init__(self): self._bar = 0 -class Issue1159(object): +class Issue1159: """Test for GitHub issue 1159""" _foo = 0 diff --git a/tests/functional/a/access/access_to_protected_members.txt b/tests/functional/a/access/access_to_protected_members.txt index d9dfd96ce1..b49a915a4a 100644 --- a/tests/functional/a/access/access_to_protected_members.txt +++ b/tests/functional/a/access/access_to_protected_members.txt @@ -1,28 +1,28 @@ -protected-access:19:14:19:31:MyClass.test:Access to a protected member _haha of a client class:UNDEFINED -protected-access:41:0:41:15::Access to a protected member _protected of a client class:UNDEFINED -protected-access:42:6:42:21::Access to a protected member _protected of a client class:UNDEFINED -protected-access:43:0:43:19::Access to a protected member _cls_protected of a client class:UNDEFINED -protected-access:44:6:44:25::Access to a protected member _cls_protected of a client class:UNDEFINED -protected-access:58:19:58:40:Issue1031.incorrect_access:Access to a protected member _protected of a client class:UNDEFINED -protected-access:72:48:72:63:Issue1802.__eq__:Access to a protected member __private of a client class:UNDEFINED -protected-access:80:32:80:42:Issue1802.not_in_special:Access to a protected member _foo of a client class:UNDEFINED -protected-access:100:32:100:42:Issue1802.__fake_special__:Access to a protected member _foo of a client class:UNDEFINED -protected-access:162:8:162:21:Issue1159.access_other_attr:Access to a protected member _bar of a client class:UNDEFINED -protected-access:163:12:163:25:Issue1159.access_other_attr:Access to a protected member _foo of a client class:UNDEFINED -no-member:194:12:194:25:Issue1159Subclass.access_missing_member:Instance of 'Issue1159Subclass' has no '_baz' member; maybe '_bar'?:INFERENCE -protected-access:194:12:194:25:Issue1159Subclass.access_missing_member:Access to a protected member _baz of a client class:UNDEFINED -attribute-defined-outside-init:203:8:203:21:Issue1159Subclass.assign_missing_member:Attribute '_qux' defined outside __init__:UNDEFINED -protected-access:212:8:212:21:Issue1159Subclass.access_other_attr:Access to a protected member _bar of a client class:UNDEFINED -protected-access:213:12:213:25:Issue1159Subclass.access_other_attr:Access to a protected member _foo of a client class:UNDEFINED -protected-access:232:8:232:30:Issue3066.foobar:Access to a protected member _attr of a client class:UNDEFINED -protected-access:233:8:233:37:Issue3066.foobar:Access to a protected member _attr of a client class:UNDEFINED -protected-access:236:8:236:29:Issue3066.foobar:Access to a protected member _bar of a client class:UNDEFINED -protected-access:237:8:237:36:Issue3066.foobar:Access to a protected member _bar of a client class:UNDEFINED -protected-access:247:12:247:27:Issue3066.Aclass.foobar:Access to a protected member _attr of a client class:UNDEFINED -protected-access:249:12:249:41:Issue3066.Aclass.foobar:Access to a protected member _attr of a client class:UNDEFINED -protected-access:251:12:251:26:Issue3066.Aclass.foobar:Access to a protected member _bar of a client class:UNDEFINED -protected-access:253:12:253:40:Issue3066.Aclass.foobar:Access to a protected member _bar of a client class:UNDEFINED -protected-access:267:16:267:31:Issue3066.Aclass.Bclass.foobar:Access to a protected member _attr of a client class:UNDEFINED -protected-access:268:16:268:38:Issue3066.Aclass.Bclass.foobar:Access to a protected member _attr of a client class:UNDEFINED -protected-access:271:16:271:30:Issue3066.Aclass.Bclass.foobar:Access to a protected member _bar of a client class:UNDEFINED -protected-access:272:16:272:37:Issue3066.Aclass.Bclass.foobar:Access to a protected member _bar of a client class:UNDEFINED +protected-access:18:14:18:31:MyClass.test:Access to a protected member _haha of a client class:UNDEFINED +protected-access:40:0:40:15::Access to a protected member _protected of a client class:UNDEFINED +protected-access:41:6:41:21::Access to a protected member _protected of a client class:UNDEFINED +protected-access:42:0:42:19::Access to a protected member _cls_protected of a client class:UNDEFINED +protected-access:43:6:43:25::Access to a protected member _cls_protected of a client class:UNDEFINED +protected-access:57:19:57:40:Issue1031.incorrect_access:Access to a protected member _protected of a client class:UNDEFINED +protected-access:71:48:71:63:Issue1802.__eq__:Access to a protected member __private of a client class:UNDEFINED +protected-access:79:32:79:42:Issue1802.not_in_special:Access to a protected member _foo of a client class:UNDEFINED +protected-access:99:32:99:42:Issue1802.__fake_special__:Access to a protected member _foo of a client class:UNDEFINED +protected-access:161:8:161:21:Issue1159.access_other_attr:Access to a protected member _bar of a client class:UNDEFINED +protected-access:162:12:162:25:Issue1159.access_other_attr:Access to a protected member _foo of a client class:UNDEFINED +no-member:193:12:193:25:Issue1159Subclass.access_missing_member:Instance of 'Issue1159Subclass' has no '_baz' member; maybe '_bar'?:INFERENCE +protected-access:193:12:193:25:Issue1159Subclass.access_missing_member:Access to a protected member _baz of a client class:UNDEFINED +attribute-defined-outside-init:202:8:202:21:Issue1159Subclass.assign_missing_member:Attribute '_qux' defined outside __init__:UNDEFINED +protected-access:211:8:211:21:Issue1159Subclass.access_other_attr:Access to a protected member _bar of a client class:UNDEFINED +protected-access:212:12:212:25:Issue1159Subclass.access_other_attr:Access to a protected member _foo of a client class:UNDEFINED +protected-access:231:8:231:30:Issue3066.foobar:Access to a protected member _attr of a client class:UNDEFINED +protected-access:232:8:232:37:Issue3066.foobar:Access to a protected member _attr of a client class:UNDEFINED +protected-access:235:8:235:29:Issue3066.foobar:Access to a protected member _bar of a client class:UNDEFINED +protected-access:236:8:236:36:Issue3066.foobar:Access to a protected member _bar of a client class:UNDEFINED +protected-access:246:12:246:27:Issue3066.Aclass.foobar:Access to a protected member _attr of a client class:UNDEFINED +protected-access:248:12:248:41:Issue3066.Aclass.foobar:Access to a protected member _attr of a client class:UNDEFINED +protected-access:250:12:250:26:Issue3066.Aclass.foobar:Access to a protected member _bar of a client class:UNDEFINED +protected-access:252:12:252:40:Issue3066.Aclass.foobar:Access to a protected member _bar of a client class:UNDEFINED +protected-access:266:16:266:31:Issue3066.Aclass.Bclass.foobar:Access to a protected member _attr of a client class:UNDEFINED +protected-access:267:16:267:38:Issue3066.Aclass.Bclass.foobar:Access to a protected member _attr of a client class:UNDEFINED +protected-access:270:16:270:30:Issue3066.Aclass.Bclass.foobar:Access to a protected member _bar of a client class:UNDEFINED +protected-access:271:16:271:37:Issue3066.Aclass.Bclass.foobar:Access to a protected member _bar of a client class:UNDEFINED diff --git a/tests/functional/a/alternative/alternative_union_syntax.py b/tests/functional/a/alternative/alternative_union_syntax.py index 05753266da..33565223aa 100644 --- a/tests/functional/a/alternative/alternative_union_syntax.py +++ b/tests/functional/a/alternative/alternative_union_syntax.py @@ -1,6 +1,8 @@ """Test PEP 604 - Alternative Union syntax""" + # pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring -# pylint: disable=inherit-non-class,too-few-public-methods,unnecessary-direct-lambda-call +# pylint: disable=inherit-non-class,too-few-public-methods,unnecessary-direct-lambda-call,superfluous-parens + import dataclasses import typing from dataclasses import dataclass @@ -82,3 +84,23 @@ class CustomDataClass3: @dataclasses.dataclass class CustomDataClass4: my_var: int | str + +class ForwardMetaclass(type): + def __or__(cls, other): + return True + +class ReverseMetaclass(type): + def __ror__(cls, other): + return True + +class WithForward(metaclass=ForwardMetaclass): + pass + +class WithReverse(metaclass=ReverseMetaclass): + pass + +class DefaultMetaclass: + pass + +class_list = [WithForward | DefaultMetaclass] +class_list_reversed = [WithReverse | DefaultMetaclass] diff --git a/tests/functional/a/alternative/alternative_union_syntax_error.py b/tests/functional/a/alternative/alternative_union_syntax_error.py index 6f74d675a9..d998b7f004 100644 --- a/tests/functional/a/alternative/alternative_union_syntax_error.py +++ b/tests/functional/a/alternative/alternative_union_syntax_error.py @@ -1,10 +1,18 @@ -"""Test PEP 604 - Alternative Union syntax -without postponed evaluation of annotations. +"""Test PEP 604 - Alternative Union syntax with postponed evaluation of +annotations enabled. For Python 3.7 - 3.9: Everything should fail. Testing only 3.8/3.9 to support TypedDict. """ -# pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring,inherit-non-class,too-few-public-methods,line-too-long,unnecessary-direct-lambda-call + +# pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring +# pylint: disable=inherit-non-class,too-few-public-methods,line-too-long,unnecessary-direct-lambda-call +# pylint: disable=unnecessary-lambda-assignment + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + import dataclasses import typing from dataclasses import dataclass @@ -15,7 +23,7 @@ lst = [typing.Dict[str, int] | None,] # [unsupported-binary-operation] var1: typing.Dict[str, int | None] # [unsupported-binary-operation] -var2: int | str | None # [unsupported-binary-operation] +var2: int | str | None # [unsupported-binary-operation, unsupported-binary-operation] var3: int | typing.List[str | int] # [unsupported-binary-operation] var4: typing.Dict[typing.Tuple[int, int] | int, None] # [unsupported-binary-operation] @@ -87,3 +95,44 @@ class CustomDataClass3: @dataclasses.dataclass class CustomDataClass4: my_var: int | str # [unsupported-binary-operation] + +# Not an error if the metaclass implements __or__ + +class ForwardMetaclass(type): + def __or__(cls, other): + return True + +class ReverseMetaclass(type): + def __ror__(cls, other): + return True + +class WithForward(metaclass=ForwardMetaclass): + pass + +class WithReverse(metaclass=ReverseMetaclass): + pass + +class DefaultMetaclass: + pass + +class_list = [WithForward | DefaultMetaclass] +class_list_reversed_invalid = [WithReverse | DefaultMetaclass] # [unsupported-binary-operation] +class_list_reversed_valid = [DefaultMetaclass | WithReverse] + + +# Pathological cases +class HorribleMetaclass(type): + __or__ = lambda x: x + +class WithHorrible(metaclass=HorribleMetaclass): + pass + +class_list = [WithHorrible | DefaultMetaclass] + +class SecondLevelMetaclass(ForwardMetaclass): + pass + +class WithSecondLevel(metaclass=SecondLevelMetaclass): + pass + +class_list_with_second_level = [WithSecondLevel | DefaultMetaclass] diff --git a/tests/functional/a/alternative/alternative_union_syntax_error.rc b/tests/functional/a/alternative/alternative_union_syntax_error.rc index c4d51566df..44063ad8c7 100644 --- a/tests/functional/a/alternative/alternative_union_syntax_error.rc +++ b/tests/functional/a/alternative/alternative_union_syntax_error.rc @@ -3,3 +3,4 @@ py-version=3.8 [testoptions] min_pyver=3.8 +max_pyver=3.10 diff --git a/tests/functional/a/alternative/alternative_union_syntax_error.txt b/tests/functional/a/alternative/alternative_union_syntax_error.txt index 5be5653f3b..6f4c19a4e4 100644 --- a/tests/functional/a/alternative/alternative_union_syntax_error.txt +++ b/tests/functional/a/alternative/alternative_union_syntax_error.txt @@ -1,24 +1,26 @@ -unsupported-binary-operation:14:8:14:30::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:15:7:15:35::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:17:23:17:33::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:18:6:18:15::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:19:6:19:34::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:20:18:20:46::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:23:23:23:32::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:25:24:25:33::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:27:14:27:23::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:29:5:29:14::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:31:14:31:23:func:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:34:15:34:24:func2:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:40:9:40:25::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:47:36:47:45::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:50:12:50:21:CustomNamedTuple2:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:53:12:53:21:CustomNamedTuple3:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:57:54:57:63::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:59:60:59:69::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:62:12:62:21:CustomTypedDict3:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:65:12:65:21:CustomTypedDict4:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:76:12:76:21:CustomDataClass:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:80:12:80:21:CustomDataClass2:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:84:12:84:21:CustomDataClass3:unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:89:12:89:21:CustomDataClass4:unsupported operand type(s) for |:UNDEFINED +unsupported-binary-operation:22:8:22:30::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:23:7:23:35::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:25:23:25:33::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:26:6:26:22::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:26:6:26:15::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:27:6:27:34::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:28:18:28:46::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:31:23:31:32::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:33:24:33:33::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:35:14:35:23::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:37:5:37:14::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:39:14:39:23:func:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:42:15:42:24:func2:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:48:9:48:25::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:55:36:55:45::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:58:12:58:21:CustomNamedTuple2:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:61:12:61:21:CustomNamedTuple3:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:65:54:65:63::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:67:60:67:69::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:70:12:70:21:CustomTypedDict3:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:73:12:73:21:CustomTypedDict4:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:84:12:84:21:CustomDataClass:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:88:12:88:21:CustomDataClass2:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:92:12:92:21:CustomDataClass3:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:97:12:97:21:CustomDataClass4:unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:119:31:119:61::unsupported operand type(s) for |:INFERENCE diff --git a/tests/functional/a/alternative/alternative_union_syntax_py37.py b/tests/functional/a/alternative/alternative_union_syntax_py37.py index 3d9e577f14..41eb75ec6f 100644 --- a/tests/functional/a/alternative/alternative_union_syntax_py37.py +++ b/tests/functional/a/alternative/alternative_union_syntax_py37.py @@ -1,10 +1,17 @@ -"""Test PEP 604 - Alternative Union syntax -with postponed evaluation of annotations enabled. +"""Test PEP 604 - Alternative Union syntax with postponed evaluation of +annotations enabled. For Python 3.7 - 3.9: Most things should work. Testing only 3.8/3.9 to support TypedDict. """ -# pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring,inherit-non-class,too-few-public-methods,line-too-long,unnecessary-direct-lambda-call + +# pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring +# pylint: disable=inherit-non-class,too-few-public-methods,line-too-long,unnecessary-direct-lambda-call + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from __future__ import annotations import dataclasses import typing diff --git a/tests/functional/a/alternative/alternative_union_syntax_py37.txt b/tests/functional/a/alternative/alternative_union_syntax_py37.txt index e92fd1d7c4..37901192c1 100644 --- a/tests/functional/a/alternative/alternative_union_syntax_py37.txt +++ b/tests/functional/a/alternative/alternative_union_syntax_py37.txt @@ -1,9 +1,9 @@ -unsupported-binary-operation:15:8:15:30::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:16:7:16:35::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:24:23:24:32::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:26:24:26:33::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:28:14:28:23::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:41:9:41:25::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:48:36:48:45::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:58:54:58:63::unsupported operand type(s) for |:UNDEFINED -unsupported-binary-operation:60:60:60:69::unsupported operand type(s) for |:UNDEFINED +unsupported-binary-operation:22:8:22:30::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:23:7:23:35::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:31:23:31:32::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:33:24:33:33::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:35:14:35:23::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:48:9:48:25::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:55:36:55:45::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:65:54:65:63::unsupported operand type(s) for |:INFERENCE +unsupported-binary-operation:67:60:67:69::unsupported operand type(s) for |:INFERENCE diff --git a/tests/functional/a/alternative/alternative_union_syntax_regession_8119.py b/tests/functional/a/alternative/alternative_union_syntax_regession_8119.py new file mode 100644 index 0000000000..8ec199958b --- /dev/null +++ b/tests/functional/a/alternative/alternative_union_syntax_regession_8119.py @@ -0,0 +1,24 @@ +"""Regression test for alternative Union syntax in runtime contexts. +Syntax support was added in Python 3.10. + +The code snipped should not raise any errors. +https://github.com/PyCQA/pylint/issues/8119 +""" +# pylint: disable=missing-docstring,too-few-public-methods +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class Coordinator(Generic[T]): + def __init__(self, update_interval=None) -> None: + self.update_interval = update_interval + + +class Child(Coordinator[int | str]): + def __init__(self) -> None: + Coordinator.__init__(self, update_interval=2) + + def _async_update_data(self): + assert self.update_interval + self.update_interval = 1 diff --git a/tests/functional/a/alternative/alternative_union_syntax_regession_8119.rc b/tests/functional/a/alternative/alternative_union_syntax_regession_8119.rc new file mode 100644 index 0000000000..68a8c8ef15 --- /dev/null +++ b/tests/functional/a/alternative/alternative_union_syntax_regession_8119.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/a/arguments.py b/tests/functional/a/arguments.py index 9e533161e3..6929b98500 100644 --- a/tests/functional/a/arguments.py +++ b/tests/functional/a/arguments.py @@ -1,13 +1,13 @@ # pylint: disable=too-few-public-methods, missing-docstring,import-error,wrong-import-position -# pylint: disable=wrong-import-order, useless-object-inheritance,unnecessary-lambda, consider-using-f-string -# pylint: disable=unnecessary-lambda-assignment +# pylint: disable=wrong-import-order, unnecessary-lambda, consider-using-f-string +# pylint: disable=unnecessary-lambda-assignment, no-self-argument, unused-argument def decorator(fun): """Decorator""" return fun -class DemoClass(object): +class DemoClass: """Test class for method invocations.""" @staticmethod @@ -84,7 +84,7 @@ def method_tests(): # Test a regression (issue #234) import sys -class Text(object): +class Text: """ Regression """ if sys.version_info > (3,): @@ -98,7 +98,7 @@ def __new__(cls): Text() -class TestStaticMethod(object): +class TestStaticMethod: @staticmethod def test(first, second=None, **kwargs): @@ -112,7 +112,7 @@ def func(self): self.test(42, 42, 42) # [too-many-function-args] -class TypeCheckConstructor(object): +class TypeCheckConstructor: def __init__(self, first, second): self.first = first self.second = second @@ -125,7 +125,7 @@ def test(self): type(self)(first=1, second=2) -class Test(object): +class Test: """ lambda needs Test instance as first argument """ lam = lambda self, icon: (self, icon) @@ -139,7 +139,7 @@ def test(self): # Don't emit a redundant-keyword-arg for this example, # it's perfectly valid -class Issue642(object): +class Issue642: attr = 0 def __str__(self): return "{self.attr}".format(self=self) @@ -261,3 +261,20 @@ def func(one, two, three): CALL = lambda *args: func(*args) + + +# Ensure `too-many-function-args` is not emitted when a function call is assigned +# to a class attribute inside the class where the function is defined. +# Reference: https://github.com/PyCQA/pylint/issues/6592 +class FruitPicker: + def _pick_fruit(fruit): + def _print_selection(self): + print(f"Selected: {fruit}!") + return _print_selection + + pick_apple = _pick_fruit("apple") + pick_pear = _pick_fruit("pear") + +picker = FruitPicker() +picker.pick_apple() +picker.pick_pear() diff --git a/tests/functional/a/arguments_differ.py b/tests/functional/a/arguments_differ.py index 1dbd40086e..f579f89a6f 100644 --- a/tests/functional/a/arguments_differ.py +++ b/tests/functional/a/arguments_differ.py @@ -1,7 +1,7 @@ """Test that we are emitting arguments-differ when the arguments are different.""" -# pylint: disable=missing-docstring, too-few-public-methods, unused-argument,useless-super-delegation, useless-object-inheritance, unused-private-member +# pylint: disable=missing-docstring, too-few-public-methods, unused-argument,useless-super-delegation, unused-private-member -class Parent(object): +class Parent: def test(self): pass @@ -13,7 +13,7 @@ def test(self, arg): # [arguments-differ] pass -class ParentDefaults(object): +class ParentDefaults: def test(self, arg=None, barg=None): pass @@ -24,7 +24,7 @@ def test(self, arg=None): # [arguments-differ] pass -class Classmethod(object): +class Classmethod: @classmethod def func(cls, data): @@ -54,7 +54,7 @@ def fromkeys(cls, arg, arg1): pass -class Varargs(object): +class Varargs: def has_kwargs(self, arg, **kwargs): pass @@ -72,7 +72,7 @@ def no_kwargs(self, arg, **kwargs): # [arguments-renamed] "Addition of kwargs does not violate LSP, but first argument's name has changed." -class Super(object): +class Super: def __init__(self): pass @@ -108,7 +108,7 @@ def method(self, param='abc'): pass -class Staticmethod(object): +class Staticmethod: @staticmethod def func(data): @@ -122,7 +122,7 @@ def func(cls, data): return data -class Property(object): +class Property: @property def close(self): @@ -145,7 +145,7 @@ def func(self, data): # [arguments-differ] super().func(data) -class SuperClass(object): +class SuperClass: @staticmethod def impl(arg1, arg2, **kwargs): @@ -169,7 +169,7 @@ def should_have_been_decorated_as_static(arg1, arg2): return arg1 + arg2 -class FirstHasArgs(object): +class FirstHasArgs: def test(self, *args): pass @@ -181,7 +181,7 @@ def test(self, first, second, *args): # [arguments-differ] pass -class Positional(object): +class Positional: def test(self, first, second): pass @@ -195,7 +195,7 @@ def test(self, *args): """ super().test(args[0], args[1]) -class Mixed(object): +class Mixed: def mixed(self, first, second, *, third, fourth): pass @@ -219,7 +219,7 @@ def mixed(self, first, *args, third, **kwargs): super().mixed(first, *args, third, **kwargs) -class HasSpecialMethod(object): +class HasSpecialMethod: def __getitem__(self, key): return key @@ -232,7 +232,7 @@ def __getitem__(self, cheie): return cheie + 1 -class ParentClass(object): +class ParentClass: def meth(self, arg, arg1): raise NotImplementedError diff --git a/tests/functional/a/arguments_differ.txt b/tests/functional/a/arguments_differ.txt index bb1abb2eb9..54a75e84e4 100644 --- a/tests/functional/a/arguments_differ.txt +++ b/tests/functional/a/arguments_differ.txt @@ -1,13 +1,13 @@ -arguments-differ:12:4:12:12:Child.test:Number of parameters was 1 in 'Parent.test' and is now 2 in overridden 'Child.test' method:UNDEFINED -arguments-differ:23:4:23:12:ChildDefaults.test:Number of parameters was 3 in 'ParentDefaults.test' and is now 2 in overridden 'ChildDefaults.test' method:UNDEFINED -arguments-differ:41:4:41:12:ClassmethodChild.func:Number of parameters was 2 in 'Classmethod.func' and is now 0 in overridden 'ClassmethodChild.func' method:UNDEFINED -arguments-differ:68:4:68:18:VarargsChild.has_kwargs:Variadics removed in overridden 'VarargsChild.has_kwargs' method:UNDEFINED -arguments-renamed:71:4:71:17:VarargsChild.no_kwargs:Parameter 'args' has been renamed to 'arg' in overridden 'VarargsChild.no_kwargs' method:UNDEFINED -arguments-differ:144:4:144:12:StaticmethodChild2.func:Number of parameters was 1 in 'Staticmethod.func' and is now 2 in overridden 'StaticmethodChild2.func' method:UNDEFINED -arguments-differ:180:4:180:12:SecondChangesArgs.test:Number of parameters was 2 in 'FirstHasArgs.test' and is now 4 in overridden 'SecondChangesArgs.test' method:UNDEFINED -arguments-differ:307:4:307:16:Foo.kwonly_1:Number of parameters was 4 in 'AbstractFoo.kwonly_1' and is now 3 in overridden 'Foo.kwonly_1' method:UNDEFINED -arguments-differ:310:4:310:16:Foo.kwonly_2:Number of parameters was 3 in 'AbstractFoo.kwonly_2' and is now 2 in overridden 'Foo.kwonly_2' method:UNDEFINED -arguments-differ:313:4:313:16:Foo.kwonly_3:Number of parameters was 3 in 'AbstractFoo.kwonly_3' and is now 3 in overridden 'Foo.kwonly_3' method:UNDEFINED -arguments-differ:316:4:316:16:Foo.kwonly_4:Number of parameters was 3 in 'AbstractFoo.kwonly_4' and is now 3 in overridden 'Foo.kwonly_4' method:UNDEFINED -arguments-differ:319:4:319:16:Foo.kwonly_5:Variadics removed in overridden 'Foo.kwonly_5' method:UNDEFINED -arguments-differ:359:4:359:14:ClassWithNewNonDefaultKeywordOnly.method:Number of parameters was 2 in 'AClass.method' and is now 3 in overridden 'ClassWithNewNonDefaultKeywordOnly.method' method:UNDEFINED +arguments-differ:12:4:12:12:Child.test:Number of parameters was 1 in 'Parent.test' and is now 2 in overriding 'Child.test' method:UNDEFINED +arguments-differ:23:4:23:12:ChildDefaults.test:Number of parameters was 3 in 'ParentDefaults.test' and is now 2 in overriding 'ChildDefaults.test' method:UNDEFINED +arguments-differ:41:4:41:12:ClassmethodChild.func:Number of parameters was 2 in 'Classmethod.func' and is now 0 in overriding 'ClassmethodChild.func' method:UNDEFINED +arguments-differ:68:4:68:18:VarargsChild.has_kwargs:Variadics removed in overriding 'VarargsChild.has_kwargs' method:UNDEFINED +arguments-renamed:71:4:71:17:VarargsChild.no_kwargs:Parameter 'args' has been renamed to 'arg' in overriding 'VarargsChild.no_kwargs' method:UNDEFINED +arguments-differ:144:4:144:12:StaticmethodChild2.func:Number of parameters was 1 in 'Staticmethod.func' and is now 2 in overriding 'StaticmethodChild2.func' method:UNDEFINED +arguments-differ:180:4:180:12:SecondChangesArgs.test:Number of parameters was 2 in 'FirstHasArgs.test' and is now 4 in overriding 'SecondChangesArgs.test' method:UNDEFINED +arguments-differ:307:4:307:16:Foo.kwonly_1:Number of parameters was 4 in 'AbstractFoo.kwonly_1' and is now 3 in overriding 'Foo.kwonly_1' method:UNDEFINED +arguments-differ:310:4:310:16:Foo.kwonly_2:Number of parameters was 3 in 'AbstractFoo.kwonly_2' and is now 2 in overriding 'Foo.kwonly_2' method:UNDEFINED +arguments-differ:313:4:313:16:Foo.kwonly_3:Number of parameters was 3 in 'AbstractFoo.kwonly_3' and is now 3 in overriding 'Foo.kwonly_3' method:UNDEFINED +arguments-differ:316:4:316:16:Foo.kwonly_4:Number of parameters was 3 in 'AbstractFoo.kwonly_4' and is now 3 in overriding 'Foo.kwonly_4' method:UNDEFINED +arguments-differ:319:4:319:16:Foo.kwonly_5:Variadics removed in overriding 'Foo.kwonly_5' method:UNDEFINED +arguments-differ:359:4:359:14:ClassWithNewNonDefaultKeywordOnly.method:Number of parameters was 2 in 'AClass.method' and is now 3 in overriding 'ClassWithNewNonDefaultKeywordOnly.method' method:UNDEFINED diff --git a/tests/functional/a/arguments_renamed.py b/tests/functional/a/arguments_renamed.py index b9145eb9ab..73105f824d 100644 --- a/tests/functional/a/arguments_renamed.py +++ b/tests/functional/a/arguments_renamed.py @@ -1,4 +1,4 @@ -# pylint: disable=unused-argument, missing-docstring, line-too-long, useless-object-inheritance, too-few-public-methods +# pylint: disable=unused-argument, missing-docstring, line-too-long, too-few-public-methods import enum @@ -27,7 +27,7 @@ def brew(self, fruit_name: bool): # No warning here def eat_with_condiment(self, fruit_name: str, condiment: Condiment, error: str): # [arguments-differ] print(f"Eating a fruit named {fruit_name} with {condiment}") -class Parent(object): +class Parent: def test(self, arg): return arg + 1 @@ -51,7 +51,7 @@ def test(self, var): # [arguments-renamed] def kwargs_test(self, *, var1, kw2): #[arguments-differ] print(f"keyword parameters are {var1} and {kw2}.") -class ParentDefaults(object): +class ParentDefaults: def test1(self, arg, barg): print(f"Argument values are {arg} and {barg}") diff --git a/tests/functional/a/arguments_renamed.txt b/tests/functional/a/arguments_renamed.txt index 47d4188dd2..10fe4c2075 100644 --- a/tests/functional/a/arguments_renamed.txt +++ b/tests/functional/a/arguments_renamed.txt @@ -1,10 +1,10 @@ -arguments-renamed:17:4:17:12:Orange.brew:Parameter 'fruit_name' has been renamed to 'orange_name' in overridden 'Orange.brew' method:UNDEFINED -arguments-renamed:20:4:20:26:Orange.eat_with_condiment:Parameter 'fruit_name' has been renamed to 'orange_name' in overridden 'Orange.eat_with_condiment' method:UNDEFINED -arguments-differ:27:4:27:26:Banana.eat_with_condiment:Number of parameters was 3 in 'Fruit.eat_with_condiment' and is now 4 in overridden 'Banana.eat_with_condiment' method:UNDEFINED -arguments-renamed:40:4:40:12:Child.test:Parameter 'arg' has been renamed to 'arg1' in overridden 'Child.test' method:UNDEFINED -arguments-differ:43:4:43:19:Child.kwargs_test:Number of parameters was 4 in 'Parent.kwargs_test' and is now 4 in overridden 'Child.kwargs_test' method:UNDEFINED -arguments-renamed:48:4:48:12:Child2.test:Parameter 'arg' has been renamed to 'var' in overridden 'Child2.test' method:UNDEFINED -arguments-differ:51:4:51:19:Child2.kwargs_test:Number of parameters was 4 in 'Parent.kwargs_test' and is now 3 in overridden 'Child2.kwargs_test' method:UNDEFINED -arguments-renamed:67:4:67:13:ChildDefaults.test1:Parameter 'barg' has been renamed to 'param2' in overridden 'ChildDefaults.test1' method:UNDEFINED -arguments-renamed:95:8:95:16:FruitOverrideConditional.brew:Parameter 'fruit_name' has been renamed to 'orange_name' in overridden 'FruitOverrideConditional.brew' method:UNDEFINED -arguments-differ:99:12:99:34:FruitOverrideConditional.eat_with_condiment:Number of parameters was 3 in 'FruitConditional.eat_with_condiment' and is now 4 in overridden 'FruitOverrideConditional.eat_with_condiment' method:UNDEFINED +arguments-renamed:17:4:17:12:Orange.brew:Parameter 'fruit_name' has been renamed to 'orange_name' in overriding 'Orange.brew' method:UNDEFINED +arguments-renamed:20:4:20:26:Orange.eat_with_condiment:Parameter 'fruit_name' has been renamed to 'orange_name' in overriding 'Orange.eat_with_condiment' method:UNDEFINED +arguments-differ:27:4:27:26:Banana.eat_with_condiment:Number of parameters was 3 in 'Fruit.eat_with_condiment' and is now 4 in overriding 'Banana.eat_with_condiment' method:UNDEFINED +arguments-renamed:40:4:40:12:Child.test:Parameter 'arg' has been renamed to 'arg1' in overriding 'Child.test' method:UNDEFINED +arguments-differ:43:4:43:19:Child.kwargs_test:Number of parameters was 4 in 'Parent.kwargs_test' and is now 4 in overriding 'Child.kwargs_test' method:UNDEFINED +arguments-renamed:48:4:48:12:Child2.test:Parameter 'arg' has been renamed to 'var' in overriding 'Child2.test' method:UNDEFINED +arguments-differ:51:4:51:19:Child2.kwargs_test:Number of parameters was 4 in 'Parent.kwargs_test' and is now 3 in overriding 'Child2.kwargs_test' method:UNDEFINED +arguments-renamed:67:4:67:13:ChildDefaults.test1:Parameter 'barg' has been renamed to 'param2' in overriding 'ChildDefaults.test1' method:UNDEFINED +arguments-renamed:95:8:95:16:FruitOverrideConditional.brew:Parameter 'fruit_name' has been renamed to 'orange_name' in overriding 'FruitOverrideConditional.brew' method:UNDEFINED +arguments-differ:99:12:99:34:FruitOverrideConditional.eat_with_condiment:Number of parameters was 3 in 'FruitConditional.eat_with_condiment' and is now 4 in overriding 'FruitOverrideConditional.eat_with_condiment' method:UNDEFINED diff --git a/tests/functional/a/assert_on_tuple.py b/tests/functional/a/assert_on_tuple.py index 3ceb6e167b..cf785d53a8 100644 --- a/tests/functional/a/assert_on_tuple.py +++ b/tests/functional/a/assert_on_tuple.py @@ -1,11 +1,11 @@ '''Assert check example''' -# pylint: disable=comparison-with-itself, comparison-of-constants -assert (1 == 1, 2 == 2), "no error" +# pylint: disable=comparison-with-itself, comparison-of-constants, line-too-long +assert (1 == 1, 2 == 2), "message is raised even when there is an assert message" # [assert-on-tuple] assert (1 == 1, 2 == 2) # [assert-on-tuple] assert 1 == 1, "no error" -assert (1 == 1, ), "no error" -assert (1 == 1, ) -assert (1 == 1, 2 == 2, 3 == 5), "no error" +assert (1 == 1, ), "message is raised even when there is an assert message" # [assert-on-tuple] +assert (1 == 1, ) # [assert-on-tuple] +assert (1 == 1, 2 == 2, 3 == 5), "message is raised even when there is an assert message" # [assert-on-tuple] assert () assert (True, 'error msg') # [assert-on-tuple] diff --git a/tests/functional/a/assert_on_tuple.txt b/tests/functional/a/assert_on_tuple.txt index 85929cd42e..d3e263f232 100644 --- a/tests/functional/a/assert_on_tuple.txt +++ b/tests/functional/a/assert_on_tuple.txt @@ -1,2 +1,6 @@ -assert-on-tuple:5:0:5:23::Assert called on a 2-item-tuple. Did you mean 'assert x,y'?:UNDEFINED -assert-on-tuple:11:0:11:26::Assert called on a 2-item-tuple. Did you mean 'assert x,y'?:UNDEFINED +assert-on-tuple:4:0:4:81::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:5:0:5:23::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:7:0:7:75::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:8:0:8:17::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:9:0:9:89::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:11:0:11:26::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH diff --git a/tests/functional/a/assigning/assigning_non_slot.py b/tests/functional/a/assigning/assigning_non_slot.py index d4e9d1d45a..7b4322e622 100644 --- a/tests/functional/a/assigning/assigning_non_slot.py +++ b/tests/functional/a/assigning/assigning_non_slot.py @@ -1,15 +1,17 @@ """ Checks assigning attributes not found in class slots will trigger assigning-non-slot warning. """ -# pylint: disable=too-few-public-methods, missing-docstring, import-error, useless-object-inheritance, redundant-u-string-prefix, unnecessary-dunder-call +# pylint: disable=too-few-public-methods, missing-docstring, import-error, redundant-u-string-prefix, unnecessary-dunder-call +# pylint: disable=attribute-defined-outside-init + from collections import deque from missing import Unknown -class Empty(object): +class Empty: """ empty """ -class Bad(object): +class Bad: """ missing not in slots. """ __slots__ = ['member'] @@ -17,7 +19,7 @@ class Bad(object): def __init__(self): self.missing = 42 # [assigning-non-slot] -class Bad2(object): +class Bad2: """ missing not in slots """ __slots__ = [deque.__name__, 'member'] @@ -45,7 +47,7 @@ class Good(Empty): def __init__(self): self.missing = 42 -class Good2(object): +class Good2: """ Using __dict__ in slots will be safe. """ __slots__ = ['__dict__', 'comp'] @@ -54,7 +56,7 @@ def __init__(self): self.comp = 4 self.missing = 5 -class PropertyGood(object): +class PropertyGood: """ Using properties is safe. """ __slots__ = ['tmp', '_value'] @@ -71,7 +73,7 @@ def test(self, value): def __init__(self): self.test = 42 -class PropertyGood2(object): +class PropertyGood2: """ Using properties in the body of the class is safe. """ __slots__ = ['_value'] @@ -87,7 +89,7 @@ def _setter(self, value): def __init__(self): self.test = 24 -class UnicodeSlots(object): +class UnicodeSlots: """Using unicode objects in __slots__ is okay. On Python 3.3 onward, u'' is equivalent to '', @@ -100,7 +102,7 @@ def __init__(self): self.second = 24 -class DataDescriptor(object): +class DataDescriptor: def __init__(self, name, default=''): self.__name = name self.__default = default @@ -112,12 +114,12 @@ def __set__(self, inst, value): setattr(inst, self.__name, value) -class NonDataDescriptor(object): +class NonDataDescriptor: def __get__(self, inst, cls): return 42 -class SlotsWithDescriptor(object): +class SlotsWithDescriptor: __slots__ = ['_err'] data_descriptor = DataDescriptor('_err') non_data_descriptor = NonDataDescriptor() @@ -129,25 +131,26 @@ def dont_emit_for_descriptors(): # This should not emit, because attr is # a data descriptor inst.data_descriptor = 'foo' - inst.non_data_descriptor = 'lala' # [assigning-non-slot] + inst.non_data_descriptor = 'lala' -class ClassWithSlots(object): +class ClassWithSlots: __slots__ = ['foobar'] -class ClassReassigningDunderClass(object): +class ClassReassigningDunderClass: __slots__ = ['foobar'] def release(self): self.__class__ = ClassWithSlots -class ClassReassingingInvalidLayoutClass(object): +class ClassReassingingInvalidLayoutClass: __slots__ = [] def release(self): - self.__class__ = ClassWithSlots # [assigning-non-slot] + self.__class__ = ClassWithSlots # [assigning-non-slot] + self.test = 'test' # [assigning-non-slot] # pylint: disable=attribute-defined-outside-init @@ -164,10 +167,10 @@ def test(self): TypeVar, ) -TYPE = TypeVar('TYPE') +TypeT = TypeVar('TypeT') -class Cls(Generic[TYPE]): +class Cls(Generic[TypeT]): """ Simple class with slots """ __slots__ = ['value'] @@ -175,7 +178,7 @@ def __init__(self, value): self.value = value -class ClassDefiningSetattr(object): +class ClassDefiningSetattr: __slots__ = ["foobar"] def __init__(self): @@ -200,3 +203,39 @@ def dont_emit_for_defined_setattr(): child = ClassWithParentDefiningSetattr() child.non_existent = "non-existent" + +class ColorCls: + __slots__ = () + COLOR = "red" + + +class Child(ColorCls): + __slots__ = () + + +repro = Child() +Child.COLOR = "blue" + +class MyDescriptor: + """Basic descriptor.""" + + def __get__(self, instance, owner): + return 42 + + def __set__(self, instance, value): + pass + + +# Regression test from https://github.com/PyCQA/pylint/issues/6001 +class Base: + __slots__ = () + + attr2 = MyDescriptor() + + +class Repro(Base): + __slots__ = () + + +repro = Repro() +repro.attr2 = "anything" diff --git a/tests/functional/a/assigning/assigning_non_slot.txt b/tests/functional/a/assigning/assigning_non_slot.txt index 8b170e0881..674ca3a205 100644 --- a/tests/functional/a/assigning/assigning_non_slot.txt +++ b/tests/functional/a/assigning/assigning_non_slot.txt @@ -1,5 +1,5 @@ -assigning-non-slot:18:8:18:20:Bad.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED -assigning-non-slot:26:8:26:20:Bad2.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED -assigning-non-slot:36:8:36:20:Bad3.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED -assigning-non-slot:132:4:132:28:dont_emit_for_descriptors:Assigning to attribute 'non_data_descriptor' not defined in class slots:UNDEFINED -assigning-non-slot:150:8:150:22:ClassReassingingInvalidLayoutClass.release:Assigning to attribute '__class__' not defined in class slots:UNDEFINED +assigning-non-slot:20:8:20:20:Bad.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE +assigning-non-slot:28:8:28:20:Bad2.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE +assigning-non-slot:38:8:38:20:Bad3.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE +assigning-non-slot:152:8:152:22:ClassReassingingInvalidLayoutClass.release:Assigning to attribute '__class__' not defined in class slots:INFERENCE +assigning-non-slot:153:8:153:17:ClassReassingingInvalidLayoutClass.release:Assigning to attribute 'test' not defined in class slots:INFERENCE diff --git a/tests/functional/a/assigning/assigning_non_slot_4509.txt b/tests/functional/a/assigning/assigning_non_slot_4509.txt index ff25a141a0..f4832591e7 100644 --- a/tests/functional/a/assigning/assigning_non_slot_4509.txt +++ b/tests/functional/a/assigning/assigning_non_slot_4509.txt @@ -1 +1 @@ -assigning-non-slot:18:8:18:17:Foo.__init__:Assigning to attribute '_bar' not defined in class slots:UNDEFINED +assigning-non-slot:18:8:18:17:Foo.__init__:Assigning to attribute '_bar' not defined in class slots:INFERENCE diff --git a/tests/functional/a/assignment/assignment_from_no_return_2.py b/tests/functional/a/assignment/assignment_from_no_return_2.py index fd1489dbc5..f42b665453 100644 --- a/tests/functional/a/assignment/assignment_from_no_return_2.py +++ b/tests/functional/a/assignment/assignment_from_no_return_2.py @@ -1,4 +1,4 @@ -# pylint: disable=useless-return, useless-object-inheritance, condition-evals-to-constant +# pylint: disable=useless-return, condition-evals-to-constant """check assignment to function call where the function doesn't return 'E1111': ('Assigning to function call which doesn\'t return', @@ -9,7 +9,6 @@ inferred function returns nothing but None.'), """ -from __future__ import generators, print_function def func_no_return(): """function without return""" @@ -50,7 +49,7 @@ def generator(): A = generator() -class Abstract(object): +class Abstract: """bla bla""" def abstract_method(self): diff --git a/tests/functional/a/assignment/assignment_from_no_return_2.txt b/tests/functional/a/assignment/assignment_from_no_return_2.txt index b6aec14dad..bc1fff92bf 100644 --- a/tests/functional/a/assignment/assignment_from_no_return_2.txt +++ b/tests/functional/a/assignment/assignment_from_no_return_2.txt @@ -1,4 +1,4 @@ -assignment-from-no-return:18:0:18:20::Assigning result of a function call, where the function has no return:UNDEFINED -assignment-from-none:26:0:26:22::Assigning result of a function call, where the function returns None:UNDEFINED -assignment-from-none:33:0:33:31::Assigning result of a function call, where the function returns None:UNDEFINED -assignment-from-none:36:0:36:14::Assigning result of a function call, where the function returns None:INFERENCE +assignment-from-no-return:17:0:17:20::Assigning result of a function call, where the function has no return:UNDEFINED +assignment-from-none:25:0:25:22::Assigning result of a function call, where the function returns None:UNDEFINED +assignment-from-none:32:0:32:31::Assigning result of a function call, where the function returns None:UNDEFINED +assignment-from-none:35:0:35:14::Assigning result of a function call, where the function returns None:INFERENCE diff --git a/tests/functional/a/async_functions.py b/tests/functional/a/async_functions.py index e2b005f28a..315055e9ca 100644 --- a/tests/functional/a/async_functions.py +++ b/tests/functional/a/async_functions.py @@ -1,6 +1,6 @@ """Check that Python 3.5's async functions are properly analyzed by Pylint.""" # pylint: disable=missing-docstring,invalid-name,too-few-public-methods -# pylint: disable=using-constant-test, useless-object-inheritance +# pylint: disable=using-constant-test async def next(): # [redefined-builtin] pass @@ -9,14 +9,14 @@ async def some_function(arg1, arg2): # [unused-argument] await arg1 -class OtherClass(object): +class OtherClass: @staticmethod def test(): return 42 -class Class(object): +class Class: async def some_method(self): super(OtherClass, self).test() # [bad-super-call] diff --git a/tests/functional/a/async_functions.txt b/tests/functional/a/async_functions.txt index 5dcaf19061..63823cced7 100644 --- a/tests/functional/a/async_functions.txt +++ b/tests/functional/a/async_functions.txt @@ -6,5 +6,5 @@ too-many-branches:26:0:26:26:complex_function:Too many branches (13/12):UNDEFINE too-many-return-statements:26:0:26:26:complex_function:Too many return statements (10/6):UNDEFINED dangerous-default-value:57:0:57:14:func:Dangerous default value [] as argument:UNDEFINED duplicate-argument-name:57:18:57:19:func:Duplicate argument name a in function definition:HIGH -disallowed-name:62:0:62:13:foo:"Disallowed name ""foo""":UNDEFINED +disallowed-name:62:0:62:13:foo:"Disallowed name ""foo""":HIGH empty-docstring:62:0:62:13:foo:Empty function docstring:HIGH diff --git a/tests/functional/a/attribute_defined_outside_init.py b/tests/functional/a/attribute_defined_outside_init.py index 1cb319c617..0b6f1ef499 100644 --- a/tests/functional/a/attribute_defined_outside_init.py +++ b/tests/functional/a/attribute_defined_outside_init.py @@ -1,6 +1,6 @@ -# pylint: disable=missing-docstring,too-few-public-methods,invalid-name, useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods,invalid-name -class A(object): +class A: def __init__(self): self.x = 0 @@ -26,7 +26,7 @@ def test(self): self.z = 44 # [attribute-defined-outside-init] -class C(object): +class C: def __init__(self): self._init() @@ -35,7 +35,7 @@ def _init(self): self.z = 44 -class D(object): +class D: def setUp(self): self.set_z() @@ -44,7 +44,7 @@ def set_z(self): self.z = 42 -class E(object): +class E: def __init__(self): i = self._init @@ -54,7 +54,7 @@ def _init(self): self.z = 44 -class Mixin(object): +class Mixin: def test_mixin(self): """Don't emit attribute-defined-outside-init for mixin classes.""" diff --git a/tests/functional/b/bad_except_order.txt b/tests/functional/b/bad_except_order.txt index c6e6b44718..70443408fd 100644 --- a/tests/functional/b/bad_except_order.txt +++ b/tests/functional/b/bad_except_order.txt @@ -1,5 +1,5 @@ -bad-except-order:9:7:9:16::Bad except clauses order (Exception is an ancestor class of TypeError):UNDEFINED -bad-except-order:16:7:16:17::Bad except clauses order (LookupError is an ancestor class of IndexError):UNDEFINED -bad-except-order:23:7:23:38::Bad except clauses order (LookupError is an ancestor class of IndexError):UNDEFINED -bad-except-order:23:7:23:38::Bad except clauses order (NameError is an ancestor class of UnboundLocalError):UNDEFINED -bad-except-order:26:0:31:8::Bad except clauses order (empty except clause should always appear last):UNDEFINED +bad-except-order:9:7:9:16::Bad except clauses order (Exception is an ancestor class of TypeError):INFERENCE +bad-except-order:16:7:16:17::Bad except clauses order (LookupError is an ancestor class of IndexError):INFERENCE +bad-except-order:23:7:23:38::Bad except clauses order (LookupError is an ancestor class of IndexError):INFERENCE +bad-except-order:23:7:23:38::Bad except clauses order (NameError is an ancestor class of UnboundLocalError):INFERENCE +bad-except-order:26:0:31:8::Bad except clauses order (empty except clause should always appear last):HIGH diff --git a/tests/functional/b/bad_exception_context.py b/tests/functional/b/bad_exception_cause.py similarity index 66% rename from tests/functional/b/bad_exception_context.py rename to tests/functional/b/bad_exception_cause.py index 2fbe2d6602..8d8db3677a 100644 --- a/tests/functional/b/bad_exception_context.py +++ b/tests/functional/b/bad_exception_cause.py @@ -1,26 +1,25 @@ -"""Check that raise ... from .. uses a proper exception context """ +"""Check that raise ... from .. uses a proper exception cause """ # pylint: disable=unreachable, import-error, multiple-imports import socket, unknown -__revision__ = 0 class ExceptionSubclass(Exception): """ subclass """ def test(): """ docstring """ - raise IndexError from 1 # [bad-exception-context] + raise IndexError from 1 # [bad-exception-cause] raise IndexError from None raise IndexError from ZeroDivisionError - raise IndexError from object() # [bad-exception-context] + raise IndexError from object() # [bad-exception-cause] raise IndexError from ExceptionSubclass raise IndexError from socket.error raise IndexError() from None raise IndexError() from ZeroDivisionError raise IndexError() from ZeroDivisionError() - raise IndexError() from object() # [bad-exception-context] + raise IndexError() from object() # [bad-exception-cause] raise IndexError() from unknown def function(): @@ -29,4 +28,4 @@ def function(): try: pass except function as exc: # [catching-non-exception] - raise Exception from exc # [bad-exception-context] + raise Exception from exc # [bad-exception-cause, broad-exception-raised] diff --git a/tests/functional/b/bad_exception_cause.txt b/tests/functional/b/bad_exception_cause.txt new file mode 100644 index 0000000000..3aa50fa37c --- /dev/null +++ b/tests/functional/b/bad_exception_cause.txt @@ -0,0 +1,6 @@ +bad-exception-cause:13:4:13:27:test:Exception cause set to something which is not an exception, nor None:INFERENCE +bad-exception-cause:16:4:16:34:test:Exception cause set to something which is not an exception, nor None:INFERENCE +bad-exception-cause:22:4:22:36:test:Exception cause set to something which is not an exception, nor None:INFERENCE +catching-non-exception:30:7:30:15::"Catching an exception which doesn't inherit from Exception: function":UNDEFINED +bad-exception-cause:31:4:31:28::Exception cause set to something which is not an exception, nor None:INFERENCE +broad-exception-raised:31:4:31:28::"Raising too general exception: Exception":INFERENCE diff --git a/tests/functional/b/bad_exception_context.txt b/tests/functional/b/bad_exception_context.txt deleted file mode 100644 index 8f9852b20b..0000000000 --- a/tests/functional/b/bad_exception_context.txt +++ /dev/null @@ -1,5 +0,0 @@ -bad-exception-context:14:4:14:27:test:Exception context set to something which is not an exception, nor None:UNDEFINED -bad-exception-context:17:4:17:34:test:Exception context set to something which is not an exception, nor None:UNDEFINED -bad-exception-context:23:4:23:36:test:Exception context set to something which is not an exception, nor None:UNDEFINED -catching-non-exception:31:7:31:15::"Catching an exception which doesn't inherit from Exception: function":UNDEFINED -bad-exception-context:32:4:32:28::Exception context set to something which is not an exception, nor None:UNDEFINED diff --git a/tests/functional/b/bad_indentation.py b/tests/functional/b/bad_indentation.py index 4c50eae0b2..3b38347e40 100644 --- a/tests/functional/b/bad_indentation.py +++ b/tests/functional/b/bad_indentation.py @@ -1,5 +1,4 @@ # pylint: disable=missing-docstring, pointless-statement -from __future__ import print_function def totoo(): diff --git a/tests/functional/b/bad_indentation.txt b/tests/functional/b/bad_indentation.txt index ac9f2cbd88..2e96434b12 100644 --- a/tests/functional/b/bad_indentation.txt +++ b/tests/functional/b/bad_indentation.txt @@ -1,2 +1,2 @@ -bad-indentation:6:0:None:None::Bad indentation. Found 1 spaces, expected 4:UNDEFINED -bad-indentation:12:0:None:None::Bad indentation. Found 5 spaces, expected 4:UNDEFINED +bad-indentation:5:0:None:None::Bad indentation. Found 1 spaces, expected 4:UNDEFINED +bad-indentation:11:0:None:None::Bad indentation. Found 5 spaces, expected 4:UNDEFINED diff --git a/tests/functional/b/bad_reversed_sequence.py b/tests/functional/b/bad_reversed_sequence.py index 7918231548..c4380491e9 100644 --- a/tests/functional/b/bad_reversed_sequence.py +++ b/tests/functional/b/bad_reversed_sequence.py @@ -1,16 +1,16 @@ """ Checks that reversed() receive proper argument """ -# pylint: disable=missing-docstring, useless-object-inheritance +# pylint: disable=missing-docstring # pylint: disable=too-few-public-methods from collections import deque, OrderedDict from enum import IntEnum -class GoodReversed(object): +class GoodReversed: """ Implements __reversed__ """ def __reversed__(self): return [1, 2, 3] -class SecondGoodReversed(object): +class SecondGoodReversed: """ Implements __len__ and __getitem__ """ def __len__(self): return 3 @@ -18,12 +18,12 @@ def __len__(self): def __getitem__(self, index): return index -class BadReversed(object): +class BadReversed: """ implements only len() """ def __len__(self): return 3 -class SecondBadReversed(object): +class SecondBadReversed: """ implements only __getitem__ """ def __getitem__(self, index): return index diff --git a/tests/functional/b/bad_reversed_sequence_py37.txt b/tests/functional/b/bad_reversed_sequence_py37.txt index d87c84690f..6fbbd2c591 100644 --- a/tests/functional/b/bad_reversed_sequence_py37.txt +++ b/tests/functional/b/bad_reversed_sequence_py37.txt @@ -1,2 +1,2 @@ -bad-reversed-sequence:5:::The first reversed() argument is not a sequence -bad-reversed-sequence:12:::The first reversed() argument is not a sequence +bad-reversed-sequence:5:0:5:26::The first reversed() argument is not a sequence:UNDEFINED +bad-reversed-sequence:12:0:12:39::The first reversed() argument is not a sequence:UNDEFINED diff --git a/tests/functional/b/bad_staticmethod_argument.py b/tests/functional/b/bad_staticmethod_argument.py index e73bf3dcdf..846058d550 100644 --- a/tests/functional/b/bad_staticmethod_argument.py +++ b/tests/functional/b/bad_staticmethod_argument.py @@ -1,6 +1,6 @@ -# pylint: disable=missing-docstring, no-staticmethod-decorator, useless-object-inheritance +# pylint: disable=missing-docstring, no-staticmethod-decorator -class Abcd(object): +class Abcd: def method1(self): # [bad-staticmethod-argument] pass diff --git a/tests/functional/b/bad_thread_instantiation.py b/tests/functional/b/bad_thread_instantiation.py index e7e02eaede..3c9aa5e55a 100644 --- a/tests/functional/b/bad_thread_instantiation.py +++ b/tests/functional/b/bad_thread_instantiation.py @@ -1,8 +1,24 @@ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, redundant-keyword-arg, invalid-name, line-too-long import threading threading.Thread(lambda: None).run() # [bad-thread-instantiation] threading.Thread(None, lambda: None) +threading.Thread(lambda: None, group=None) # [bad-thread-instantiation] +threading.Thread() # [bad-thread-instantiation] + threading.Thread(group=None, target=lambda: None).run() -threading.Thread() # [bad-thread-instantiation] +threading.Thread(group=None, target=None, name=None, args=(), kwargs={}) +threading.Thread(None, None, "name") + +def thread_target(n): + print(n ** 2) + + +thread = threading.Thread(thread_target, args=(10,)) # [bad-thread-instantiation] + + +kw = {'target_typo': lambda x: x} +threading.Thread(None, **kw) # [unexpected-keyword-arg, bad-thread-instantiation] + +threading.Thread(None, target_typo=lambda x: x) # [unexpected-keyword-arg, bad-thread-instantiation] diff --git a/tests/functional/b/bad_thread_instantiation.txt b/tests/functional/b/bad_thread_instantiation.txt index e969a2473c..91358d30af 100644 --- a/tests/functional/b/bad_thread_instantiation.txt +++ b/tests/functional/b/bad_thread_instantiation.txt @@ -1,2 +1,8 @@ -bad-thread-instantiation:5:0:5:30::threading.Thread needs the target function:UNDEFINED -bad-thread-instantiation:8:0:8:18::threading.Thread needs the target function:UNDEFINED +bad-thread-instantiation:5:0:5:30::threading.Thread needs the target function:HIGH +bad-thread-instantiation:7:0:7:42::threading.Thread needs the target function:HIGH +bad-thread-instantiation:8:0:8:18::threading.Thread needs the target function:HIGH +bad-thread-instantiation:18:9:18:52::threading.Thread needs the target function:HIGH +bad-thread-instantiation:22:0:22:28::threading.Thread needs the target function:HIGH +unexpected-keyword-arg:22:0:22:28::Unexpected keyword argument 'target_typo' in constructor call:UNDEFINED +bad-thread-instantiation:24:0:24:47::threading.Thread needs the target function:HIGH +unexpected-keyword-arg:24:0:24:47::Unexpected keyword argument 'target_typo' in constructor call:UNDEFINED diff --git a/tests/functional/b/bare_except.txt b/tests/functional/b/bare_except.txt index 584f1be6da..7957bc144b 100644 --- a/tests/functional/b/bare_except.txt +++ b/tests/functional/b/bare_except.txt @@ -1 +1 @@ -bare-except:5:0:6:8::No exception type(s) specified:UNDEFINED +bare-except:5:0:6:8::No exception type(s) specified:HIGH diff --git a/tests/functional/b/base_init_vars.py b/tests/functional/b/base_init_vars.py index d103aa968c..c2c25baabc 100644 --- a/tests/functional/b/base_init_vars.py +++ b/tests/functional/b/base_init_vars.py @@ -1,8 +1,8 @@ -# pylint:disable=too-few-public-methods, useless-object-inheritance +# pylint:disable=too-few-public-methods """Checks that class variables are seen as inherited !""" -class BaseClass(object): +class BaseClass: """A simple base class """ diff --git a/tests/functional/b/blacklisted_name.py b/tests/functional/b/blacklisted_name.py deleted file mode 100644 index 84067e3ffc..0000000000 --- a/tests/functional/b/blacklisted_name.py +++ /dev/null @@ -1,4 +0,0 @@ -# pylint: disable=missing-docstring - -def baz(): # [disallowed-name] - pass diff --git a/tests/functional/b/blacklisted_name.txt b/tests/functional/b/blacklisted_name.txt deleted file mode 100644 index b6b96c5901..0000000000 --- a/tests/functional/b/blacklisted_name.txt +++ /dev/null @@ -1 +0,0 @@ -disallowed-name:3:0:3:7:baz:"Disallowed name ""baz""":UNDEFINED diff --git a/tests/functional/b/boolean_datetime.py b/tests/functional/b/boolean_datetime.py new file mode 100644 index 0000000000..cde355c011 --- /dev/null +++ b/tests/functional/b/boolean_datetime.py @@ -0,0 +1,15 @@ +"""Test boolean-datetime + +'py-version' needs to be set to <= '3.5'. +""" +import datetime + +if datetime.time(0, 0, 0): # [boolean-datetime] + print("datetime.time(0,0,0) is not a bug!") +else: + print("datetime.time(0,0,0) is a bug!") + +if datetime.time(0, 0, 1): # [boolean-datetime] + print("datetime.time(0,0,1) is not a bug!") +else: + print("datetime.time(0,0,1) is a bug!") diff --git a/tests/functional/b/boolean_datetime.rc b/tests/functional/b/boolean_datetime.rc new file mode 100644 index 0000000000..068be2d4cb --- /dev/null +++ b/tests/functional/b/boolean_datetime.rc @@ -0,0 +1,2 @@ +[MAIN] +py-version=3.4 diff --git a/tests/functional/b/boolean_datetime.txt b/tests/functional/b/boolean_datetime.txt new file mode 100644 index 0000000000..316453a6ed --- /dev/null +++ b/tests/functional/b/boolean_datetime.txt @@ -0,0 +1,2 @@ +boolean-datetime:7:3:7:25::Using datetime.time in a boolean context.:UNDEFINED +boolean-datetime:12:3:12:25::Using datetime.time in a boolean context.:UNDEFINED diff --git a/tests/functional/b/broad_except.py b/tests/functional/b/broad_except.py deleted file mode 100644 index 24c8399515..0000000000 --- a/tests/functional/b/broad_except.py +++ /dev/null @@ -1,14 +0,0 @@ -# pylint: disable=missing-docstring -from __future__ import print_function -__revision__ = 0 - -try: - __revision__ += 1 -except Exception: # [broad-except] - print('error') - - -try: - __revision__ += 1 -except BaseException: # [broad-except] - print('error') diff --git a/tests/functional/b/broad_except.txt b/tests/functional/b/broad_except.txt deleted file mode 100644 index 55fce74ca3..0000000000 --- a/tests/functional/b/broad_except.txt +++ /dev/null @@ -1,2 +0,0 @@ -broad-except:7:7:7:16::Catching too general exception Exception:UNDEFINED -broad-except:13:7:13:20::Catching too general exception BaseException:UNDEFINED diff --git a/tests/functional/b/broad_exception_caught.py b/tests/functional/b/broad_exception_caught.py new file mode 100644 index 0000000000..0a69a70156 --- /dev/null +++ b/tests/functional/b/broad_exception_caught.py @@ -0,0 +1,39 @@ +# pylint: disable=missing-docstring +__revision__ = 0 + +class CustomBroadException(Exception): + pass + + +class CustomNarrowException(CustomBroadException): + pass + + +try: + __revision__ += 1 +except Exception: # [broad-exception-caught] + print('error') + + +try: + __revision__ += 1 +except BaseException: # [broad-exception-caught] + print('error') + + +try: + __revision__ += 1 +except ValueError: + print('error') + + +try: + __revision__ += 1 +except CustomBroadException: # [broad-exception-caught] + print('error') + + +try: + __revision__ += 1 +except CustomNarrowException: + print('error') diff --git a/tests/functional/b/broad_exception_caught.rc b/tests/functional/b/broad_exception_caught.rc new file mode 100644 index 0000000000..e0e1a7b6c5 --- /dev/null +++ b/tests/functional/b/broad_exception_caught.rc @@ -0,0 +1,4 @@ +[EXCEPTIONS] +overgeneral-exceptions=builtins.BaseException, + builtins.Exception, + functional.b.broad_exception_caught.CustomBroadException diff --git a/tests/functional/b/broad_exception_caught.txt b/tests/functional/b/broad_exception_caught.txt new file mode 100644 index 0000000000..386423b63f --- /dev/null +++ b/tests/functional/b/broad_exception_caught.txt @@ -0,0 +1,3 @@ +broad-exception-caught:14:7:14:16::Catching too general exception Exception:INFERENCE +broad-exception-caught:20:7:20:20::Catching too general exception BaseException:INFERENCE +broad-exception-caught:32:7:32:27::Catching too general exception CustomBroadException:INFERENCE diff --git a/tests/functional/b/broad_exception_raised.py b/tests/functional/b/broad_exception_raised.py new file mode 100644 index 0000000000..c6ce64b462 --- /dev/null +++ b/tests/functional/b/broad_exception_raised.py @@ -0,0 +1,52 @@ +# pylint: disable=missing-docstring, unreachable + +ExceptionAlias = Exception + +class CustomBroadException(Exception): + pass + + +class CustomNarrowException(CustomBroadException): + pass + + +def exploding_apple(apple): + print(f"{apple} is about to explode") + raise Exception("{apple} exploded !") # [broad-exception-raised] + + +def raise_and_catch(): + try: + raise Exception("Oh No!!") # [broad-exception-raised] + except Exception as ex: # [broad-exception-caught] + print(ex) + + +def raise_catch_reraise(): + try: + exploding_apple("apple") + except Exception as ex: + print(ex) + raise ex + + +def raise_catch_raise(): + try: + exploding_apple("apple") + except Exception as ex: + print(ex) + raise Exception() from None # [broad-exception-raised] + + +def raise_catch_raise_using_alias(): + try: + exploding_apple("apple") + except Exception as ex: + print(ex) + raise ExceptionAlias() from None # [broad-exception-raised] + +raise Exception() # [broad-exception-raised] +raise BaseException() # [broad-exception-raised] +raise CustomBroadException() # [broad-exception-raised] +raise IndexError from None +raise CustomNarrowException() from None diff --git a/tests/functional/b/broad_exception_raised.rc b/tests/functional/b/broad_exception_raised.rc new file mode 100644 index 0000000000..4f85d2933f --- /dev/null +++ b/tests/functional/b/broad_exception_raised.rc @@ -0,0 +1,4 @@ +[EXCEPTIONS] +overgeneral-exceptions=builtins.BaseException, + builtins.Exception, + functional.b.broad_exception_raised.CustomBroadException diff --git a/tests/functional/b/broad_exception_raised.txt b/tests/functional/b/broad_exception_raised.txt new file mode 100644 index 0000000000..1e27b23f98 --- /dev/null +++ b/tests/functional/b/broad_exception_raised.txt @@ -0,0 +1,8 @@ +broad-exception-raised:15:4:15:41:exploding_apple:"Raising too general exception: Exception":INFERENCE +broad-exception-raised:20:8:20:34:raise_and_catch:"Raising too general exception: Exception":INFERENCE +broad-exception-caught:21:11:21:20:raise_and_catch:Catching too general exception Exception:INFERENCE +broad-exception-raised:38:8:38:35:raise_catch_raise:"Raising too general exception: Exception":INFERENCE +broad-exception-raised:46:8:46:40:raise_catch_raise_using_alias:"Raising too general exception: Exception":INFERENCE +broad-exception-raised:48:0:48:17::"Raising too general exception: Exception":INFERENCE +broad-exception-raised:49:0:49:21::"Raising too general exception: BaseException":INFERENCE +broad-exception-raised:50:0:50:28::"Raising too general exception: CustomBroadException":INFERENCE diff --git a/tests/functional/b/builtin_module_test.py b/tests/functional/b/builtin_module_test.py index 9b1e7ce8e9..ae59217a04 100644 --- a/tests/functional/b/builtin_module_test.py +++ b/tests/functional/b/builtin_module_test.py @@ -3,8 +3,6 @@ from __future__ import absolute_import from math import log10 -__revision__ = None - def log10_2(): """bla bla bla""" diff --git a/tests/functional/c/cellvar_escaping_loop.py b/tests/functional/c/cellvar_escaping_loop.py index 0de26bede7..8781e6c5cc 100644 --- a/tests/functional/c/cellvar_escaping_loop.py +++ b/tests/functional/c/cellvar_escaping_loop.py @@ -1,6 +1,6 @@ # pylint: disable=unnecessary-comprehension,missing-docstring,too-few-public-methods,unnecessary-direct-lambda-call """Tests for loopvar-in-closure.""" -from __future__ import print_function + from enum import Enum diff --git a/tests/functional/c/class_attributes.py b/tests/functional/c/class_attributes.py index 1d41f9d7a1..ea38588768 100644 --- a/tests/functional/c/class_attributes.py +++ b/tests/functional/c/class_attributes.py @@ -1,8 +1,8 @@ """Test that valid class attribute doesn't trigger errors""" -__revision__ = 'sponge bob' -# pylint: disable=useless-object-inheritance,missing-docstring,too-few-public-methods +# pylint: disable=missing-docstring,too-few-public-methods -class Clazz(object): + +class Clazz: "dummy class" def __init__(self): diff --git a/tests/functional/c/class_members_py30.py b/tests/functional/c/class_members_py30.py index afaee58726..4566ff44e4 100644 --- a/tests/functional/c/class_members_py30.py +++ b/tests/functional/c/class_members_py30.py @@ -1,7 +1,7 @@ """ Various tests for class members access. """ -# pylint: disable=too-few-public-methods,import-error,missing-docstring, wrong-import-position,wrong-import-order, useless-object-inheritance, unnecessary-dunder-call +# pylint: disable=too-few-public-methods,import-error,missing-docstring, wrong-import-position,wrong-import-order, unnecessary-dunder-call from missing import Missing -class MyClass(object): +class MyClass: """class docstring""" def __init__(self): @@ -16,7 +16,7 @@ def test(self): self.nonexistent1.truc() # [no-member] self.nonexistent2[1] = 'hehe' # [no-member] -class XYZMixin(object): +class XYZMixin: """access to undefined members should be ignored in mixin classes by default """ @@ -24,23 +24,23 @@ def __init__(self): print(self.nonexistent) -class NewClass(object): +class NewClass: """use object.__setattr__""" def __init__(self): self.__setattr__('toto', 'tutu') from abc import ABCMeta -class TestMetaclass(object, metaclass=ABCMeta): +class TestMetaclass(metaclass=ABCMeta): """ Test attribute access for metaclasses. """ class Metaclass(type): """ metaclass """ @classmethod - def test(cls): + def test(mcs): """ classmethod """ -class UsingMetaclass(object, metaclass=Metaclass): +class UsingMetaclass(metaclass=Metaclass): """ empty """ TestMetaclass.register(int) @@ -55,7 +55,7 @@ class NoKnownBases(Missing): NoKnownBases().lalala() -class MetaClass(object): +class MetaClass: """Look some methods in the implicit metaclass.""" @classmethod diff --git a/tests/functional/c/class_scope.py b/tests/functional/c/class_scope.py index 74db274431..85ee86fbfa 100644 --- a/tests/functional/c/class_scope.py +++ b/tests/functional/c/class_scope.py @@ -1,9 +1,8 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods, unnecessary-lambda-assignment """check for scope problems""" -__revision__ = None -class Well(object): +class Well: """well""" attr = 42 get_attr = lambda arg=attr: arg * 24 @@ -13,7 +12,7 @@ class Well(object): bad_lambda = lambda: get_attr_bad # [undefined-variable] bad_gen = list(attr + i for i in range(10)) # [undefined-variable] - class Data(object): + class Data: """base hidden class""" class Sub(Data): """whaou, is Data found???""" diff --git a/tests/functional/c/class_scope.txt b/tests/functional/c/class_scope.txt index d065c1b975..081387a22e 100644 --- a/tests/functional/c/class_scope.txt +++ b/tests/functional/c/class_scope.txt @@ -1,7 +1,7 @@ -undefined-variable:11:39:11:46:Well.:Undefined variable 'revattr':UNDEFINED -used-before-assignment:11:30:11:37:Well.:Using variable 'revattr' before assignment:HIGH -undefined-variable:13:25:13:37:Well.:Undefined variable 'get_attr_bad':UNDEFINED -undefined-variable:14:19:14:23:Well:Undefined variable 'attr':UNDEFINED -undefined-variable:20:15:20:19:Well.Sub:Undefined variable 'Data':UNDEFINED -undefined-variable:23:15:23:18:Well.func:Undefined variable 'Sub':UNDEFINED -undefined-variable:41:22:41:26:Wrong.work:Undefined variable 'self':UNDEFINED +undefined-variable:10:39:10:46:Well.:Undefined variable 'revattr':UNDEFINED +used-before-assignment:10:30:10:37:Well.:Using variable 'revattr' before assignment:HIGH +undefined-variable:12:25:12:37:Well.:Undefined variable 'get_attr_bad':UNDEFINED +undefined-variable:13:19:13:23:Well:Undefined variable 'attr':UNDEFINED +undefined-variable:19:15:19:19:Well.Sub:Undefined variable 'Data':UNDEFINED +undefined-variable:22:15:22:18:Well.func:Undefined variable 'Sub':UNDEFINED +undefined-variable:40:22:40:26:Wrong.work:Undefined variable 'self':UNDEFINED diff --git a/tests/functional/c/classes_meth_could_be_a_function.py b/tests/functional/c/classes_meth_could_be_a_function.py index 4a967a5b5f..82f627dfbf 100644 --- a/tests/functional/c/classes_meth_could_be_a_function.py +++ b/tests/functional/c/classes_meth_could_be_a_function.py @@ -1,20 +1,20 @@ -# pylint: disable=missing-docstring,too-few-public-methods,useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods """ #2479 R0201 (formerly W0212), Method could be a function shouldn't be emitted in case like factory method pattern """ -__revision__ = 1 -class XAsub(object): + +class XAsub: pass class XBsub(XAsub): pass class XCsub(XAsub): pass -class Aimpl(object): +class Aimpl: # disable "method could be a function" on classes which are not overriding # the factory method because in that case the usage of polymorphism is not # detected diff --git a/tests/functional/c/classes_protected_member_access.py b/tests/functional/c/classes_protected_member_access.py index 516efd7d48..fe540be853 100644 --- a/tests/functional/c/classes_protected_member_access.py +++ b/tests/functional/c/classes_protected_member_access.py @@ -1,10 +1,10 @@ """ #3123: W0212 false positive on static method """ -__revision__ = 1 -# pylint: disable=no-classmethod-decorator, no-staticmethod-decorator, useless-object-inheritance -class A3123(object): + +# pylint: disable=no-classmethod-decorator, no-staticmethod-decorator +class A3123: """oypuee""" _protected = 1 def __init__(self): diff --git a/tests/functional/c/comparison_of_constants.py b/tests/functional/c/comparison_of_constants.py index bfe4b8cdb9..4c98de6e88 100644 --- a/tests/functional/c/comparison_of_constants.py +++ b/tests/functional/c/comparison_of_constants.py @@ -1,9 +1,5 @@ # pylint: disable=missing-docstring, comparison-with-itself, invalid-name - -if 2 is 2: # [literal-comparison, comparison-of-constants] - pass - while 2 == 2: # [comparison-of-constants] pass @@ -22,14 +18,6 @@ CONST = 24 -if CONST is 0: # [literal-comparison] - pass - -if CONST is 1: # [literal-comparison] - pass - -if CONST is 42: # [literal-comparison] - pass if 0 < CONST < 42: pass diff --git a/tests/functional/c/comparison_of_constants.txt b/tests/functional/c/comparison_of_constants.txt index 1a09840245..b89bdbdc87 100644 --- a/tests/functional/c/comparison_of_constants.txt +++ b/tests/functional/c/comparison_of_constants.txt @@ -1,9 +1,4 @@ -comparison-of-constants:4:3:4:9::"Comparison between constants: '2 is 2' has a constant value":HIGH -literal-comparison:4:3:4:9::Comparison to literal:UNDEFINED -comparison-of-constants:7:6:7:12::"Comparison between constants: '2 == 2' has a constant value":HIGH -comparison-of-constants:10:6:10:11::"Comparison between constants: '2 > 2' has a constant value":HIGH -comparison-of-constants:20:3:20:15::"Comparison between constants: 'True == True' has a constant value":HIGH -singleton-comparison:20:3:20:15::Comparison 'True == True' should be 'True is True' if checking for the singleton value True, or 'True' if testing for truthiness:UNDEFINED -literal-comparison:25:3:25:13::Comparison to literal:UNDEFINED -literal-comparison:28:3:28:13::Comparison to literal:UNDEFINED -literal-comparison:31:3:31:14::Comparison to literal:UNDEFINED +comparison-of-constants:3:6:3:12::"Comparison between constants: '2 == 2' has a constant value":HIGH +comparison-of-constants:6:6:6:11::"Comparison between constants: '2 > 2' has a constant value":HIGH +comparison-of-constants:16:3:16:15::"Comparison between constants: 'True == True' has a constant value":HIGH +singleton-comparison:16:3:16:15::Comparison 'True == True' should be 'True is True' if checking for the singleton value True, or 'True' if testing for truthiness:UNDEFINED diff --git a/tests/functional/c/comparison_with_callable.py b/tests/functional/c/comparison_with_callable.py index 777594d754..7006d4960b 100644 --- a/tests/functional/c/comparison_with_callable.py +++ b/tests/functional/c/comparison_with_callable.py @@ -1,4 +1,4 @@ -# pylint: disable = disallowed-name, missing-docstring, useless-return, invalid-name, line-too-long, useless-object-inheritance, comparison-of-constants +# pylint: disable = disallowed-name, missing-docstring, useless-return, invalid-name, line-too-long, comparison-of-constants, broad-exception-raised def foo(): return None @@ -18,7 +18,7 @@ def goo(): pass -class FakeClass(object): +class FakeClass: def __init__(self): self._fake_prop = 'fake it till you make it!!' diff --git a/tests/functional/c/confidence_filter.py b/tests/functional/c/confidence_filter.py index 42351998d8..5f8dfd4f0d 100644 --- a/tests/functional/c/confidence_filter.py +++ b/tests/functional/c/confidence_filter.py @@ -1,8 +1,7 @@ """Test for the confidence filter.""" -from __future__ import print_function -# pylint: disable=useless-object-inheritance -class Client(object): + +class Client: """use provider class""" def __init__(self): diff --git a/tests/functional/c/confidence_filter.txt b/tests/functional/c/confidence_filter.txt index 86a0f28c44..ede967f680 100644 --- a/tests/functional/c/confidence_filter.txt +++ b/tests/functional/c/confidence_filter.txt @@ -1 +1 @@ -no-member:16:6:16:18::Instance of 'Client' has no 'foo' member:INFERENCE +no-member:15:6:15:18::Instance of 'Client' has no 'foo' member:INFERENCE diff --git a/tests/functional/c/consider/consider_iterating_dictionary.py b/tests/functional/c/consider/consider_iterating_dictionary.py index 1498100420..8c75b4e3ef 100644 --- a/tests/functional/c/consider/consider_iterating_dictionary.py +++ b/tests/functional/c/consider/consider_iterating_dictionary.py @@ -1,11 +1,11 @@ # pylint: disable=missing-docstring, expression-not-assigned, too-few-public-methods -# pylint: disable=no-member, import-error, line-too-long, useless-object-inheritance +# pylint: disable=no-member, import-error, line-too-long # pylint: disable=unnecessary-comprehension, use-dict-literal, use-implicit-booleaness-not-comparison from unknown import Unknown -class CustomClass(object): +class CustomClass: def keys(self): return [] @@ -92,3 +92,25 @@ def inner_function(): print("a" in another_metadata.keys()) # [consider-iterating-dictionary] return inner_function() return InnerClass().another_function() + +a_dict = {"a": 1, "b": 2, "c": 3} +a_set = {"c", "d"} + +# Test bitwise operations. These should not raise msg because removing `.keys()` +# either gives error or ends in a different result +print(a_dict.keys() | a_set) + +if "a" in a_dict.keys() | a_set: + pass + +if "a" in a_dict.keys() & a_set: + pass + +if 1 in a_dict.keys() ^ [1, 2]: + pass + +if "a" in a_dict.keys() or a_set: # [consider-iterating-dictionary] + pass + +if "a" in a_dict.keys() and a_set: # [consider-iterating-dictionary] + pass diff --git a/tests/functional/c/consider/consider_iterating_dictionary.txt b/tests/functional/c/consider/consider_iterating_dictionary.txt index f251fa2869..5190e77890 100644 --- a/tests/functional/c/consider/consider_iterating_dictionary.txt +++ b/tests/functional/c/consider/consider_iterating_dictionary.txt @@ -1,26 +1,28 @@ -consider-iterating-dictionary:25:16:25:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:26:16:26:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:27:16:27:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:28:21:28:30::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:29:24:29:33::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:30:24:30:33::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:31:24:31:33::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:32:29:32:38::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:33:11:33:20::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:38:24:38:35::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:38:55:38:66::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:39:31:39:42::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:39:61:39:72::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:40:30:40:41::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:40:60:40:71::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:43:8:43:21::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:45:8:45:17::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:65:11:65:20::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:73:19:73:34::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:75:14:75:29::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:77:15:77:30::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:79:10:79:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:89:42:89:65:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:90:37:90:60:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:91:38:91:61:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:92:33:92:56:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED +consider-iterating-dictionary:25:16:25:25::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:26:16:26:25::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:27:16:27:25::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:28:21:28:30::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:29:24:29:33::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:30:24:30:33::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:31:24:31:33::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:32:29:32:38::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:33:11:33:20::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:38:24:38:35::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:38:55:38:66::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:39:31:39:42::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:39:61:39:72::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:40:30:40:41::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:40:60:40:71::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:43:8:43:21::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:45:8:45:17::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:65:11:65:20::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:73:19:73:34::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:75:14:75:29::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:77:15:77:30::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:79:10:79:25::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:89:42:89:65:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:90:37:90:60:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:91:38:91:61:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:92:33:92:56:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:112:10:112:23::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:115:10:115:23::Consider iterating the dictionary directly instead of calling .keys():INFERENCE diff --git a/tests/functional/c/consider/consider_join.py b/tests/functional/c/consider/consider_join.py index 24cd6dd49b..9f785b64d9 100644 --- a/tests/functional/c/consider/consider_join.py +++ b/tests/functional/c/consider/consider_join.py @@ -17,6 +17,33 @@ for number in ['1', '2', '3']: result += number # [consider-using-join] +result = 'a' +for number in ['1', '2', '3']: + result += f'b{number}' # [consider-using-join] +assert result == 'ab1b2b3' +assert result == 'b'.join(['a', '1', '2', '3']) + +result = 'a' +for number in ['1', '2', '3']: + result += f'{number}c' # [consider-using-join] +assert result == 'a1c2c3c' +assert result == 'a' + 'c'.join(['1', '2', '3']) + 'c' + +result = 'a' +for number in ['1', '2', '3']: + result += f'b{number}c' # [consider-using-join] +assert result == 'ab1cb2cb3c' +assert result == 'ab' + 'cb'.join(['1', '2', '3']) + 'c' + +result = '' +for number in ['1', '2', '3']: + result += number # [consider-using-join] + +result = '' +for number in ['1', '2', '3']: + result += f"{number}, " # [consider-using-join] +result = result[:-2] + result = 0 # result is not a string for number in ['1', '2', '3']: result += number @@ -124,3 +151,11 @@ result['context'] = 0 for number in ['1']: result['context'] += 24 + +result = '' +for number in ['1', '2', '3']: + result += f' {result}' # f-string contains wrong name + +result = '' +for number in ['1', '2', '3']: + result += f' {number} {number} {number} ' # f-string contains several names diff --git a/tests/functional/c/consider/consider_join.txt b/tests/functional/c/consider/consider_join.txt index baea768be8..fa9b427e38 100644 --- a/tests/functional/c/consider/consider_join.txt +++ b/tests/functional/c/consider/consider_join.txt @@ -2,10 +2,15 @@ consider-using-join:6:4:6:20::Consider using str.join(sequence) for concatenatin consider-using-join:10:4:10:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED consider-using-join:14:4:14:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED consider-using-join:18:4:18:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:58:4:58:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:62:4:62:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:66:4:66:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:71:4:71:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:75:4:75:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:79:4:79:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:110:31:110:47::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:22:4:22:26::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:28:4:28:26::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:34:4:34:27::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:40:4:40:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:44:4:44:27::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:85:4:85:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:89:4:89:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:93:4:93:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:98:4:98:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:102:4:102:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:106:4:106:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:137:31:137:47::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index d43c0f0cf7..280ffecf37 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -3,7 +3,7 @@ consider-using-dict-items:9:4:10:30:bad:Consider iterating with .items():UNDEFIN 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():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 @@ -11,6 +11,6 @@ consider-using-dict-items:68:0:None:None::Consider iterating with .items():UNDEF 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():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 diff --git a/tests/functional/c/consider/consider_using_enumerate.py b/tests/functional/c/consider/consider_using_enumerate.py index 359bb29fa3..352669f6a6 100644 --- a/tests/functional/c/consider/consider_using_enumerate.py +++ b/tests/functional/c/consider/consider_using_enumerate.py @@ -1,6 +1,6 @@ """Emit a message for iteration through range and len is encountered.""" -# pylint: disable=missing-docstring, import-error, useless-object-inheritance, unsubscriptable-object, too-few-public-methods, unnecessary-list-index-lookup +# pylint: disable=missing-docstring, import-error, unsubscriptable-object, too-few-public-methods, unnecessary-list-index-lookup def bad(): iterable = [1, 2, 3] @@ -10,7 +10,7 @@ def bad(): yield iterable[obj] -class Bad(object): +class Bad: def __iter__(self): iterable = [1, 2, 3] @@ -60,7 +60,7 @@ def test(iterable): yield test([1, 2, 3]) -class Good(object): +class Good: def __iter__(self): # Should not suggest enumerate on self @@ -74,7 +74,7 @@ def does_not_crash_on_range_without_args(): # False negative described in #3657 # https://github.com/PyCQA/pylint/issues/3657 -class MyClass(object): +class MyClass: def __init__(self): self.my_list = [] diff --git a/tests/functional/c/consider/consider_using_in.txt b/tests/functional/c/consider/consider_using_in.txt index a2b402378c..08a9172ad0 100644 --- a/tests/functional/c/consider/consider_using_in.txt +++ b/tests/functional/c/consider/consider_using_in.txt @@ -1,14 +1,14 @@ -consider-using-in:10:0:10:24::"Consider merging these comparisons with ""in"" to 'value in (1,)'":UNDEFINED -consider-using-in:11:0:11:24::"Consider merging these comparisons with ""in"" to 'value in (1, 2)'":UNDEFINED -consider-using-in:12:0:12:36::"Consider merging these comparisons with ""in"" to ""value in ('value',)""":UNDEFINED -consider-using-in:13:0:13:34::"Consider merging these comparisons with ""in"" to 'value in (1, undef_value)'":UNDEFINED -consider-using-in:14:0:14:38::"Consider merging these comparisons with ""in"" to 'value in (1, 2, 3)'":UNDEFINED -consider-using-in:15:0:15:26::"Consider merging these comparisons with ""in"" to ""value in ('2', 1)""":UNDEFINED -consider-using-in:16:0:16:24::"Consider merging these comparisons with ""in"" to 'value in (1, 2)'":UNDEFINED -consider-using-in:17:0:17:24::"Consider merging these comparisons with ""in"" to 'value in (1, 2)'":UNDEFINED -consider-using-in:18:0:18:29::"Consider merging these comparisons with ""in"" to 'value in (1, a_list)'":UNDEFINED -consider-using-in:19:0:19:51::"Consider merging these comparisons with ""in"" to 'value in (a_set, a_list, a_str)'":UNDEFINED -consider-using-in:20:0:20:25::"Consider merging these comparisons with ""in"" to 'value not in (1, 2)'":UNDEFINED -consider-using-in:21:0:21:36::"Consider merging these comparisons with ""in"" to 'value1 in (value2,)'":UNDEFINED -consider-using-in:22:0:22:35::"Consider merging these comparisons with ""in"" to 'a_list in ([1, 2, 3], [])'":UNDEFINED -consider-using-in:53:0:53:28::"Consider merging these comparisons with ""in"" to 'A.value in (1, 2)'":UNDEFINED +consider-using-in:10:0:10:24::Consider merging these comparisons with 'in' by using 'value in (1,)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:11:0:11:24::Consider merging these comparisons with 'in' by using 'value in (1, 2)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:12:0:12:36::Consider merging these comparisons with 'in' by using 'value in ('value',)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:13:0:13:34::Consider merging these comparisons with 'in' by using 'value in (1, undef_value)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:14:0:14:38::Consider merging these comparisons with 'in' by using 'value in (1, 2, 3)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:15:0:15:26::Consider merging these comparisons with 'in' by using 'value in ('2', 1)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:16:0:16:24::Consider merging these comparisons with 'in' by using 'value in (1, 2)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:17:0:17:24::Consider merging these comparisons with 'in' by using 'value in (1, 2)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:18:0:18:29::Consider merging these comparisons with 'in' by using 'value in (1, a_list)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:19:0:19:51::Consider merging these comparisons with 'in' by using 'value in (a_set, a_list, a_str)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:20:0:20:25::Consider merging these comparisons with 'in' by using 'value not in (1, 2)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:21:0:21:36::Consider merging these comparisons with 'in' by using 'value1 in (value2,)'. Use a set instead if elements are hashable.:HIGH +consider-using-in:22:0:22:35::Consider merging these comparisons with 'in' by using 'a_list in ([1, 2, 3], [])'. Use a set instead if elements are hashable.:HIGH +consider-using-in:53:0:53:28::Consider merging these comparisons with 'in' by using 'A.value in (1, 2)'. Use a set instead if elements are hashable.:HIGH diff --git a/tests/functional/c/consider/consider_using_sys_exit.txt b/tests/functional/c/consider/consider_using_sys_exit.txt index 8cdeb6ba84..5c48cddc05 100644 --- a/tests/functional/c/consider/consider_using_sys_exit.txt +++ b/tests/functional/c/consider/consider_using_sys_exit.txt @@ -1,3 +1,3 @@ -consider-using-sys-exit:5:4:5:10:foo:Consider using sys.exit():UNDEFINED -consider-using-sys-exit:8:4:8:10:foo_1:Consider using sys.exit():UNDEFINED -consider-using-sys-exit:14:0:14:6::Consider using sys.exit():UNDEFINED +consider-using-sys-exit:5:4:5:10:foo:Consider using 'sys.exit' instead:HIGH +consider-using-sys-exit:8:4:8:10:foo_1:Consider using 'sys.exit' instead:HIGH +consider-using-sys-exit:14:0:14:6::Consider using 'sys.exit' instead:HIGH diff --git a/tests/functional/c/consider/consider_using_with.py b/tests/functional/c/consider/consider_using_with.py index 3466e15c0a..1c87582ee2 100644 --- a/tests/functional/c/consider/consider_using_with.py +++ b/tests/functional/c/consider/consider_using_with.py @@ -2,6 +2,7 @@ import codecs import contextlib import multiprocessing +import pathlib import subprocess import tarfile import tempfile @@ -9,6 +10,16 @@ import urllib import zipfile from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor +from pathlib import Path + + +def test_pathlib_open(): + _ = pathlib.Path("foo").open(encoding="utf8") # [consider-using-with] + _ = Path("foo").open(encoding="utf8") # [consider-using-with] + path = Path("foo") + _ = path.open(encoding="utf8") # [consider-using-with] + with Path("foo").open(encoding="utf8") as file: # must not trigger + _ = file.read() def test_codecs_open(): diff --git a/tests/functional/c/consider/consider_using_with.txt b/tests/functional/c/consider/consider_using_with.txt index 973d974035..455762f57d 100644 --- a/tests/functional/c/consider/consider_using_with.txt +++ b/tests/functional/c/consider/consider_using_with.txt @@ -1,25 +1,28 @@ -consider-using-with:15:9:15:40:test_codecs_open:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:20:8:20:55:test_urlopen:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:28:8:28:40:test_named_temporary_file:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:32:8:32:42:test_spooled_temporary_file:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:36:8:36:37:test_temporary_directory:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:40:12:40:44:test_zipfile:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:41:8:41:30:test_zipfile:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:45:12:45:46:test_pyzipfile:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:50:8:50:30:test_pyzipfile:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:57:9:57:43:test_tarfile:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:63:9:63:47:test_tarfile:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:72:4:72:18:test_lock_acquisition:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:79:4:79:19:test_lock_acquisition:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:86:4:86:18:test_lock_acquisition:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:93:4:93:26:test_lock_acquisition:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:129:8:129:30:test_multiprocessing:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:134:4:134:19:test_multiprocessing:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:139:4:139:19:test_multiprocessing:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:145:8:145:30:test_popen:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:201:4:201:26::Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:202:4:202:26::Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:207:4:207:26::Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:17:8:17:49:test_pathlib_open:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:18:8:18:41:test_pathlib_open:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:20:8:20:34:test_pathlib_open:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:26:9:26:40:test_codecs_open:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:31:8:31:55:test_urlopen:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:39:8:39:40:test_named_temporary_file:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:43:8:43:42:test_spooled_temporary_file:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:47:8:47:37:test_temporary_directory:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:51:12:51:44:test_zipfile:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:52:8:52:30:test_zipfile:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:56:12:56:46:test_pyzipfile:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:61:8:61:30:test_pyzipfile:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:68:9:68:43:test_tarfile:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:74:9:74:47:test_tarfile:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:83:4:83:18:test_lock_acquisition:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:90:4:90:19:test_lock_acquisition:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:97:4:97:18:test_lock_acquisition:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:104:4:104:26:test_lock_acquisition:Consider using 'with' for resource-allocating operations:UNDEFINED +consider-using-with:140:8:140:30:test_multiprocessing:Consider using 'with' for resource-allocating operations:UNDEFINED +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:229:18:229:40:test_subscript_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED -consider-using-with:231:24:231:46:test_subscript_assignment: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 diff --git a/tests/functional/c/continue_in_finally.rc b/tests/functional/c/continue_in_finally.rc index 67a28a36aa..77eb3be645 100644 --- a/tests/functional/c/continue_in_finally.rc +++ b/tests/functional/c/continue_in_finally.rc @@ -1,2 +1,2 @@ -[testoptions] -max_pyver=3.8 +[main] +py-version=3.7 diff --git a/tests/functional/c/continue_in_finally.txt b/tests/functional/c/continue_in_finally.txt index 3a5e542e1a..a427b33691 100644 --- a/tests/functional/c/continue_in_finally.txt +++ b/tests/functional/c/continue_in_finally.txt @@ -1 +1 @@ -continue-in-finally:9:::'continue' not supported inside 'finally' clause +continue-in-finally:9:8:9:16::'continue' not supported inside 'finally' clause:UNDEFINED diff --git a/tests/functional/c/crash_missing_module_type.py b/tests/functional/c/crash_missing_module_type.py index d80d27afcb..94b9af6ec9 100644 --- a/tests/functional/c/crash_missing_module_type.py +++ b/tests/functional/c/crash_missing_module_type.py @@ -1,12 +1,12 @@ """ Test for a crash found in https://bitbucket.org/logilab/astroid/issue/45/attributeerror-module-object-has-no#comment-11944673 """ -# pylint: disable=invalid-name, too-few-public-methods, redefined-outer-name, useless-object-inheritance +# pylint: disable=invalid-name, too-few-public-methods, redefined-outer-name def decor(trop): """ decorator """ return trop -class Foo(object): +class Foo: """ Class """ @decor def prop(self): diff --git a/tests/functional/c/ctor_arguments.py b/tests/functional/c/ctor_arguments.py index ee10413e3b..4ed96c2d3c 100644 --- a/tests/functional/c/ctor_arguments.py +++ b/tests/functional/c/ctor_arguments.py @@ -2,18 +2,18 @@ Based on tests/functional/a/arguments.py """ -# pylint: disable=missing-docstring,too-few-public-methods,super-init-not-called,useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods,super-init-not-called -class Class1Arg(object): +class Class1Arg: def __init__(self, first_argument): """one argument function""" -class Class3Arg(object): +class Class3Arg: def __init__(self, first_argument, second_argument, third_argument): """three arguments function""" -class ClassDefaultArg(object): +class ClassDefaultArg: def __init__(self, one=1, two=2): """function with default value""" @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): class ClassMultiInheritance(Class1Arg, Class3Arg): pass -class ClassNew(object): +class ClassNew: def __new__(cls, first_argument, kwarg=None): return first_argument, kwarg @@ -65,8 +65,8 @@ def __new__(cls, first_argument, kwarg=None): class Metaclass(type): - def __new__(cls, name, bases, namespace): - return type.__new__(cls, name, bases, namespace) + def __new__(mcs, name, bases, namespace): + return type.__new__(mcs, name, bases, namespace) def with_metaclass(meta, base=object): """Create a new type that can be used as a metaclass.""" @@ -82,10 +82,10 @@ class BuiltinExc(Exception): def __init__(self, val=True): self.val = val -BuiltinExc(42, 24, badarg=1) # [too-many-function-args,unexpected-keyword-arg] +BuiltinExc(42, 24, badarg=1) # [line-too-long,pointless-exception-statement,too-many-function-args,unexpected-keyword-arg] -class Clsmethod(object): +class Clsmethod: def __init__(self, first, second): self.first = first self.second = second diff --git a/tests/functional/c/ctor_arguments.txt b/tests/functional/c/ctor_arguments.txt index 286a56bd79..f096eed4ea 100644 --- a/tests/functional/c/ctor_arguments.txt +++ b/tests/functional/c/ctor_arguments.txt @@ -15,6 +15,8 @@ too-many-function-args:60:0:60:30::Too many positional arguments for constructor too-many-function-args:63:0:63:17::Too many positional arguments for constructor call:UNDEFINED no-value-for-parameter:64:0:64:15::No value for argument 'first_argument' in constructor call:UNDEFINED unexpected-keyword-arg:64:0:64:15::Unexpected keyword argument 'one' in constructor call:UNDEFINED +line-too-long:85:0::::Line too long (122/100):UNDEFINED +pointless-exception-statement:85:0:85:28::Exception statement has no effect:INFERENCE too-many-function-args:85:0:85:28::Too many positional arguments for constructor call:UNDEFINED unexpected-keyword-arg:85:0:85:28::Unexpected keyword argument 'badarg' in constructor call:UNDEFINED too-many-function-args:95:15:95:30:Clsmethod.from_nothing:Too many positional arguments for constructor call:UNDEFINED diff --git a/tests/functional/d/dangerous_default_value.py b/tests/functional/d/dangerous_default_value.py index 161eaceed3..a7ef4c3899 100644 --- a/tests/functional/d/dangerous_default_value.py +++ b/tests/functional/d/dangerous_default_value.py @@ -109,3 +109,14 @@ def function23(value=collections.UserList()): # [dangerous-default-value] def function24(*, value=[]): # [dangerous-default-value] """dangerous default value in kwarg.""" return value + + +class Clazz: + # pylint: disable=too-few-public-methods + def __init__( # [dangerous-default-value] + self, + arg: str = None, + *, + kk: dict = {}, + ) -> None: + pass diff --git a/tests/functional/d/dangerous_default_value.txt b/tests/functional/d/dangerous_default_value.txt index 98d55c2b62..2376b8e296 100644 --- a/tests/functional/d/dangerous_default_value.txt +++ b/tests/functional/d/dangerous_default_value.txt @@ -20,3 +20,4 @@ dangerous-default-value:97:0:97:14:function21:Dangerous default value defaultdic dangerous-default-value:101:0:101:14:function22:Dangerous default value UserDict() (collections.UserDict) as argument:UNDEFINED dangerous-default-value:105:0:105:14:function23:Dangerous default value UserList() (collections.UserList) as argument:UNDEFINED dangerous-default-value:109:0:109:14:function24:Dangerous default value [] as argument:UNDEFINED +dangerous-default-value:116:4:116:16:Clazz.__init__:Dangerous default value {} as argument:UNDEFINED diff --git a/tests/functional/d/dataclass/dataclass_kw_only.py b/tests/functional/d/dataclass/dataclass_kw_only.py new file mode 100644 index 0000000000..9cc5a23bbd --- /dev/null +++ b/tests/functional/d/dataclass/dataclass_kw_only.py @@ -0,0 +1,26 @@ +"""Test the behaviour of the kw_only keyword.""" + +# pylint: disable=invalid-name + +from dataclasses import dataclass + + +@dataclass(kw_only=True) +class FooBar: + """Simple dataclass with a kw_only parameter.""" + + a: int + b: str + + +@dataclass(kw_only=False) +class BarFoo(FooBar): + """Simple dataclass with a negated kw_only parameter.""" + + c: int + + +BarFoo(1, a=2, b="") +BarFoo( # [missing-kwoa,missing-kwoa,redundant-keyword-arg,too-many-function-args] + 1, 2, c=2 +) diff --git a/tests/functional/d/dataclass/dataclass_kw_only.rc b/tests/functional/d/dataclass/dataclass_kw_only.rc new file mode 100644 index 0000000000..68a8c8ef15 --- /dev/null +++ b/tests/functional/d/dataclass/dataclass_kw_only.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/d/dataclass/dataclass_kw_only.txt b/tests/functional/d/dataclass/dataclass_kw_only.txt new file mode 100644 index 0000000000..cacf0dbde9 --- /dev/null +++ b/tests/functional/d/dataclass/dataclass_kw_only.txt @@ -0,0 +1,4 @@ +missing-kwoa:24:0:26:1::Missing mandatory keyword argument 'a' in constructor call:INFERENCE +missing-kwoa:24:0:26:1::Missing mandatory keyword argument 'b' in constructor call:INFERENCE +redundant-keyword-arg:24:0:26:1::Argument 'c' passed by position and keyword in constructor call:UNDEFINED +too-many-function-args:24:0:26:1::Too many positional arguments for constructor call:UNDEFINED diff --git a/tests/functional/d/dataclass/dataclass_parameter.py b/tests/functional/d/dataclass/dataclass_parameter.py new file mode 100644 index 0000000000..34388c5048 --- /dev/null +++ b/tests/functional/d/dataclass/dataclass_parameter.py @@ -0,0 +1,27 @@ +"""Tests for dataclass and checks that check for parameters.""" + +import dataclasses +from dataclasses import KW_ONLY as keyword_only +from dataclasses import dataclass + + +@dataclass +class MyDataClass: + """Simple dataclass with a KW_ONLY parameter.""" + + _: dataclasses.KW_ONLY + data: str + + +MyDataClass(data="test") + + +@dataclass +class MyDataClassWithAliases: + """Simple dataclass with an aliased KW_ONLY parameter.""" + + _: keyword_only + data: str + + +MyDataClassWithAliases(data="test") diff --git a/tests/functional/d/dataclass/dataclass_parameter.rc b/tests/functional/d/dataclass/dataclass_parameter.rc new file mode 100644 index 0000000000..68a8c8ef15 --- /dev/null +++ b/tests/functional/d/dataclass/dataclass_parameter.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/d/dataclass_typecheck.py b/tests/functional/d/dataclass/dataclass_typecheck.py similarity index 92% rename from tests/functional/d/dataclass_typecheck.py rename to tests/functional/d/dataclass/dataclass_typecheck.py index 0de6e43b35..fbd7707049 100644 --- a/tests/functional/d/dataclass_typecheck.py +++ b/tests/functional/d/dataclass/dataclass_typecheck.py @@ -2,7 +2,13 @@ Tests for regressions from https://github.com/PyCQA/astroid/pull/1126 """ + # pylint: disable=missing-docstring,too-few-public-methods,pointless-statement,redefined-builtin, fixme + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from dataclasses import dataclass from typing import Callable, Dict, List, Optional @@ -102,7 +108,7 @@ class Test2(metaclass=obj.attr1): # [invalid-metaclass] {}[obj.attr0] = 1 {}[obj.attr1] = 1 -{}[obj.attr5] = 1 # [unhashable-dict-key] +{}[obj.attr5] = 1 # [unhashable-member] for k, v in obj.attr5: # TODO: Should be a dict-iter-missing-items error print(k, v) diff --git a/tests/functional/d/dataclass_typecheck.rc b/tests/functional/d/dataclass/dataclass_typecheck.rc similarity index 100% rename from tests/functional/d/dataclass_typecheck.rc rename to tests/functional/d/dataclass/dataclass_typecheck.rc diff --git a/tests/functional/d/dataclass/dataclass_typecheck.txt b/tests/functional/d/dataclass/dataclass_typecheck.txt new file mode 100644 index 0000000000..5afac5849c --- /dev/null +++ b/tests/functional/d/dataclass/dataclass_typecheck.txt @@ -0,0 +1,12 @@ +invalid-sequence-index:38:6:38:20::Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-slice-index:42:10:42:19::Slice index is not an int, None, or instance with __index__:UNDEFINED +not-callable:45:0:45:14::obj.attr1 is not callable:UNDEFINED +invalid-unary-operand-type:50:6:50:16::"bad operand type for unary -: str":UNDEFINED +unsupported-membership-test:57:11:57:20::Value 'obj.attr1' doesn't support membership test:UNDEFINED +unsubscriptable-object:62:6:62:15::Value 'obj.attr1' is unsubscriptable:UNDEFINED +unsupported-assignment-operation:67:0:67:9::'obj.attr1' does not support item assignment:UNDEFINED +unsupported-delete-operation:72:4:72:13::'obj.attr1' does not support item deletion:UNDEFINED +not-context-manager:97:0:98:8::Context manager 'str' doesn't implement __enter__ and __exit__.:UNDEFINED +invalid-metaclass:105:0:105:11:Test2:Invalid metaclass 'Instance of builtins.int' used:UNDEFINED +unhashable-member:111:0:111:2::'obj.attr5' is unhashable and can't be used as a key in a dict:INFERENCE +isinstance-second-argument-not-valid-type:121:6:121:30::Second argument of isinstance is not a type:UNDEFINED diff --git a/tests/functional/d/dataclass_with_default_factory.py b/tests/functional/d/dataclass/dataclass_with_default_factory.py similarity index 100% rename from tests/functional/d/dataclass_with_default_factory.py rename to tests/functional/d/dataclass/dataclass_with_default_factory.py diff --git a/tests/functional/d/dataclass_with_default_factory.rc b/tests/functional/d/dataclass/dataclass_with_default_factory.rc similarity index 100% rename from tests/functional/d/dataclass_with_default_factory.rc rename to tests/functional/d/dataclass/dataclass_with_default_factory.rc diff --git a/tests/functional/d/dataclass_with_default_factory.txt b/tests/functional/d/dataclass/dataclass_with_default_factory.txt similarity index 100% rename from tests/functional/d/dataclass_with_default_factory.txt rename to tests/functional/d/dataclass/dataclass_with_default_factory.txt diff --git a/tests/functional/d/dataclass_with_field.py b/tests/functional/d/dataclass/dataclass_with_field.py similarity index 100% rename from tests/functional/d/dataclass_with_field.py rename to tests/functional/d/dataclass/dataclass_with_field.py diff --git a/tests/functional/d/dataclass_with_field.rc b/tests/functional/d/dataclass/dataclass_with_field.rc similarity index 100% rename from tests/functional/d/dataclass_with_field.rc rename to tests/functional/d/dataclass/dataclass_with_field.rc diff --git a/tests/functional/d/dataclass_with_field.txt b/tests/functional/d/dataclass/dataclass_with_field.txt similarity index 100% rename from tests/functional/d/dataclass_with_field.txt rename to tests/functional/d/dataclass/dataclass_with_field.txt diff --git a/tests/functional/d/dataclass_typecheck.txt b/tests/functional/d/dataclass_typecheck.txt deleted file mode 100644 index 97f5860bb6..0000000000 --- a/tests/functional/d/dataclass_typecheck.txt +++ /dev/null @@ -1,12 +0,0 @@ -invalid-sequence-index:32:6:32:20::Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-slice-index:36:10:36:19::Slice index is not an int, None, or instance with __index__:UNDEFINED -not-callable:39:0:39:14::obj.attr1 is not callable:UNDEFINED -invalid-unary-operand-type:44:6:44:16::"bad operand type for unary -: str":UNDEFINED -unsupported-membership-test:51:11:51:20::Value 'obj.attr1' doesn't support membership test:UNDEFINED -unsubscriptable-object:56:6:56:15::Value 'obj.attr1' is unsubscriptable:UNDEFINED -unsupported-assignment-operation:61:0:61:9::'obj.attr1' does not support item assignment:UNDEFINED -unsupported-delete-operation:66:4:66:13::'obj.attr1' does not support item deletion:UNDEFINED -not-context-manager:91:0:92:8::Context manager 'str' doesn't implement __enter__ and __exit__.:UNDEFINED -invalid-metaclass:99:0:99:11:Test2:Invalid metaclass 'Instance of builtins.int' used:UNDEFINED -unhashable-dict-key:105:0:105:2::Dict key is unhashable:UNDEFINED -isinstance-second-argument-not-valid-type:115:6:115:30::Second argument of isinstance is not a type:UNDEFINED diff --git a/tests/functional/d/decorator_scope.py b/tests/functional/d/decorator_scope.py index 2f6d037c6a..f499e15db9 100644 --- a/tests/functional/d/decorator_scope.py +++ b/tests/functional/d/decorator_scope.py @@ -1,4 +1,4 @@ -# -*- pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-lambda-assignment +# -*- pylint: disable=too-few-public-methods, unnecessary-lambda-assignment """Test that decorators sees the class namespace - just like function default values does but function body doesn't. @@ -6,9 +6,8 @@ https://www.logilab.net/elo/ticket/5626 - name resolution bug inside classes """ -from __future__ import print_function -class Test(object): +class Test: """test class""" ident = lambda x: x diff --git a/tests/functional/d/defined_and_used_on_same_line.py b/tests/functional/d/defined_and_used_on_same_line.py index e7d1ab3e4a..bec45419c5 100644 --- a/tests/functional/d/defined_and_used_on_same_line.py +++ b/tests/functional/d/defined_and_used_on_same_line.py @@ -1,6 +1,6 @@ """Check for definitions and usage happening on the same line.""" #pylint: disable=missing-docstring,multiple-statements,wrong-import-position,unnecessary-comprehension,unspecified-encoding,unnecessary-lambda-assignment -from __future__ import print_function + print([index for index in range(10)]) diff --git a/tests/functional/d/deprecated/deprecated_methods_py38.py b/tests/functional/d/deprecated/deprecated_methods_py38.py index 34f08a8be9..3a7dfe862b 100644 --- a/tests/functional/d/deprecated/deprecated_methods_py38.py +++ b/tests/functional/d/deprecated/deprecated_methods_py38.py @@ -4,10 +4,10 @@ import inspect import logging import nntplib +import time import unittest import xml.etree.ElementTree - class MyTest(unittest.TestCase): def test(self): self.assert_(True) # [deprecated-method] @@ -51,3 +51,7 @@ class Deprecated: # pylint: disable=too-few-public-methods d = Deprecated() d.deprecated_method() # [deprecated-method] + +def test(clock = time.time): + """time.clock is deprecated but time.time via an alias is not!""" + clock() diff --git a/tests/functional/d/deprecated/deprecated_methods_py39.rc b/tests/functional/d/deprecated/deprecated_methods_py39.rc index 16b75eea75..062f6df19c 100644 --- a/tests/functional/d/deprecated/deprecated_methods_py39.rc +++ b/tests/functional/d/deprecated/deprecated_methods_py39.rc @@ -1,2 +1,3 @@ [testoptions] min_pyver=3.9 +max_pyver=3.10 diff --git a/tests/functional/d/deprecated/deprecated_module_py39.rc b/tests/functional/d/deprecated/deprecated_module_py39.rc index 16b75eea75..062f6df19c 100644 --- a/tests/functional/d/deprecated/deprecated_module_py39.rc +++ b/tests/functional/d/deprecated/deprecated_module_py39.rc @@ -1,2 +1,3 @@ [testoptions] min_pyver=3.9 +max_pyver=3.10 diff --git a/tests/functional/d/deprecated/deprecated_module_py39_earlier_pyversion.rc b/tests/functional/d/deprecated/deprecated_module_py39_earlier_pyversion.rc index 85f6999e08..09ceaa5e54 100644 --- a/tests/functional/d/deprecated/deprecated_module_py39_earlier_pyversion.rc +++ b/tests/functional/d/deprecated/deprecated_module_py39_earlier_pyversion.rc @@ -3,3 +3,4 @@ py-version=3.8 [testoptions] min_pyver=3.9 +max_pyver=3.10 diff --git a/tests/functional/d/disable_msg_next_line.py b/tests/functional/d/disable_msg_next_line.py index f500feb1ea..ea283a4276 100644 --- a/tests/functional/d/disable_msg_next_line.py +++ b/tests/functional/d/disable_msg_next_line.py @@ -18,3 +18,10 @@ def function_C(): def function_D(arg1, arg2): # [unused-argument, invalid-name] return arg1 + + +def function_E(): # [invalid-name] + # pylint: disable-next=unused-variable + + test = 43 # [unused-variable] + blah = 123 # [unused-variable] diff --git a/tests/functional/d/disable_msg_next_line.txt b/tests/functional/d/disable_msg_next_line.txt index 36ba9527d2..794cfbb98d 100644 --- a/tests/functional/d/disable_msg_next_line.txt +++ b/tests/functional/d/disable_msg_next_line.txt @@ -3,3 +3,6 @@ unused-variable:15:4:15:5:function_C:Unused variable 'x':UNDEFINED f-string-without-interpolation:16:11:16:44:function_C:Using an f-string that does not have any interpolated variables:UNDEFINED invalid-name:19:0:19:14:function_D:"Function name ""function_D"" doesn't conform to snake_case naming style":HIGH unused-argument:19:21:19:25:function_D:Unused argument 'arg2':HIGH +invalid-name:23:0:23:14:function_E:"Function name ""function_E"" doesn't conform to snake_case naming style":HIGH +unused-variable:26:4:26:8:function_E:Unused variable 'test':UNDEFINED +unused-variable:27:4:27:8:function_E:Unused variable 'blah':UNDEFINED diff --git a/tests/functional/d/disallowed_name.py b/tests/functional/d/disallowed_name.py new file mode 100644 index 0000000000..a6155dd346 --- /dev/null +++ b/tests/functional/d/disallowed_name.py @@ -0,0 +1,11 @@ +# pylint: disable=missing-docstring,too-few-public-methods + +def baz(): # [disallowed-name] + pass + +class foo(): # [disallowed-name] + pass + +foo = {}.keys() # [disallowed-name] +foo = 42 # [disallowed-name] +aaa = 42 # [invalid-name] diff --git a/tests/functional/d/disallowed_name.txt b/tests/functional/d/disallowed_name.txt new file mode 100644 index 0000000000..535510a532 --- /dev/null +++ b/tests/functional/d/disallowed_name.txt @@ -0,0 +1,5 @@ +disallowed-name:3:0:3:7:baz:"Disallowed name ""baz""":HIGH +disallowed-name:6:0:6:9:foo:"Disallowed name ""foo""":HIGH +disallowed-name:9:0:9:3::"Disallowed name ""foo""":HIGH +disallowed-name:10:0:10:3::"Disallowed name ""foo""":HIGH +invalid-name:11:0:11:3::"Constant name ""aaa"" doesn't conform to UPPER_CASE naming style":HIGH diff --git a/tests/functional/d/docstrings.py b/tests/functional/d/docstrings.py index b9827ef964..f86c702ce9 100644 --- a/tests/functional/d/docstrings.py +++ b/tests/functional/d/docstrings.py @@ -1,6 +1,5 @@ -# pylint: disable=useless-object-inheritance, unnecessary-pass, consider-using-f-string +# pylint: disable=unnecessary-pass, consider-using-f-string # -1: [missing-module-docstring] -from __future__ import print_function # +1: [empty-docstring] def function0(): @@ -20,7 +19,7 @@ def function3(value): print(value) # +1: [missing-class-docstring] -class AAAA(object): +class AAAA: # missing docstring ## class BBBB: diff --git a/tests/functional/d/docstrings.txt b/tests/functional/d/docstrings.txt index d1e21f7af5..b66920fc82 100644 --- a/tests/functional/d/docstrings.txt +++ b/tests/functional/d/docstrings.txt @@ -1,8 +1,8 @@ missing-module-docstring:1:0:None:None::Missing module docstring:HIGH -empty-docstring:6:0:6:13:function0:Empty function docstring:HIGH -missing-function-docstring:10:0:10:13:function1:Missing function or method docstring:HIGH -missing-class-docstring:23:0:23:10:AAAA:Missing class docstring:HIGH -missing-function-docstring:40:4:40:15:AAAA.method1:Missing function or method docstring:INFERENCE -empty-docstring:48:4:48:15:AAAA.method3:Empty method docstring:INFERENCE -empty-docstring:62:4:62:15:DDDD.method2:Empty method docstring:INFERENCE -missing-function-docstring:70:4:70:15:DDDD.method4:Missing function or method docstring:INFERENCE +empty-docstring:5:0:5:13:function0:Empty function docstring:HIGH +missing-function-docstring:9:0:9:13:function1:Missing function or method docstring:HIGH +missing-class-docstring:22:0:22:10:AAAA:Missing class docstring:HIGH +missing-function-docstring:39:4:39:15:AAAA.method1:Missing function or method docstring:INFERENCE +empty-docstring:47:4:47:15:AAAA.method3:Empty method docstring:INFERENCE +empty-docstring:61:4:61:15:DDDD.method2:Empty method docstring:INFERENCE +missing-function-docstring:69:4:69:15:DDDD.method4:Missing function or method docstring:INFERENCE diff --git a/tests/functional/d/duplicate_argument_name.py b/tests/functional/d/duplicate/duplicate_argument_name.py similarity index 100% rename from tests/functional/d/duplicate_argument_name.py rename to tests/functional/d/duplicate/duplicate_argument_name.py diff --git a/tests/functional/d/duplicate_argument_name.txt b/tests/functional/d/duplicate/duplicate_argument_name.txt similarity index 100% rename from tests/functional/d/duplicate_argument_name.txt rename to tests/functional/d/duplicate/duplicate_argument_name.txt diff --git a/tests/functional/d/duplicate_argument_name_py3.py b/tests/functional/d/duplicate/duplicate_argument_name_py3.py similarity index 100% rename from tests/functional/d/duplicate_argument_name_py3.py rename to tests/functional/d/duplicate/duplicate_argument_name_py3.py diff --git a/tests/functional/d/duplicate_argument_name_py3.txt b/tests/functional/d/duplicate/duplicate_argument_name_py3.txt similarity index 100% rename from tests/functional/d/duplicate_argument_name_py3.txt rename to tests/functional/d/duplicate/duplicate_argument_name_py3.txt diff --git a/tests/functional/d/duplicate_bases.py b/tests/functional/d/duplicate/duplicate_bases.py similarity index 100% rename from tests/functional/d/duplicate_bases.py rename to tests/functional/d/duplicate/duplicate_bases.py diff --git a/tests/functional/d/duplicate_bases.txt b/tests/functional/d/duplicate/duplicate_bases.txt similarity index 100% rename from tests/functional/d/duplicate_bases.txt rename to tests/functional/d/duplicate/duplicate_bases.txt diff --git a/tests/functional/d/duplicate_dict_literal_key.py b/tests/functional/d/duplicate/duplicate_dict_literal_key.py similarity index 100% rename from tests/functional/d/duplicate_dict_literal_key.py rename to tests/functional/d/duplicate/duplicate_dict_literal_key.py diff --git a/tests/functional/d/duplicate_dict_literal_key.txt b/tests/functional/d/duplicate/duplicate_dict_literal_key.txt similarity index 100% rename from tests/functional/d/duplicate_dict_literal_key.txt rename to tests/functional/d/duplicate/duplicate_dict_literal_key.txt diff --git a/tests/functional/d/duplicate_except.py b/tests/functional/d/duplicate/duplicate_except.py similarity index 100% rename from tests/functional/d/duplicate_except.py rename to tests/functional/d/duplicate/duplicate_except.py diff --git a/tests/functional/d/duplicate_except.txt b/tests/functional/d/duplicate/duplicate_except.txt similarity index 67% rename from tests/functional/d/duplicate_except.txt rename to tests/functional/d/duplicate/duplicate_except.txt index 8753f44b19..2bd56881af 100644 --- a/tests/functional/d/duplicate_except.txt +++ b/tests/functional/d/duplicate/duplicate_except.txt @@ -1 +1 @@ -duplicate-except:9:11:9:21:main:Catching previously caught exception type ValueError:UNDEFINED +duplicate-except:9:11:9:21:main:Catching previously caught exception type ValueError:INFERENCE diff --git a/tests/functional/d/duplicate_string_formatting_argument.py b/tests/functional/d/duplicate/duplicate_string_formatting_argument.py similarity index 100% rename from tests/functional/d/duplicate_string_formatting_argument.py rename to tests/functional/d/duplicate/duplicate_string_formatting_argument.py diff --git a/tests/functional/d/duplicate_string_formatting_argument.txt b/tests/functional/d/duplicate/duplicate_string_formatting_argument.txt similarity index 100% rename from tests/functional/d/duplicate_string_formatting_argument.txt rename to tests/functional/d/duplicate/duplicate_string_formatting_argument.txt diff --git a/tests/functional/d/duplicate_value.py b/tests/functional/d/duplicate/duplicate_value.py similarity index 100% rename from tests/functional/d/duplicate_value.py rename to tests/functional/d/duplicate/duplicate_value.py diff --git a/tests/functional/d/duplicate_value.txt b/tests/functional/d/duplicate/duplicate_value.txt similarity index 100% rename from tests/functional/d/duplicate_value.txt rename to tests/functional/d/duplicate/duplicate_value.txt diff --git a/tests/functional/e/e1101_9588_base_attr_aug_assign.py b/tests/functional/e/e1101_9588_base_attr_aug_assign.py index 7306840cb1..131dfc2c9b 100644 --- a/tests/functional/e/e1101_9588_base_attr_aug_assign.py +++ b/tests/functional/e/e1101_9588_base_attr_aug_assign.py @@ -1,4 +1,4 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods """ False positive case of E1101: @@ -7,9 +7,9 @@ https://www.logilab.org/ticket/9588 """ -__revision__ = 0 -class BaseClass(object): + +class BaseClass: "The base class" def __init__(self): "Set an attribute." @@ -31,7 +31,7 @@ class NegativeClass(BaseClass): def __init__(self): "Ordinary assignment is OK." BaseClass.__init__(self) - self.e1101 = self.e1101 + 1 + self.e1101 += self.e1101 def countup(self): "No problem." diff --git a/tests/functional/e/enum_subclasses.py b/tests/functional/e/enum_subclasses.py index c8493da785..6ad453a4b8 100644 --- a/tests/functional/e/enum_subclasses.py +++ b/tests/functional/e/enum_subclasses.py @@ -1,5 +1,5 @@ # pylint: disable=missing-docstring, invalid-name -from enum import Enum, IntEnum, auto +from enum import Enum, Flag, IntEnum, auto class Issue1932(IntEnum): @@ -60,6 +60,7 @@ class MyEnum(BaseEnum): print(MyEnum.FOO.value) + class TestBase(Enum): """Adds a special method to enums.""" @@ -77,3 +78,18 @@ class TestEnum(TestBase): test_enum = TestEnum.a assert test_enum.hello_pylint() == test_enum.name + + +# Check combinations of Flag members using the bitwise operators (&, |, ^, ~) +# https://github.com/PyCQA/pylint/issues/7381 +class Colour(Flag): + NONE = 0 + RED = 2 + GREEN = 2 + BLUE = 4 + + +and_expr = Colour.RED & Colour.GREEN & Colour.BLUE +and_expr_with_complement = ~Colour.RED & ~Colour.GREEN & ~Colour.BLUE +or_expr = Colour.RED | Colour.GREEN | Colour.BLUE +xor_expr = Colour.RED ^ Colour.GREEN ^ Colour.BLUE diff --git a/tests/functional/e/exception_is_binary_op.py b/tests/functional/e/exception_is_binary_op.py index f71163d1f6..6840d820d7 100644 --- a/tests/functional/e/exception_is_binary_op.py +++ b/tests/functional/e/exception_is_binary_op.py @@ -1,5 +1,5 @@ """Warn about binary operations used as exceptions.""" -from __future__ import print_function + try: pass except Exception or BaseException: # [binary-op-exception] diff --git a/tests/functional/e/exception_is_binary_op.txt b/tests/functional/e/exception_is_binary_op.txt index 4871a71d2d..de371e42ed 100644 --- a/tests/functional/e/exception_is_binary_op.txt +++ b/tests/functional/e/exception_is_binary_op.txt @@ -1,4 +1,4 @@ -binary-op-exception:5:0:6:20::"Exception to catch is the result of a binary ""or"" operation":UNDEFINED -binary-op-exception:7:0:8:20::"Exception to catch is the result of a binary ""and"" operation":UNDEFINED -binary-op-exception:9:0:10:20::"Exception to catch is the result of a binary ""or"" operation":UNDEFINED -binary-op-exception:11:0:12:20::"Exception to catch is the result of a binary ""or"" operation":UNDEFINED +binary-op-exception:5:0:6:20::"Exception to catch is the result of a binary ""or"" operation":HIGH +binary-op-exception:7:0:8:20::"Exception to catch is the result of a binary ""and"" operation":HIGH +binary-op-exception:9:0:10:20::"Exception to catch is the result of a binary ""or"" operation":HIGH +binary-op-exception:11:0:12:20::"Exception to catch is the result of a binary ""or"" operation":HIGH diff --git a/tests/functional/e/excess_escapes.py b/tests/functional/e/excess_escapes.py index d680c2b5e0..75836572ca 100644 --- a/tests/functional/e/excess_escapes.py +++ b/tests/functional/e/excess_escapes.py @@ -2,7 +2,6 @@ """Stray backslash escapes may be missing a raw-string prefix.""" # pylint: disable=redundant-u-string-prefix -__revision__ = '$Id$' # Bad escape sequences, which probably don't do what you expect. A = "\[\]\\" # [anomalous-backslash-in-string,anomalous-backslash-in-string] diff --git a/tests/functional/e/excess_escapes.txt b/tests/functional/e/excess_escapes.txt index 1555d8ebb8..c9446828fb 100644 --- a/tests/functional/e/excess_escapes.txt +++ b/tests/functional/e/excess_escapes.txt @@ -1,9 +1,9 @@ -anomalous-backslash-in-string:8:5:None:None::"Anomalous backslash in string: '\['. String constant might be missing an r prefix.":UNDEFINED -anomalous-backslash-in-string:8:7:None:None::"Anomalous backslash in string: '\]'. String constant might be missing an r prefix.":UNDEFINED -anomalous-backslash-in-string:9:8:None:None::"Anomalous backslash in string: '\/'. String constant might be missing an r prefix.":UNDEFINED -anomalous-backslash-in-string:10:20:None:None::"Anomalous backslash in string: '\`'. String constant might be missing an r prefix.":UNDEFINED -anomalous-backslash-in-string:17:15:None:None::"Anomalous backslash in string: '\o'. String constant might be missing an r prefix.":UNDEFINED -anomalous-backslash-in-string:17:20:None:None::"Anomalous backslash in string: '\o'. String constant might be missing an r prefix.":UNDEFINED -anomalous-backslash-in-string:19:13:None:None::"Anomalous backslash in string: '\8'. String constant might be missing an r prefix.":UNDEFINED -anomalous-backslash-in-string:19:17:None:None::"Anomalous backslash in string: '\9'. String constant might be missing an r prefix.":UNDEFINED -anomalous-backslash-in-string:32:42:None:None::"Anomalous backslash in string: '\P'. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:7:5:None:None::"Anomalous backslash in string: '\['. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:7:7:None:None::"Anomalous backslash in string: '\]'. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:8:8:None:None::"Anomalous backslash in string: '\/'. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:9:20:None:None::"Anomalous backslash in string: '\`'. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:16:15:None:None::"Anomalous backslash in string: '\o'. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:16:20:None:None::"Anomalous backslash in string: '\o'. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:18:13:None:None::"Anomalous backslash in string: '\8'. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:18:17:None:None::"Anomalous backslash in string: '\9'. String constant might be missing an r prefix.":UNDEFINED +anomalous-backslash-in-string:31:42:None:None::"Anomalous backslash in string: '\P'. String constant might be missing an r prefix.":UNDEFINED diff --git a/tests/functional/e/exec_used.py b/tests/functional/e/exec_used.py index 5f849480e8..4f7e3b8cc5 100644 --- a/tests/functional/e/exec_used.py +++ b/tests/functional/e/exec_used.py @@ -1,6 +1,6 @@ # pylint: disable=missing-docstring -exec('a = __revision__') # [exec-used] +exec('a = 42') # [exec-used] exec('a = 1', globals={}) # [exec-used] exec('a = 1', globals=globals()) # [exec-used] diff --git a/tests/functional/e/exec_used.txt b/tests/functional/e/exec_used.txt index d25bcfd288..964a1fd162 100644 --- a/tests/functional/e/exec_used.txt +++ b/tests/functional/e/exec_used.txt @@ -1,4 +1,4 @@ -exec-used:3:0:3:24::Use of exec:UNDEFINED +exec-used:3:0:3:14::Use of exec:UNDEFINED exec-used:4:0:4:25::Use of exec:UNDEFINED exec-used:6:0:6:32::Use of exec:UNDEFINED exec-used:9:4:9:17:func:Use of exec:UNDEFINED diff --git a/tests/functional/e/external_classmethod_crash.py b/tests/functional/e/external_classmethod_crash.py index c24bbd8723..5e3e0f1715 100644 --- a/tests/functional/e/external_classmethod_crash.py +++ b/tests/functional/e/external_classmethod_crash.py @@ -1,4 +1,4 @@ -# pylint: disable=too-few-public-methods,unused-argument,useless-object-inheritance +# pylint: disable=too-few-public-methods,unused-argument """tagging a function as a class method cause a crash when checking for signature overriding """ @@ -14,8 +14,6 @@ def fetch_order(cls, attr, var): fetch_order = classmethod(fetch_order) return fetch_order -class Aaa(object): +class Aaa: """hop""" fetch_order = fetch_config('A') - -__revision__ = None diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.py b/tests/functional/ext/bad_dunder/bad_dunder_name.py new file mode 100644 index 0000000000..48247aba03 --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.py @@ -0,0 +1,54 @@ +# pylint: disable=missing-module-docstring, missing-class-docstring, +# pylint: disable=missing-function-docstring, unused-private-member + + +class Apples: + __slots__ = ("a", "b") + + def __hello__(self): # [bad-dunder-name] + # not one of the explicitly defined dunder name methods + print("hello") + + def hello(self): + print("hello") + + def __init__(self): + pass + + def init(self): + # valid name even though someone could accidentally mean __init__ + pass + + def __init_(self): # [bad-dunder-name] + # author likely unintentionally misspelled the correct init dunder. + pass + + def _init_(self): # [bad-dunder-name] + # author likely unintentionally misspelled the correct init dunder. + pass + + def ___neg__(self): # [bad-dunder-name] + # author likely accidentally added an additional `_` + pass + + def __inv__(self): # [bad-dunder-name] + # author likely meant to call the invert dunder method + pass + + def __allowed__(self): + # user-configured allowed dunder name + pass + + def _protected_method(self): + print("Protected") + + def __private_method(self): + print("Private") + + @property + def __doc__(self): + return "Docstring" + + +def __increase_me__(val): + return val + 1 diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.rc b/tests/functional/ext/bad_dunder/bad_dunder_name.rc new file mode 100644 index 0000000000..0b449f3a30 --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.rc @@ -0,0 +1,4 @@ +[MAIN] +load-plugins=pylint.extensions.dunder + +good-dunder-names = __allowed__, diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.txt b/tests/functional/ext/bad_dunder/bad_dunder_name.txt new file mode 100644 index 0000000000..bb1d1e692c --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.txt @@ -0,0 +1,5 @@ +bad-dunder-name:8:4:8:17:Apples.__hello__:Bad or misspelled dunder method name __hello__.:HIGH +bad-dunder-name:22:4:22:15:Apples.__init_:Bad or misspelled dunder method name __init_.:HIGH +bad-dunder-name:26:4:26:14:Apples._init_:Bad or misspelled dunder method name _init_.:HIGH +bad-dunder-name:30:4:30:16:Apples.___neg__:Bad or misspelled dunder method name ___neg__.:HIGH +bad-dunder-name:34:4:34:15:Apples.__inv__:Bad or misspelled dunder method name __inv__.:HIGH diff --git a/tests/functional/ext/code_style/cs_consider_using_augmented_assign.py b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.py new file mode 100644 index 0000000000..417bc5c0be --- /dev/null +++ b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.py @@ -0,0 +1,135 @@ +"""Tests for consider-using-augmented-assign.""" + +# pylint: disable=invalid-name,too-few-public-methods,import-error,consider-using-f-string,missing-docstring + +from unknown import Unknown + +x = 1 + +# summation is commutative (for integer and float, but not for string) +x = x + 3 # [consider-using-augmented-assign] +x = 3 + x # [consider-using-augmented-assign] +x = x + "3" # [consider-using-augmented-assign] +x = "3" + x + +# We don't warn on intricate expressions as we lack knowledge of simplifying such +# expressions which is necessary to see if they can become augmented +x, y = 1 + x, 2 + x +x = 1 + x - 2 +x = 1 + x + 2 + +# For anything other than a float or an int we only want to warn on +# assignments where the 'itself' is on the left side of the assignment +my_list = [2, 3, 4] +my_list = [1] + my_list + + +class MyClass: + """Simple base class.""" + + def __init__(self) -> None: + self.x = 1 + self.x = self.x + 1 # [consider-using-augmented-assign] + self.x = 1 + self.x # [consider-using-augmented-assign] + + x = 1 # [redefined-outer-name] + self.x = x + + +instance = MyClass() + +x = instance.x + 1 + +my_str = "" +my_str = my_str + "foo" # [consider-using-augmented-assign] +my_str = "foo" + my_str + +my_bytes = b"" +my_bytes = my_bytes + b"foo" # [consider-using-augmented-assign] +my_bytes = b"foo" + my_bytes + + +def return_str() -> str: + """Return a string.""" + return "" + + +# Currently we disregard all calls +my_str = return_str() + my_str +my_str = my_str % return_str() +my_str = my_str % 1 # [consider-using-augmented-assign] +my_str = my_str % (1, 2) # [consider-using-augmented-assign] +my_str = "%s" % my_str +my_str = return_str() % my_str +my_str = Unknown % my_str +my_str = my_str % Unknown # [consider-using-augmented-assign] + +# subtraction is anti-commutative +x = x - 3 # [consider-using-augmented-assign] +x = 3 - x + +# multiplication is commutative +x = x * 3 # [consider-using-augmented-assign] +x = 3 * x # [consider-using-augmented-assign] + +# division is not commutative +x = x / 3 # [consider-using-augmented-assign] +x = 3 / x + +# integer division is not commutative +x = x // 3 # [consider-using-augmented-assign] +x = 3 // x + +# Left shift operator is not commutative +x = x << 3 # [consider-using-augmented-assign] +x = 3 << x + +# Right shift operator is not commutative +x = x >> 3 # [consider-using-augmented-assign] +x = 3 >> x + +# modulo is not commutative +x = x % 3 # [consider-using-augmented-assign] +x = 3 % x + +# exponential is not commutative +x = x**3 # [consider-using-augmented-assign] +x = 3**x + +# XOR is commutative +x = x ^ 3 # [consider-using-augmented-assign] +x = 3 ^ x # [consider-using-augmented-assign] + +# Bitwise AND operator is commutative +x = x & 3 # [consider-using-augmented-assign] +x = 3 & x # [consider-using-augmented-assign] + +# Bitwise OR operator is commutative +x = x | 3 # [consider-using-augmented-assign] +x = 3 | x # [consider-using-augmented-assign] + +x = x > 3 +x = 3 > x + +x = x < 3 +x = 3 < x + +x = x >= 3 +x = 3 >= x + +x = x <= 3 +x = 3 <= x + + +# https://github.com/PyCQA/pylint/issues/8086 +# consider-using-augmented-assign should only be flagged +# if names attribute names match exactly. + +class A: + def __init__(self) -> None: + self.a = 1 + self.b = A() + + def test(self) -> None: + self.a = self.a + 1 # [consider-using-augmented-assign] + self.b.a = self.a + 1 # Names don't match! diff --git a/tests/functional/ext/code_style/cs_consider_using_augmented_assign.rc b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.rc new file mode 100644 index 0000000000..5846022946 --- /dev/null +++ b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.rc @@ -0,0 +1,3 @@ +[MAIN] +load-plugins=pylint.extensions.code_style +enable=consider-using-augmented-assign diff --git a/tests/functional/ext/code_style/cs_consider_using_augmented_assign.txt b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.txt new file mode 100644 index 0000000000..f820eb67bf --- /dev/null +++ b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.txt @@ -0,0 +1,27 @@ +consider-using-augmented-assign:10:0:10:9::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:11:0:11:9::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:12:0:12:11::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:32:8:32:27:MyClass.__init__:Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:33:8:33:27:MyClass.__init__:Use '+=' to do an augmented assign directly:INFERENCE +redefined-outer-name:35:8:35:9:MyClass.__init__:Redefining name 'x' from outer scope (line 7):UNDEFINED +consider-using-augmented-assign:44:0:44:23::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:48:0:48:28::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:60:0:60:19::Use '%=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:61:0:61:24::Use '%=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:65:0:65:25::Use '%=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:68:0:68:9::Use '-=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:72:0:72:9::Use '*=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:73:0:73:9::Use '*=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:76:0:76:9::Use '/=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:80:0:80:10::Use '//=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:84:0:84:10::Use '<<=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:88:0:88:10::Use '>>=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:92:0:92:9::Use '%=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:96:0:96:8::Use '**=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:100:0:100:9::Use '^=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:101:0:101:9::Use '^=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:104:0:104:9::Use '&=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:105:0:105:9::Use '&=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:108:0:108:9::Use '|=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:109:0:109:9::Use '|=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:134:8:134:27:A.test:Use '+=' to do an augmented assign directly:INFERENCE diff --git a/tests/functional/ext/code_style/cs_consider_using_namedtuple_or_dataclass.py b/tests/functional/ext/code_style/cs_consider_using_namedtuple_or_dataclass.py index 627de76842..c72ca3d06c 100644 --- a/tests/functional/ext/code_style/cs_consider_using_namedtuple_or_dataclass.py +++ b/tests/functional/ext/code_style/cs_consider_using_namedtuple_or_dataclass.py @@ -23,6 +23,11 @@ class Foo: "entry_2": {0: None, 1: None}, } +# Subdicts have no common keys +MAPPING_4 = { + "entry_1": {"key_3": 0, "key_4": 1, "key_diff_1": 2}, + "entry_2": {"key_1": 0, "key_2": 1, "key_diff_2": 3}, +} def func(): # Not in module scope diff --git a/tests/functional/ext/code_style/cs_consider_using_namedtuple_or_dataclass.txt b/tests/functional/ext/code_style/cs_consider_using_namedtuple_or_dataclass.txt index bb1857fce2..d8772c1a42 100644 --- a/tests/functional/ext/code_style/cs_consider_using_namedtuple_or_dataclass.txt +++ b/tests/functional/ext/code_style/cs_consider_using_namedtuple_or_dataclass.txt @@ -1,5 +1,5 @@ consider-using-namedtuple-or-dataclass:11:12:14:1::Consider using namedtuple or dataclass for dictionary values:UNDEFINED consider-using-namedtuple-or-dataclass:15:12:18:1::Consider using namedtuple or dataclass for dictionary values:UNDEFINED -consider-using-namedtuple-or-dataclass:34:23:37:5:func:Consider using namedtuple or dataclass for dictionary values:UNDEFINED -consider-using-namedtuple-or-dataclass:41:12:44:1::Consider using namedtuple or dataclass for dictionary values:UNDEFINED -consider-using-namedtuple-or-dataclass:53:12:56:1::Consider using namedtuple or dataclass for dictionary values:UNDEFINED +consider-using-namedtuple-or-dataclass:39:23:42:5:func:Consider using namedtuple or dataclass for dictionary values:UNDEFINED +consider-using-namedtuple-or-dataclass:46:12:49:1::Consider using namedtuple or dataclass for dictionary values:UNDEFINED +consider-using-namedtuple-or-dataclass:58:12:61:1::Consider using namedtuple or dataclass for dictionary values:UNDEFINED diff --git a/tests/functional/ext/code_style/cs_consider_using_tuple.py b/tests/functional/ext/code_style/cs_consider_using_tuple.py index d243960795..57178c34ea 100644 --- a/tests/functional/ext/code_style/cs_consider_using_tuple.py +++ b/tests/functional/ext/code_style/cs_consider_using_tuple.py @@ -28,4 +28,4 @@ # Don't emit warning for sets as this is handled by builtin checker (x for x in {1, 2, 3}) # [use-sequence-for-iteration] -[x for x in {*var, 2}] # [use-sequence-for-iteration] +[x for x in {*var, 2}] diff --git a/tests/functional/ext/code_style/cs_consider_using_tuple.txt b/tests/functional/ext/code_style/cs_consider_using_tuple.txt index cd8ffb1e7b..565f5f7784 100644 --- a/tests/functional/ext/code_style/cs_consider_using_tuple.txt +++ b/tests/functional/ext/code_style/cs_consider_using_tuple.txt @@ -4,5 +4,4 @@ consider-using-tuple:18:12:18:21::Consider using an in-place tuple instead of li consider-using-tuple:21:9:21:15::Consider using an in-place tuple instead of list:UNDEFINED consider-using-tuple:23:9:23:18::Consider using an in-place tuple instead of list:UNDEFINED consider-using-tuple:26:12:26:21::Consider using an in-place tuple instead of list:UNDEFINED -use-sequence-for-iteration:30:12:30:21::Use a sequence type when iterating over values:UNDEFINED -use-sequence-for-iteration:31:12:31:21::Use a sequence type when iterating over values:UNDEFINED +use-sequence-for-iteration:30:12:30:21::Use a sequence type when iterating over values:HIGH diff --git a/tests/functional/ext/code_style/cs_default.py b/tests/functional/ext/code_style/cs_default.py new file mode 100644 index 0000000000..bd4edab36e --- /dev/null +++ b/tests/functional/ext/code_style/cs_default.py @@ -0,0 +1,6 @@ +"""Test default configuration for code-style checker.""" +# pylint: disable=invalid-name + +# consider-using-augmented-assign is disabled by default +x = 1 +x = x + 1 diff --git a/tests/functional/ext/code_style/cs_default.rc b/tests/functional/ext/code_style/cs_default.rc new file mode 100644 index 0000000000..8663ab085d --- /dev/null +++ b/tests/functional/ext/code_style/cs_default.rc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.code_style diff --git a/tests/functional/ext/comparetozero/comparetozero.py b/tests/functional/ext/comparetozero/compare_to_zero.py similarity index 51% rename from tests/functional/ext/comparetozero/comparetozero.py rename to tests/functional/ext/comparetozero/compare_to_zero.py index 29fd13994a..6a14b8bc95 100644 --- a/tests/functional/ext/comparetozero/comparetozero.py +++ b/tests/functional/ext/comparetozero/compare_to_zero.py @@ -1,4 +1,4 @@ -# pylint: disable=literal-comparison,missing-docstring +# pylint: disable=literal-comparison,missing-docstring, singleton-comparison X = 123 Y = len('test') @@ -6,15 +6,33 @@ if X is 0: # [compare-to-zero] pass +if X is False: + pass + if Y is not 0: # [compare-to-zero] pass +if Y is not False: + pass + if X == 0: # [compare-to-zero] pass +if X == False: + pass + +if 0 == Y: # [compare-to-zero] + pass + if Y != 0: # [compare-to-zero] pass +if 0 != X: # [compare-to-zero] + pass + +if Y != False: + pass + if X > 0: pass diff --git a/tests/functional/ext/comparetozero/comparetozero.rc b/tests/functional/ext/comparetozero/compare_to_zero.rc similarity index 100% rename from tests/functional/ext/comparetozero/comparetozero.rc rename to tests/functional/ext/comparetozero/compare_to_zero.rc diff --git a/tests/functional/ext/comparetozero/compare_to_zero.txt b/tests/functional/ext/comparetozero/compare_to_zero.txt new file mode 100644 index 0000000000..a413a32682 --- /dev/null +++ b/tests/functional/ext/comparetozero/compare_to_zero.txt @@ -0,0 +1,6 @@ +compare-to-zero:6:3:6:9::"""X is 0"" can be simplified to ""not X"" as 0 is falsey":HIGH +compare-to-zero:12:3:12:13::"""Y is not 0"" can be simplified to ""Y"" as 0 is falsey":HIGH +compare-to-zero:18:3:18:9::"""X == 0"" can be simplified to ""not X"" as 0 is falsey":HIGH +compare-to-zero:24:3:24:9::"""0 == Y"" can be simplified to ""not Y"" as 0 is falsey":HIGH +compare-to-zero:27:3:27:9::"""Y != 0"" can be simplified to ""Y"" as 0 is falsey":HIGH +compare-to-zero:30:3:30:9::"""0 != X"" can be simplified to ""X"" as 0 is falsey":HIGH diff --git a/tests/functional/ext/comparetozero/comparetozero.txt b/tests/functional/ext/comparetozero/comparetozero.txt deleted file mode 100644 index 34f76c94e2..0000000000 --- a/tests/functional/ext/comparetozero/comparetozero.txt +++ /dev/null @@ -1,4 +0,0 @@ -compare-to-zero:6:3:6:9::Avoid comparisons to zero:UNDEFINED -compare-to-zero:9:3:9:13::Avoid comparisons to zero:UNDEFINED -compare-to-zero:12:3:12:9::Avoid comparisons to zero:UNDEFINED -compare-to-zero:15:3:15:9::Avoid comparisons to zero:UNDEFINED diff --git a/tests/functional/ext/comparison_placement/misplaced_comparison_constant.py b/tests/functional/ext/comparison_placement/misplaced_comparison_constant.py index 0d02967420..82751f2b26 100644 --- a/tests/functional/ext/comparison_placement/misplaced_comparison_constant.py +++ b/tests/functional/ext/comparison_placement/misplaced_comparison_constant.py @@ -1,9 +1,9 @@ """Check that the constants are on the right side of the comparisons""" -# pylint: disable=singleton-comparison, missing-docstring, too-few-public-methods, useless-object-inheritance +# pylint: disable=singleton-comparison, missing-docstring, too-few-public-methods # pylint: disable=comparison-of-constants -class MyClass(object): +class MyClass: def __init__(self): self.attr = 1 diff --git a/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.py b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.py new file mode 100644 index 0000000000..404e00c825 --- /dev/null +++ b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.py @@ -0,0 +1,335 @@ +"""Emit a message for breaking out of a while True loop immediately.""" +# pylint: disable=missing-function-docstring,missing-class-docstring,unrecognized-inline-option,invalid-name,literal-comparison, undefined-variable, too-many-public-methods, no-else-break + +class Issue8015: + def bad(self): + k = 1 + while 1: # [consider-refactoring-into-while-condition] + if k == 10: + break + k += 1 + + def another_bad(self): + current_scope = None + while 2: # [consider-refactoring-into-while-condition] + if current_scope is None: + break + current_scope = True + + def good(self): + k = 1 + while True: + k += 1 + if k == 10: + break + + def another_good(self): + k = 1 + while k < 10: + k += 1 + + def test_error_message_multiple_break(self, k: int) -> None: + while True: # [consider-refactoring-into-while-condition] + if k <= 1: + break + if k > 10: + break + k -= 1 + + def test_error_message(self): + a_list = [1,2,3,4,5] + # Should recommend `while a_list` + while True: # [consider-refactoring-into-while-condition] + if not a_list: + break + a_list.pop() + + def test_error_message_2(self): + a_list = [] + # Should recommend `while not a_list` + while True: # [consider-refactoring-into-while-condition] + if a_list: + break + a_list.append(1) + + def test_error_message_3(self): + a_var = "defined" + # Should recommend `while not a_var` + while True: # [consider-refactoring-into-while-condition] + if a_var is not None: + break + a_var = None + + def test_error_message_4(self): + a_list = [] + # Should recommend `while a_list is []` + while True: # [consider-refactoring-into-while-condition] + if a_list is not []: + break + a_list.append(1) + + def test_error_message_5(self): + a_dict = {} + a_var = a_dict.get("undefined_key") + # Should recommend `while a_var` + while True: # [consider-refactoring-into-while-condition] + if a_var is None: + break + a_var = "defined" + + def test_error_message_6(self): + a_list = [] + # Should recommend `while a_list is not []` + while True: # [consider-refactoring-into-while-condition] + if a_list is []: + break + a_list.append(1) + + + def test_error_message_7(self): + # while not a and b is not correct + # Expeccted message should be `while not (a and b)`` + a = True + b = False + while True: # [consider-refactoring-into-while-condition] + if a and b: + break + a = 1 + b = 1 + + def test_error_message_8(self): + # while not a and not b is not correct + # Expeccted message should be `while not (a and not b)`` + a = True + b = False + while True: # [consider-refactoring-into-while-condition] + if a and not b: + break + a = 1 + b = 1 + + def test_error_message_9(self): + k = 1 + while True: # [consider-refactoring-into-while-condition] + if k != 1: + break + k += 1 + + def test_error_message_10(self): + a = [1,2,3,4,5] + while True: # [consider-refactoring-into-while-condition] + if 5 not in a: + break + a.pop() + + def test_error_message_11(self): + a = [] + k = 1 + while True: # [consider-refactoring-into-while-condition] + if 5 in a: + break + a.append(k) + k += 1 + + def test_error_message_12(self): + k = 1 + while True: # [consider-refactoring-into-while-condition] + if k > 10: + break + k += 1 + + def test_error_message_13(self): + k = 1 + while True: # [consider-refactoring-into-while-condition] + if k >= 10: + break + k += 1 + + def test_error_message_14(self): + k = 10 + while True: # [consider-refactoring-into-while-condition] + if k < 1: + break + k -= 1 + + def test_error_message_15(self): + k = 1 + while True: # [consider-refactoring-into-while-condition] + if k <= 1: + break + k -= 1 + + def test_error_message_16(self): + # Silly example but needed for coverage + k = None + while True: # [consider-refactoring-into-while-condition] + if (lambda x: x) == k: + break + break + while True: # [consider-refactoring-into-while-condition] + if k == (lambda x: x): + break + break + + def test_error_message_17(self): + a = True + b = False + c = True + d = False + while True: # [consider-refactoring-into-while-condition] + if (a or b) == (c and d): + break + a = not a if random.randint(0,1) == 1 else a + b = not b if random.randint(0,1) == 1 else b + c = not c if random.randint(0,1) == 1 else c + d = not d if random.randint(0,1) == 1 else d + + while True: # [consider-refactoring-into-while-condition] + if (not a) == (not d): + break + a = not a if random.randint(0,1) == 1 else a + d = not d if random.randint(0,1) == 1 else d + + def test_error_message_18(self): + x = 0 + while True: # [consider-refactoring-into-while-condition] + if x ** 2: + break + x += 1 + + def test_multi_break_condition_1(self): + x = 0 + # This should chain conditions into + # While (x == 0) and (x >= 0) and (x != 0): + while True: # [consider-refactoring-into-while-condition] + if x != 0: + break + elif x > 0: + x -= 1 + elif x < 0: + break + elif x == 0: + break + x -= 10 + + def test_multi_break_condition_2(self): + x = 0 + # This should chain both conditions + while True: # [consider-refactoring-into-while-condition] + if x != 0: + break + if x == 0: + break + x -= 10 + + def test_multi_break_condition_3(self): + x = 0 + # This should chain all conditions + while True: # [consider-refactoring-into-while-condition] + if x != 0: + break + elif x < 0: + break + elif x == 0: + break + if x != 100: + break + if x == 1000: + break + x -= 10 + + def test_multi_break_condition_4(self): + x = 0 + # This should chain all conditions except last 2. + # The else clause taints the first if-elif-else block by introducing mutation + while True: # [consider-refactoring-into-while-condition] + if x != 0: + break + elif x < 0: + break + elif x == 0: + break + else: + x += 1 + if x != 100: + break + if x == 1000: + break + x -= 10 + + def falsy_1(self): + x = 0 + while []: + if x > 10: + break + x += 1 + + def falsy_2(self): + x = 0 + while (): + if x > 10: + break + x += 1 + + def falsy_3(self): + x = 0 + while {}: + if x > 10: + break + x += 1 + + def falsy_4(self): + x = 0 + while set(): + if x > 10: + break + x += 1 + + def falsy_5(self): + x = 0 + while "": + if x > 10: + break + x += 1 + + def falsy_6(self): + x = 0 + while range(0): + if x > 10: + break + x += 1 + + def falsy_7(self): + x = 0 + while 0: + if x > 10: + break + x += 1 + + def falsy_8(self): + x = 0 + while 0.0: + if x > 10: + break + x += 1 + + def falsy_9(self): + x = 0 + while 0j: + if x > 10: + break + x += 1 + + def falsy_10(self): + x = 0 + while None: + if x > 10: + break + x += 1 + + def falsy_11(self): + x = 0 + while False: + if x > 10: + break + x += 1 diff --git a/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.rc b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.rc new file mode 100644 index 0000000000..7895977367 --- /dev/null +++ b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.rc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.consider_refactoring_into_while_condition, diff --git a/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.txt b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.txt new file mode 100644 index 0000000000..09ec2ee0af --- /dev/null +++ b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition.txt @@ -0,0 +1,27 @@ +consider-refactoring-into-while-condition:7:8:10:18:Issue8015.bad:"Consider using 'while k != 10' instead of 'while 1:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:14:8:17:32:Issue8015.another_bad:"Consider using 'while current_scope is not None' instead of 'while 2:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:32:8:37:18:Issue8015.test_error_message_multiple_break:"Consider using 'while (k > 1) and (k <= 10)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:42:8:45:24:Issue8015.test_error_message:"Consider using 'while a_list' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:50:8:53:28:Issue8015.test_error_message_2:"Consider using 'while not a_list' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:58:8:61:24:Issue8015.test_error_message_3:"Consider using 'while a_var is None' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:66:8:69:28:Issue8015.test_error_message_4:"Consider using 'while a_list is []' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:75:8:78:29:Issue8015.test_error_message_5:"Consider using 'while a_var is not None' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:83:8:86:28:Issue8015.test_error_message_6:"Consider using 'while a_list is not []' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:94:8:98:17:Issue8015.test_error_message_7:"Consider using 'while not (a and b)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:105:8:109:17:Issue8015.test_error_message_8:"Consider using 'while not (a and not b)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:113:8:116:18:Issue8015.test_error_message_9:"Consider using 'while k == 1' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:120:8:123:19:Issue8015.test_error_message_10:"Consider using 'while 5 in a' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:128:8:132:18:Issue8015.test_error_message_11:"Consider using 'while 5 not in a' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:136:8:139:18:Issue8015.test_error_message_12:"Consider using 'while k <= 10' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:143:8:146:18:Issue8015.test_error_message_13:"Consider using 'while k < 10' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:150:8:153:18:Issue8015.test_error_message_14:"Consider using 'while k >= 1' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:157:8:160:18:Issue8015.test_error_message_15:"Consider using 'while k > 1' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:165:8:168:17:Issue8015.test_error_message_16:"Consider using 'while (lambda x: x) != k' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:169:8:172:17:Issue8015.test_error_message_16:"Consider using 'while k != (lambda x: x)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:179:8:185:56:Issue8015.test_error_message_17:"Consider using 'while (a or b) != (c and d)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:187:8:191:56:Issue8015.test_error_message_17:"Consider using 'while (not a) != (not d)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:195:8:198:18:Issue8015.test_error_message_18:"Consider using 'while not x**2' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:204:8:213:19:Issue8015.test_multi_break_condition_1:"Consider using 'while (x == 0) and (x >= 0) and (x != 0)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:218:8:223:19:Issue8015.test_multi_break_condition_2:"Consider using 'while (x == 0) and (x != 0)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:228:8:239:19:Issue8015.test_multi_break_condition_3:"Consider using 'while (x == 0) and (x >= 0) and (x != 0) and (x == 100) and (x != 1000)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:245:8:258:19:Issue8015.test_multi_break_condition_4:"Consider using 'while (x == 0) and (x >= 0) and (x != 0)' instead of 'while True:' an 'if', and a 'break'":HIGH diff --git a/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.py b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.py new file mode 100644 index 0000000000..34bb98e48c --- /dev/null +++ b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.py @@ -0,0 +1,12 @@ +"""Emit a message for breaking out of a while True loop immediately.""" +# pylint: disable=missing-function-docstring,missing-class-docstring,unrecognized-inline-option,invalid-name,literal-comparison, undefined-variable, too-many-public-methods, too-few-public-methods + +class Issue8015: + def test_assignment_expr(self): + b = 10 + while True: # [consider-refactoring-into-while-condition] + if (a := 10) == (a := 10): + break + while True: # [consider-refactoring-into-while-condition] + if (a if a == 10 else 0) == (b if b == 10 else 0): + break diff --git a/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.rc b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.rc new file mode 100644 index 0000000000..8c697ac8e1 --- /dev/null +++ b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.rc @@ -0,0 +1,4 @@ +[MAIN] +load-plugins=pylint.extensions.consider_refactoring_into_while_condition, +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.txt b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.txt new file mode 100644 index 0000000000..05c57ad843 --- /dev/null +++ b/tests/functional/ext/consider_refactoring_into_while_condition/consider_refactoring_into_while_condition_py38.txt @@ -0,0 +1,2 @@ +consider-refactoring-into-while-condition:7:8:9:21:Issue8015.test_assignment_expr:"Consider using 'while (a := 10) != (a := 10)' instead of 'while True:' an 'if', and a 'break'":HIGH +consider-refactoring-into-while-condition:10:8:12:21:Issue8015.test_assignment_expr:"Consider using 'while (a if a == 10 else 0) != (b if b == 10 else 0)' instead of 'while True:' an 'if', and a 'break'":HIGH diff --git a/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.py b/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.py index 5ed5eaac40..5a6700ebef 100644 --- a/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.py +++ b/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.py @@ -1,3 +1,6 @@ +# pylint: disable=invalid-name, undefined-variable, unused-variable, missing-function-docstring, missing-module-docstring +# pylint: disable=unsupported-assignment-operation, line-too-long + if f(): # [consider-ternary-expression] x = 4 else: @@ -15,3 +18,19 @@ def a(): z = 4 else: z = 5 + +if f(): + x = 4 + print(x) +else: + x = 5 + +if f(): + x[0] = 4 +else: + x = 5 + +if f(): + x = 4 +else: + y = 5 diff --git a/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.rc b/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.rc index 7aecce4d31..11dbbe8f79 100644 --- a/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.rc +++ b/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.rc @@ -1,10 +1,2 @@ [MAIN] load-plugins=pylint.extensions.consider_ternary_expression, - -[MESSAGES CONTROL] -disable= - invalid-name, - undefined-variable, - unused-variable, - missing-function-docstring, - missing-module-docstring, diff --git a/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.txt b/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.txt index 02af32f45f..d7898e61bf 100644 --- a/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.txt +++ b/tests/functional/ext/consider_ternary_expression/consider_ternary_expression.txt @@ -1,2 +1,2 @@ -consider-ternary-expression:1:0:4:9::Consider rewriting as a ternary expression:UNDEFINED -consider-ternary-expression:14:4:17:13:a:Consider rewriting as a ternary expression:UNDEFINED +consider-ternary-expression:4:0:7:9::Consider rewriting as a ternary expression:UNDEFINED +consider-ternary-expression:17:4:20:13:a:Consider rewriting as a ternary expression:UNDEFINED diff --git a/tests/functional/ext/dict_init_mutate.py b/tests/functional/ext/dict_init_mutate.py new file mode 100644 index 0000000000..624f1a5eb1 --- /dev/null +++ b/tests/functional/ext/dict_init_mutate.py @@ -0,0 +1,41 @@ +"""Example cases for dict-init-mutate""" +# pylint: disable=use-dict-literal, invalid-name + +base = {} + +fruits = {} +for fruit in ["apple", "orange"]: + fruits[fruit] = 1 + fruits[fruit] += 1 + +count = 10 +fruits = {"apple": 1} +fruits["apple"] += count + +config = {} # [dict-init-mutate] +config['pwd'] = 'hello' + +config = {} # [dict-init-mutate] +config['dir'] = 'bin' +config['user'] = 'me' +config['workers'] = 5 +print(config) + +config = {} # Not flagging calls to update for now +config.update({"dir": "bin"}) + +config = {} # [dict-init-mutate] +config['options'] = {} # Identifying nested assignment not supporting this yet. +config['options']['debug'] = False +config['options']['verbose'] = True + + +config = {} +def update_dict(di): + """Update a dictionary""" + di["one"] = 1 + +update_dict(config) + +config = {} +globals()["config"]["dir"] = "bin" diff --git a/tests/functional/ext/dict_init_mutate.rc b/tests/functional/ext/dict_init_mutate.rc new file mode 100644 index 0000000000..bbe6bd1f78 --- /dev/null +++ b/tests/functional/ext/dict_init_mutate.rc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.dict_init_mutate, diff --git a/tests/functional/ext/dict_init_mutate.txt b/tests/functional/ext/dict_init_mutate.txt new file mode 100644 index 0000000000..c3702491f9 --- /dev/null +++ b/tests/functional/ext/dict_init_mutate.txt @@ -0,0 +1,3 @@ +dict-init-mutate:15:0:15:11::Declare all known key/values when initializing the dictionary.:HIGH +dict-init-mutate:18:0:18:11::Declare all known key/values when initializing the dictionary.:HIGH +dict-init-mutate:27:0:27:11::Declare all known key/values when initializing the dictionary.:HIGH diff --git a/tests/functional/ext/docparams/docparams.py b/tests/functional/ext/docparams/docparams.py index 295cf430d6..0d033d4ca8 100644 --- a/tests/functional/ext/docparams/docparams.py +++ b/tests/functional/ext/docparams/docparams.py @@ -1,19 +1,23 @@ """Fixture for testing missing documentation in docparams.""" +# pylint: disable=broad-exception-raised - -def _private_func1(param1): # [missing-return-doc, missing-return-type-doc] +def _private_func1( # [missing-return-doc, missing-return-type-doc, missing-any-param-doc] + param1, +): """This is a test docstring without returns""" return param1 -def _private_func2(param1): # [missing-yield-doc, missing-yield-type-doc] +def _private_func2( # [missing-yield-doc, missing-yield-type-doc, missing-any-param-doc] + param1, +): """This is a test docstring without yields""" yield param1 -def _private_func3(param1): # [missing-raises-doc] +def _private_func3(param1): # [missing-raises-doc, missing-any-param-doc] """This is a test docstring without raises""" - raise Exception('Example') + raise Exception("Example") def public_func1(param1): # [missing-any-param-doc] @@ -21,19 +25,25 @@ def public_func1(param1): # [missing-any-param-doc] print(param1) -async def _async_private_func1(param1): # [missing-return-doc, missing-return-type-doc] +# pylint: disable-next=line-too-long +async def _async_private_func1( # [missing-return-doc, missing-return-type-doc, missing-any-param-doc] + param1, +): """This is a test docstring without returns""" return param1 -async def _async_private_func2(param1): # [missing-yield-doc, missing-yield-type-doc] +# pylint: disable-next=line-too-long +async def _async_private_func2( # [missing-yield-doc, missing-yield-type-doc, missing-any-param-doc] + param1, +): """This is a test docstring without yields""" yield param1 -async def _async_private_func3(param1): # [missing-raises-doc] +async def _async_private_func3(param1): # [missing-raises-doc, missing-any-param-doc] """This is a test docstring without raises""" - raise Exception('Example') + raise Exception("Example") async def async_public_func1(param1): # [missing-any-param-doc] @@ -92,3 +102,7 @@ def params_are_documented(par1: int, *, par2: int) -> int: """ return par1 + par2 + + +# Only check raise nodes within FunctionDefs +raise Exception() diff --git a/tests/functional/ext/docparams/docparams.rc b/tests/functional/ext/docparams/docparams.rc index 24fa52ef43..2a09f2f6d5 100644 --- a/tests/functional/ext/docparams/docparams.rc +++ b/tests/functional/ext/docparams/docparams.rc @@ -1,5 +1,6 @@ [MAIN] load-plugins = pylint.extensions.docparams +no-docstring-rgx = ONLYVERYSPECIFICFUNCTIONS [BASIC] accept-no-param-doc = no diff --git a/tests/functional/ext/docparams/docparams.txt b/tests/functional/ext/docparams/docparams.txt index 69baf08503..2504e2b630 100644 --- a/tests/functional/ext/docparams/docparams.txt +++ b/tests/functional/ext/docparams/docparams.txt @@ -1,16 +1,22 @@ -missing-return-doc:4:0:4:18:_private_func1:Missing return documentation:UNDEFINED -missing-return-type-doc:4:0:4:18:_private_func1:Missing return type documentation:UNDEFINED -missing-yield-doc:9:0:9:18:_private_func2:Missing yield documentation:UNDEFINED -missing-yield-type-doc:9:0:9:18:_private_func2:Missing yield type documentation:UNDEFINED -missing-raises-doc:14:0:14:18:_private_func3:"""Exception"" not documented as being raised":UNDEFINED -missing-any-param-doc:19:0:19:16:public_func1:"Missing any documentation in ""public_func1""":UNDEFINED -missing-return-doc:24:0:24:30:_async_private_func1:Missing return documentation:UNDEFINED -missing-return-type-doc:24:0:24:30:_async_private_func1:Missing return type documentation:UNDEFINED -missing-yield-doc:29:0:29:30:_async_private_func2:Missing yield documentation:UNDEFINED -missing-yield-type-doc:29:0:29:30:_async_private_func2:Missing yield type documentation:UNDEFINED -missing-raises-doc:34:0:34:30:_async_private_func3:"""Exception"" not documented as being raised":UNDEFINED -missing-any-param-doc:39:0:39:28:async_public_func1:"Missing any documentation in ""async_public_func1""":UNDEFINED -differing-param-doc:44:0:44:23:differing_param_doc:"""param"" differing in parameter documentation":UNDEFINED -differing-param-doc:55:0:55:35:differing_param_doc_kwords_only:"""param"" differing in parameter documentation":UNDEFINED -missing-type-doc:66:0:66:20:missing_type_doc:"""par1"" missing in parameter type documentation":UNDEFINED -missing-type-doc:76:0:76:32:missing_type_doc_kwords_only:"""par1"" missing in parameter type documentation":UNDEFINED +missing-any-param-doc:4:0:4:18:_private_func1:"Missing any documentation in ""_private_func1""":HIGH +missing-return-doc:4:0:4:18:_private_func1:Missing return documentation:HIGH +missing-return-type-doc:4:0:4:18:_private_func1:Missing return type documentation:HIGH +missing-any-param-doc:11:0:11:18:_private_func2:"Missing any documentation in ""_private_func2""":HIGH +missing-yield-doc:11:0:11:18:_private_func2:Missing yield documentation:HIGH +missing-yield-type-doc:11:0:11:18:_private_func2:Missing yield type documentation:HIGH +missing-any-param-doc:18:0:18:18:_private_func3:"Missing any documentation in ""_private_func3""":HIGH +missing-raises-doc:18:0:18:18:_private_func3:"""Exception"" not documented as being raised":HIGH +missing-any-param-doc:23:0:23:16:public_func1:"Missing any documentation in ""public_func1""":HIGH +missing-any-param-doc:29:0:29:30:_async_private_func1:"Missing any documentation in ""_async_private_func1""":HIGH +missing-return-doc:29:0:29:30:_async_private_func1:Missing return documentation:HIGH +missing-return-type-doc:29:0:29:30:_async_private_func1:Missing return type documentation:HIGH +missing-any-param-doc:37:0:37:30:_async_private_func2:"Missing any documentation in ""_async_private_func2""":HIGH +missing-yield-doc:37:0:37:30:_async_private_func2:Missing yield documentation:HIGH +missing-yield-type-doc:37:0:37:30:_async_private_func2:Missing yield type documentation:HIGH +missing-any-param-doc:44:0:44:30:_async_private_func3:"Missing any documentation in ""_async_private_func3""":HIGH +missing-raises-doc:44:0:44:30:_async_private_func3:"""Exception"" not documented as being raised":HIGH +missing-any-param-doc:49:0:49:28:async_public_func1:"Missing any documentation in ""async_public_func1""":HIGH +differing-param-doc:54:0:54:23:differing_param_doc:"""param"" differing in parameter documentation":HIGH +differing-param-doc:65:0:65:35:differing_param_doc_kwords_only:"""param"" differing in parameter documentation":HIGH +missing-type-doc:76:0:76:20:missing_type_doc:"""par1"" missing in parameter type documentation":HIGH +missing-type-doc:86:0:86:32:missing_type_doc_kwords_only:"""par1"" missing in parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/docparams_py38.txt b/tests/functional/ext/docparams/docparams_py38.txt index 96eaa445ad..ce2ac77615 100644 --- a/tests/functional/ext/docparams/docparams_py38.txt +++ b/tests/functional/ext/docparams/docparams_py38.txt @@ -1,2 +1,2 @@ -differing-param-doc:4:0:4:32:differing_param_doc_pos_only:"""param"" differing in parameter documentation":UNDEFINED -missing-type-doc:15:0:15:29:missing_type_doc_pos_only:"""par1"" missing in parameter type documentation":UNDEFINED +differing-param-doc:4:0:4:32:differing_param_doc_pos_only:"""param"" differing in parameter documentation":HIGH +missing-type-doc:15:0:15:29:missing_type_doc_pos_only:"""par1"" missing in parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/missing_param_doc.py b/tests/functional/ext/docparams/missing_param_doc.py index 75ca394129..e72507a785 100644 --- a/tests/functional/ext/docparams/missing_param_doc.py +++ b/tests/functional/ext/docparams/missing_param_doc.py @@ -30,7 +30,7 @@ def foobar4(arg1, arg2): #[missing-param-doc, missing-type-doc] """ print(arg1, arg2) -def foobar5(arg1, arg2): #[missing-param-doc, missing-type-doc] +def foobar5(arg1, arg2): #[missing-type-doc] """function foobar ... Parameters ---------- @@ -63,7 +63,7 @@ def foobar8(arg1): #[missing-any-param-doc] print(arg1) -def foobar9(arg1, arg2, arg3): #[missing-param-doc] +def foobar9(arg1, arg2, arg3): """function foobar ... Parameters ---------- @@ -73,7 +73,7 @@ def foobar9(arg1, arg2, arg3): #[missing-param-doc] """ print(arg1, arg2, arg3) -def foobar10(arg1, arg2, arg3): #[missing-param-doc, missing-type-doc] +def foobar10(arg1, arg2, arg3): #[missing-type-doc] """function foobar ... Parameters ---------- diff --git a/tests/functional/ext/docparams/missing_param_doc.txt b/tests/functional/ext/docparams/missing_param_doc.txt index c43bdbd7ec..fdf4da93f4 100644 --- a/tests/functional/ext/docparams/missing_param_doc.txt +++ b/tests/functional/ext/docparams/missing_param_doc.txt @@ -1,18 +1,15 @@ -missing-any-param-doc:3:0:3:11:foobar1:"Missing any documentation in ""foobar1""":UNDEFINED -missing-any-param-doc:8:0:8:11:foobar2:"Missing any documentation in ""foobar2""":UNDEFINED -missing-param-doc:15:0:15:11:foobar3:"""arg1, arg2, arg3"" missing in parameter documentation":UNDEFINED -missing-type-doc:15:0:15:11:foobar3:"""arg2"" missing in parameter type documentation":UNDEFINED -missing-param-doc:24:0:24:11:foobar4:"""arg2"" missing in parameter documentation":UNDEFINED -missing-type-doc:24:0:24:11:foobar4:"""arg2"" missing in parameter type documentation":UNDEFINED -missing-param-doc:33:0:33:11:foobar5:"""arg2"" missing in parameter documentation":UNDEFINED -missing-type-doc:33:0:33:11:foobar5:"""arg1"" missing in parameter type documentation":UNDEFINED -missing-param-doc:43:0:43:11:foobar6:"""arg2, arg3"" missing in parameter documentation":UNDEFINED -missing-type-doc:43:0:43:11:foobar6:"""arg3"" missing in parameter type documentation":UNDEFINED -missing-any-param-doc:53:0:53:11:foobar7:"Missing any documentation in ""foobar7""":UNDEFINED -missing-any-param-doc:61:0:61:11:foobar8:"Missing any documentation in ""foobar8""":UNDEFINED -missing-param-doc:66:0:66:11:foobar9:"""arg1, arg2, arg3"" missing in parameter documentation":UNDEFINED -missing-param-doc:76:0:76:12:foobar10:"""arg2"" missing in parameter documentation":UNDEFINED -missing-type-doc:76:0:76:12:foobar10:"""arg1, arg3"" missing in parameter type documentation":UNDEFINED -missing-any-param-doc:88:0:88:12:foobar11:"Missing any documentation in ""foobar11""":UNDEFINED -missing-param-doc:97:0:97:12:foobar12:"""arg1, arg3"" missing in parameter documentation":UNDEFINED -missing-type-doc:97:0:97:12:foobar12:"""arg2, arg3"" missing in parameter type documentation":UNDEFINED +missing-any-param-doc:3:0:3:11:foobar1:"Missing any documentation in ""foobar1""":HIGH +missing-any-param-doc:8:0:8:11:foobar2:"Missing any documentation in ""foobar2""":HIGH +missing-param-doc:15:0:15:11:foobar3:"""arg2"" missing in parameter documentation":HIGH +missing-type-doc:15:0:15:11:foobar3:"""arg2"" missing in parameter type documentation":HIGH +missing-param-doc:24:0:24:11:foobar4:"""arg2"" missing in parameter documentation":HIGH +missing-type-doc:24:0:24:11:foobar4:"""arg2"" missing in parameter type documentation":HIGH +missing-type-doc:33:0:33:11:foobar5:"""arg1"" missing in parameter type documentation":HIGH +missing-param-doc:43:0:43:11:foobar6:"""arg3"" missing in parameter documentation":HIGH +missing-type-doc:43:0:43:11:foobar6:"""arg3"" missing in parameter type documentation":HIGH +missing-any-param-doc:53:0:53:11:foobar7:"Missing any documentation in ""foobar7""":HIGH +missing-any-param-doc:61:0:61:11:foobar8:"Missing any documentation in ""foobar8""":HIGH +missing-type-doc:76:0:76:12:foobar10:"""arg1, arg3"" missing in parameter type documentation":HIGH +missing-any-param-doc:88:0:88:12:foobar11:"Missing any documentation in ""foobar11""":HIGH +missing-param-doc:97:0:97:12:foobar12:"""arg3"" missing in parameter documentation":HIGH +missing-type-doc:97:0:97:12:foobar12:"""arg2, arg3"" missing in parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required.txt b/tests/functional/ext/docparams/parameter/missing_param_doc_required.txt index 1db477b902..3f1ebda576 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required.txt +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required.txt @@ -1,3 +1,3 @@ -missing-any-param-doc:7:0:7:53:test_don_t_tolerate_no_param_documentation_at_all:"Missing any documentation in ""test_don_t_tolerate_no_param_documentation_at_all""":UNDEFINED -missing-param-doc:44:0:44:40:test_kwonlyargs_are_taken_in_account:"""missing_kwonly"" missing in parameter documentation":UNDEFINED -missing-type-doc:44:0:44:40:test_kwonlyargs_are_taken_in_account:"""missing_kwonly"" missing in parameter type documentation":UNDEFINED +missing-any-param-doc:7:0:7:53:test_don_t_tolerate_no_param_documentation_at_all:"Missing any documentation in ""test_don_t_tolerate_no_param_documentation_at_all""":HIGH +missing-param-doc:44:0:44:40:test_kwonlyargs_are_taken_in_account:"""missing_kwonly"" missing in parameter documentation":HIGH +missing-type-doc:44:0:44:40:test_kwonlyargs_are_taken_in_account:"""missing_kwonly"" missing in parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Google.py b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Google.py index 18fbaf0490..92646a87f4 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Google.py +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Google.py @@ -433,3 +433,15 @@ def test_finds_multiple_complex_types_google( named_arg_nine, named_arg_ten, ) + +def test_escape_underscore(something: int, raise_: bool = False) -> bool: + """Tests param with escaped _ is handled correctly. + + Args: + something: the something + raise\\_: the other + + Returns: + something + """ + return something and raise_ diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Google.txt b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Google.txt index 578d8a8c32..33a479d11d 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Google.txt +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Google.txt @@ -1,26 +1,26 @@ -missing-param-doc:24:0:24:48:test_missing_func_params_in_google_docstring:"""y"" missing in parameter documentation":UNDEFINED -missing-type-doc:24:0:24:48:test_missing_func_params_in_google_docstring:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-type-doc:80:0:80:73:test_missing_func_params_with_partial_annotations_in_google_docstring:"""x"" missing in parameter type documentation":UNDEFINED -differing-param-doc:131:0:131:65:test_func_params_and_wrong_keyword_params_in_google_docstring:"""these"" differing in parameter documentation":UNDEFINED -differing-type-doc:131:0:131:65:test_func_params_and_wrong_keyword_params_in_google_docstring:"""these"" differing in parameter type documentation":UNDEFINED -missing-param-doc:131:0:131:65:test_func_params_and_wrong_keyword_params_in_google_docstring:"""that"" missing in parameter documentation":UNDEFINED -missing-type-doc:131:0:131:65:test_func_params_and_wrong_keyword_params_in_google_docstring:"""that"" missing in parameter type documentation":UNDEFINED -missing-param-doc:148:4:148:54:Foo.test_missing_method_params_in_google_docstring:"""y"" missing in parameter documentation":UNDEFINED -missing-type-doc:148:4:148:54:Foo.test_missing_method_params_in_google_docstring:"""x, y"" missing in parameter type documentation":UNDEFINED -differing-param-doc:179:0:179:58:test_wrong_name_of_func_params_in_google_docstring_one:"""xarg1, zarg1"" differing in parameter documentation":UNDEFINED -differing-type-doc:179:0:179:58:test_wrong_name_of_func_params_in_google_docstring_one:"""xarg1, zarg1"" differing in parameter type documentation":UNDEFINED -missing-param-doc:179:0:179:58:test_wrong_name_of_func_params_in_google_docstring_one:"""xarg, zarg"" missing in parameter documentation":UNDEFINED -missing-type-doc:179:0:179:58:test_wrong_name_of_func_params_in_google_docstring_one:"""xarg, zarg"" missing in parameter type documentation":UNDEFINED -differing-param-doc:194:0:194:58:test_wrong_name_of_func_params_in_google_docstring_two:"""yarg1"" differing in parameter documentation":UNDEFINED -differing-type-doc:194:0:194:58:test_wrong_name_of_func_params_in_google_docstring_two:"""yarg1"" differing in parameter type documentation":UNDEFINED -missing-param-doc:221:0:221:14:ClassFoo:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:221:0:221:14:ClassFoo:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:239:4:239:16:ClassFoo.__init__:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:239:4:239:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:251:0:251:14:ClassFoo:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:251:0:251:14:ClassFoo:"""x, y"" missing in parameter type documentation":UNDEFINED -multiple-constructor-doc:251:0:251:14:ClassFoo:"""ClassFoo"" has constructor parameters documented in class and __init__":UNDEFINED -missing-param-doc:265:4:265:16:ClassFoo.__init__:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:265:4:265:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:275:0:275:34:test_warns_missing_args_google:"""*args"" missing in parameter documentation":UNDEFINED -missing-param-doc:288:0:288:36:test_warns_missing_kwargs_google:"""**kwargs"" missing in parameter documentation":UNDEFINED +missing-param-doc:24:0:24:48:test_missing_func_params_in_google_docstring:"""y"" missing in parameter documentation":HIGH +missing-type-doc:24:0:24:48:test_missing_func_params_in_google_docstring:"""x, y"" missing in parameter type documentation":HIGH +missing-type-doc:80:0:80:73:test_missing_func_params_with_partial_annotations_in_google_docstring:"""x"" missing in parameter type documentation":HIGH +differing-param-doc:131:0:131:65:test_func_params_and_wrong_keyword_params_in_google_docstring:"""these"" differing in parameter documentation":HIGH +differing-type-doc:131:0:131:65:test_func_params_and_wrong_keyword_params_in_google_docstring:"""these"" differing in parameter type documentation":HIGH +missing-param-doc:131:0:131:65:test_func_params_and_wrong_keyword_params_in_google_docstring:"""that"" missing in parameter documentation":HIGH +missing-type-doc:131:0:131:65:test_func_params_and_wrong_keyword_params_in_google_docstring:"""that"" missing in parameter type documentation":HIGH +missing-param-doc:148:4:148:54:Foo.test_missing_method_params_in_google_docstring:"""y"" missing in parameter documentation":HIGH +missing-type-doc:148:4:148:54:Foo.test_missing_method_params_in_google_docstring:"""x, y"" missing in parameter type documentation":HIGH +differing-param-doc:179:0:179:58:test_wrong_name_of_func_params_in_google_docstring_one:"""xarg1, zarg1"" differing in parameter documentation":HIGH +differing-type-doc:179:0:179:58:test_wrong_name_of_func_params_in_google_docstring_one:"""xarg1, zarg1"" differing in parameter type documentation":HIGH +missing-param-doc:179:0:179:58:test_wrong_name_of_func_params_in_google_docstring_one:"""xarg, zarg"" missing in parameter documentation":HIGH +missing-type-doc:179:0:179:58:test_wrong_name_of_func_params_in_google_docstring_one:"""xarg, zarg"" missing in parameter type documentation":HIGH +differing-param-doc:194:0:194:58:test_wrong_name_of_func_params_in_google_docstring_two:"""yarg1"" differing in parameter documentation":HIGH +differing-type-doc:194:0:194:58:test_wrong_name_of_func_params_in_google_docstring_two:"""yarg1"" differing in parameter type documentation":HIGH +missing-param-doc:221:0:221:14:ClassFoo:"""x"" missing in parameter documentation":HIGH +missing-type-doc:221:0:221:14:ClassFoo:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:239:4:239:16:ClassFoo.__init__:"""x"" missing in parameter documentation":HIGH +missing-type-doc:239:4:239:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:251:0:251:14:ClassFoo:"""x"" missing in parameter documentation":HIGH +missing-type-doc:251:0:251:14:ClassFoo:"""x, y"" missing in parameter type documentation":HIGH +multiple-constructor-doc:251:0:251:14:ClassFoo:"""ClassFoo"" has constructor parameters documented in class and __init__":HIGH +missing-param-doc:265:4:265:16:ClassFoo.__init__:"""x"" missing in parameter documentation":HIGH +missing-type-doc:265:4:265:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:275:0:275:34:test_warns_missing_args_google:"""*args"" missing in parameter documentation":HIGH +missing-param-doc:288:0:288:36:test_warns_missing_kwargs_google:"""**kwargs"" missing in parameter documentation":HIGH diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.py b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.py index 6e725980fb..5626ad385b 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.py +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.py @@ -392,6 +392,7 @@ def test_ignores_optional_specifier_numpy(param, param2="all"): """ return param, param2 + def test_with_list_of_default_values(arg, option, option2): """Reported in https://github.com/PyCQA/pylint/issues/4035. @@ -406,3 +407,18 @@ def test_with_list_of_default_values(arg, option, option2): """ return arg, option, option2 + + +def test_with_descriptions_instead_of_typing(arg, axis, option): + """We choose to accept description in place of typing as well. + + See: https://github.com/PyCQA/pylint/pull/7398. + + Parameters + ---------- + arg : a number type. + axis : int or None + option : {"y", "n"} + Do I do it? + """ + return arg, option diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.txt b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.txt index bd73de9cce..a58b9c7cad 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.txt +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.txt @@ -1,22 +1,22 @@ -missing-param-doc:9:0:9:47:test_missing_func_params_in_numpy_docstring:"""y"" missing in parameter documentation":UNDEFINED -missing-type-doc:9:0:9:47:test_missing_func_params_in_numpy_docstring:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:27:4:27:53:Foo.test_missing_method_params_in_numpy_docstring:"""y"" missing in parameter documentation":UNDEFINED -missing-type-doc:27:4:27:53:Foo.test_missing_method_params_in_numpy_docstring:"""x, y"" missing in parameter type documentation":UNDEFINED -differing-param-doc:66:0:66:53:test_wrong_name_of_func_params_in_numpy_docstring:"""xarg1, zarg1"" differing in parameter documentation":UNDEFINED -differing-type-doc:66:0:66:53:test_wrong_name_of_func_params_in_numpy_docstring:"""xarg1, zarg1"" differing in parameter type documentation":UNDEFINED -missing-param-doc:66:0:66:53:test_wrong_name_of_func_params_in_numpy_docstring:"""xarg, zarg"" missing in parameter documentation":UNDEFINED -missing-type-doc:66:0:66:53:test_wrong_name_of_func_params_in_numpy_docstring:"""xarg, zarg"" missing in parameter type documentation":UNDEFINED -differing-param-doc:85:0:85:57:test_wrong_name_of_func_params_in_numpy_docstring_two:"""yarg1"" differing in parameter documentation":UNDEFINED -differing-type-doc:85:0:85:57:test_wrong_name_of_func_params_in_numpy_docstring_two:"""yarg1"" differing in parameter type documentation":UNDEFINED -missing-param-doc:116:0:116:14:ClassFoo:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:116:0:116:14:ClassFoo:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:156:4:156:16:ClassFoo.__init__:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:156:4:156:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:172:0:172:14:ClassFoo:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:172:0:172:14:ClassFoo:"""x, y"" missing in parameter type documentation":UNDEFINED -multiple-constructor-doc:172:0:172:14:ClassFoo:"""ClassFoo"" has constructor parameters documented in class and __init__":UNDEFINED -missing-param-doc:188:4:188:16:ClassFoo.__init__:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:188:4:188:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:200:0:200:33:test_warns_missing_args_numpy:"""*args"" missing in parameter documentation":UNDEFINED -missing-param-doc:217:0:217:35:test_warns_missing_kwargs_numpy:"""**kwargs"" missing in parameter documentation":UNDEFINED -missing-type-doc:234:0:234:38:test_finds_args_without_type_numpy:"""untyped_arg"" missing in parameter type documentation":UNDEFINED +missing-param-doc:9:0:9:47:test_missing_func_params_in_numpy_docstring:"""y"" missing in parameter documentation":HIGH +missing-type-doc:9:0:9:47:test_missing_func_params_in_numpy_docstring:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:27:4:27:53:Foo.test_missing_method_params_in_numpy_docstring:"""y"" missing in parameter documentation":HIGH +missing-type-doc:27:4:27:53:Foo.test_missing_method_params_in_numpy_docstring:"""x, y"" missing in parameter type documentation":HIGH +differing-param-doc:66:0:66:53:test_wrong_name_of_func_params_in_numpy_docstring:"""xarg1, zarg1"" differing in parameter documentation":HIGH +differing-type-doc:66:0:66:53:test_wrong_name_of_func_params_in_numpy_docstring:"""xarg1, zarg1"" differing in parameter type documentation":HIGH +missing-param-doc:66:0:66:53:test_wrong_name_of_func_params_in_numpy_docstring:"""xarg, zarg"" missing in parameter documentation":HIGH +missing-type-doc:66:0:66:53:test_wrong_name_of_func_params_in_numpy_docstring:"""xarg, zarg"" missing in parameter type documentation":HIGH +differing-param-doc:85:0:85:57:test_wrong_name_of_func_params_in_numpy_docstring_two:"""yarg1"" differing in parameter documentation":HIGH +differing-type-doc:85:0:85:57:test_wrong_name_of_func_params_in_numpy_docstring_two:"""yarg1"" differing in parameter type documentation":HIGH +missing-param-doc:116:0:116:14:ClassFoo:"""x"" missing in parameter documentation":HIGH +missing-type-doc:116:0:116:14:ClassFoo:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:156:4:156:16:ClassFoo.__init__:"""x"" missing in parameter documentation":HIGH +missing-type-doc:156:4:156:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:172:0:172:14:ClassFoo:"""x"" missing in parameter documentation":HIGH +missing-type-doc:172:0:172:14:ClassFoo:"""x, y"" missing in parameter type documentation":HIGH +multiple-constructor-doc:172:0:172:14:ClassFoo:"""ClassFoo"" has constructor parameters documented in class and __init__":HIGH +missing-param-doc:188:4:188:16:ClassFoo.__init__:"""x"" missing in parameter documentation":HIGH +missing-type-doc:188:4:188:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:200:0:200:33:test_warns_missing_args_numpy:"""*args"" missing in parameter documentation":HIGH +missing-param-doc:217:0:217:35:test_warns_missing_kwargs_numpy:"""**kwargs"" missing in parameter documentation":HIGH +missing-type-doc:234:0:234:38:test_finds_args_without_type_numpy:"""untyped_arg"" missing in parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.rc b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.rc index 671820dbc2..2900924f8f 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.rc +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.rc @@ -3,5 +3,6 @@ load-plugins = pylint.extensions.docparams [BASIC] accept-no-param-doc=no +accept-no-raise-doc=no no-docstring-rgx=^$ docstring-min-length: -1 diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.txt b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.txt index e7e1a55495..b3819fff79 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.txt +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.txt @@ -1,39 +1,39 @@ -missing-param-doc:8:0:8:48:test_missing_func_params_in_sphinx_docstring:"""y"" missing in parameter documentation":UNDEFINED -missing-type-doc:8:0:8:48:test_missing_func_params_in_sphinx_docstring:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:22:4:22:54:Foo.test_missing_method_params_in_sphinx_docstring:"""y"" missing in parameter documentation":UNDEFINED -missing-type-doc:22:4:22:54:Foo.test_missing_method_params_in_sphinx_docstring:"""x, y"" missing in parameter type documentation":UNDEFINED -differing-param-doc:55:0:55:54:test_wrong_name_of_func_params_in_sphinx_docstring:"""xarg1, zarg1"" differing in parameter documentation":UNDEFINED -differing-type-doc:55:0:55:54:test_wrong_name_of_func_params_in_sphinx_docstring:"""yarg1, zarg1"" differing in parameter type documentation":UNDEFINED -missing-param-doc:55:0:55:54:test_wrong_name_of_func_params_in_sphinx_docstring:"""xarg, zarg"" missing in parameter documentation":UNDEFINED -missing-type-doc:55:0:55:54:test_wrong_name_of_func_params_in_sphinx_docstring:"""yarg, zarg"" missing in parameter type documentation":UNDEFINED -differing-param-doc:72:0:72:58:test_wrong_name_of_func_params_in_sphinx_docstring_two:"""yarg1"" differing in parameter documentation":UNDEFINED -differing-type-doc:72:0:72:58:test_wrong_name_of_func_params_in_sphinx_docstring_two:"""yarg1"" differing in parameter type documentation":UNDEFINED -missing-param-doc:99:0:99:14:ClassFoo:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:99:0:99:14:ClassFoo:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:116:4:116:16:ClassFoo.__init__:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:116:4:116:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":UNDEFINED -missing-param-doc:131:0:131:14:ClassFoo:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:131:0:131:14:ClassFoo:"""x, y"" missing in parameter type documentation":UNDEFINED -multiple-constructor-doc:131:0:131:14:ClassFoo:"""ClassFoo"" has constructor parameters documented in class and __init__":UNDEFINED -missing-param-doc:144:4:144:16:ClassFoo.__init__:"""x"" missing in parameter documentation":UNDEFINED -missing-type-doc:144:4:144:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":UNDEFINED +missing-param-doc:8:0:8:48:test_missing_func_params_in_sphinx_docstring:"""y"" missing in parameter documentation":HIGH +missing-type-doc:8:0:8:48:test_missing_func_params_in_sphinx_docstring:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:22:4:22:54:Foo.test_missing_method_params_in_sphinx_docstring:"""y"" missing in parameter documentation":HIGH +missing-type-doc:22:4:22:54:Foo.test_missing_method_params_in_sphinx_docstring:"""x, y"" missing in parameter type documentation":HIGH +differing-param-doc:55:0:55:54:test_wrong_name_of_func_params_in_sphinx_docstring:"""xarg1, zarg1"" differing in parameter documentation":HIGH +differing-type-doc:55:0:55:54:test_wrong_name_of_func_params_in_sphinx_docstring:"""yarg1, zarg1"" differing in parameter type documentation":HIGH +missing-param-doc:55:0:55:54:test_wrong_name_of_func_params_in_sphinx_docstring:"""xarg, zarg"" missing in parameter documentation":HIGH +missing-type-doc:55:0:55:54:test_wrong_name_of_func_params_in_sphinx_docstring:"""yarg, zarg"" missing in parameter type documentation":HIGH +differing-param-doc:72:0:72:58:test_wrong_name_of_func_params_in_sphinx_docstring_two:"""yarg1"" differing in parameter documentation":HIGH +differing-type-doc:72:0:72:58:test_wrong_name_of_func_params_in_sphinx_docstring_two:"""yarg1"" differing in parameter type documentation":HIGH +missing-param-doc:99:0:99:14:ClassFoo:"""x"" missing in parameter documentation":HIGH +missing-type-doc:99:0:99:14:ClassFoo:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:116:4:116:16:ClassFoo.__init__:"""x"" missing in parameter documentation":HIGH +missing-type-doc:116:4:116:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":HIGH +missing-param-doc:131:0:131:14:ClassFoo:"""x"" missing in parameter documentation":HIGH +missing-type-doc:131:0:131:14:ClassFoo:"""x, y"" missing in parameter type documentation":HIGH +multiple-constructor-doc:131:0:131:14:ClassFoo:"""ClassFoo"" has constructor parameters documented in class and __init__":HIGH +missing-param-doc:144:4:144:16:ClassFoo.__init__:"""x"" missing in parameter documentation":HIGH +missing-type-doc:144:4:144:16:ClassFoo.__init__:"""x, y"" missing in parameter type documentation":HIGH inconsistent-return-statements:154:0:154:34:test_warns_missing_args_sphinx:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED -missing-param-doc:154:0:154:34:test_warns_missing_args_sphinx:"""*args"" missing in parameter documentation":UNDEFINED +missing-param-doc:154:0:154:34:test_warns_missing_args_sphinx:"""*args"" missing in parameter documentation":HIGH inconsistent-return-statements:169:0:169:36:test_warns_missing_kwargs_sphinx:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED -missing-param-doc:169:0:169:36:test_warns_missing_kwargs_sphinx:"""**kwargs"" missing in parameter documentation":UNDEFINED +missing-param-doc:169:0:169:36:test_warns_missing_kwargs_sphinx:"""**kwargs"" missing in parameter documentation":HIGH inconsistent-return-statements:184:0:184:39:test_finds_args_without_type_sphinx:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED -missing-param-doc:184:0:184:39:test_finds_args_without_type_sphinx:"""*args"" missing in parameter documentation":UNDEFINED +missing-param-doc:184:0:184:39:test_finds_args_without_type_sphinx:"""*args"" missing in parameter documentation":HIGH inconsistent-return-statements:201:0:201:41:test_finds_kwargs_without_type_sphinx:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED -missing-param-doc:201:0:201:41:test_finds_kwargs_without_type_sphinx:"""**kwargs"" missing in parameter documentation":UNDEFINED +missing-param-doc:201:0:201:41:test_finds_kwargs_without_type_sphinx:"""**kwargs"" missing in parameter documentation":HIGH inconsistent-return-statements:218:0:218:39:test_finds_args_without_type_sphinx:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED inconsistent-return-statements:237:0:237:41:test_finds_kwargs_without_type_sphinx:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED inconsistent-return-statements:256:0:256:39:test_finds_args_without_type_sphinx:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED inconsistent-return-statements:274:0:274:41:test_finds_kwargs_without_type_sphinx:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED -missing-raises-doc:299:4:299:11:Foo.foo:"""AttributeError"" not documented as being raised":UNDEFINED -unreachable:325:8:325:17:Foo.foo:Unreachable code:UNDEFINED -missing-param-doc:328:4:328:11:Foo.foo:"""value"" missing in parameter documentation":UNDEFINED -missing-raises-doc:328:4:328:11:Foo.foo:"""AttributeError"" not documented as being raised":UNDEFINED -missing-type-doc:328:4:328:11:Foo.foo:"""value"" missing in parameter type documentation":UNDEFINED -unreachable:364:8:364:17:Foo.foo:Unreachable code:UNDEFINED -useless-param-doc:368:4:368:55:Foo.test_useless_docs_ignored_argument_names_sphinx:"""_, _ignored"" useless ignored parameter documentation":UNDEFINED -useless-type-doc:368:4:368:55:Foo.test_useless_docs_ignored_argument_names_sphinx:"""_"" useless ignored parameter type documentation":UNDEFINED +missing-raises-doc:299:4:299:11:Foo.foo:"""AttributeError"" not documented as being raised":HIGH +unreachable:325:8:325:17:Foo.foo:Unreachable code:HIGH +missing-param-doc:328:4:328:11:Foo.foo:"""value"" missing in parameter documentation":HIGH +missing-raises-doc:328:4:328:11:Foo.foo:"""AttributeError"" not documented as being raised":HIGH +missing-type-doc:328:4:328:11:Foo.foo:"""value"" missing in parameter type documentation":HIGH +unreachable:364:8:364:17:Foo.foo:Unreachable code:HIGH +useless-param-doc:368:4:368:55:Foo.test_useless_docs_ignored_argument_names_sphinx:"""_, _ignored"" useless ignored parameter documentation":HIGH +useless-type-doc:368:4:368:55:Foo.test_useless_docs_ignored_argument_names_sphinx:"""_"" useless ignored parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_no_doc_rgx_check_init.txt b/tests/functional/ext/docparams/parameter/missing_param_doc_required_no_doc_rgx_check_init.txt index 7b30afcb55..bc5000bd17 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_no_doc_rgx_check_init.txt +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_no_doc_rgx_check_init.txt @@ -1 +1 @@ -missing-param-doc:10:4:10:16:MyClass.__init__:"""my_param"" missing in parameter documentation":UNDEFINED +missing-param-doc:10:4:10:16:MyClass.__init__:"""my_param"" missing in parameter documentation":HIGH diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_no_doc_rgx_test_all.txt b/tests/functional/ext/docparams/parameter/missing_param_doc_required_no_doc_rgx_test_all.txt index d42bc96251..d845b5f17a 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_no_doc_rgx_test_all.txt +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_no_doc_rgx_test_all.txt @@ -1 +1 @@ -missing-param-doc:25:4:25:16:MyClass.__init__:"""my_param"" missing in parameter documentation":UNDEFINED +missing-param-doc:25:4:25:16:MyClass.__init__:"""my_param"" missing in parameter documentation":HIGH diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc.txt b/tests/functional/ext/docparams/raise/missing_raises_doc.txt index 7a93e4b1cd..d770776ef9 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc.txt @@ -1,4 +1,4 @@ -unreachable:25:4:25:25:test_ignores_raise_uninferable:Unreachable code:UNDEFINED -missing-raises-doc:28:0:28:45:test_ignores_returns_from_inner_functions:"""RuntimeError"" not documented as being raised":UNDEFINED -unreachable:42:4:42:25:test_ignores_returns_from_inner_functions:Unreachable code:UNDEFINED -raising-bad-type:54:4:54:22:test_ignores_returns_use_only_names:Raising int while only classes or instances are allowed:UNDEFINED +unreachable:25:4:25:25:test_ignores_raise_uninferable:Unreachable code:HIGH +missing-raises-doc:28:0:28:45:test_ignores_returns_from_inner_functions:"""RuntimeError"" not documented as being raised":HIGH +unreachable:42:4:42:25:test_ignores_returns_from_inner_functions:Unreachable code:HIGH +raising-bad-type:54:4:54:22:test_ignores_returns_use_only_names:Raising int while only classes or instances are allowed:INFERENCE diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_Google.txt b/tests/functional/ext/docparams/raise/missing_raises_doc_Google.txt index 0c6022c32b..f59d271769 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Google.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Google.txt @@ -1,14 +1,14 @@ -missing-raises-doc:6:0:6:35:test_find_missing_google_raises:"""RuntimeError"" not documented as being raised":UNDEFINED -unreachable:13:4:13:25:test_find_missing_google_raises:Unreachable code:UNDEFINED -missing-raises-doc:38:0:38:46:test_find_valid_missing_google_attr_raises:"""error"" not documented as being raised":UNDEFINED -unreachable:83:4:83:25:test_find_all_google_raises:Unreachable code:UNDEFINED -unreachable:94:4:94:25:test_find_multiple_google_raises:Unreachable code:UNDEFINED -unreachable:95:4:95:30:test_find_multiple_google_raises:Unreachable code:UNDEFINED -unreachable:96:4:96:27:test_find_multiple_google_raises:Unreachable code:UNDEFINED -missing-raises-doc:99:0:99:36:test_find_rethrown_google_raises:"""RuntimeError"" not documented as being raised":UNDEFINED -missing-raises-doc:113:0:113:45:test_find_rethrown_google_multiple_raises:"""RuntimeError, ValueError"" not documented as being raised":UNDEFINED -missing-raises-doc:148:4:148:18:Foo.foo_method:"""AttributeError"" not documented as being raised":UNDEFINED -unreachable:158:8:158:17:Foo.foo_method:Unreachable code:UNDEFINED -unreachable:180:8:180:17:Foo.foo_method:Unreachable code:UNDEFINED -missing-raises-doc:183:4:183:18:Foo.foo_method:"""AttributeError"" not documented as being raised":UNDEFINED -using-constant-test:190:11:190:15:Foo.foo_method:Using a conditional statement with a constant value:UNDEFINED +missing-raises-doc:6:0:6:35:test_find_missing_google_raises:"""RuntimeError"" not documented as being raised":HIGH +unreachable:13:4:13:25:test_find_missing_google_raises:Unreachable code:HIGH +missing-raises-doc:38:0:38:46:test_find_valid_missing_google_attr_raises:"""error"" not documented as being raised":HIGH +unreachable:83:4:83:25:test_find_all_google_raises:Unreachable code:HIGH +unreachable:94:4:94:25:test_find_multiple_google_raises:Unreachable code:HIGH +unreachable:95:4:95:30:test_find_multiple_google_raises:Unreachable code:HIGH +unreachable:96:4:96:27:test_find_multiple_google_raises:Unreachable code:HIGH +missing-raises-doc:99:0:99:36:test_find_rethrown_google_raises:"""RuntimeError"" not documented as being raised":HIGH +missing-raises-doc:113:0:113:45:test_find_rethrown_google_multiple_raises:"""RuntimeError, ValueError"" not documented as being raised":HIGH +missing-raises-doc:148:4:148:18:Foo.foo_method:"""AttributeError"" not documented as being raised":HIGH +unreachable:158:8:158:17:Foo.foo_method:Unreachable code:HIGH +unreachable:180:8:180:17:Foo.foo_method:Unreachable code:HIGH +missing-raises-doc:183:4:183:18:Foo.foo_method:"""AttributeError"" not documented as being raised":HIGH +using-constant-test:190:11:190:15:Foo.foo_method:Using a conditional statement with a constant value:INFERENCE diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.txt b/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.txt index 91002c02df..43c6ba89b9 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.txt @@ -1,11 +1,11 @@ -missing-raises-doc:11:0:11:34:test_find_missing_numpy_raises:"""RuntimeError"" not documented as being raised":UNDEFINED -unreachable:20:4:20:25:test_find_missing_numpy_raises:Unreachable code:UNDEFINED -unreachable:34:4:34:25:test_find_all_numpy_raises:Unreachable code:UNDEFINED -missing-raises-doc:37:0:37:35:test_find_rethrown_numpy_raises:"""RuntimeError"" not documented as being raised":UNDEFINED -missing-raises-doc:53:0:53:44:test_find_rethrown_numpy_multiple_raises:"""RuntimeError, ValueError"" not documented as being raised":UNDEFINED -missing-raises-doc:111:0:111:45:test_find_valid_missing_numpy_attr_raises:"""error"" not documented as being raised":UNDEFINED -missing-raises-doc:146:4:146:11:Foo.foo:"""AttributeError"" not documented as being raised":UNDEFINED -unreachable:158:8:158:17:Foo.foo:Unreachable code:UNDEFINED -unreachable:182:8:182:17:Foo.foo:Unreachable code:UNDEFINED -missing-raises-doc:185:4:185:11:Foo.foo:"""AttributeError"" not documented as being raised":UNDEFINED -unreachable:215:8:215:17:Foo.foo:Unreachable code:UNDEFINED +missing-raises-doc:11:0:11:34:test_find_missing_numpy_raises:"""RuntimeError"" not documented as being raised":HIGH +unreachable:20:4:20:25:test_find_missing_numpy_raises:Unreachable code:HIGH +unreachable:34:4:34:25:test_find_all_numpy_raises:Unreachable code:HIGH +missing-raises-doc:37:0:37:35:test_find_rethrown_numpy_raises:"""RuntimeError"" not documented as being raised":HIGH +missing-raises-doc:53:0:53:44:test_find_rethrown_numpy_multiple_raises:"""RuntimeError, ValueError"" not documented as being raised":HIGH +missing-raises-doc:111:0:111:45:test_find_valid_missing_numpy_attr_raises:"""error"" not documented as being raised":HIGH +missing-raises-doc:146:4:146:11:Foo.foo:"""AttributeError"" not documented as being raised":HIGH +unreachable:158:8:158:17:Foo.foo:Unreachable code:HIGH +unreachable:182:8:182:17:Foo.foo:Unreachable code:HIGH +missing-raises-doc:185:4:185:11:Foo.foo:"""AttributeError"" not documented as being raised":HIGH +unreachable:215:8:215:17:Foo.foo:Unreachable code:HIGH diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.txt b/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.txt index 20c2b4d380..599c8beda3 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.txt @@ -1,13 +1,13 @@ -missing-raises-doc:7:0:7:35:test_find_missing_sphinx_raises:"""RuntimeError"" not documented as being raised":UNDEFINED -unreachable:13:4:13:25:test_find_missing_sphinx_raises:Unreachable code:UNDEFINED -unreachable:36:4:36:25:test_find_all_sphinx_raises:Unreachable code:UNDEFINED -unreachable:37:4:37:30:test_find_all_sphinx_raises:Unreachable code:UNDEFINED -unreachable:38:4:38:27:test_find_all_sphinx_raises:Unreachable code:UNDEFINED -unreachable:48:4:48:25:test_find_multiple_sphinx_raises:Unreachable code:UNDEFINED -missing-raises-doc:51:0:51:37:test_finds_rethrown_sphinx_raises:"""RuntimeError"" not documented as being raised":UNDEFINED -missing-raises-doc:64:0:64:46:test_finds_rethrown_sphinx_multiple_raises:"""RuntimeError, ValueError"" not documented as being raised":UNDEFINED -missing-raises-doc:90:0:90:55:test_find_missing_sphinx_raises_infer_from_instance:"""RuntimeError"" not documented as being raised":UNDEFINED -unreachable:97:4:97:25:test_find_missing_sphinx_raises_infer_from_instance:Unreachable code:UNDEFINED -missing-raises-doc:100:0:100:55:test_find_missing_sphinx_raises_infer_from_function:"""RuntimeError"" not documented as being raised":UNDEFINED -unreachable:110:4:110:25:test_find_missing_sphinx_raises_infer_from_function:Unreachable code:UNDEFINED -missing-raises-doc:133:0:133:46:test_find_valid_missing_sphinx_attr_raises:"""error"" not documented as being raised":UNDEFINED +missing-raises-doc:7:0:7:35:test_find_missing_sphinx_raises:"""RuntimeError"" not documented as being raised":HIGH +unreachable:13:4:13:25:test_find_missing_sphinx_raises:Unreachable code:HIGH +unreachable:36:4:36:25:test_find_all_sphinx_raises:Unreachable code:HIGH +unreachable:37:4:37:30:test_find_all_sphinx_raises:Unreachable code:HIGH +unreachable:38:4:38:27:test_find_all_sphinx_raises:Unreachable code:HIGH +unreachable:48:4:48:25:test_find_multiple_sphinx_raises:Unreachable code:HIGH +missing-raises-doc:51:0:51:37:test_finds_rethrown_sphinx_raises:"""RuntimeError"" not documented as being raised":HIGH +missing-raises-doc:64:0:64:46:test_finds_rethrown_sphinx_multiple_raises:"""RuntimeError, ValueError"" not documented as being raised":HIGH +missing-raises-doc:90:0:90:55:test_find_missing_sphinx_raises_infer_from_instance:"""RuntimeError"" not documented as being raised":HIGH +unreachable:97:4:97:25:test_find_missing_sphinx_raises_infer_from_instance:Unreachable code:HIGH +missing-raises-doc:100:0:100:55:test_find_missing_sphinx_raises_infer_from_function:"""RuntimeError"" not documented as being raised":HIGH +unreachable:110:4:110:25:test_find_missing_sphinx_raises_infer_from_function:Unreachable code:HIGH +missing-raises-doc:133:0:133:46:test_find_valid_missing_sphinx_attr_raises:"""error"" not documented as being raised":HIGH diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_options.py b/tests/functional/ext/docparams/raise/missing_raises_doc_options.py new file mode 100644 index 0000000000..eb3cd3ac32 --- /dev/null +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_options.py @@ -0,0 +1,15 @@ +"""Minimal example where a W9006 message is displayed even if the +accept-no-raise-doc option is set to True. + +Requires at least one matching section (`Docstring.matching_sections`). + +Taken from https://github.com/PyCQA/pylint/issues/7208 +""" + + +def w9006issue(dummy: int): + """Sample function. + + :param dummy: Unused + """ + raise AssertionError() diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_options.rc b/tests/functional/ext/docparams/raise/missing_raises_doc_options.rc new file mode 100644 index 0000000000..b36bb87a30 --- /dev/null +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_options.rc @@ -0,0 +1,5 @@ +[MAIN] +load-plugins = pylint.extensions.docparams + +[BASIC] +accept-no-raise-doc = yes diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_required.py b/tests/functional/ext/docparams/raise/missing_raises_doc_required.py index 860a3ecf55..96f2297a28 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_required.py +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_required.py @@ -6,3 +6,10 @@ def test_warns_unknown_style(self): # [missing-raises-doc] """This is a docstring.""" raise RuntimeError("hi") + + +# This function doesn't require a docstring, because its name starts +# with an '_' (no-docstring-rgx): +def _function(some_arg: int): + """This is a docstring.""" + raise ValueError diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_required.txt b/tests/functional/ext/docparams/raise/missing_raises_doc_required.txt index f04a2b9fdf..6b4c70dc5d 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_required.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_required.txt @@ -1 +1 @@ -missing-raises-doc:6:0:6:28:test_warns_unknown_style:"""RuntimeError"" not documented as being raised":UNDEFINED +missing-raises-doc:6:0:6:28:test_warns_unknown_style:"""RuntimeError"" not documented as being raised":HIGH diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_required_exc_inheritance.txt b/tests/functional/ext/docparams/raise/missing_raises_doc_required_exc_inheritance.txt index e955a4aec5..eceeb438c3 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_required_exc_inheritance.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_required_exc_inheritance.txt @@ -1 +1 @@ -missing-raises-doc:12:0:12:38:test_find_missing_raise_for_parent:"""NameError"" not documented as being raised":UNDEFINED +missing-raises-doc:12:0:12:38:test_find_missing_raise_for_parent:"""NameError"" not documented as being raised":HIGH diff --git a/tests/functional/ext/docparams/return/missing_return_doc_Google.txt b/tests/functional/ext/docparams/return/missing_return_doc_Google.txt index 836114036a..3009696a2d 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_Google.txt +++ b/tests/functional/ext/docparams/return/missing_return_doc_Google.txt @@ -1,7 +1,7 @@ -redundant-returns-doc:43:0:43:11:my_func:Redundant returns documentation:UNDEFINED -redundant-returns-doc:52:0:52:11:my_func:Redundant returns documentation:UNDEFINED -redundant-returns-doc:61:0:61:11:my_func:Redundant returns documentation:UNDEFINED -unreachable:95:8:95:17:Foo.foo_method:Unreachable code:UNDEFINED -unreachable:112:8:112:17:Foo.foo_method:Unreachable code:UNDEFINED -useless-param-doc:167:4:167:18:Foo.foo_method:"""_, _ignored"" useless ignored parameter documentation":UNDEFINED -useless-type-doc:167:4:167:18:Foo.foo_method:"""_"" useless ignored parameter type documentation":UNDEFINED +redundant-returns-doc:43:0:43:11:my_func:Redundant returns documentation:HIGH +redundant-returns-doc:52:0:52:11:my_func:Redundant returns documentation:HIGH +redundant-returns-doc:61:0:61:11:my_func:Redundant returns documentation:HIGH +unreachable:95:8:95:17:Foo.foo_method:Unreachable code:HIGH +unreachable:112:8:112:17:Foo.foo_method:Unreachable code:HIGH +useless-param-doc:167:4:167:18:Foo.foo_method:"""_, _ignored"" useless ignored parameter documentation":HIGH +useless-type-doc:167:4:167:18:Foo.foo_method:"""_"" useless ignored parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/return/missing_return_doc_Numpy.txt b/tests/functional/ext/docparams/return/missing_return_doc_Numpy.txt index fbcfd1287c..975d7e5028 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_Numpy.txt +++ b/tests/functional/ext/docparams/return/missing_return_doc_Numpy.txt @@ -1,5 +1,5 @@ -redundant-returns-doc:62:0:62:11:my_func:Redundant returns documentation:UNDEFINED -redundant-returns-doc:73:0:73:11:my_func:Redundant returns documentation:UNDEFINED -redundant-returns-doc:98:0:98:11:my_func:Redundant returns documentation:UNDEFINED -useless-param-doc:164:4:164:11:Foo.foo:"""_, _ignored"" useless ignored parameter documentation":UNDEFINED -useless-type-doc:164:4:164:11:Foo.foo:"""_"" useless ignored parameter type documentation":UNDEFINED +redundant-returns-doc:62:0:62:11:my_func:Redundant returns documentation:HIGH +redundant-returns-doc:73:0:73:11:my_func:Redundant returns documentation:HIGH +redundant-returns-doc:98:0:98:11:my_func:Redundant returns documentation:HIGH +useless-param-doc:164:4:164:11:Foo.foo:"""_, _ignored"" useless ignored parameter documentation":HIGH +useless-type-doc:164:4:164:11:Foo.foo:"""_"" useless ignored parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/return/missing_return_doc_Sphinx.txt b/tests/functional/ext/docparams/return/missing_return_doc_Sphinx.txt index 51d324ba7b..30e1817fd8 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_Sphinx.txt +++ b/tests/functional/ext/docparams/return/missing_return_doc_Sphinx.txt @@ -1,2 +1,2 @@ -redundant-returns-doc:44:0:44:11:my_func:Redundant returns documentation:UNDEFINED -redundant-returns-doc:52:0:52:11:my_func:Redundant returns documentation:UNDEFINED +redundant-returns-doc:44:0:44:11:my_func:Redundant returns documentation:HIGH +redundant-returns-doc:52:0:52:11:my_func:Redundant returns documentation:HIGH diff --git a/tests/functional/ext/docparams/return/missing_return_doc_required.py b/tests/functional/ext/docparams/return/missing_return_doc_required.py index c34b35777f..bd56e7e070 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_required.py +++ b/tests/functional/ext/docparams/return/missing_return_doc_required.py @@ -5,3 +5,10 @@ def warns_no_docstring(self): # [missing-return-doc, missing-return-type-doc] return False + + +# this function doesn't require a docstring, because its name starts +# with an '_' (no-docstring-rgx): +def _function(some_arg: int) -> int: + _ = some_arg + return 0 diff --git a/tests/functional/ext/docparams/return/missing_return_doc_required.txt b/tests/functional/ext/docparams/return/missing_return_doc_required.txt index 8e15b91a20..871628b1db 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_required.txt +++ b/tests/functional/ext/docparams/return/missing_return_doc_required.txt @@ -1,2 +1,2 @@ -missing-return-doc:6:0:6:22:warns_no_docstring:Missing return documentation:UNDEFINED -missing-return-type-doc:6:0:6:22:warns_no_docstring:Missing return type documentation:UNDEFINED +missing-return-doc:6:0:6:22:warns_no_docstring:Missing return documentation:HIGH +missing-return-type-doc:6:0:6:22:warns_no_docstring:Missing return type documentation:HIGH diff --git a/tests/functional/ext/docparams/return/missing_return_doc_required_Google.txt b/tests/functional/ext/docparams/return/missing_return_doc_required_Google.txt index dac5ad2808..eea513b3f5 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_required_Google.txt +++ b/tests/functional/ext/docparams/return/missing_return_doc_required_Google.txt @@ -1,10 +1,10 @@ -missing-return-type-doc:7:0:7:11:my_func:Missing return type documentation:UNDEFINED -missing-return-doc:16:0:16:11:my_func:Missing return documentation:UNDEFINED -missing-return-doc:25:0:25:11:my_func:Missing return documentation:UNDEFINED -missing-return-type-doc:25:0:25:11:my_func:Missing return type documentation:UNDEFINED -missing-return-doc:34:0:34:11:my_func:Missing return documentation:UNDEFINED -missing-return-type-doc:50:4:50:18:Foo.foo_method:Missing return type documentation:UNDEFINED -unreachable:57:8:57:17:Foo.foo_method:Unreachable code:UNDEFINED -missing-return-doc:66:4:66:18:Foo.foo_method:Missing return documentation:UNDEFINED -missing-return-type-doc:66:4:66:18:Foo.foo_method:Missing return type documentation:UNDEFINED -unreachable:74:8:74:17:Foo.foo_method:Unreachable code:UNDEFINED +missing-return-type-doc:7:0:7:11:my_func:Missing return type documentation:HIGH +missing-return-doc:16:0:16:11:my_func:Missing return documentation:HIGH +missing-return-doc:25:0:25:11:my_func:Missing return documentation:HIGH +missing-return-type-doc:25:0:25:11:my_func:Missing return type documentation:HIGH +missing-return-doc:34:0:34:11:my_func:Missing return documentation:HIGH +missing-return-type-doc:50:4:50:18:Foo.foo_method:Missing return type documentation:HIGH +unreachable:57:8:57:17:Foo.foo_method:Unreachable code:HIGH +missing-return-doc:66:4:66:18:Foo.foo_method:Missing return documentation:HIGH +missing-return-type-doc:66:4:66:18:Foo.foo_method:Missing return type documentation:HIGH +unreachable:74:8:74:17:Foo.foo_method:Unreachable code:HIGH diff --git a/tests/functional/ext/docparams/return/missing_return_doc_required_Numpy.txt b/tests/functional/ext/docparams/return/missing_return_doc_required_Numpy.txt index 61aac4ebb6..5be8a6009f 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_required_Numpy.txt +++ b/tests/functional/ext/docparams/return/missing_return_doc_required_Numpy.txt @@ -1,11 +1,11 @@ -missing-return-doc:7:0:7:11:my_func:Missing return documentation:UNDEFINED -missing-return-doc:22:0:22:11:my_func:Missing return documentation:UNDEFINED -missing-return-type-doc:22:0:22:11:my_func:Missing return type documentation:UNDEFINED -missing-return-doc:33:0:33:11:my_func:Missing return documentation:UNDEFINED -missing-return-type-doc:50:4:50:16:Foo.foo_prop:Missing return type documentation:UNDEFINED -unreachable:59:8:59:17:Foo.foo_prop:Unreachable code:UNDEFINED -missing-return-doc:68:4:68:18:Foo.foo_method:Missing return documentation:UNDEFINED -missing-return-type-doc:68:4:68:18:Foo.foo_method:Missing return type documentation:UNDEFINED -unreachable:78:8:78:17:Foo.foo_method:Unreachable code:UNDEFINED -missing-return-doc:87:4:87:18:Foo.foo_method:Missing return documentation:UNDEFINED -unreachable:97:8:97:17:Foo.foo_method:Unreachable code:UNDEFINED +missing-return-doc:7:0:7:11:my_func:Missing return documentation:HIGH +missing-return-doc:22:0:22:11:my_func:Missing return documentation:HIGH +missing-return-type-doc:22:0:22:11:my_func:Missing return type documentation:HIGH +missing-return-doc:33:0:33:11:my_func:Missing return documentation:HIGH +missing-return-type-doc:50:4:50:16:Foo.foo_prop:Missing return type documentation:HIGH +unreachable:59:8:59:17:Foo.foo_prop:Unreachable code:HIGH +missing-return-doc:68:4:68:18:Foo.foo_method:Missing return documentation:HIGH +missing-return-type-doc:68:4:68:18:Foo.foo_method:Missing return type documentation:HIGH +unreachable:78:8:78:17:Foo.foo_method:Unreachable code:HIGH +missing-return-doc:87:4:87:18:Foo.foo_method:Missing return documentation:HIGH +unreachable:97:8:97:17:Foo.foo_method:Unreachable code:HIGH diff --git a/tests/functional/ext/docparams/return/missing_return_doc_required_Sphinx.txt b/tests/functional/ext/docparams/return/missing_return_doc_required_Sphinx.txt index f9c156e0aa..216c8643f9 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_required_Sphinx.txt +++ b/tests/functional/ext/docparams/return/missing_return_doc_required_Sphinx.txt @@ -1,9 +1,9 @@ -missing-return-type-doc:8:0:8:11:my_func:Missing return type documentation:UNDEFINED -missing-return-doc:24:0:24:11:my_func:Missing return documentation:UNDEFINED -missing-return-doc:32:0:32:31:warn_missing_sphinx_returns:Missing return documentation:UNDEFINED -missing-return-type-doc:32:0:32:31:warn_missing_sphinx_returns:Missing return type documentation:UNDEFINED -missing-return-doc:43:0:43:11:my_func:Missing return documentation:UNDEFINED -missing-return-type-doc:58:4:58:11:Foo.foo:Missing return type documentation:UNDEFINED -unreachable:64:8:64:17:Foo.foo:Unreachable code:UNDEFINED -missing-return-doc:72:4:72:52:Foo.test_ignores_non_property_return_type_sphinx:Missing return documentation:UNDEFINED -missing-return-type-doc:72:4:72:52:Foo.test_ignores_non_property_return_type_sphinx:Missing return type documentation:UNDEFINED +missing-return-type-doc:8:0:8:11:my_func:Missing return type documentation:HIGH +missing-return-doc:24:0:24:11:my_func:Missing return documentation:HIGH +missing-return-doc:32:0:32:31:warn_missing_sphinx_returns:Missing return documentation:HIGH +missing-return-type-doc:32:0:32:31:warn_missing_sphinx_returns:Missing return type documentation:HIGH +missing-return-doc:43:0:43:11:my_func:Missing return documentation:HIGH +missing-return-type-doc:58:4:58:11:Foo.foo:Missing return type documentation:HIGH +unreachable:64:8:64:17:Foo.foo:Unreachable code:HIGH +missing-return-doc:72:4:72:52:Foo.test_ignores_non_property_return_type_sphinx:Missing return documentation:HIGH +missing-return-type-doc:72:4:72:52:Foo.test_ignores_non_property_return_type_sphinx:Missing return type documentation:HIGH diff --git a/tests/functional/ext/docparams/useless_type_doc.txt b/tests/functional/ext/docparams/useless_type_doc.txt index 3408f18036..cc6b7148d6 100644 --- a/tests/functional/ext/docparams/useless_type_doc.txt +++ b/tests/functional/ext/docparams/useless_type_doc.txt @@ -1,4 +1,4 @@ -useless-param-doc:34:0:34:24:function_useless_doc:"""_some_private_param"" useless ignored parameter documentation":UNDEFINED -useless-type-doc:34:0:34:24:function_useless_doc:"""_some_private_param"" useless ignored parameter type documentation":UNDEFINED -useless-param-doc:67:0:67:12:test_two:"""_new"" useless ignored parameter documentation":UNDEFINED -useless-type-doc:67:0:67:12:test_two:"""_new"" useless ignored parameter type documentation":UNDEFINED +useless-param-doc:34:0:34:24:function_useless_doc:"""_some_private_param"" useless ignored parameter documentation":HIGH +useless-type-doc:34:0:34:24:function_useless_doc:"""_some_private_param"" useless ignored parameter type documentation":HIGH +useless-param-doc:67:0:67:12:test_two:"""_new"" useless ignored parameter documentation":HIGH +useless-type-doc:67:0:67:12:test_two:"""_new"" useless ignored parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/yield/missing_yield_doc_required.py b/tests/functional/ext/docparams/yield/missing_yield_doc_required.py index e307063b36..bfaae4f03c 100644 --- a/tests/functional/ext/docparams/yield/missing_yield_doc_required.py +++ b/tests/functional/ext/docparams/yield/missing_yield_doc_required.py @@ -1,7 +1,15 @@ """Tests for missing-yield-doc and missing-yield-type-doc with accept-no-yields-doc = no""" # pylint: disable=missing-function-docstring, unused-argument, function-redefined +from typing import Iterator + # Test missing docstring def my_func(self): # [missing-yield-doc, missing-yield-type-doc] yield False + + +# This function doesn't require a docstring, because its name starts +# with an '_' (no-docstring-rgx): +def _function(some_arg: int) -> Iterator[int]: + yield some_arg diff --git a/tests/functional/ext/docparams/yield/missing_yield_doc_required.txt b/tests/functional/ext/docparams/yield/missing_yield_doc_required.txt index d9162494e4..c7bd4b0333 100644 --- a/tests/functional/ext/docparams/yield/missing_yield_doc_required.txt +++ b/tests/functional/ext/docparams/yield/missing_yield_doc_required.txt @@ -1,2 +1,2 @@ -missing-yield-doc:6:0:6:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-type-doc:6:0:6:11:my_func:Missing yield type documentation:UNDEFINED +missing-yield-doc:8:0:8:11:my_func:Missing yield documentation:HIGH +missing-yield-type-doc:8:0:8:11:my_func:Missing yield type documentation:HIGH diff --git a/tests/functional/ext/docparams/yield/missing_yield_doc_required_Google.txt b/tests/functional/ext/docparams/yield/missing_yield_doc_required_Google.txt index 0a655a744d..a08ffd329d 100644 --- a/tests/functional/ext/docparams/yield/missing_yield_doc_required_Google.txt +++ b/tests/functional/ext/docparams/yield/missing_yield_doc_required_Google.txt @@ -1,5 +1,5 @@ -missing-yield-doc:34:0:34:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-type-doc:43:0:43:11:my_func:Missing yield type documentation:UNDEFINED -missing-yield-doc:52:0:52:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-doc:61:0:61:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-type-doc:61:0:61:11:my_func:Missing yield type documentation:UNDEFINED +missing-yield-doc:34:0:34:11:my_func:Missing yield documentation:HIGH +missing-yield-type-doc:43:0:43:11:my_func:Missing yield type documentation:HIGH +missing-yield-doc:52:0:52:11:my_func:Missing yield documentation:HIGH +missing-yield-doc:61:0:61:11:my_func:Missing yield documentation:HIGH +missing-yield-type-doc:61:0:61:11:my_func:Missing yield type documentation:HIGH diff --git a/tests/functional/ext/docparams/yield/missing_yield_doc_required_Numpy.txt b/tests/functional/ext/docparams/yield/missing_yield_doc_required_Numpy.txt index 7ca1f80b0a..683dd99129 100644 --- a/tests/functional/ext/docparams/yield/missing_yield_doc_required_Numpy.txt +++ b/tests/functional/ext/docparams/yield/missing_yield_doc_required_Numpy.txt @@ -1,3 +1,3 @@ -missing-yield-doc:40:0:40:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-doc:50:0:50:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-type-doc:50:0:50:11:my_func:Missing yield type documentation:UNDEFINED +missing-yield-doc:40:0:40:11:my_func:Missing yield documentation:HIGH +missing-yield-doc:50:0:50:11:my_func:Missing yield documentation:HIGH +missing-yield-type-doc:50:0:50:11:my_func:Missing yield type documentation:HIGH diff --git a/tests/functional/ext/docparams/yield/missing_yield_doc_required_Sphinx.txt b/tests/functional/ext/docparams/yield/missing_yield_doc_required_Sphinx.txt index 3b1931e017..f6467a9c19 100644 --- a/tests/functional/ext/docparams/yield/missing_yield_doc_required_Sphinx.txt +++ b/tests/functional/ext/docparams/yield/missing_yield_doc_required_Sphinx.txt @@ -1,5 +1,5 @@ -missing-yield-doc:35:0:35:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-type-doc:43:0:43:11:my_func:Missing yield type documentation:UNDEFINED -missing-yield-doc:51:0:51:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-doc:59:0:59:11:my_func:Missing yield documentation:UNDEFINED -missing-yield-type-doc:59:0:59:11:my_func:Missing yield type documentation:UNDEFINED +missing-yield-doc:35:0:35:11:my_func:Missing yield documentation:HIGH +missing-yield-type-doc:43:0:43:11:my_func:Missing yield type documentation:HIGH +missing-yield-doc:51:0:51:11:my_func:Missing yield documentation:HIGH +missing-yield-doc:59:0:59:11:my_func:Missing yield documentation:HIGH +missing-yield-type-doc:59:0:59:11:my_func:Missing yield type documentation:HIGH diff --git a/tests/functional/ext/emptystring/empty_string_comparison.py b/tests/functional/ext/emptystring/empty_string_comparison.py index c6dcf8ea8f..b61caeff61 100644 --- a/tests/functional/ext/emptystring/empty_string_comparison.py +++ b/tests/functional/ext/emptystring/empty_string_comparison.py @@ -14,3 +14,9 @@ if Y != '': # [compare-to-empty-string] pass + +if "" == Y: # [compare-to-empty-string] + pass + +if '' != X: # [compare-to-empty-string] + pass diff --git a/tests/functional/ext/emptystring/empty_string_comparison.txt b/tests/functional/ext/emptystring/empty_string_comparison.txt index 5b259bc464..be9c91bc57 100644 --- a/tests/functional/ext/emptystring/empty_string_comparison.txt +++ b/tests/functional/ext/emptystring/empty_string_comparison.txt @@ -1,4 +1,6 @@ -compare-to-empty-string:6:3:6:10::Avoid comparisons to empty string:UNDEFINED -compare-to-empty-string:9:3:9:14::Avoid comparisons to empty string:UNDEFINED -compare-to-empty-string:12:3:12:10::Avoid comparisons to empty string:UNDEFINED -compare-to-empty-string:15:3:15:10::Avoid comparisons to empty string:UNDEFINED +compare-to-empty-string:6:3:6:10::"""X is ''"" can be simplified to ""not X"" as an empty string is falsey":HIGH +compare-to-empty-string:9:3:9:14::"""Y is not ''"" can be simplified to ""Y"" as an empty string is falsey":HIGH +compare-to-empty-string:12:3:12:10::"""X == ''"" can be simplified to ""not X"" as an empty string is falsey":HIGH +compare-to-empty-string:15:3:15:10::"""Y != ''"" can be simplified to ""Y"" as an empty string is falsey":HIGH +compare-to-empty-string:18:3:18:10::"""'' == Y"" can be simplified to ""not Y"" as an empty string is falsey":HIGH +compare-to-empty-string:21:3:21:10::"""'' != X"" can be simplified to ""X"" as an empty string is falsey":HIGH diff --git a/tests/functional/ext/for_any_all/for_any_all.py b/tests/functional/ext/for_any_all/for_any_all.py index 8b4c7275ca..649739c374 100644 --- a/tests/functional/ext/for_any_all/for_any_all.py +++ b/tests/functional/ext/for_any_all/for_any_all.py @@ -1,4 +1,5 @@ """Functional test""" +# pylint: disable=missing-function-docstring, invalid-name def any_even(items): """Return True if the list contains any even numbers""" @@ -144,3 +145,94 @@ def is_from_decorator(node): if parent in parent.selected_annotations: return False return False + +def optimized_any_with_break(split_lines, max_chars): + """False negative found in https://github.com/PyCQA/pylint/pull/7697""" + potential_line_length_warning = False + for line in split_lines: # [consider-using-any-or-all] + if len(line) > max_chars: + potential_line_length_warning = True + break + return potential_line_length_warning + +def optimized_any_without_break(split_lines, max_chars): + potential_line_length_warning = False + for line in split_lines: # [consider-using-any-or-all] + if len(line) > max_chars: + potential_line_length_warning = True + return potential_line_length_warning + +def print_line_without_break(split_lines, max_chars): + potential_line_length_warning = False + for line in split_lines: + print(line) + if len(line) > max_chars: + potential_line_length_warning = True + return potential_line_length_warning + +def print_line_without_reassign(split_lines, max_chars): + potential_line_length_warning = False + for line in split_lines: + if len(line) > max_chars: + print(line) + return potential_line_length_warning + +def multiple_flags(split_lines, max_chars): + potential_line_length_warning = False + for line in split_lines: + if len(line) > max_chars: + num = 1 + print(num) + potential_line_length_warning = True + return potential_line_length_warning + +s = ["hi", "hello", "goodbye", None] + +flag = True +for i, elem in enumerate(s): + if elem is None: + continue + cnt_s = cnt_t = 0 + for j in range(i, len(s)): + if s[j] == elem: + cnt_s += 1 + s[j] = None + Flag = False + +def with_elif(split_lines, max_chars): + """ + Do not raise consider-using-any-or-all because the intent in this code + is to iterate over all the lines (not short-circuit) and see what + the last value would be. + """ + last_longest_line = False + for line in split_lines: + if len(line) > max_chars: + last_longest_line = True + elif len(line) == max_chars: + last_longest_line = False + return last_longest_line + +def first_even(items): + """Return first even number""" + for item in items: + if item % 2 == 0: + return item + return None + +def even(items): + for item in items: + if item % 2 == 0: + return True + return None + +def iterate_leaves(leaves, current_node): + results = [] + + current_node.was_checked = True + for leaf in leaves: + if isinstance(leaf, bool): + current_node.was_checked = False + else: + results.append(leaf) + return results diff --git a/tests/functional/ext/for_any_all/for_any_all.txt b/tests/functional/ext/for_any_all/for_any_all.txt index bc09876e4b..dca0ad3d38 100644 --- a/tests/functional/ext/for_any_all/for_any_all.txt +++ b/tests/functional/ext/for_any_all/for_any_all.txt @@ -1,12 +1,14 @@ -consider-using-any-or-all:5:4:7:23:any_even:`for` loop could be `any(item % 2 == 0 for item in items)`:UNDEFINED -consider-using-any-or-all:12:4:14:24:all_even:`for` loop could be `all(item % 2 == 0 for item in items)`:UNDEFINED -consider-using-any-or-all:19:4:21:23:any_uneven:`for` loop could be `not all(item % 2 == 0 for item in items)`:UNDEFINED -consider-using-any-or-all:26:4:28:24:all_uneven:`for` loop could be `not any(item % 2 == 0 for item in items)`:UNDEFINED -consider-using-any-or-all:33:4:35:23:is_from_string:`for` loop could be `any(isinstance(parent, str) for parent in item.parents())`:UNDEFINED -consider-using-any-or-all:40:4:42:23:is_not_from_string:`for` loop could be `not all(isinstance(parent, str) for parent in item.parents())`:UNDEFINED -consider-using-any-or-all:49:8:51:28:nested_check:`for` loop could be `not any(item in (1, 2, 3) for item in items)`:UNDEFINED -consider-using-any-or-all:58:4:60:23:words_contains_word:`for` loop could be `any(word == 'word' for word in words)`:UNDEFINED -consider-using-any-or-all:65:4:67:24:complicated_condition_check:`for` loop could be `not any(item % 2 == 0 and (item % 3 == 0 or item > 15) for item in items)`:UNDEFINED -consider-using-any-or-all:72:4:77:23:is_from_decorator1:`for` loop could be `any(ancestor.name in ('Exception', 'BaseException') and ancestor.root().name == 'Exception' for ancestor in node)`:UNDEFINED -consider-using-any-or-all:82:4:84:24:is_from_decorator2:`for` loop could be `all(item % 2 == 0 and (item % 3 == 0 or item > 15) for item in items)`:UNDEFINED -consider-using-any-or-all:89:4:94:23:is_from_decorator3:`for` loop could be `not all(ancestor.name in ('Exception', 'BaseException') and ancestor.root().name == 'Exception' for ancestor in node)`:UNDEFINED +consider-using-any-or-all:6:4:8:23:any_even:`for` loop could be `any(item % 2 == 0 for item in items)`:HIGH +consider-using-any-or-all:13:4:15:24:all_even:`for` loop could be `all(item % 2 == 0 for item in items)`:HIGH +consider-using-any-or-all:20:4:22:23:any_uneven:`for` loop could be `not all(item % 2 == 0 for item in items)`:HIGH +consider-using-any-or-all:27:4:29:24:all_uneven:`for` loop could be `not any(item % 2 == 0 for item in items)`:HIGH +consider-using-any-or-all:34:4:36:23:is_from_string:`for` loop could be `any(isinstance(parent, str) for parent in item.parents())`:HIGH +consider-using-any-or-all:41:4:43:23:is_not_from_string:`for` loop could be `not all(isinstance(parent, str) for parent in item.parents())`:HIGH +consider-using-any-or-all:50:8:52:28:nested_check:`for` loop could be `not any(item in (1, 2, 3) for item in items)`:HIGH +consider-using-any-or-all:59:4:61:23:words_contains_word:`for` loop could be `any(word == 'word' for word in words)`:HIGH +consider-using-any-or-all:66:4:68:24:complicated_condition_check:`for` loop could be `not any(item % 2 == 0 and (item % 3 == 0 or item > 15) for item in items)`:HIGH +consider-using-any-or-all:73:4:78:23:is_from_decorator1:`for` loop could be `any(ancestor.name in ('Exception', 'BaseException') and ancestor.root().name == 'Exception' for ancestor in node)`:HIGH +consider-using-any-or-all:83:4:85:24:is_from_decorator2:`for` loop could be `all(item % 2 == 0 and (item % 3 == 0 or item > 15) for item in items)`:HIGH +consider-using-any-or-all:90:4:95:23:is_from_decorator3:`for` loop could be `not all(ancestor.name in ('Exception', 'BaseException') and ancestor.root().name == 'Exception' for ancestor in node)`:HIGH +consider-using-any-or-all:152:4:155:17:optimized_any_with_break:`for` loop could be `not any(len(line) > max_chars for line in split_lines)`:HIGH +consider-using-any-or-all:160:4:162:48:optimized_any_without_break:`for` loop could be `not any(len(line) > max_chars for line in split_lines)`:HIGH diff --git a/tests/functional/ext/magic_value_comparison/magic_value_comparison.py b/tests/functional/ext/magic_value_comparison/magic_value_comparison.py new file mode 100644 index 0000000000..f6fb9369be --- /dev/null +++ b/tests/functional/ext/magic_value_comparison/magic_value_comparison.py @@ -0,0 +1,36 @@ +""" +Checks that magic values are not used in comparisons +""" +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,wrong-import-position + +from enum import Enum + + +class Christmas(Enum): + EVE = 25 + DAY = 26 + MONTH = 12 + + +var = 7 +if var > 5: # [magic-value-comparison] + pass + +if (var + 5) > 10: # [magic-value-comparison] + pass + +is_big = 100 < var # [magic-value-comparison] + +shouldnt_raise = 5 > 7 # [comparison-of-constants] +shouldnt_raise = var == '__main__' +shouldnt_raise = var == 1 +shouldnt_raise = var == 0 +shouldnt_raise = var == -1 +shouldnt_raise = var == True # [singleton-comparison] +shouldnt_raise = var == False # [singleton-comparison] +shouldnt_raise = var == None # [singleton-comparison] +celebration_started = Christmas.EVE.value == Christmas.MONTH.value +shouldnt_raise = var == "" + +shouldnt_raise = var == '\n' +shouldnt_raise = var == '\\b' diff --git a/tests/functional/ext/magic_value_comparison/magic_value_comparison.rc b/tests/functional/ext/magic_value_comparison/magic_value_comparison.rc new file mode 100644 index 0000000000..183613432d --- /dev/null +++ b/tests/functional/ext/magic_value_comparison/magic_value_comparison.rc @@ -0,0 +1,4 @@ +[MAIN] +load-plugins=pylint.extensions.magic_value, + +valid-magic-values=0, 1,-1, __main__,'', \n, \\b diff --git a/tests/functional/ext/magic_value_comparison/magic_value_comparison.txt b/tests/functional/ext/magic_value_comparison/magic_value_comparison.txt new file mode 100644 index 0000000000..63976ff68b --- /dev/null +++ b/tests/functional/ext/magic_value_comparison/magic_value_comparison.txt @@ -0,0 +1,7 @@ +magic-value-comparison:16:3:16:10::Consider using a named constant or an enum instead of '5'.:HIGH +magic-value-comparison:19:3:19:17::Consider using a named constant or an enum instead of '10'.:HIGH +magic-value-comparison:22:9:22:18::Consider using a named constant or an enum instead of '100'.:HIGH +comparison-of-constants:24:17:24:22::"Comparison between constants: '5 > 7' has a constant value":HIGH +singleton-comparison:29:17:29:28::Comparison 'var == True' should be 'var is True' if checking for the singleton value True, or 'bool(var)' if testing for truthiness:UNDEFINED +singleton-comparison:30:17:30:29::Comparison 'var == False' should be 'var is False' if checking for the singleton value False, or 'not var' if testing for falsiness:UNDEFINED +singleton-comparison:31:17:31:28::Comparison 'var == None' should be 'var is None':UNDEFINED diff --git a/tests/functional/ext/mccabe/mccabe.py b/tests/functional/ext/mccabe/mccabe.py index e57d13e2e7..92623cd1c1 100644 --- a/tests/functional/ext/mccabe/mccabe.py +++ b/tests/functional/ext/mccabe/mccabe.py @@ -1,7 +1,7 @@ # pylint: disable=invalid-name,unnecessary-pass,no-else-return,useless-else-on-loop # pylint: disable=undefined-variable,consider-using-sys-exit,unused-variable,too-many-return-statements -# pylint: disable=redefined-outer-name,useless-object-inheritance,using-constant-test,unused-argument -# pylint: disable=broad-except, not-context-manager, no-method-argument, unspecified-encoding +# pylint: disable=redefined-outer-name,using-constant-test,unused-argument +# pylint: disable=broad-except, not-context-manager, no-method-argument, unspecified-encoding, broad-exception-raised """Checks use of "too-complex" check""" @@ -130,7 +130,7 @@ def f10(): # [too-complex] return myint -class MyClass1(object): +class MyClass1: """Class of example to test mccabe""" _name = "MyClass" # To force a tail.node=None diff --git a/tests/functional/ext/no_self_use/no_self_use.py b/tests/functional/ext/no_self_use/no_self_use.py index dfe3f6f35e..d362a1d3e7 100644 --- a/tests/functional/ext/no_self_use/no_self_use.py +++ b/tests/functional/ext/no_self_use/no_self_use.py @@ -1,10 +1,10 @@ -# pylint: disable=too-few-public-methods,missing-docstring,useless-object-inheritance,invalid-name +# pylint: disable=too-few-public-methods,missing-docstring,invalid-name """test detection of method which could be a function""" from abc import ABC, abstractmethod from typing import Protocol, overload -class Toto(object): +class Toto: """bla bal abl""" def __init__(self): @@ -26,7 +26,7 @@ async def async_function_method(self): # [no-self-use] """this async method isn't a real method since it doesn't need self""" print('hello') -class Base(object): +class Base: """an abstract class""" def __init__(self): @@ -46,7 +46,7 @@ def check(self, arg): """ return arg == 0 -class Super(object): +class Super: """same as before without abstract""" attr = 1 def method(self): @@ -74,7 +74,7 @@ def __getstate__(self): return 42 -class Prop(object): +class Prop: @property def count(self): diff --git a/tests/functional/ext/private_import/private_import.py b/tests/functional/ext/private_import/private_import.py index 917e01c9c2..a73e69e758 100644 --- a/tests/functional/ext/private_import/private_import.py +++ b/tests/functional/ext/private_import/private_import.py @@ -137,3 +137,6 @@ def save(self): # Treat relative imports as internal from .other_file import _private from ..parent import _private + +from _private_module_x import some_name # [import-private-name] +var = some_name diff --git a/tests/functional/ext/private_import/private_import.txt b/tests/functional/ext/private_import/private_import.txt index f618d58587..8beee9ae68 100644 --- a/tests/functional/ext/private_import/private_import.txt +++ b/tests/functional/ext/private_import/private_import.txt @@ -18,3 +18,4 @@ import-private-name:107:0:107:41::Imported private module (_private_module5):HIG import-private-name:111:0:111:42::Imported private module (_private_module6):HIGH import-private-name:114:0:114:40::Imported private object (_PrivateClass3):HIGH import-private-name:119:0:119:34::Imported private module (_private_module_unreachable):HIGH +import-private-name:141:0:141:39::Imported private module (_private_module_x):HIGH diff --git a/tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.py b/tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.py index 1a62749768..4b64ebe46b 100644 --- a/tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.py +++ b/tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.py @@ -1,6 +1,5 @@ """Tests for redefining an outer loop variable.""" -from __future__ import print_function -__revision__ = 0 + # Simple nested loop for i in range(10): diff --git a/tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.txt b/tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.txt index ea26e01d1f..055bd4aa56 100644 --- a/tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.txt +++ b/tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.txt @@ -1,5 +1,5 @@ -redefined-loop-name:7:4:8:16::Redefining 'i' from loop (line 6):HIGH -redefined-loop-name:12:4:13:25::Redefining 'i' from loop (line 11):HIGH -redefined-loop-name:17:4:18:25::Redefining 'i' from loop (line 16):HIGH -redefined-loop-name:22:4:23:25::Redefining 'a' from loop (line 21):HIGH -redefined-loop-name:41:4:42:16::Redefining 'j' from loop (line 40):HIGH +redefined-loop-name:6:4:7:16::Redefining 'i' from loop (line 5):HIGH +redefined-loop-name:11:4:12:25::Redefining 'i' from loop (line 10):HIGH +redefined-loop-name:16:4:17:25::Redefining 'i' from loop (line 15):HIGH +redefined-loop-name:21:4:22:25::Redefining 'a' from loop (line 20):HIGH +redefined-loop-name:40:4:41:16::Redefining 'j' from loop (line 39):HIGH diff --git a/tests/functional/ext/redefined_variable_type/redefined_variable_type.py b/tests/functional/ext/redefined_variable_type/redefined_variable_type.py index aa89383d9e..1d31bc9683 100644 --- a/tests/functional/ext/redefined_variable_type/redefined_variable_type.py +++ b/tests/functional/ext/redefined_variable_type/redefined_variable_type.py @@ -1,12 +1,12 @@ """Checks variable types aren't redefined within a method or a function""" -# pylint: disable=too-few-public-methods,missing-docstring,unused-variable,invalid-name, useless-object-inheritance +# pylint: disable=too-few-public-methods,missing-docstring,unused-variable,invalid-name _OK = True -class MyClass(object): +class MyClass: - class Klass(object): + class Klass: def __init__(self): self.var2 = 'var' @@ -83,3 +83,28 @@ def func2(x): else: var4 = 2. var4 = 'baz' # [redefined-variable-type] + + +# Test that ``redefined-variable-type`` is not emitted +# https://github.com/PyCQA/pylint/issues/8120 + +async def test_a(): + data = [ + {'test': 1}, + {'test': 2}, + ] + return data + +async def test_b(): + data = {'test': 1} + return data + + +class AsyncFunctions: + async def funtion1(self): + potato = 1 + print(potato) + + async def funtion2(self): + potato = {} + print(potato) diff --git a/tests/functional/ext/set_membership/use_set_membership.py b/tests/functional/ext/set_membership/use_set_membership.py index 50e07f4ddd..7872d7f98f 100644 --- a/tests/functional/ext/set_membership/use_set_membership.py +++ b/tests/functional/ext/set_membership/use_set_membership.py @@ -33,7 +33,7 @@ x in (1, "Hello World", False, None) # [use-set-for-membership] x in (1, []) # List is not hashable -if some_var: +if x: var2 = 2 else: var2 = [] diff --git a/tests/functional/ext/typing/redundant_typehint_argument.py b/tests/functional/ext/typing/redundant_typehint_argument.py new file mode 100644 index 0000000000..2b423dc7ab --- /dev/null +++ b/tests/functional/ext/typing/redundant_typehint_argument.py @@ -0,0 +1,21 @@ +""""Checks for redundant Union typehints in assignments""" +# pylint: disable=deprecated-typing-alias,consider-alternative-union-syntax,consider-using-alias + +from __future__ import annotations +from typing import Union, Optional, Sequence + +# +1: [redundant-typehint-argument, redundant-typehint-argument] +ANSWER_0: Union[int, int, str, bool, float, str] = 0 +ANSWER_1: Optional[int] = 1 +ANSWER_2: Sequence[int] = [2] +ANSWER_3: Union[list[int], str, int, bool, list[int]] = 3 # [redundant-typehint-argument] +ANSWER_4: Optional[None] = None # [redundant-typehint-argument] +ANSWER_5: Optional[list[int]] = None +ANSWER_6: Union[None, None] = None # [redundant-typehint-argument] +# +1: [redundant-typehint-argument] +ANSWER_7: Union[list[int], dict[int], dict[list[int]], list[str], list[str]] = [7] +ANSWER_8: int | int = 8 # [redundant-typehint-argument] +ANSWER_9: str | int | None | int | bool = 9 # [redundant-typehint-argument] +ANSWER_10: dict | list[int] | float | str | int | bool = 10 +# +1: [redundant-typehint-argument] +ANSWER_11: list[int] | dict[int] | dict[list[int]] | list[str] | list[str] = ['string'] diff --git a/tests/functional/ext/typing/redundant_typehint_argument.rc b/tests/functional/ext/typing/redundant_typehint_argument.rc new file mode 100644 index 0000000000..7ffc1704bb --- /dev/null +++ b/tests/functional/ext/typing/redundant_typehint_argument.rc @@ -0,0 +1,8 @@ +[main] +load-plugins=pylint.extensions.typing + +[testoptions] +min_pyver=3.7 + +[TYPING] +runtime-typing=no diff --git a/tests/functional/ext/typing/redundant_typehint_argument.txt b/tests/functional/ext/typing/redundant_typehint_argument.txt new file mode 100644 index 0000000000..e76dc562d9 --- /dev/null +++ b/tests/functional/ext/typing/redundant_typehint_argument.txt @@ -0,0 +1,9 @@ +redundant-typehint-argument:8:0:8:52::Type `int` is used more than once in union type annotation. Remove redundant typehints.:HIGH +redundant-typehint-argument:8:0:8:52::Type `str` is used more than once in union type annotation. Remove redundant typehints.:HIGH +redundant-typehint-argument:11:0:11:57::Type `list[int]` is used more than once in union type annotation. Remove redundant typehints.:HIGH +redundant-typehint-argument:12:10:12:24::Type `None` is used more than once in union type annotation. Remove redundant typehints.:HIGH +redundant-typehint-argument:14:0:14:34::Type `None` is used more than once in union type annotation. Remove redundant typehints.:HIGH +redundant-typehint-argument:16:0:16:82::Type `list[str]` is used more than once in union type annotation. Remove redundant typehints.:HIGH +redundant-typehint-argument:17:0:17:23::Type `int` is used more than once in union type annotation. Remove redundant typehints.:HIGH +redundant-typehint-argument:18:0:18:43::Type `int` is used more than once in union type annotation. Remove redundant typehints.:HIGH +redundant-typehint-argument:21:0:21:87::Type `list[str]` is used more than once in union type annotation. Remove redundant typehints.:HIGH diff --git a/tests/functional/ext/typing/typing_broken_noreturn.py b/tests/functional/ext/typing/typing_broken_noreturn.py index 4d10ed13ab..e7b5643ae0 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn.py +++ b/tests/functional/ext/typing/typing_broken_noreturn.py @@ -1,10 +1,10 @@ """ -'typing.NoReturn' is broken inside compond types for Python 3.7.0 +'typing.NoReturn' is broken inside compound types for Python 3.7.0 https://bugs.python.org/issue34921 If no runtime introspection is required, use string annotations instead. """ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised import typing from typing import TYPE_CHECKING, Callable, NoReturn, Union diff --git a/tests/functional/ext/typing/typing_broken_noreturn_future_import.py b/tests/functional/ext/typing/typing_broken_noreturn_future_import.py index 4743750bc5..e0ea7761ba 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn_future_import.py +++ b/tests/functional/ext/typing/typing_broken_noreturn_future_import.py @@ -7,7 +7,7 @@ With 'from __future__ import annotations', only emit errors for nodes not in a type annotation context. """ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised from __future__ import annotations import typing diff --git a/tests/functional/ext/typing/typing_broken_noreturn_py372.py b/tests/functional/ext/typing/typing_broken_noreturn_py372.py index 4ff1a71b75..6bd31f0695 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn_py372.py +++ b/tests/functional/ext/typing/typing_broken_noreturn_py372.py @@ -6,7 +6,7 @@ Don't emit errors if py-version set to >= 3.7.2. """ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised import typing from typing import TYPE_CHECKING, Callable, NoReturn, Union diff --git a/tests/functional/ext/typing/typing_consider_using_alias.py b/tests/functional/ext/typing/typing_consider_using_alias.py index 9fe636a96f..8fe3b59186 100644 --- a/tests/functional/ext/typing/typing_consider_using_alias.py +++ b/tests/functional/ext/typing/typing_consider_using_alias.py @@ -3,7 +3,13 @@ 'py-version' needs to be set to '3.7' or '3.8' and 'runtime-typing=no'. With 'from __future__ import annotations' present. """ + # pylint: disable=missing-docstring,invalid-name,unused-argument,line-too-long,unnecessary-direct-lambda-call + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from __future__ import annotations import collections diff --git a/tests/functional/ext/typing/typing_consider_using_alias.txt b/tests/functional/ext/typing/typing_consider_using_alias.txt index 0845e2c9cd..2cd299d904 100644 --- a/tests/functional/ext/typing/typing_consider_using_alias.txt +++ b/tests/functional/ext/typing/typing_consider_using_alias.txt @@ -1,20 +1,20 @@ -consider-using-alias:16:6:16:17::'typing.Dict' will be deprecated with PY39, consider using 'dict' instead:INFERENCE -consider-using-alias:17:6:17:10::'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE -consider-using-alias:19:6:19:24::'typing.OrderedDict' will be deprecated with PY39, consider using 'collections.OrderedDict' instead:INFERENCE -consider-using-alias:20:6:20:22::'typing.Awaitable' will be deprecated with PY39, consider using 'collections.abc.Awaitable' instead:INFERENCE -consider-using-alias:21:6:21:21::'typing.Iterable' will be deprecated with PY39, consider using 'collections.abc.Iterable' instead:INFERENCE -consider-using-alias:22:6:22:21::'typing.Hashable' will be deprecated with PY39, consider using 'collections.abc.Hashable' instead:INFERENCE -consider-using-alias:23:6:23:27::'typing.ContextManager' will be deprecated with PY39, consider using 'contextlib.AbstractContextManager' instead:INFERENCE -consider-using-alias:24:6:24:20::'typing.Pattern' will be deprecated with PY39, consider using 're.Pattern' instead:INFERENCE -consider-using-alias:25:7:25:22::'typing.Match' will be deprecated with PY39, consider using 're.Match' instead:INFERENCE -consider-using-alias:34:9:34:13::'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE -consider-using-alias:36:7:36:11::'typing.Type' will be deprecated with PY39, consider using 'type' instead:INFERENCE -consider-using-alias:37:7:37:12::'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead:INFERENCE -consider-using-alias:38:7:38:15::'typing.Callable' will be deprecated with PY39, consider using 'collections.abc.Callable' instead:INFERENCE -consider-using-alias:44:74:44:78:func1:'typing.Dict' will be deprecated with PY39, consider using 'dict' instead:INFERENCE -consider-using-alias:44:16:44:20:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE -consider-using-alias:44:37:44:41:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE -consider-using-alias:44:93:44:105:func1:'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead:INFERENCE -consider-using-alias:60:12:60:16:CustomNamedTuple:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE -consider-using-alias:65:12:65:16:CustomTypedDict2:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE -consider-using-alias:69:12:69:16:CustomDataClass:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:22:6:22:17::'typing.Dict' will be deprecated with PY39, consider using 'dict' instead:INFERENCE +consider-using-alias:23:6:23:10::'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:25:6:25:24::'typing.OrderedDict' will be deprecated with PY39, consider using 'collections.OrderedDict' instead:INFERENCE +consider-using-alias:26:6:26:22::'typing.Awaitable' will be deprecated with PY39, consider using 'collections.abc.Awaitable' instead:INFERENCE +consider-using-alias:27:6:27:21::'typing.Iterable' will be deprecated with PY39, consider using 'collections.abc.Iterable' instead:INFERENCE +consider-using-alias:28:6:28:21::'typing.Hashable' will be deprecated with PY39, consider using 'collections.abc.Hashable' instead:INFERENCE +consider-using-alias:29:6:29:27::'typing.ContextManager' will be deprecated with PY39, consider using 'contextlib.AbstractContextManager' instead:INFERENCE +consider-using-alias:30:6:30:20::'typing.Pattern' will be deprecated with PY39, consider using 're.Pattern' instead:INFERENCE +consider-using-alias:31:7:31:22::'typing.Match' will be deprecated with PY39, consider using 're.Match' instead:INFERENCE +consider-using-alias:40:9:40:13::'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:42:7:42:11::'typing.Type' will be deprecated with PY39, consider using 'type' instead:INFERENCE +consider-using-alias:43:7:43:12::'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead:INFERENCE +consider-using-alias:44:7:44:15::'typing.Callable' will be deprecated with PY39, consider using 'collections.abc.Callable' instead:INFERENCE +consider-using-alias:50:74:50:78:func1:'typing.Dict' will be deprecated with PY39, consider using 'dict' instead:INFERENCE +consider-using-alias:50:16:50:20:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:50:37:50:41:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:50:93:50:105:func1:'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead:INFERENCE +consider-using-alias:66:12:66:16:CustomNamedTuple:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:71:12:71:16:CustomTypedDict2:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:75:12:75:16:CustomDataClass:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE diff --git a/tests/functional/ext/typing/typing_consider_using_alias_without_future.py b/tests/functional/ext/typing/typing_consider_using_alias_without_future.py index 17daab9939..943a161887 100644 --- a/tests/functional/ext/typing/typing_consider_using_alias_without_future.py +++ b/tests/functional/ext/typing/typing_consider_using_alias_without_future.py @@ -2,7 +2,14 @@ 'py-version' needs to be set to '3.7' or '3.8' and 'runtime-typing=no'. """ -# pylint: disable=missing-docstring,invalid-name,unused-argument,line-too-long,unsubscriptable-object,unnecessary-direct-lambda-call + +# pylint: disable=missing-docstring,invalid-name,unused-argument,line-too-long,unsubscriptable-object +# pylint: disable=unnecessary-direct-lambda-call + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + import collections import collections.abc import typing diff --git a/tests/functional/ext/typing/typing_consider_using_alias_without_future.txt b/tests/functional/ext/typing/typing_consider_using_alias_without_future.txt index d7ed5fc845..7cf15a63c8 100644 --- a/tests/functional/ext/typing/typing_consider_using_alias_without_future.txt +++ b/tests/functional/ext/typing/typing_consider_using_alias_without_future.txt @@ -1,20 +1,20 @@ -consider-using-alias:13:6:13:17::'typing.Dict' will be deprecated with PY39, consider using 'dict' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:14:6:14:10::'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:16:6:16:24::'typing.OrderedDict' will be deprecated with PY39, consider using 'collections.OrderedDict' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:17:6:17:22::'typing.Awaitable' will be deprecated with PY39, consider using 'collections.abc.Awaitable' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:18:6:18:21::'typing.Iterable' will be deprecated with PY39, consider using 'collections.abc.Iterable' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:19:6:19:21::'typing.Hashable' will be deprecated with PY39, consider using 'collections.abc.Hashable' instead:INFERENCE -consider-using-alias:20:6:20:27::'typing.ContextManager' will be deprecated with PY39, consider using 'contextlib.AbstractContextManager' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:21:6:21:20::'typing.Pattern' will be deprecated with PY39, consider using 're.Pattern' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:22:7:22:22::'typing.Match' will be deprecated with PY39, consider using 're.Match' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:31:9:31:13::'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE -consider-using-alias:33:7:33:11::'typing.Type' will be deprecated with PY39, consider using 'type' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:34:7:34:12::'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:35:7:35:15::'typing.Callable' will be deprecated with PY39, consider using 'collections.abc.Callable' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:41:74:41:78:func1:'typing.Dict' will be deprecated with PY39, consider using 'dict' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:41:16:41:20:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:41:37:41:41:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:41:93:41:105:func1:'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:57:12:57:16:CustomNamedTuple:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:62:12:62:16:CustomTypedDict2:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE -consider-using-alias:66:12:66:16:CustomDataClass:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:20:6:20:17::'typing.Dict' will be deprecated with PY39, consider using 'dict' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:21:6:21:10::'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:23:6:23:24::'typing.OrderedDict' will be deprecated with PY39, consider using 'collections.OrderedDict' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:24:6:24:22::'typing.Awaitable' will be deprecated with PY39, consider using 'collections.abc.Awaitable' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:25:6:25:21::'typing.Iterable' will be deprecated with PY39, consider using 'collections.abc.Iterable' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:26:6:26:21::'typing.Hashable' will be deprecated with PY39, consider using 'collections.abc.Hashable' instead:INFERENCE +consider-using-alias:27:6:27:27::'typing.ContextManager' will be deprecated with PY39, consider using 'contextlib.AbstractContextManager' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:28:6:28:20::'typing.Pattern' will be deprecated with PY39, consider using 're.Pattern' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:29:7:29:22::'typing.Match' will be deprecated with PY39, consider using 're.Match' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:38:9:38:13::'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:40:7:40:11::'typing.Type' will be deprecated with PY39, consider using 'type' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:41:7:41:12::'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:42:7:42:15::'typing.Callable' will be deprecated with PY39, consider using 'collections.abc.Callable' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:48:74:48:78:func1:'typing.Dict' will be deprecated with PY39, consider using 'dict' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:48:16:48:20:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:48:37:48:41:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:48:93:48:105:func1:'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:64:12:64:16:CustomNamedTuple:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:69:12:69:16:CustomTypedDict2:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:73:12:73:16:CustomDataClass:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE diff --git a/tests/functional/ext/typing/typing_consider_using_union.py b/tests/functional/ext/typing/typing_consider_using_union.py index bb92e6fc70..780a961000 100644 --- a/tests/functional/ext/typing/typing_consider_using_union.py +++ b/tests/functional/ext/typing/typing_consider_using_union.py @@ -3,8 +3,14 @@ 'py-version' needs to be set to >= '3.7' and 'runtime-typing=no'. With 'from __future__ import annotations' present. """ + # pylint: disable=missing-docstring,invalid-name,unused-argument,line-too-long # pylint: disable=consider-using-alias,unnecessary-direct-lambda-call + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from __future__ import annotations from dataclasses import dataclass import typing diff --git a/tests/functional/ext/typing/typing_consider_using_union.txt b/tests/functional/ext/typing/typing_consider_using_union.txt index 4a1e7521d2..5f2746815f 100644 --- a/tests/functional/ext/typing/typing_consider_using_union.txt +++ b/tests/functional/ext/typing/typing_consider_using_union.txt @@ -1,10 +1,10 @@ -consider-alternative-union-syntax:13:6:13:11::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:14:11:14:16::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:15:16:15:28::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:16:6:16:14::Consider using alternative Union syntax instead of 'Optional':INFERENCE -consider-alternative-union-syntax:24:10:24:18:func1:Consider using alternative Union syntax instead of 'Optional':INFERENCE -consider-alternative-union-syntax:25:24:25:29:func1:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:26:5:26:10:func1:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:38:12:38:17:CustomNamedTuple:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:43:27:43:32:CustomTypedDict2:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:47:12:47:20:CustomDataClass:Consider using alternative Union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:19:6:19:11::Consider using alternative Union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:20:11:20:16::Consider using alternative Union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:21:16:21:28::Consider using alternative Union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:22:6:22:14::Consider using alternative Union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:30:10:30:18:func1:Consider using alternative Union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:31:24:31:29:func1:Consider using alternative Union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:32:5:32:10:func1:Consider using alternative Union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:44:12:44:17:CustomNamedTuple:Consider using alternative Union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:49:27:49:32:CustomTypedDict2:Consider using alternative Union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:53:12:53:20:CustomDataClass:Consider using alternative Union syntax instead of 'Optional':INFERENCE diff --git a/tests/functional/ext/typing/typing_consider_using_union_without_future.py b/tests/functional/ext/typing/typing_consider_using_union_without_future.py index 1fb43c65ac..d29ba306e9 100644 --- a/tests/functional/ext/typing/typing_consider_using_union_without_future.py +++ b/tests/functional/ext/typing/typing_consider_using_union_without_future.py @@ -2,8 +2,14 @@ 'py-version' needs to be set to >= '3.7' and 'runtime-typing=no'. """ + # pylint: disable=missing-docstring,invalid-name,unused-argument,line-too-long,unnecessary-direct-lambda-call # pylint: disable=consider-using-alias + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from dataclasses import dataclass import typing from typing import Dict, List, Optional, Union, TypedDict diff --git a/tests/functional/ext/typing/typing_consider_using_union_without_future.txt b/tests/functional/ext/typing/typing_consider_using_union_without_future.txt index bc8ca4c075..5d48e9afca 100644 --- a/tests/functional/ext/typing/typing_consider_using_union_without_future.txt +++ b/tests/functional/ext/typing/typing_consider_using_union_without_future.txt @@ -1,10 +1,10 @@ -consider-alternative-union-syntax:11:6:11:11::Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:12:11:12:16::Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:13:16:13:28::Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:14:6:14:14::Consider using alternative Union syntax instead of 'Optional'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:22:10:22:18:func1:Consider using alternative Union syntax instead of 'Optional'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:23:24:23:29:func1:Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:24:5:24:10:func1:Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:36:12:36:17:CustomNamedTuple:Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:41:27:41:32:CustomTypedDict2:Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE -consider-alternative-union-syntax:45:12:45:20:CustomDataClass:Consider using alternative Union syntax instead of 'Optional'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:17:6:17:11::Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:18:11:18:16::Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:19:16:19:28::Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:20:6:20:14::Consider using alternative Union syntax instead of 'Optional'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:28:10:28:18:func1:Consider using alternative Union syntax instead of 'Optional'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:29:24:29:29:func1:Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:30:5:30:10:func1:Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:42:12:42:17:CustomNamedTuple:Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:47:27:47:32:CustomTypedDict2:Consider using alternative Union syntax instead of 'Union'. Add 'from __future__ import annotations' as well:INFERENCE +consider-alternative-union-syntax:51:12:51:20:CustomDataClass:Consider using alternative Union syntax instead of 'Optional'. Add 'from __future__ import annotations' as well:INFERENCE diff --git a/tests/functional/f/first_arg.py b/tests/functional/f/first_arg.py index 16824d4f28..ac5c6bcf68 100644 --- a/tests/functional/f/first_arg.py +++ b/tests/functional/f/first_arg.py @@ -1,11 +1,9 @@ -# pylint: disable=missing-docstring, useless-object-inheritance +# pylint: disable=missing-docstring """check for methods first arguments """ -__revision__ = 0 - -class Obj(object): +class Obj: # C0202, classmethod def __new__(something): # [bad-classmethod-argument] pass @@ -33,7 +31,7 @@ def method2(other): # [bad-mcs-method-argument] pass # C0205, metaclass classmethod - def class1(cls): + def class1(mcs): pass class1 = classmethod(class1) # [no-classmethod-decorator] diff --git a/tests/functional/f/first_arg.txt b/tests/functional/f/first_arg.txt index e75743b33e..3bfb8a0f50 100644 --- a/tests/functional/f/first_arg.txt +++ b/tests/functional/f/first_arg.txt @@ -1,9 +1,9 @@ -bad-classmethod-argument:10:4:10:15:Obj.__new__:Class method __new__ should have 'cls' as first argument:UNDEFINED -no-classmethod-decorator:16:4:16:10:Obj:Consider using a decorator instead of calling classmethod:UNDEFINED -bad-classmethod-argument:18:4:18:14:Obj.class2:Class method class2 should have 'cls' as first argument:UNDEFINED -no-classmethod-decorator:20:4:20:10:Obj:Consider using a decorator instead of calling classmethod:UNDEFINED -bad-mcs-classmethod-argument:25:4:25:15:Meta.__new__:Metaclass class method __new__ should have 'cls' as first argument:UNDEFINED -bad-mcs-method-argument:32:4:32:15:Meta.method2:Metaclass method method2 should have 'cls' as first argument:UNDEFINED -no-classmethod-decorator:38:4:38:10:Meta:Consider using a decorator instead of calling classmethod:UNDEFINED -bad-mcs-classmethod-argument:40:4:40:14:Meta.class2:Metaclass class method class2 should have 'cls' as first argument:UNDEFINED -no-classmethod-decorator:42:4:42:10:Meta:Consider using a decorator instead of calling classmethod:UNDEFINED +bad-classmethod-argument:8:4:8:15:Obj.__new__:Class method __new__ should have 'cls' as first argument:UNDEFINED +no-classmethod-decorator:14:4:14:10:Obj:Consider using a decorator instead of calling classmethod:UNDEFINED +bad-classmethod-argument:16:4:16:14:Obj.class2:Class method class2 should have 'cls' as first argument:UNDEFINED +no-classmethod-decorator:18:4:18:10:Obj:Consider using a decorator instead of calling classmethod:UNDEFINED +bad-mcs-classmethod-argument:23:4:23:15:Meta.__new__:Metaclass class method __new__ should have 'mcs' as first argument:UNDEFINED +bad-mcs-method-argument:30:4:30:15:Meta.method2:Metaclass method method2 should have 'cls' as first argument:UNDEFINED +no-classmethod-decorator:36:4:36:10:Meta:Consider using a decorator instead of calling classmethod:UNDEFINED +bad-mcs-classmethod-argument:38:4:38:14:Meta.class2:Metaclass class method class2 should have 'mcs' as first argument:UNDEFINED +no-classmethod-decorator:40:4:40:10:Meta:Consider using a decorator instead of calling classmethod:UNDEFINED diff --git a/tests/functional/f/func_disable_linebased.py b/tests/functional/f/func_disable_linebased.py index bb4ad232ca..6a807d9ea1 100644 --- a/tests/functional/f/func_disable_linebased.py +++ b/tests/functional/f/func_disable_linebased.py @@ -10,6 +10,5 @@ """ # pylint: enable=line-too-long -from __future__ import print_function print('This is a very long line which the linter will warn about, now that line-too-long has been enabled again.') # [line-too-long] diff --git a/tests/functional/f/func_disable_linebased.txt b/tests/functional/f/func_disable_linebased.txt index 92a551d4b1..727ef79bb9 100644 --- a/tests/functional/f/func_disable_linebased.txt +++ b/tests/functional/f/func_disable_linebased.txt @@ -1,2 +1,2 @@ line-too-long:1:0:None:None::Line too long (146/100):UNDEFINED -line-too-long:15:0:None:None::Line too long (133/100):UNDEFINED +line-too-long:14:0:None:None::Line too long (133/100):UNDEFINED diff --git a/tests/functional/f/function_redefined.py b/tests/functional/f/function_redefined.py index eab8a6d1f1..a87c79680a 100644 --- a/tests/functional/f/function_redefined.py +++ b/tests/functional/f/function_redefined.py @@ -1,11 +1,11 @@ -# pylint: disable=missing-docstring,using-constant-test, useless-object-inheritance +# pylint: disable=missing-docstring,using-constant-test # pylint: disable=unused-import,wrong-import-position,reimported, unnecessary-pass from __future__ import division from typing import Callable __revision__ = '' -class AAAA(object): +class AAAA: """docstring""" def __init__(self): pass @@ -18,7 +18,7 @@ def method2(self): def method2(self): # [function-redefined] """docstring""" -class AAAA(object): # [function-redefined] +class AAAA: # [function-redefined] """docstring""" def __init__(self): pass diff --git a/tests/functional/g/generated_members.py b/tests/functional/g/generated_members.py index 72878329d8..ffb3de6314 100644 --- a/tests/functional/g/generated_members.py +++ b/tests/functional/g/generated_members.py @@ -1,10 +1,10 @@ """Test the generated-members config option.""" -# pylint: disable=pointless-statement, invalid-name, useless-object-inheritance +# pylint: disable=pointless-statement, invalid-name from __future__ import annotations from astroid import nodes from pylint import checkers -class Klass(object): +class Klass: """A class with a generated member.""" print(Klass().DoesNotExist) diff --git a/tests/functional/g/generic_alias/generic_alias_collections.txt b/tests/functional/g/generic_alias/generic_alias_collections.txt index 663b81abb2..4abaa03388 100644 --- a/tests/functional/g/generic_alias/generic_alias_collections.txt +++ b/tests/functional/g/generic_alias/generic_alias_collections.txt @@ -1,16 +1,16 @@ unsubscriptable-object:66:0:66:24::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:67:0:67:21::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED -abstract-method:74:0:74:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:77:0:77:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:80:0:80:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden:UNDEFINED -abstract-method:80:0:80:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:80:0:80:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:99:0:99:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:99:0:99:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:104:0:104:24:CustomAbstractCls2:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:104:0:104:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:106:0:106:26:CustomImplementation:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:106:0:106:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:74:0:74:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE +abstract-method:77:0:77:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable':INFERENCE +abstract-method:80:0:80:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:80:0:80:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:80:0:80:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:99:0:99:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:99:0:99:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:104:0:104:24:CustomAbstractCls2:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'CustomAbstractCls2':INFERENCE +abstract-method:104:0:104:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE +abstract-method:106:0:106:26:CustomImplementation:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'CustomImplementation':INFERENCE +abstract-method:106:0:106:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE unsubscriptable-object:125:9:125:12::Value 'int' is unsubscriptable:UNDEFINED unsubscriptable-object:126:15:126:39::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:127:12:127:33::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37.txt b/tests/functional/g/generic_alias/generic_alias_collections_py37.txt index 84a217d2fc..72104b4bed 100644 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37.txt +++ b/tests/functional/g/generic_alias/generic_alias_collections_py37.txt @@ -39,7 +39,7 @@ unsubscriptable-object:63:0:63:8::Value 're.Match' is unsubscriptable:UNDEFINED unsubscriptable-object:69:0:69:24::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:70:0:70:21::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED unsubscriptable-object:73:0:73:26::Value 'collections.abc.ByteString' is unsubscriptable:UNDEFINED -abstract-method:77:0:77:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED +abstract-method:77:0:77:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE unsubscriptable-object:80:22:80:46:DerivedIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:83:24:83:50:DerivedCollection:Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED unsubscriptable-object:88:18:88:22:DerivedList:Value 'list' is unsubscriptable:UNDEFINED @@ -47,11 +47,11 @@ unsubscriptable-object:91:17:91:20:DerivedSet:Value 'set' is unsubscriptable:UND unsubscriptable-object:94:25:94:48:DerivedOrderedDict:Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED unsubscriptable-object:97:31:97:55:DerivedListIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:97:26:97:30:DerivedListIterable:Value 'list' is unsubscriptable:UNDEFINED -abstract-method:102:0:102:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:102:0:102:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:107:0:107:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:102:0:102:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:102:0:102:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:107:0:107:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE unsubscriptable-object:107:48:107:72:CustomAbstractCls2:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -abstract-method:109:0:109:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:109:0:109:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE unsubscriptable-object:114:11:114:16::Value 'tuple' is unsubscriptable:UNDEFINED unsubscriptable-object:115:10:115:14::Value 'dict' is unsubscriptable:UNDEFINED unsubscriptable-object:116:17:116:40::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt b/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt index ee1407bdb6..0dd989f2e8 100644 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt +++ b/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt @@ -39,7 +39,7 @@ unsubscriptable-object:65:0:65:8::Value 're.Match' is unsubscriptable:UNDEFINED unsubscriptable-object:71:0:71:24::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:72:0:72:21::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED unsubscriptable-object:75:0:75:26::Value 'collections.abc.ByteString' is unsubscriptable:UNDEFINED -abstract-method:79:0:79:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED +abstract-method:79:0:79:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE unsubscriptable-object:82:22:82:46:DerivedIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:85:24:85:50:DerivedCollection:Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED unsubscriptable-object:90:18:90:22:DerivedList:Value 'list' is unsubscriptable:UNDEFINED @@ -47,11 +47,11 @@ unsubscriptable-object:93:17:93:20:DerivedSet:Value 'set' is unsubscriptable:UND unsubscriptable-object:96:25:96:48:DerivedOrderedDict:Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED unsubscriptable-object:99:31:99:55:DerivedListIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:99:26:99:30:DerivedListIterable:Value 'list' is unsubscriptable:UNDEFINED -abstract-method:104:0:104:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:104:0:104:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:109:0:109:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:104:0:104:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:104:0:104:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:109:0:109:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE unsubscriptable-object:109:48:109:72:CustomAbstractCls2:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -abstract-method:111:0:111:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:111:0:111:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE unsubscriptable-object:116:11:116:16::Value 'tuple' is unsubscriptable:UNDEFINED unsubscriptable-object:117:10:117:14::Value 'dict' is unsubscriptable:UNDEFINED unsubscriptable-object:118:17:118:40::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt b/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt index 2bafe20ed8..188039c60d 100644 --- a/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt +++ b/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt @@ -1,5 +1,5 @@ -abstract-method:34:0:34:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:37:0:37:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:40:0:40:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden:UNDEFINED -abstract-method:40:0:40:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:40:0:40:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:34:0:34:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE +abstract-method:37:0:37:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable':INFERENCE +abstract-method:40:0:40:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:40:0:40:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:40:0:40:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedCollection':INFERENCE diff --git a/tests/functional/g/generic_alias/generic_alias_mixed_py39.txt b/tests/functional/g/generic_alias/generic_alias_mixed_py39.txt index 06dbdc1976..58b7e9860e 100644 --- a/tests/functional/g/generic_alias/generic_alias_mixed_py39.txt +++ b/tests/functional/g/generic_alias/generic_alias_mixed_py39.txt @@ -1,5 +1,5 @@ -abstract-method:29:0:29:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:32:0:32:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:35:0:35:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden:UNDEFINED -abstract-method:35:0:35:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:35:0:35:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:29:0:29:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE +abstract-method:32:0:32:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable':INFERENCE +abstract-method:35:0:35:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:35:0:35:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:35:0:35:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedCollection':INFERENCE diff --git a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt b/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt index d481f7ac60..cbf46bfef4 100644 --- a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt +++ b/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt @@ -39,7 +39,7 @@ unsubscriptable-object:68:0:68:8::Value 're.Match' is unsubscriptable:UNDEFINED unsubscriptable-object:74:0:74:24::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:75:0:75:21::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED unsubscriptable-object:78:0:78:26::Value 'collections.abc.ByteString' is unsubscriptable:UNDEFINED -abstract-method:82:0:82:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED +abstract-method:82:0:82:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE unsubscriptable-object:85:22:85:46:DerivedIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:88:24:88:50:DerivedCollection:Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED unsubscriptable-object:93:18:93:22:DerivedList:Value 'list' is unsubscriptable:UNDEFINED @@ -47,9 +47,9 @@ unsubscriptable-object:96:17:96:20:DerivedSet:Value 'set' is unsubscriptable:UND unsubscriptable-object:99:25:99:48:DerivedOrderedDict:Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED unsubscriptable-object:102:31:102:55:DerivedListIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:102:26:102:30:DerivedListIterable:Value 'list' is unsubscriptable:UNDEFINED -abstract-method:107:0:107:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:107:0:107:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:112:0:112:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:107:0:107:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:107:0:107:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:112:0:112:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE unsubscriptable-object:112:48:112:72:CustomAbstractCls2:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -abstract-method:114:0:114:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:114:0:114:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE unsubscriptable-object:188:8:188:9:B:Value 'A' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_related.txt b/tests/functional/g/generic_alias/generic_alias_related.txt index d13f75fa76..b2123350c4 100644 --- a/tests/functional/g/generic_alias/generic_alias_related.txt +++ b/tests/functional/g/generic_alias/generic_alias_related.txt @@ -2,4 +2,4 @@ unsubscriptable-object:34:0:34:20::Value 'ClsUnsubscriptable()' is unsubscriptab unsubscriptable-object:35:0:35:18::Value 'ClsUnsubscriptable' is unsubscriptable:UNDEFINED unsubscriptable-object:38:0:38:10::Value 'ClsGetItem' is unsubscriptable:UNDEFINED unsubscriptable-object:40:0:40:17::Value 'ClsClassGetItem()' is unsubscriptable:UNDEFINED -abstract-method:53:0:53:13:Derived:Method 'abstract_method' is abstract in class 'ClsAbstract' but is not overridden:UNDEFINED +abstract-method:53:0:53:13:Derived:Method 'abstract_method' is abstract in class 'ClsAbstract' but is not overridden in child class 'Derived':INFERENCE diff --git a/tests/functional/g/generic_alias/generic_alias_related_py39.txt b/tests/functional/g/generic_alias/generic_alias_related_py39.txt index 114376f5ef..c24c0f98ba 100644 --- a/tests/functional/g/generic_alias/generic_alias_related_py39.txt +++ b/tests/functional/g/generic_alias/generic_alias_related_py39.txt @@ -2,4 +2,4 @@ unsubscriptable-object:36:0:36:20::Value 'ClsUnsubscriptable()' is unsubscriptab unsubscriptable-object:37:0:37:18::Value 'ClsUnsubscriptable' is unsubscriptable:UNDEFINED unsubscriptable-object:40:0:40:10::Value 'ClsGetItem' is unsubscriptable:UNDEFINED unsubscriptable-object:42:0:42:17::Value 'ClsClassGetItem()' is unsubscriptable:UNDEFINED -abstract-method:55:0:55:13:Derived:Method 'abstract_method' is abstract in class 'ClsAbstract' but is not overridden:UNDEFINED +abstract-method:55:0:55:13:Derived:Method 'abstract_method' is abstract in class 'ClsAbstract' but is not overridden in child class 'Derived':INFERENCE diff --git a/tests/functional/g/generic_alias/generic_alias_typing.txt b/tests/functional/g/generic_alias/generic_alias_typing.txt index f33f49f91f..2a433cd24f 100644 --- a/tests/functional/g/generic_alias/generic_alias_typing.txt +++ b/tests/functional/g/generic_alias/generic_alias_typing.txt @@ -1,18 +1,18 @@ unsubscriptable-object:66:0:66:17::Value 'typing.ByteString' is unsubscriptable:UNDEFINED unsubscriptable-object:67:0:67:15::Value 'typing.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:68:0:68:12::Value 'typing.Sized' is unsubscriptable:UNDEFINED -abstract-method:72:0:72:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:75:0:75:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:78:0:78:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden:UNDEFINED -abstract-method:78:0:78:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:78:0:78:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:100:0:100:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:100:0:100:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:105:0:105:24:CustomAbstractCls2:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:105:0:105:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:107:0:107:26:CustomImplementation:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:107:0:107:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:118:0:118:22:DerivedIterable2:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED +abstract-method:72:0:72:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE +abstract-method:75:0:75:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable':INFERENCE +abstract-method:78:0:78:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:78:0:78:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:78:0:78:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:100:0:100:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:100:0:100:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:105:0:105:24:CustomAbstractCls2:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'CustomAbstractCls2':INFERENCE +abstract-method:105:0:105:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE +abstract-method:107:0:107:26:CustomImplementation:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'CustomImplementation':INFERENCE +abstract-method:107:0:107:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE +abstract-method:118:0:118:22:DerivedIterable2:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable2':INFERENCE unsubscriptable-object:138:9:138:12::Value 'int' is unsubscriptable:UNDEFINED unsubscriptable-object:139:17:139:34::Value 'typing.ByteString' is unsubscriptable:UNDEFINED unsubscriptable-object:140:15:140:30::Value 'typing.Hashable' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/genexp_in_class_scope.py b/tests/functional/g/genexp_in_class_scope.py index 93e0ceaae2..73585c2be1 100644 --- a/tests/functional/g/genexp_in_class_scope.py +++ b/tests/functional/g/genexp_in_class_scope.py @@ -1,6 +1,5 @@ # pylint: disable=too-few-public-methods, missing-docstring -# pylint: disable=useless-object-inheritance """Class scope must be handled correctly in genexps""" -class MyClass(object): +class MyClass: var1 = [] var2 = list(value*2 for value in var1) diff --git a/tests/functional/g/genexpr_variable_scope.py b/tests/functional/g/genexpr_variable_scope.py index a00d72d328..721038739c 100644 --- a/tests/functional/g/genexpr_variable_scope.py +++ b/tests/functional/g/genexpr_variable_scope.py @@ -1,5 +1,5 @@ """test name defined in generator expression are not available outside the genexpr scope """ -from __future__ import print_function + print(n) # [undefined-variable] diff --git a/tests/functional/g/globals.py b/tests/functional/g/globals.py index f12c06800c..ce289acf85 100644 --- a/tests/functional/g/globals.py +++ b/tests/functional/g/globals.py @@ -1,6 +1,5 @@ """Warnings about global statements and usage of global variables.""" # pylint: disable=invalid-name, redefined-outer-name, missing-function-docstring, missing-class-docstring, import-outside-toplevel, too-few-public-methods -from __future__ import print_function global CSTE # [global-at-module-level] print(CSTE) # [undefined-variable] @@ -32,9 +31,15 @@ def define_constant(): def global_with_import(): - """should only warn for global-statement""" + """should only warn for global-statement when using `Import` node""" global sys # [global-statement] - import sys # pylint: disable=import-outside-toplevel + import sys + + +def global_with_import_from(): + """should only warn for global-statement when using `ImportFrom` node""" + global namedtuple # [global-statement] + from collections import namedtuple def global_no_assign(): @@ -76,11 +81,6 @@ def FUNC(): FUNC() -def func(): - """Overriding a global with an import should only throw a global statement error""" - global sys # [global-statement] - - import sys def override_class(): """Overriding a class should only throw a global statement error""" @@ -90,3 +90,12 @@ class CLASS(): pass CLASS() + + +# Prevent emitting `invalid-name` for the line on which `global` is declared +# https://github.com/PyCQA/pylint/issues/8307 + +_foo: str = "tomato" +def setup_shared_foo(): + global _foo # [global-statement] + _foo = "potato" diff --git a/tests/functional/g/globals.txt b/tests/functional/g/globals.txt index 975506cc8e..da267fdde2 100644 --- a/tests/functional/g/globals.txt +++ b/tests/functional/g/globals.txt @@ -1,14 +1,15 @@ -global-at-module-level:5:0:5:11::Using the global statement at the module level:UNDEFINED -undefined-variable:6:6:6:10::Undefined variable 'CSTE':UNDEFINED -global-statement:17:4:17:19:fix_contant:Using the global statement:UNDEFINED -global-variable-not-assigned:24:4:24:14:other:Using global for 'HOP' but no assignment is done:UNDEFINED -undefined-variable:25:10:25:13:other:Undefined variable 'HOP':UNDEFINED -global-variable-undefined:30:4:30:18:define_constant:Global variable 'SOMEVAR' undefined at the module level:UNDEFINED -global-statement:36:4:36:14:global_with_import:Using the global statement:UNDEFINED -global-variable-not-assigned:42:4:42:19:global_no_assign:Using global for 'CONSTANT' but no assignment is done:UNDEFINED -global-statement:48:4:48:19:global_del:Using the global statement:UNDEFINED -global-statement:55:4:55:19:global_operator_assign:Using the global statement:UNDEFINED -global-statement:62:4:62:19:global_function_assign:Using the global statement:UNDEFINED -global-statement:72:4:72:15:override_func:Using the global statement:UNDEFINED -global-statement:81:4:81:14:func:Using the global statement:UNDEFINED +global-at-module-level:4:0:4:11::Using the global statement at the module level:UNDEFINED +undefined-variable:5:6:5:10::Undefined variable 'CSTE':UNDEFINED +global-statement:16:4:16:19:fix_contant:Using the global statement:UNDEFINED +global-variable-not-assigned:23:4:23:14:other:Using global for 'HOP' but no assignment is done:UNDEFINED +undefined-variable:24:10:24:13:other:Undefined variable 'HOP':UNDEFINED +global-variable-undefined:29:4:29:18:define_constant:Global variable 'SOMEVAR' undefined at the module level:UNDEFINED +global-statement:35:4:35:14:global_with_import:Using the global statement:UNDEFINED +global-statement:41:4:41:21:global_with_import_from:Using the global statement:UNDEFINED +global-variable-not-assigned:47:4:47:19:global_no_assign:Using global for 'CONSTANT' but no assignment is done:UNDEFINED +global-statement:53:4:53:19:global_del:Using the global statement:UNDEFINED +global-statement:60:4:60:19:global_operator_assign:Using the global statement:UNDEFINED +global-statement:67:4:67:19:global_function_assign:Using the global statement:UNDEFINED +global-statement:77:4:77:15:override_func:Using the global statement:UNDEFINED global-statement:87:4:87:16:override_class:Using the global statement:UNDEFINED +global-statement:100:4:100:15:setup_shared_foo:Using the global statement:UNDEFINED diff --git a/tests/functional/i/implicit/implicit_str_concat.py b/tests/functional/i/implicit/implicit_str_concat.py index 920b29258d..7e28b4cc2d 100644 --- a/tests/functional/i/implicit/implicit_str_concat.py +++ b/tests/functional/i/implicit/implicit_str_concat.py @@ -1,4 +1,4 @@ -# pylint: disable=invalid-name, missing-docstring, redundant-u-string-prefix, line-too-long +# pylint: disable=invalid-name, missing-docstring, redundant-u-string-prefix, line-too-long, superfluous-parens # Basic test with a list TEST_LIST1 = ['a' 'b'] # [implicit-str-concat] diff --git a/tests/functional/i/import_aliasing.py b/tests/functional/i/import_aliasing.py index 3926534f10..5858aa5e47 100644 --- a/tests/functional/i/import_aliasing.py +++ b/tests/functional/i/import_aliasing.py @@ -1,4 +1,4 @@ -# pylint: disable=unused-import, missing-docstring, invalid-name, reimported, import-error, wrong-import-order, no-name-in-module +# pylint: disable=unused-import, missing-docstring, invalid-name, reimported, import-error, wrong-import-order, no-name-in-module, shadowed-import # Functional tests for import aliasing # 1. useless-import-alias # 2. consider-using-from-import diff --git a/tests/functional/i/import_aliasing.txt b/tests/functional/i/import_aliasing.txt index a79bd962f9..94e57d4181 100644 --- a/tests/functional/i/import_aliasing.txt +++ b/tests/functional/i/import_aliasing.txt @@ -1,10 +1,10 @@ -useless-import-alias:6:0:6:50::Import alias does not rename original package:UNDEFINED +useless-import-alias:6:0:6:50::Import alias does not rename original package:HIGH consider-using-from-import:8:0:8:22::Use 'from os import path' instead:UNDEFINED consider-using-from-import:10:0:10:31::Use 'from foo.bar import foobar' instead:UNDEFINED -useless-import-alias:14:0:14:24::Import alias does not rename original package:UNDEFINED -useless-import-alias:17:0:17:28::Import alias does not rename original package:UNDEFINED -useless-import-alias:18:0:18:38::Import alias does not rename original package:UNDEFINED -useless-import-alias:20:0:20:38::Import alias does not rename original package:UNDEFINED -useless-import-alias:21:0:21:38::Import alias does not rename original package:UNDEFINED -useless-import-alias:23:0:23:36::Import alias does not rename original package:UNDEFINED +useless-import-alias:14:0:14:24::Import alias does not rename original package:HIGH +useless-import-alias:17:0:17:28::Import alias does not rename original package:HIGH +useless-import-alias:18:0:18:38::Import alias does not rename original package:HIGH +useless-import-alias:20:0:20:38::Import alias does not rename original package:HIGH +useless-import-alias:21:0:21:38::Import alias does not rename original package:HIGH +useless-import-alias:23:0:23:36::Import alias does not rename original package:HIGH relative-beyond-top-level:26:0:26:27::Attempted relative import beyond top-level package:UNDEFINED diff --git a/tests/functional/i/import_error.py b/tests/functional/i/import_error.py index 2ce63544ba..12f56f9eac 100644 --- a/tests/functional/i/import_error.py +++ b/tests/functional/i/import_error.py @@ -79,3 +79,32 @@ import foo import bar + +# Issues with contextlib.suppress reported in +# https://github.com/PyCQA/pylint/issues/7270 +import contextlib +with contextlib.suppress(ImportError): + import foo2 + +with contextlib.suppress(ValueError): + import foo2 # [import-error] + +with contextlib.suppress(ImportError, ValueError): + import foo2 + +with contextlib.suppress((ImportError, ValueError)): + import foo2 + +with contextlib.suppress((ImportError,), (ValueError,)): + import foo2 + +x = True +with contextlib.suppress(ImportError): + if x: + import foo2 + else: + pass + +with contextlib.suppress(ImportError): + with contextlib.suppress(TypeError): + import foo2 diff --git a/tests/functional/i/import_error.txt b/tests/functional/i/import_error.txt index 28e94dc72e..80a5732f03 100644 --- a/tests/functional/i/import_error.txt +++ b/tests/functional/i/import_error.txt @@ -1,5 +1,6 @@ import-error:3:0:3:22::Unable to import 'totally_missing':UNDEFINED import-error:21:4:21:26::Unable to import 'maybe_missing_2':UNDEFINED no-name-in-module:33:0:33:49::No name 'syntax_error' in module 'functional.s.syntax':UNDEFINED -syntax-error:33:0:None:None::Cannot import 'functional.s.syntax.syntax_error' due to syntax error 'invalid syntax (, line 1)':UNDEFINED +syntax-error:33:0:None:None::Cannot import 'functional.s.syntax.syntax_error' due to 'invalid syntax (, line 1)':HIGH multiple-imports:78:0:78:15::Multiple imports on one line (foo, bar):UNDEFINED +import-error:90:4:90:15::Unable to import 'foo2':UNDEFINED diff --git a/tests/functional/i/import_itself.py b/tests/functional/i/import_itself.py index 1f4928f3e1..213532dbde 100644 --- a/tests/functional/i/import_itself.py +++ b/tests/functional/i/import_itself.py @@ -1,6 +1,5 @@ """test module importing itself""" # pylint: disable=using-constant-test -from __future__ import print_function from . import import_itself # [import-self] __revision__ = 0 diff --git a/tests/functional/i/import_itself.txt b/tests/functional/i/import_itself.txt index 67f19cd979..ea30b61413 100644 --- a/tests/functional/i/import_itself.txt +++ b/tests/functional/i/import_itself.txt @@ -1 +1 @@ -import-self:4:0:4:27::Module import itself:UNDEFINED +import-self:3:0:3:27::Module import itself:UNDEFINED diff --git a/tests/functional/i/import_outside_toplevel.py b/tests/functional/i/import_outside_toplevel.py index 46c4ba6b89..455d693527 100644 --- a/tests/functional/i/import_outside_toplevel.py +++ b/tests/functional/i/import_outside_toplevel.py @@ -27,7 +27,7 @@ class C: import tokenize # [import-outside-toplevel] def j(self): - import turtle # [import-outside-toplevel] + import trace # [import-outside-toplevel] def k(flag): diff --git a/tests/functional/i/import_outside_toplevel.txt b/tests/functional/i/import_outside_toplevel.txt index 66a7349256..ee51b42796 100644 --- a/tests/functional/i/import_outside_toplevel.txt +++ b/tests/functional/i/import_outside_toplevel.txt @@ -3,7 +3,7 @@ import-outside-toplevel:15:4:15:18:g:Import outside toplevel (os, sys):UNDEFINED import-outside-toplevel:19:4:19:24:h:Import outside toplevel (time):UNDEFINED import-outside-toplevel:23:4:23:41:i:Import outside toplevel (random, socket):UNDEFINED import-outside-toplevel:27:4:27:19:C:Import outside toplevel (tokenize):UNDEFINED -import-outside-toplevel:30:8:30:21:C.j:Import outside toplevel (turtle):UNDEFINED +import-outside-toplevel:30:8:30:20:C.j:Import outside toplevel (trace):UNDEFINED import-outside-toplevel:35:8:35:23:k:Import outside toplevel (tabnanny):UNDEFINED import-outside-toplevel:39:4:39:39:j:Import outside toplevel (collections.defaultdict):UNDEFINED import-outside-toplevel:43:4:43:48:m:Import outside toplevel (math.sin, math.cos):UNDEFINED diff --git a/tests/functional/i/inconsistent/inconsistent_returns.py b/tests/functional/i/inconsistent/inconsistent_returns.py index 08dde253e9..c1183b288e 100644 --- a/tests/functional/i/inconsistent/inconsistent_returns.py +++ b/tests/functional/i/inconsistent/inconsistent_returns.py @@ -91,13 +91,13 @@ def explicit_returns6(x, y, z): def explicit_returns7(arg): if arg < 0: - arg = 2 * arg + arg *= 2 return 'below 0' elif arg == 0: print("Null arg") return '0' else: - arg = 3 * arg + arg *= 3 return 'above 0' def bug_1772(): @@ -184,7 +184,7 @@ def explicit_implicit_returns3(arg): # [inconsistent-return-statements] def returns_missing_in_catched_exceptions(arg): # [inconsistent-return-statements] try: - arg = arg**2 + arg **= arg raise ValueError('test') except ValueError: print('ValueError') diff --git a/tests/functional/i/inherit_non_class.py b/tests/functional/i/inherit_non_class.py index b0ed93df7f..fb00d6f99d 100644 --- a/tests/functional/i/inherit_non_class.py +++ b/tests/functional/i/inherit_non_class.py @@ -1,8 +1,8 @@ """Test that inheriting from something which is not a class emits a warning. """ -# pylint: disable=import-error, invalid-name, using-constant-test, useless-object-inheritance -# pylint: disable=missing-docstring, too-few-public-methods +# pylint: disable=import-error, invalid-name, using-constant-test +# pylint: disable=missing-docstring, too-few-public-methods, useless-object-inheritance from missing import Missing @@ -11,7 +11,7 @@ else: Ambiguous = int -class Empty(object): +class Empty: """ Empty class. """ def return_class(): @@ -33,7 +33,7 @@ class Bad3(return_class): # [inherit-non-class] class Bad4(Empty()): # [inherit-non-class] """ Can't inherit from instance. """ -class Good(object): +class Good: pass class Good1(int): @@ -48,7 +48,7 @@ class Good3(type(int)): class Good4(return_class()): pass -class Good5(Good4, int, object): +class Good5(Good4, int): pass class Good6(Ambiguous): diff --git a/tests/functional/i/init_is_generator.py b/tests/functional/i/init_is_generator.py index 17f96db305..a2ce1e8b4e 100644 --- a/tests/functional/i/init_is_generator.py +++ b/tests/functional/i/init_is_generator.py @@ -1,5 +1,5 @@ -# pylint: disable=missing-docstring,too-few-public-methods, useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods -class SomeClass(object): +class SomeClass: def __init__(self): # [init-is-generator] yield None diff --git a/tests/functional/i/init_not_called.py b/tests/functional/i/init_not_called.py index a95efadf6c..ee8c4f5a1f 100644 --- a/tests/functional/i/init_not_called.py +++ b/tests/functional/i/init_not_called.py @@ -1,7 +1,6 @@ # pylint: disable=too-few-public-methods, import-error, missing-docstring, wrong-import-position -# pylint: disable=useless-super-delegation, useless-object-inheritance, unnecessary-pass +# pylint: disable=useless-super-delegation, unnecessary-pass -from __future__ import print_function from typing import overload @@ -33,7 +32,7 @@ def __init__(self): # [super-init-not-called] AAAA.__init__(self) -class NewStyleA(object): +class NewStyleA: """new style class""" def __init__(self): @@ -48,7 +47,7 @@ def __init__(self): super().__init__() -class NewStyleC(object): +class NewStyleC: """__init__ defined by assignment.""" def xx_init(self): @@ -59,9 +58,9 @@ def xx_init(self): class AssignedInit(NewStyleC): - """No init called.""" + """No init called, but abstract so that is fine.""" - def __init__(self): # [super-init-not-called] + def __init__(self): self.arg = 0 @@ -85,3 +84,14 @@ def __init__(self, num: float): def __init__(self, num): super().__init__(round(num)) + + +# https://github.com/PyCQA/pylint/issues/7742 +# Crash when parent class has a class attribute named `__init__` +class NoInitMethod: + __init__ = 42 + + +class ChildNoInitMethod(NoInitMethod): + def __init__(self): + ... diff --git a/tests/functional/i/init_not_called.txt b/tests/functional/i/init_not_called.txt index 9015d1e27b..2f2d51b4fb 100644 --- a/tests/functional/i/init_not_called.txt +++ b/tests/functional/i/init_not_called.txt @@ -1,2 +1 @@ -super-init-not-called:32:4:32:16:ZZZZ.__init__:__init__ method from base class 'BBBB' is not called:INFERENCE -super-init-not-called:64:4:64:16:AssignedInit.__init__:__init__ method from base class 'NewStyleC' is not called:INFERENCE +super-init-not-called:31:4:31:16:ZZZZ.__init__:__init__ method from base class 'BBBB' is not called:INFERENCE diff --git a/tests/functional/i/init_return_from_inner_function.py b/tests/functional/i/init_return_from_inner_function.py index 065a1ad70f..662d18a872 100644 --- a/tests/functional/i/init_return_from_inner_function.py +++ b/tests/functional/i/init_return_from_inner_function.py @@ -1,9 +1,8 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods """#10075""" -__revision__ = 1 -class Aaa(object): +class Aaa: """docstring""" def __init__(self): def inner_function(arg): diff --git a/tests/functional/i/init_subclass_classmethod.py b/tests/functional/i/init_subclass_classmethod.py index 8153aa2d4a..81fa8799be 100644 --- a/tests/functional/i/init_subclass_classmethod.py +++ b/tests/functional/i/init_subclass_classmethod.py @@ -1,6 +1,6 @@ -# pylint: disable=too-few-public-methods, missing-docstring, useless-object-inheritance +# pylint: disable=too-few-public-methods, missing-docstring -class PluginBase(object): +class PluginBase: subclasses = [] def __init_subclass__(cls, **kwargs): diff --git a/tests/functional/i/inner_classes.py b/tests/functional/i/inner_classes.py index cabae57347..3e26045a38 100644 --- a/tests/functional/i/inner_classes.py +++ b/tests/functional/i/inner_classes.py @@ -1,9 +1,8 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-pass, unnecessary-dunder-call +# pylint: disable=too-few-public-methods, unnecessary-pass, unnecessary-dunder-call """Backend Base Classes for the schwelm user DB""" -__revision__ = "alpha" -class Aaa(object): +class Aaa: """docstring""" def __init__(self): self.__setattr__('a', 'b') diff --git a/tests/functional/i/invalid/invalid_all_format.py b/tests/functional/i/invalid/invalid_all_format.py index 1d1c0b18f4..10537c6fb1 100644 --- a/tests/functional/i/invalid/invalid_all_format.py +++ b/tests/functional/i/invalid/invalid_all_format.py @@ -2,6 +2,6 @@ Tuples with one element MUST contain a comma! Otherwise it's a string. """ -__all__ = ("CONST") # [invalid-all-format] +__all__ = ("CONST") # [invalid-all-format, superfluous-parens] CONST = 42 diff --git a/tests/functional/i/invalid/invalid_all_format.txt b/tests/functional/i/invalid/invalid_all_format.txt index 2ba8dc17fe..2f6ac363bc 100644 --- a/tests/functional/i/invalid/invalid_all_format.txt +++ b/tests/functional/i/invalid/invalid_all_format.txt @@ -1 +1,2 @@ invalid-all-format:5:11:None:None::Invalid format for __all__, must be tuple or list:UNDEFINED +superfluous-parens:5:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED diff --git a/tests/functional/i/invalid/invalid_bool_returned.py b/tests/functional/i/invalid/invalid_bool_returned.py index 72e91f5115..eb888155b3 100644 --- a/tests/functional/i/invalid/invalid_bool_returned.py +++ b/tests/functional/i/invalid/invalid_bool_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __bool__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import six from missing import Missing -class FirstGoodBool(object): +class FirstGoodBool: """__bool__ returns """ def __bool__(self): return True -class SecondGoodBool(object): +class SecondGoodBool: """__bool__ returns """ def __bool__(self): @@ -26,37 +26,37 @@ def __bool__(cls): @six.add_metaclass(BoolMetaclass) -class ThirdGoodBool(object): +class ThirdGoodBool: """Bool through the metaclass.""" -class FirstBadBool(object): +class FirstBadBool: """ __bool__ returns an integer """ def __bool__(self): # [invalid-bool-returned] return 1 -class SecondBadBool(object): +class SecondBadBool: """ __bool__ returns str """ def __bool__(self): # [invalid-bool-returned] return "True" -class ThirdBadBool(object): +class ThirdBadBool: """ __bool__ returns node which does not have 'value' in AST """ def __bool__(self): # [invalid-bool-returned] return lambda: 3 -class AmbigousBool(object): +class AmbigousBool: """ Uninferable return value """ __bool__ = lambda self: Missing -class AnotherAmbiguousBool(object): +class AnotherAmbiguousBool: """Potential uninferable return value""" def __bool__(self): return bool(Missing) diff --git a/tests/functional/i/invalid/invalid_bytes_returned.py b/tests/functional/i/invalid/invalid_bytes_returned.py index 993bd87771..5ba8325231 100644 --- a/tests/functional/i/invalid/invalid_bytes_returned.py +++ b/tests/functional/i/invalid/invalid_bytes_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __bytes__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import six from missing import Missing -class FirstGoodBytes(object): +class FirstGoodBytes: """__bytes__ returns """ def __bytes__(self): return b"some bytes" -class SecondGoodBytes(object): +class SecondGoodBytes: """__bytes__ returns """ def __bytes__(self): @@ -26,38 +26,38 @@ def __bytes__(cls): @six.add_metaclass(BytesMetaclass) -class ThirdGoodBytes(object): +class ThirdGoodBytes: """Bytes through the metaclass.""" -class FirstBadBytes(object): +class FirstBadBytes: """ __bytes__ returns bytes """ def __bytes__(self): # [invalid-bytes-returned] return "123" -class SecondBadBytes(object): +class SecondBadBytes: """ __bytes__ returns int """ def __bytes__(self): # [invalid-bytes-returned] return 1 -class ThirdBadBytes(object): +class ThirdBadBytes: """ __bytes__ returns node which does not have 'value' in AST """ def __bytes__(self): # [invalid-bytes-returned] return lambda: b"some bytes" -class AmbiguousBytes(object): +class AmbiguousBytes: """ Uninferable return value """ __bytes__ = lambda self: Missing -class AnotherAmbiguousBytes(object): +class AnotherAmbiguousBytes: """Potential uninferable return value""" def __bytes__(self): diff --git a/tests/functional/i/invalid/invalid_class_object.py b/tests/functional/i/invalid/invalid_class_object.py index 7c08ebae81..b7363e9c87 100644 --- a/tests/functional/i/invalid/invalid_class_object.py +++ b/tests/functional/i/invalid/invalid_class_object.py @@ -1,12 +1,15 @@ # pylint: disable=missing-docstring,too-few-public-methods,invalid-name from collections import defaultdict + class A: pass + class B: pass + A.__class__ = B A.__class__ = str A.__class__ = float @@ -30,3 +33,43 @@ def __deepcopy__(self, memo): obj = C() obj.__class__ = self.__class__ return obj + + +class AnotherClass: + ... + + +class Pylint7429Good: + """See https://github.com/PyCQA/pylint/issues/7467""" + + def class_defining_function_good(self): + self.__class__, myvar = AnotherClass, "myvalue" + print(myvar) + + def class_defining_function_bad(self): + self.__class__, myvar = 1, "myvalue" # [invalid-class-object] + print(myvar) + + def class_defining_function_good_inverted(self): + myvar, self.__class__ = "myvalue", AnotherClass + print(myvar) + + def class_defining_function_bad_inverted(self): + myvar, self.__class__ = "myvalue", 1 # [invalid-class-object] + print(myvar) + + def class_defining_function_complex_bad(self): + myvar, self.__class__, other = ( # [invalid-class-object] + "myvalue", + 1, + "othervalue", + ) + print(myvar, other) + + def class_defining_function_complex_good(self): + myvar, self.__class__, other = ( + "myvalue", + str, + "othervalue", + ) + print(myvar, other) diff --git a/tests/functional/i/invalid/invalid_class_object.txt b/tests/functional/i/invalid/invalid_class_object.txt index 221431b48b..96e4d42c8a 100644 --- a/tests/functional/i/invalid/invalid_class_object.txt +++ b/tests/functional/i/invalid/invalid_class_object.txt @@ -1,2 +1,5 @@ -invalid-class-object:17:0:17:11::Invalid __class__ object:UNDEFINED -invalid-class-object:18:0:18:11::Invalid __class__ object:UNDEFINED +invalid-class-object:20:0:20:11::Invalid assignment to '__class__'. Should be a class definition but got a 'Instance':INFERENCE +invalid-class-object:21:0:21:11::Invalid assignment to '__class__'. Should be a class definition but got a 'Const':INFERENCE +invalid-class-object:50:8:50:22:Pylint7429Good.class_defining_function_bad:Invalid assignment to '__class__'. Should be a class definition but got a 'Const':INFERENCE +invalid-class-object:58:15:58:29:Pylint7429Good.class_defining_function_bad_inverted:Invalid assignment to '__class__'. Should be a class definition but got a 'Const':INFERENCE +invalid-class-object:62:15:62:29:Pylint7429Good.class_defining_function_complex_bad:Invalid assignment to '__class__'. Should be a class definition but got a 'Const':INFERENCE diff --git a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_caught.py b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_caught.py index ef4957fc48..1b513f9724 100644 --- a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_caught.py +++ b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_caught.py @@ -1,13 +1,12 @@ -# pylint: disable=missing-docstring, too-few-public-methods, useless-object-inheritance, use-list-literal +# pylint: disable=missing-docstring, too-few-public-methods, use-list-literal # pylint: disable=too-many-ancestors, import-error, multiple-imports,wrong-import-position -from __future__ import print_function import socket, binascii, abc, six -class MyException(object): +class MyException: """Custom 'exception'.""" -class MySecondException(object): +class MySecondException: """Custom 'exception'.""" class MyGoodException(Exception): diff --git a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_caught.txt b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_caught.txt index 3c906b6614..6d35a1e22e 100644 --- a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_caught.txt +++ b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_caught.txt @@ -1,12 +1,12 @@ -catching-non-exception:27:7:27:18::"Catching an exception which doesn't inherit from Exception: MyException":UNDEFINED -catching-non-exception:33:7:33:39::"Catching an exception which doesn't inherit from Exception: MyException":UNDEFINED -catching-non-exception:33:7:33:39::"Catching an exception which doesn't inherit from Exception: MySecondException":UNDEFINED -catching-non-exception:54:7:54:21::"Catching an exception which doesn't inherit from Exception: None":UNDEFINED -catching-non-exception:54:7:54:21::"Catching an exception which doesn't inherit from Exception: list()":UNDEFINED -catching-non-exception:59:7:59:11::"Catching an exception which doesn't inherit from Exception: None":UNDEFINED -catching-non-exception:72:7:72:52::"Catching an exception which doesn't inherit from Exception: 4":UNDEFINED -catching-non-exception:72:7:72:52::"Catching an exception which doesn't inherit from Exception: None":UNDEFINED -catching-non-exception:72:7:72:52::"Catching an exception which doesn't inherit from Exception: list([4, 5, 6])":UNDEFINED -catching-non-exception:85:7:85:26::"Catching an exception which doesn't inherit from Exception: NON_EXCEPTION_TUPLE":UNDEFINED -catching-non-exception:103:7:103:13::"Catching an exception which doesn't inherit from Exception: object":UNDEFINED -catching-non-exception:108:7:108:12::"Catching an exception which doesn't inherit from Exception: range":UNDEFINED +catching-non-exception:26:7:26:18::"Catching an exception which doesn't inherit from Exception: MyException":UNDEFINED +catching-non-exception:32:7:32:39::"Catching an exception which doesn't inherit from Exception: MyException":UNDEFINED +catching-non-exception:32:7:32:39::"Catching an exception which doesn't inherit from Exception: MySecondException":UNDEFINED +catching-non-exception:53:7:53:21::"Catching an exception which doesn't inherit from Exception: None":UNDEFINED +catching-non-exception:53:7:53:21::"Catching an exception which doesn't inherit from Exception: list()":UNDEFINED +catching-non-exception:58:7:58:11::"Catching an exception which doesn't inherit from Exception: None":UNDEFINED +catching-non-exception:71:7:71:52::"Catching an exception which doesn't inherit from Exception: 4":UNDEFINED +catching-non-exception:71:7:71:52::"Catching an exception which doesn't inherit from Exception: None":UNDEFINED +catching-non-exception:71:7:71:52::"Catching an exception which doesn't inherit from Exception: list([4, 5, 6])":UNDEFINED +catching-non-exception:84:7:84:26::"Catching an exception which doesn't inherit from Exception: NON_EXCEPTION_TUPLE":UNDEFINED +catching-non-exception:102:7:102:13::"Catching an exception which doesn't inherit from Exception: object":UNDEFINED +catching-non-exception:107:7:107:12::"Catching an exception which doesn't inherit from Exception: range":UNDEFINED diff --git a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py index f7e52819df..a0b5ad39f0 100644 --- a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py +++ b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py @@ -1,4 +1,4 @@ -# pylint:disable=too-few-public-methods,import-error,missing-docstring, not-callable, useless-object-inheritance,import-outside-toplevel +# pylint:disable=too-few-public-methods,import-error,missing-docstring, not-callable, import-outside-toplevel """test pb with exceptions and old/new style classes""" @@ -8,7 +8,7 @@ class ValidException(Exception): class OldStyleClass: """Not an exception.""" -class NewStyleClass(object): +class NewStyleClass: """Not an exception.""" diff --git a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.txt b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.txt index 9e8e7ae002..f2ccd8a052 100644 --- a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.txt +++ b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.txt @@ -1,11 +1,11 @@ -raising-non-exception:38:4:38:30:bad_case0:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:42:4:42:25:bad_case1:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:48:4:48:30:bad_case2:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:52:4:52:23:bad_case3:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -notimplemented-raised:56:4:56:31:bad_case4:NotImplemented raised - should raise NotImplementedError:UNDEFINED -raising-bad-type:60:4:60:11:bad_case5:Raising int while only classes or instances are allowed:UNDEFINED -raising-bad-type:64:4:64:14:bad_case6:Raising NoneType while only classes or instances are allowed:UNDEFINED -raising-non-exception:68:4:68:14:bad_case7:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:72:4:72:15:bad_case8:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:76:4:76:14:bad_case9:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-bad-type:110:4:110:18:bad_case10:Raising str while only classes or instances are allowed:UNDEFINED +raising-non-exception:38:4:38:30:bad_case0:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:42:4:42:25:bad_case1:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:48:4:48:30:bad_case2:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:52:4:52:23:bad_case3:Raising a new style class which doesn't inherit from BaseException:INFERENCE +notimplemented-raised:56:4:56:31:bad_case4:NotImplemented raised - should raise NotImplementedError:HIGH +raising-bad-type:60:4:60:11:bad_case5:Raising int while only classes or instances are allowed:INFERENCE +raising-bad-type:64:4:64:14:bad_case6:Raising NoneType while only classes or instances are allowed:INFERENCE +raising-non-exception:68:4:68:14:bad_case7:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:72:4:72:15:bad_case8:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:76:4:76:14:bad_case9:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-bad-type:110:4:110:18:bad_case10:Raising str while only classes or instances are allowed:INFERENCE diff --git a/tests/functional/i/invalid/invalid_format_returned.py b/tests/functional/i/invalid/invalid_format_returned.py index b46feb4fe7..f97d0f29d5 100644 --- a/tests/functional/i/invalid/invalid_format_returned.py +++ b/tests/functional/i/invalid/invalid_format_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __format__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import six from missing import Missing -class FirstGoodFormat(object): +class FirstGoodFormat: """__format__ returns """ def __format__(self, format_spec): return "some format" -class SecondGoodFormat(object): +class SecondGoodFormat: """__format__ returns """ def __format__(self, format_spec): @@ -26,38 +26,38 @@ def __format__(cls, format_spec): @six.add_metaclass(FormatMetaclass) -class ThirdGoodFormat(object): +class ThirdGoodFormat: """Format through the metaclass.""" -class FirstBadFormat(object): +class FirstBadFormat: """ __format__ returns bytes """ def __format__(self, format_spec): # [invalid-format-returned] return b"123" -class SecondBadFormat(object): +class SecondBadFormat: """ __format__ returns int """ def __format__(self, format_spec): # [invalid-format-returned] return 1 -class ThirdBadFormat(object): +class ThirdBadFormat: """ __format__ returns node which does not have 'value' in AST """ def __format__(self, format_spec): # [invalid-format-returned] return lambda: "some format" -class AmbiguousFormat(object): +class AmbiguousFormat: """ Uninferable return value """ __format__ = lambda self, format_spec: Missing -class AnotherAmbiguousFormat(object): +class AnotherAmbiguousFormat: """Potential uninferable return value""" def __format__(self, format_spec): diff --git a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py index 5e55ad197d..efe6ba25fb 100644 --- a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py +++ b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __getnewargs_ex__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,use-dict-literal,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,use-dict-literal,unnecessary-lambda-assignment,use-dict-literal import six from missing import Missing -class FirstGoodGetNewArgsEx(object): +class FirstGoodGetNewArgsEx: """__getnewargs_ex__ returns """ def __getnewargs_ex__(self): return ((1,), {"2": "2"}) -class SecondGoodGetNewArgsEx(object): +class SecondGoodGetNewArgsEx: """__getnewargs_ex__ returns """ def __getnewargs_ex__(self): @@ -26,59 +26,59 @@ def __getnewargs_ex__(cls): @six.add_metaclass(GetNewArgsExMetaclass) -class ThirdGoodGetNewArgsEx(object): +class ThirdGoodGetNewArgsEx: """GetNewArgsEx through the metaclass.""" -class FirstBadGetNewArgsEx(object): +class FirstBadGetNewArgsEx: """ __getnewargs_ex__ returns an integer """ def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] return 1 -class SecondBadGetNewArgsEx(object): +class SecondBadGetNewArgsEx: """ __getnewargs_ex__ returns tuple with incorrect arg length""" def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] return (tuple(1), dict(x="y"), 1) -class ThirdBadGetNewArgsEx(object): +class ThirdBadGetNewArgsEx: """ __getnewargs_ex__ returns tuple with wrong type for first arg """ def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] return (dict(x="y"), dict(x="y")) -class FourthBadGetNewArgsEx(object): +class FourthBadGetNewArgsEx: """ __getnewargs_ex__ returns tuple with wrong type for second arg """ def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] return ((1, ), (1, )) -class FifthBadGetNewArgsEx(object): +class FifthBadGetNewArgsEx: """ __getnewargs_ex__ returns tuple with wrong type for both args """ def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] return ({'x': 'y'}, (2,)) -class SixthBadGetNewArgsEx(object): +class SixthBadGetNewArgsEx: """ __getnewargs_ex__ returns node which does not have 'value' in AST """ def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] return lambda: (1, 2) -class AmbigousGetNewArgsEx(object): +class AmbigousGetNewArgsEx: """ Uninferable return value """ __getnewargs_ex__ = lambda self: Missing -class AnotherAmbiguousGetNewArgsEx(object): +class AnotherAmbiguousGetNewArgsEx: """Potential uninferable return value""" def __getnewargs_ex__(self): diff --git a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_returned.py b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_returned.py index b4fd2cb4cb..06cd81dd08 100644 --- a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_returned.py +++ b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __getnewargs__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment,use-dict-literal import six from missing import Missing -class FirstGoodGetNewArgs(object): +class FirstGoodGetNewArgs: """__getnewargs__ returns """ def __getnewargs__(self): return (1, "2", 3) -class SecondGoodGetNewArgs(object): +class SecondGoodGetNewArgs: """__getnewargs__ returns """ def __getnewargs__(self): @@ -26,37 +26,37 @@ def __getnewargs__(cls): @six.add_metaclass(GetNewArgsMetaclass) -class ThirdGoodGetNewArgs(object): +class ThirdGoodGetNewArgs: """GetNewArgs through the metaclass.""" -class FirstBadGetNewArgs(object): +class FirstBadGetNewArgs: """ __getnewargs__ returns an integer """ def __getnewargs__(self): # [invalid-getnewargs-returned] return 1 -class SecondBadGetNewArgs(object): +class SecondBadGetNewArgs: """ __getnewargs__ returns str """ def __getnewargs__(self): # [invalid-getnewargs-returned] return "(1, 2, 3)" -class ThirdBadGetNewArgs(object): +class ThirdBadGetNewArgs: """ __getnewargs__ returns node which does not have 'value' in AST """ def __getnewargs__(self): # [invalid-getnewargs-returned] return lambda: tuple(1, 2) -class AmbigousGetNewArgs(object): +class AmbigousGetNewArgs: """ Uninferable return value """ __getnewargs__ = lambda self: Missing -class AnotherAmbiguousGetNewArgs(object): +class AnotherAmbiguousGetNewArgs: """Potential uninferable return value""" def __getnewargs__(self): return tuple(Missing) diff --git a/tests/functional/i/invalid/invalid_hash_returned.py b/tests/functional/i/invalid/invalid_hash_returned.py index 501d3dd829..4fedb63bc9 100644 --- a/tests/functional/i/invalid/invalid_hash_returned.py +++ b/tests/functional/i/invalid/invalid_hash_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __hash__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import six from missing import Missing -class FirstGoodHash(object): +class FirstGoodHash: """__hash__ returns """ def __hash__(self): return 1 -class SecondGoodHash(object): +class SecondGoodHash: """__hash__ returns """ def __hash__(self): @@ -26,45 +26,45 @@ def __hash__(cls): @six.add_metaclass(HashMetaclass) -class ThirdGoodHash(object): +class ThirdGoodHash: """Hash through the metaclass.""" -class FirstBadHash(object): +class FirstBadHash: """ __hash__ returns a dict """ def __hash__(self): # [invalid-hash-returned] return {} -class SecondBadHash(object): +class SecondBadHash: """ __hash__ returns str """ def __hash__(self): # [invalid-hash-returned] return "True" -class ThirdBadHash(object): +class ThirdBadHash: """ __hash__ returns a float""" def __hash__(self): # [invalid-hash-returned] return 1.11 -class FourthBadHash(object): +class FourthBadHash: """ __hash__ returns node which does not have 'value' in AST """ def __hash__(self): # [invalid-hash-returned] return lambda: 3 -class AmbigousHash(object): +class AmbigousHash: """ Uninferable return value """ __hash__ = lambda self: Missing -class AnotherAmbiguousHash(object): +class AnotherAmbiguousHash: """Potential uninferable return value""" def __hash__(self): diff --git a/tests/functional/i/invalid/invalid_index_returned.py b/tests/functional/i/invalid/invalid_index_returned.py index 8e452fcced..83c7fb02bd 100644 --- a/tests/functional/i/invalid/invalid_index_returned.py +++ b/tests/functional/i/invalid/invalid_index_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __index__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import six from missing import Missing -class FirstGoodIndex(object): +class FirstGoodIndex: """__index__ returns """ def __index__(self): return 1 -class SecondGoodIndex(object): +class SecondGoodIndex: """__index__ returns """ def __index__(self): @@ -26,45 +26,45 @@ def __index__(cls): @six.add_metaclass(IndexMetaclass) -class ThirdGoodIndex(object): +class ThirdGoodIndex: """Index through the metaclass.""" -class FirstBadIndex(object): +class FirstBadIndex: """ __index__ returns a dict """ def __index__(self): # [invalid-index-returned] return {'1': '1'} -class SecondBadIndex(object): +class SecondBadIndex: """ __index__ returns str """ def __index__(self): # [invalid-index-returned] return "42" -class ThirdBadIndex(object): +class ThirdBadIndex: """ __index__ returns a float""" def __index__(self): # [invalid-index-returned] return 1.11 -class FourthBadIndex(object): +class FourthBadIndex: """ __index__ returns node which does not have 'value' in AST """ def __index__(self): # [invalid-index-returned] return lambda: 3 -class AmbigousIndex(object): +class AmbigousIndex: """ Uninferable return value """ __index__ = lambda self: Missing -class AnotherAmbiguousIndex(object): +class AnotherAmbiguousIndex: """Potential uninferable return value""" def __index__(self): diff --git a/tests/functional/i/invalid/invalid_length/invalid_length_hint_returned.py b/tests/functional/i/invalid/invalid_length/invalid_length_hint_returned.py index 2e3273692b..e8178863ca 100644 --- a/tests/functional/i/invalid/invalid_length/invalid_length_hint_returned.py +++ b/tests/functional/i/invalid/invalid_length/invalid_length_hint_returned.py @@ -1,6 +1,6 @@ """Check invalid value returned by __length_hint__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import sys import six @@ -8,14 +8,14 @@ from missing import Missing -class FirstGoodLengthHint(object): +class FirstGoodLengthHint: """__length_hint__ returns """ def __length_hint__(self): return 0 -class SecondGoodLengthHint(object): +class SecondGoodLengthHint: """__length_hint__ returns """ def __length_hint__(self): @@ -28,37 +28,37 @@ def __length_hint__(cls): @six.add_metaclass(LengthHintMetaclass) -class ThirdGoodLengthHint(object): +class ThirdGoodLengthHint: """LengthHintgth through the metaclass.""" -class FirstBadLengthHint(object): +class FirstBadLengthHint: """ __length_hint__ returns a negative integer """ def __length_hint__(self): # [invalid-length-hint-returned] return -1 -class SecondBadLengthHint(object): +class SecondBadLengthHint: """ __length_hint__ returns non-int """ def __length_hint__(self): # [invalid-length-hint-returned] return 3.0 -class ThirdBadLengthHint(object): +class ThirdBadLengthHint: """ __length_hint__ returns node which does not have 'value' in AST """ def __length_hint__(self): # [invalid-length-hint-returned] return lambda: 3 -class AmbigousLengthHint(object): +class AmbigousLengthHint: """ Uninferable return value """ __length_hint__ = lambda self: Missing -class AnotherAmbiguousLengthHint(object): +class AnotherAmbiguousLengthHint: """Potential uninferable return value""" def __length_hint__(self): return int(Missing) diff --git a/tests/functional/i/invalid/invalid_length/invalid_length_returned.py b/tests/functional/i/invalid/invalid_length/invalid_length_returned.py index 7ffdbf93ad..14758687d5 100644 --- a/tests/functional/i/invalid/invalid_length/invalid_length_returned.py +++ b/tests/functional/i/invalid/invalid_length/invalid_length_returned.py @@ -1,6 +1,6 @@ """Check invalid value returned by __len__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import sys import six @@ -8,14 +8,14 @@ from missing import Missing -class FirstGoodLen(object): +class FirstGoodLen: """__len__ returns """ def __len__(self): return 0 -class SecondGoodLen(object): +class SecondGoodLen: """__len__ returns """ def __len__(self): @@ -28,44 +28,44 @@ def __len__(cls): @six.add_metaclass(LenMetaclass) -class ThirdGoodLen(object): +class ThirdGoodLen: """Length through the metaclass.""" -class FirstBadLen(object): +class FirstBadLen: """ __len__ returns a negative integer """ def __len__(self): # [invalid-length-returned] return -1 -class SecondBadLen(object): +class SecondBadLen: """ __len__ returns non-int """ def __len__(self): # [invalid-length-returned] return 3.0 -class ThirdBadLen(object): +class ThirdBadLen: """ __len__ returns node which does not have 'value' in AST """ def __len__(self): # [invalid-length-returned] return lambda: 3 -class NonRegression(object): +class NonRegression: """ __len__ returns nothing """ def __len__(self): # [invalid-length-returned] print(3.0) -class AmbigousLen(object): +class AmbigousLen: """ Uninferable return value """ __len__ = lambda self: Missing -class AnotherAmbiguousLen(object): +class AnotherAmbiguousLen: """Potential uninferable return value""" def __len__(self): return int(Missing) diff --git a/tests/functional/i/invalid/invalid_metaclass.py b/tests/functional/i/invalid/invalid_metaclass.py index ec251e47dd..3b264d693e 100644 --- a/tests/functional/i/invalid/invalid_metaclass.py +++ b/tests/functional/i/invalid/invalid_metaclass.py @@ -1,4 +1,8 @@ -# pylint: disable=missing-docstring, too-few-public-methods, import-error,unused-argument, useless-object-inheritance +# pylint: disable=missing-docstring, too-few-public-methods, import-error,unused-argument + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements import abc @@ -6,7 +10,7 @@ from unknown import Unknown -class InvalidAsMetaclass(object): +class InvalidAsMetaclass: pass @@ -15,38 +19,38 @@ class ValidAsMetaclass(type): @six.add_metaclass(type) -class FirstGood(object): +class FirstGood: pass @six.add_metaclass(abc.ABCMeta) -class SecondGood(object): +class SecondGood: pass @six.add_metaclass(Unknown) -class ThirdGood(object): +class ThirdGood: pass @six.add_metaclass(ValidAsMetaclass) -class FourthGood(object): +class FourthGood: pass -class FirstInvalid(object, metaclass=int): # [invalid-metaclass] +class FirstInvalid(metaclass=int): # [invalid-metaclass] pass -class SecondInvalid(object, metaclass=InvalidAsMetaclass): # [invalid-metaclass] +class SecondInvalid(metaclass=InvalidAsMetaclass): # [invalid-metaclass] pass -class ThirdInvalid(object, metaclass=2): # [invalid-metaclass] +class ThirdInvalid(metaclass=2): # [invalid-metaclass] pass -class FourthInvalid(object, metaclass=InvalidAsMetaclass()): # [invalid-metaclass] +class FourthInvalid(metaclass=InvalidAsMetaclass()): # [invalid-metaclass] pass diff --git a/tests/functional/i/invalid/invalid_metaclass.txt b/tests/functional/i/invalid/invalid_metaclass.txt index 006065705e..21b31f3c6e 100644 --- a/tests/functional/i/invalid/invalid_metaclass.txt +++ b/tests/functional/i/invalid/invalid_metaclass.txt @@ -1,6 +1,6 @@ -invalid-metaclass:37:0:37:18:FirstInvalid:Invalid metaclass 'int' used:UNDEFINED -invalid-metaclass:41:0:41:19:SecondInvalid:Invalid metaclass 'InvalidAsMetaclass' used:UNDEFINED -invalid-metaclass:45:0:45:18:ThirdInvalid:Invalid metaclass '2' used:UNDEFINED -invalid-metaclass:49:0:49:19:FourthInvalid:Invalid metaclass 'Instance of invalid_metaclass.InvalidAsMetaclass' used:UNDEFINED -invalid-metaclass:61:0:61:13:Invalid:Invalid metaclass 'int' used:UNDEFINED -invalid-metaclass:65:0:65:19:InvalidSecond:Invalid metaclass '1' used:UNDEFINED +invalid-metaclass:41:0:41:18:FirstInvalid:Invalid metaclass 'int' used:UNDEFINED +invalid-metaclass:45:0:45:19:SecondInvalid:Invalid metaclass 'InvalidAsMetaclass' used:UNDEFINED +invalid-metaclass:49:0:49:18:ThirdInvalid:Invalid metaclass '2' used:UNDEFINED +invalid-metaclass:53:0:53:19:FourthInvalid:Invalid metaclass 'Instance of invalid_metaclass.InvalidAsMetaclass' used:UNDEFINED +invalid-metaclass:65:0:65:13:Invalid:Invalid metaclass 'int' used:UNDEFINED +invalid-metaclass:69:0:69:19:InvalidSecond:Invalid metaclass '1' used:UNDEFINED diff --git a/tests/functional/i/invalid/invalid_name/invalid_name-module-disable.py b/tests/functional/i/invalid/invalid_name/invalid_name-module-disable.py new file mode 100644 index 0000000000..f6074eebbc --- /dev/null +++ b/tests/functional/i/invalid/invalid_name/invalid_name-module-disable.py @@ -0,0 +1,6 @@ +# pylint: disable=invalid-name + +"""Regression test for disabling of invalid-name for module names. + +See https://github.com/PyCQA/pylint/issues/3973. +""" diff --git a/tests/functional/i/invalid/invalid_repr_returned.py b/tests/functional/i/invalid/invalid_repr_returned.py index 80fe38bb50..afcc217a8d 100644 --- a/tests/functional/i/invalid/invalid_repr_returned.py +++ b/tests/functional/i/invalid/invalid_repr_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __repr__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import six from missing import Missing -class FirstGoodRepr(object): +class FirstGoodRepr: """__repr__ returns """ def __repr__(self): return "some repr" -class SecondGoodRepr(object): +class SecondGoodRepr: """__repr__ returns """ def __repr__(self): @@ -26,38 +26,38 @@ def __repr__(cls): @six.add_metaclass(ReprMetaclass) -class ThirdGoodRepr(object): +class ThirdGoodRepr: """Repr through the metaclass.""" -class FirstBadRepr(object): +class FirstBadRepr: """ __repr__ returns bytes """ def __repr__(self): # [invalid-repr-returned] return b"123" -class SecondBadRepr(object): +class SecondBadRepr: """ __repr__ returns int """ def __repr__(self): # [invalid-repr-returned] return 1 -class ThirdBadRepr(object): +class ThirdBadRepr: """ __repr__ returns node which does not have 'value' in AST """ def __repr__(self): # [invalid-repr-returned] return lambda: "some repr" -class AmbiguousRepr(object): +class AmbiguousRepr: """ Uninferable return value """ __repr__ = lambda self: Missing -class AnotherAmbiguousRepr(object): +class AnotherAmbiguousRepr: """Potential uninferable return value""" def __repr__(self): diff --git a/tests/functional/i/invalid/invalid_sequence_index.py b/tests/functional/i/invalid/invalid_sequence_index.py index 3dd941f9fc..56ade211b8 100644 --- a/tests/functional/i/invalid/invalid_sequence_index.py +++ b/tests/functional/i/invalid/invalid_sequence_index.py @@ -1,5 +1,9 @@ -"""Errors for invalid sequence indices""" -# pylint: disable=too-few-public-methods, import-error, missing-docstring, useless-object-inheritance, unnecessary-pass +# pylint: disable=too-few-public-methods, import-error, missing-docstring, unnecessary-pass + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + import six from unknown import Unknown @@ -26,7 +30,7 @@ def function4(): def function5(): """list index does not implement __index__""" - class NonIndexType(object): + class NonIndexType: """Class without __index__ method""" pass @@ -62,7 +66,7 @@ def function11(): def function12(): """list index implements __index__""" - class IndexType(object): + class IndexType: """Class with __index__ method""" def __index__(self): """Allow objects of this class to be used as slice indices""" @@ -72,7 +76,7 @@ def __index__(self): def function13(): """list index implements __index__ in a superclass""" - class IndexType(object): + class IndexType: """Class with __index__ method""" def __index__(self): """Allow objects of this class to be used as slice indices""" @@ -202,14 +206,14 @@ def __getitem__(self, key): test[0] = 0 # setitem with int, no error del test[0] # delitem with int, no error -# Teest ExtSlice usage +# Test ExtSlice usage def function25(): """Extended slice used with a list""" return TESTLIST[..., 0] # [invalid-sequence-index] def function26(): """Extended slice used with an object that implements __getitem__""" - class ExtSliceTest(object): + class ExtSliceTest: """Permit extslice syntax by implementing __getitem__""" def __getitem__(self, index): return 0 @@ -232,7 +236,7 @@ def __getitem__(cls, arg): return 24 @six.add_metaclass(Meta) - class Works(object): + class Works: pass @six.add_metaclass(Meta) diff --git a/tests/functional/i/invalid/invalid_sequence_index.txt b/tests/functional/i/invalid/invalid_sequence_index.txt index dc04046bee..81758b446d 100644 --- a/tests/functional/i/invalid/invalid_sequence_index.txt +++ b/tests/functional/i/invalid/invalid_sequence_index.txt @@ -1,19 +1,19 @@ -invalid-sequence-index:13:11:13:23:function1:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:17:11:17:25:function2:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:21:11:21:29:function3:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:25:11:25:24:function4:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:33:11:33:35:function5:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:37:11:37:26:function6:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:41:11:41:24:function7:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:48:11:48:28:function8:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:128:4:128:18:function19:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:133:8:133:22:function20:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:144:4:144:14:function21:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:145:8:145:18:function21:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:160:4:160:14:function22:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:162:8:162:18:function22:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:178:4:178:14:function23:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:180:4:180:14:function23:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:196:4:196:14:function24:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:198:8:198:18:function24:Sequence index is not an int, slice, or instance with __index__:UNDEFINED -invalid-sequence-index:208:11:208:27:function25:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:17:11:17:23:function1:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:21:11:21:25:function2:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:25:11:25:29:function3:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:29:11:29:24:function4:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:37:11:37:35:function5:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:41:11:41:26:function6:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:45:11:45:24:function7:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:52:11:52:28:function8:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:132:4:132:18:function19:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:137:8:137:22:function20:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:148:4:148:14:function21:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:149:8:149:18:function21:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:164:4:164:14:function22:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:166:8:166:18:function22:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:182:4:182:14:function23:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:184:4:184:14:function23:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:200:4:200:14:function24:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:202:8:202:18:function24:Sequence index is not an int, slice, or instance with __index__:UNDEFINED +invalid-sequence-index:212:11:212:27:function25:Sequence index is not an int, slice, or instance with __index__:UNDEFINED diff --git a/tests/functional/i/invalid/invalid_slice_index.py b/tests/functional/i/invalid/invalid_slice_index.py index 894742a2f6..253d01ae11 100644 --- a/tests/functional/i/invalid/invalid_slice_index.py +++ b/tests/functional/i/invalid/invalid_slice_index.py @@ -1,6 +1,6 @@ """Errors for invalid slice indices""" -# pylint: disable=too-few-public-methods,missing-docstring,expression-not-assigned,useless-object-inheritance,unnecessary-pass - +# pylint: disable=too-few-public-methods,missing-docstring,expression-not-assigned,unnecessary-pass +# pylint: disable=pointless-statement TESTLIST = [1, 2, 3] @@ -11,21 +11,41 @@ def function1(): def function2(): """strings used as indices""" - return TESTLIST['0':'1':] # [invalid-slice-index,invalid-slice-index] + TESTLIST['0':'1':] # [invalid-slice-index,invalid-slice-index] + ()['0':'1'] # [invalid-slice-index,invalid-slice-index] + ""["a":"z"] # [invalid-slice-index,invalid-slice-index] + b""["a":"z"] # [invalid-slice-index,invalid-slice-index] def function3(): """class without __index__ used as index""" - class NoIndexTest(object): + class NoIndexTest: """Class with no __index__ method""" pass return TESTLIST[NoIndexTest()::] # [invalid-slice-index] +def invalid_step(): + """0 is an invalid value for slice step with most builtin sequences.""" + TESTLIST[::0] # [invalid-slice-step] + [][::0] # [invalid-slice-step] + ""[::0] # [invalid-slice-step] + b""[::0] # [invalid-slice-step] + + class Custom: + def __getitem__(self, indices): + ... + + Custom()[::0] # no error -> custom __getitem__ method + +def invalid_slice_range(): + range(5)['0':'1'] # [invalid-slice-index,invalid-slice-index] + + # Valid indices def function4(): """integers used as indices""" - return TESTLIST[0:0:0] # no error + return TESTLIST[0:1:1] def function5(): """None used as indices""" @@ -33,7 +53,7 @@ def function5(): def function6(): """class with __index__ used as index""" - class IndexTest(object): + class IndexTest: """Class with __index__ method""" def __index__(self): """Allow objects of this class to be used as slice indices""" @@ -43,7 +63,7 @@ def __index__(self): def function7(): """class with __index__ in superclass used as index""" - class IndexType(object): + class IndexType: """Class with __index__ method""" def __index__(self): """Allow objects of this class to be used as slice indices""" diff --git a/tests/functional/i/invalid/invalid_slice_index.txt b/tests/functional/i/invalid/invalid_slice_index.txt index 97754e840d..3e7713ba7b 100644 --- a/tests/functional/i/invalid/invalid_slice_index.txt +++ b/tests/functional/i/invalid/invalid_slice_index.txt @@ -1,5 +1,17 @@ invalid-slice-index:10:20:10:22:function1:Slice index is not an int, None, or instance with __index__:UNDEFINED invalid-slice-index:10:23:10:25:function1:Slice index is not an int, None, or instance with __index__:UNDEFINED -invalid-slice-index:14:20:14:23:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED -invalid-slice-index:14:24:14:27:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED -invalid-slice-index:23:20:23:33:function3:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:14:13:14:16:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:14:17:14:20:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:15:7:15:10:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:15:11:15:14:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:16:7:16:10:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:16:11:16:14:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:17:8:17:11:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:17:12:17:15:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:26:20:26:33:function3:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-step:30:15:30:16:invalid_step:Slice step cannot be 0:HIGH +invalid-slice-step:31:9:31:10:invalid_step:Slice step cannot be 0:HIGH +invalid-slice-step:32:9:32:10:invalid_step:Slice step cannot be 0:HIGH +invalid-slice-step:33:10:33:11:invalid_step:Slice step cannot be 0:HIGH +invalid-slice-index:42:13:42:16:invalid_slice_range:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:42:17:42:20:invalid_slice_range:Slice index is not an int, None, or instance with __index__:UNDEFINED diff --git a/tests/functional/i/invalid/invalid_str_returned.py b/tests/functional/i/invalid/invalid_str_returned.py index dac98b50a6..d02aef148f 100644 --- a/tests/functional/i/invalid/invalid_str_returned.py +++ b/tests/functional/i/invalid/invalid_str_returned.py @@ -1,19 +1,19 @@ """Check invalid value returned by __str__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,useless-object-inheritance,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment import six from missing import Missing -class FirstGoodStr(object): +class FirstGoodStr: """__str__ returns """ def __str__(self): return "some str" -class SecondGoodStr(object): +class SecondGoodStr: """__str__ returns """ def __str__(self): @@ -26,38 +26,38 @@ def __str__(cls): @six.add_metaclass(StrMetaclass) -class ThirdGoodStr(object): +class ThirdGoodStr: """Str through the metaclass.""" -class FirstBadStr(object): +class FirstBadStr: """ __str__ returns bytes """ def __str__(self): # [invalid-str-returned] return b"123" -class SecondBadStr(object): +class SecondBadStr: """ __str__ returns int """ def __str__(self): # [invalid-str-returned] return 1 -class ThirdBadStr(object): +class ThirdBadStr: """ __str__ returns node which does not have 'value' in AST """ def __str__(self): # [invalid-str-returned] return lambda: "some str" -class AmbiguousStr(object): +class AmbiguousStr: """ Uninferable return value """ __str__ = lambda self: Missing -class AnotherAmbiguousStr(object): +class AnotherAmbiguousStr: """Potential uninferable return value""" def __str__(self): diff --git a/tests/functional/i/invalid/invalid_unary_operand_type.py b/tests/functional/i/invalid/invalid_unary_operand_type.py index e7231c34da..5881f89ec9 100644 --- a/tests/functional/i/invalid/invalid_unary_operand_type.py +++ b/tests/functional/i/invalid/invalid_unary_operand_type.py @@ -1,11 +1,11 @@ """Detect problems with invalid operands used on invalid objects.""" # pylint: disable=missing-docstring,too-few-public-methods,invalid-name -# pylint: disable=unused-variable, useless-object-inheritance, use-dict-literal +# pylint: disable=unused-variable, use-dict-literal import collections -class Implemented(object): +class Implemented: def __invert__(self): return 42 def __pos__(self): @@ -42,7 +42,7 @@ def these_are_bad(): neg_str = -"" # [invalid-unary-operand-type] invert_str = ~"" # [invalid-unary-operand-type] pos_str = +"" # [invalid-unary-operand-type] - class A(object): + class A: pass invert_func = ~(lambda: None) # [invalid-unary-operand-type] invert_class = ~A # [invalid-unary-operand-type] diff --git a/tests/functional/i/iterable_context.py b/tests/functional/i/iterable_context.py index 643cacd531..fb035c4df2 100644 --- a/tests/functional/i/iterable_context.py +++ b/tests/functional/i/iterable_context.py @@ -2,9 +2,9 @@ Checks that primitive values are not used in an iterating/mapping context. """ -# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,unused-argument,bad-mcs-method-argument,wrong-import-position,no-else-return, useless-object-inheritance, unnecessary-comprehension,redundant-u-string-prefix -from __future__ import print_function - +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,unused-argument,bad-mcs-method-argument, +# pylint: disable=wrong-import-position,no-else-return, unnecessary-comprehension,redundant-u-string-prefix +# pylint: disable=use-dict-literal # primitives numbers = [1, 2, 3] @@ -59,10 +59,10 @@ def powers_of_two(): pass # check for custom iterators -class A(object): +class A: pass -class B(object): +class B: def __iter__(self): return self @@ -72,7 +72,7 @@ def __next__(self): def next(self): return 1 -class C(object): +class C: "old-style iterator" def __getitem__(self, k): if k > 10: @@ -128,7 +128,7 @@ class MyClass(Iterable): print(i) # skip checks if statement is inside mixin/base/abstract class -class ManagedAccessViewMixin(object): +class ManagedAccessViewMixin: access_requirements = None def get_access_requirements(self): @@ -141,7 +141,7 @@ def dispatch(self, *_args, **_kwargs): for requirement in classes: print(requirement) -class BaseType(object): +class BaseType: valid_values = None def validate(self, value): @@ -154,7 +154,7 @@ def validate(self, value): return True return False -class AbstractUrlMarkManager(object): +class AbstractUrlMarkManager: def __init__(self): self._lineparser = None self._init_lineparser() @@ -167,7 +167,7 @@ def _init_lineparser(self): # class is not named as abstract # but still is deduceably abstract -class UrlMarkManager(object): +class UrlMarkManager: def __init__(self): self._lineparser = None self._init_lineparser() @@ -179,7 +179,7 @@ def _init_lineparser(self): raise NotImplementedError -class HasDynamicGetattr(object): +class HasDynamicGetattr: def __init__(self): self._obj = [] diff --git a/tests/functional/i/iterable_context_asyncio.py b/tests/functional/i/iterable_context_asyncio.py new file mode 100644 index 0000000000..1012fbee30 --- /dev/null +++ b/tests/functional/i/iterable_context_asyncio.py @@ -0,0 +1,43 @@ +""" +Checks that we don't erroneously emit not-an-iterable errors for +coroutines built with asyncio.coroutine. + +These decorators were deprecated in 3.8 and removed in 3.11. +""" +# pylint: disable=missing-docstring,too-few-public-methods,unused-argument,bad-mcs-method-argument +# pylint: disable=wrong-import-position +import asyncio + + +@asyncio.coroutine +def coroutine_function_return_none(): + return + + +@asyncio.coroutine +def coroutine_function_return_object(): + return 12 + + +@asyncio.coroutine +def coroutine_function_return_future(): + return asyncio.Future() + + +@asyncio.coroutine +def coroutine_function_pass(): + pass + + +@asyncio.coroutine +def coroutine_generator(): + yield + + +@asyncio.coroutine +def main(): + yield from coroutine_function_return_none() + yield from coroutine_function_return_object() + yield from coroutine_function_return_future() + yield from coroutine_function_pass() + yield from coroutine_generator() diff --git a/tests/functional/i/iterable_context_asyncio.rc b/tests/functional/i/iterable_context_asyncio.rc new file mode 100644 index 0000000000..4e2b748313 --- /dev/null +++ b/tests/functional/i/iterable_context_asyncio.rc @@ -0,0 +1,2 @@ +[testoptions] +max_pyver=3.10 diff --git a/tests/functional/i/iterable_context_py3.py b/tests/functional/i/iterable_context_py3.py index 07eda00f49..c09cba12d0 100644 --- a/tests/functional/i/iterable_context_py3.py +++ b/tests/functional/i/iterable_context_py3.py @@ -16,40 +16,3 @@ class SomeClass(metaclass=Meta): print(i) for i in SomeClass(): # [not-an-iterable] print(i) - - -import asyncio - - -@asyncio.coroutine -def coroutine_function_return_none(): - return - - -@asyncio.coroutine -def coroutine_function_return_object(): - return 12 - - -@asyncio.coroutine -def coroutine_function_return_future(): - return asyncio.Future() - - -@asyncio.coroutine -def coroutine_function_pass(): - pass - - -@asyncio.coroutine -def coroutine_generator(): - yield - - -@asyncio.coroutine -def main(): - yield from coroutine_function_return_none() - yield from coroutine_function_return_object() - yield from coroutine_function_return_future() - yield from coroutine_function_pass() - yield from coroutine_generator() diff --git a/tests/functional/k/keyword_arg_before_vararg.py b/tests/functional/k/keyword_arg_before_vararg.py index 1192874107..9824aac95b 100644 --- a/tests/functional/k/keyword_arg_before_vararg.py +++ b/tests/functional/k/keyword_arg_before_vararg.py @@ -1,7 +1,6 @@ """Unittests for W1125 (kw args before *args)""" -from __future__ import absolute_import, print_function -# pylint: disable=unused-argument, useless-object-inheritance, unnecessary-pass +# pylint: disable=unused-argument, unnecessary-pass def check_kwargs_before_args(param1, param2=2, *args): # [keyword-arg-before-vararg] """docstring""" pass @@ -9,7 +8,7 @@ def check_kwargs_before_args(param1, param2=2, *args): # [keyword-arg-before-var check_kwargs_before_args(5) # pylint: disable=too-few-public-methods, invalid-name -class AAAA(object): +class AAAA: """class AAAA""" def func_in_class(self, param1, param2=2, *args): # [keyword-arg-before-vararg] "method in class AAAA" diff --git a/tests/functional/k/keyword_arg_before_vararg.txt b/tests/functional/k/keyword_arg_before_vararg.txt index 87a88f1a4e..2a59872005 100644 --- a/tests/functional/k/keyword_arg_before_vararg.txt +++ b/tests/functional/k/keyword_arg_before_vararg.txt @@ -1,4 +1,4 @@ -keyword-arg-before-vararg:5:0:5:28:check_kwargs_before_args:Keyword argument before variable positional arguments list in the definition of check_kwargs_before_args function:UNDEFINED -keyword-arg-before-vararg:14:4:14:21:AAAA.func_in_class:Keyword argument before variable positional arguments list in the definition of func_in_class function:UNDEFINED -keyword-arg-before-vararg:19:4:19:30:AAAA.static_method_in_class:Keyword argument before variable positional arguments list in the definition of static_method_in_class function:UNDEFINED -keyword-arg-before-vararg:24:4:24:29:AAAA.class_method_in_class:Keyword argument before variable positional arguments list in the definition of class_method_in_class function:UNDEFINED +keyword-arg-before-vararg:4:0:4:28:check_kwargs_before_args:Keyword argument before variable positional arguments list in the definition of check_kwargs_before_args function:UNDEFINED +keyword-arg-before-vararg:13:4:13:21:AAAA.func_in_class:Keyword argument before variable positional arguments list in the definition of func_in_class function:UNDEFINED +keyword-arg-before-vararg:18:4:18:30:AAAA.static_method_in_class:Keyword argument before variable positional arguments list in the definition of static_method_in_class function:UNDEFINED +keyword-arg-before-vararg:23:4:23:29:AAAA.class_method_in_class:Keyword argument before variable positional arguments list in the definition of class_method_in_class function:UNDEFINED diff --git a/tests/functional/l/lambda_use_before_assign.py b/tests/functional/l/lambda_use_before_assign.py index f3df31bd44..c604e8fbfa 100644 --- a/tests/functional/l/lambda_use_before_assign.py +++ b/tests/functional/l/lambda_use_before_assign.py @@ -1,8 +1,7 @@ """https://www.logilab.net/elo/ticket/18862""" # pylint: disable=unnecessary-lambda-assignment -from __future__ import print_function -__revision__ = 1 + def function(): """hop""" ggg = lambda: xxx diff --git a/tests/functional/l/line/__init__.py b/tests/functional/l/line/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/functional/l/line_endings.py b/tests/functional/l/line/line_endings.py similarity index 100% rename from tests/functional/l/line_endings.py rename to tests/functional/l/line/line_endings.py diff --git a/tests/functional/l/line_endings.rc b/tests/functional/l/line/line_endings.rc similarity index 100% rename from tests/functional/l/line_endings.rc rename to tests/functional/l/line/line_endings.rc diff --git a/tests/functional/l/line_endings.txt b/tests/functional/l/line/line_endings.txt similarity index 100% rename from tests/functional/l/line_endings.txt rename to tests/functional/l/line/line_endings.txt diff --git a/tests/functional/l/line_too_long.py b/tests/functional/l/line/line_too_long.py similarity index 100% rename from tests/functional/l/line_too_long.py rename to tests/functional/l/line/line_too_long.py diff --git a/tests/functional/l/line_too_long.txt b/tests/functional/l/line/line_too_long.txt similarity index 100% rename from tests/functional/l/line_too_long.txt rename to tests/functional/l/line/line_too_long.txt diff --git a/tests/functional/l/line_too_long_end_of_module.py b/tests/functional/l/line/line_too_long_end_of_module.py similarity index 100% rename from tests/functional/l/line_too_long_end_of_module.py rename to tests/functional/l/line/line_too_long_end_of_module.py diff --git a/tests/functional/l/long_lines_with_utf8.py b/tests/functional/l/line/line_too_long_with_utf8.py similarity index 100% rename from tests/functional/l/long_lines_with_utf8.py rename to tests/functional/l/line/line_too_long_with_utf8.py diff --git a/tests/functional/l/long_lines_with_utf8.txt b/tests/functional/l/line/line_too_long_with_utf8.txt similarity index 100% rename from tests/functional/l/long_lines_with_utf8.txt rename to tests/functional/l/line/line_too_long_with_utf8.txt diff --git a/tests/functional/l/long_utf8_lines.py b/tests/functional/l/line/line_too_long_with_utf8_2.py similarity index 94% rename from tests/functional/l/long_utf8_lines.py rename to tests/functional/l/line/line_too_long_with_utf8_2.py index 138e4e299a..288f63db0d 100644 --- a/tests/functional/l/long_utf8_lines.py +++ b/tests/functional/l/line/line_too_long_with_utf8_2.py @@ -2,7 +2,6 @@ """this utf-8 doc string have some non ASCII characters like 'é', or '¢»ß'""" ### check also comments with some more non ASCII characters like 'é' or '¢»ß' -__revision__ = 1100 ASCII = "----------------------------------------------------------------------" UTF_8 = "--------------------------------------------------------------------éé" diff --git a/tests/functional/l/literal_comparison.py b/tests/functional/l/literal_comparison.py index b5e31a0e71..603c0da7a0 100644 --- a/tests/functional/l/literal_comparison.py +++ b/tests/functional/l/literal_comparison.py @@ -1,25 +1,25 @@ # pylint: disable=missing-docstring, comparison-with-itself -if 2 is 2: # [literal-comparison, comparison-of-constants] +if 2 is 2: # [literal-comparison, comparison-of-constants] pass -if "a" is b"a": # [literal-comparison, comparison-of-constants] +if "a" is b"a": # [literal-comparison, comparison-of-constants] pass -if 2.0 is 3.0: # [literal-comparison, comparison-of-constants] +if 2.0 is 3.0: # [literal-comparison, comparison-of-constants] pass if () is (1, 2, 3): pass -if () is {1:2, 2:3}: # [literal-comparison] +if () is {1:2, 2:3}: # [literal-comparison] pass -if [] is [4, 5, 6]: # [literal-comparison] +if [] is [4, 5, 6]: # [literal-comparison] pass -if () is {1, 2, 3}: # [literal-comparison] +if () is {1, 2, 3}: # [literal-comparison] pass if () is not {1:2, 2:3}: # [literal-comparison] diff --git a/tests/functional/l/literal_comparison.txt b/tests/functional/l/literal_comparison.txt index c1a679c76b..f14c732199 100644 --- a/tests/functional/l/literal_comparison.txt +++ b/tests/functional/l/literal_comparison.txt @@ -1,15 +1,15 @@ comparison-of-constants:4:3:4:9::"Comparison between constants: '2 is 2' has a constant value":HIGH -literal-comparison:4:3:4:9::Comparison to literal:UNDEFINED +literal-comparison:4:3:4:9::In '2 is 2', use '==' when comparing constant literals not 'is' ('2 == 2'):HIGH comparison-of-constants:7:3:7:14::"Comparison between constants: 'a is b'a'' has a constant value":HIGH -literal-comparison:7:3:7:14::Comparison to literal:UNDEFINED +literal-comparison:7:3:7:14::In ''a' is b'a'', use '==' when comparing constant literals not 'is' (''a' == b'a''):HIGH comparison-of-constants:10:3:10:13::"Comparison between constants: '2.0 is 3.0' has a constant value":HIGH -literal-comparison:10:3:10:13::Comparison to literal:UNDEFINED -literal-comparison:16:3:16:19::Comparison to literal:UNDEFINED -literal-comparison:19:3:19:18::Comparison to literal:UNDEFINED -literal-comparison:22:3:22:18::Comparison to literal:UNDEFINED -literal-comparison:25:3:25:23::Comparison to literal:UNDEFINED -literal-comparison:28:3:28:22::Comparison to literal:UNDEFINED -literal-comparison:31:3:31:22::Comparison to literal:UNDEFINED -literal-comparison:38:3:38:13::Comparison to literal:UNDEFINED -literal-comparison:41:3:41:13::Comparison to literal:UNDEFINED -literal-comparison:44:3:44:14::Comparison to literal:UNDEFINED +literal-comparison:10:3:10:13::In '2.0 is 3.0', use '==' when comparing constant literals not 'is' ('2.0 == 3.0'):HIGH +literal-comparison:16:3:16:19::"In '() is {1: 2, 2: 3}', use '==' when comparing constant literals not 'is' ('() == {1: 2, 2: 3}')":HIGH +literal-comparison:19:3:19:18::In '[] is [4, 5, 6]', use '==' when comparing constant literals not 'is' ('[] == [4, 5, 6]'):HIGH +literal-comparison:22:3:22:18::In '() is {1, 2, 3}', use '==' when comparing constant literals not 'is' ('() == {1, 2, 3}'):HIGH +literal-comparison:25:3:25:23::"In '() is not {1: 2, 2: 3}', use '!=' when comparing constant literals not 'is not' ('() != {1: 2, 2: 3}')":HIGH +literal-comparison:28:3:28:22::In '[] is not [4, 5, 6]', use '!=' when comparing constant literals not 'is not' ('[] != [4, 5, 6]'):HIGH +literal-comparison:31:3:31:22::In '() is not {1, 2, 3}', use '!=' when comparing constant literals not 'is not' ('() != {1, 2, 3}'):HIGH +literal-comparison:38:3:38:13::In 'CONST is 0', use '==' when comparing constant literals not 'is' ('CONST == 0'):HIGH +literal-comparison:41:3:41:13::In 'CONST is 1', use '==' when comparing constant literals not 'is' ('CONST == 1'):HIGH +literal-comparison:44:3:44:14::In 'CONST is 42', use '==' when comparing constant literals not 'is' ('CONST == 42'):HIGH diff --git a/tests/functional/l/logging/__init__.py b/tests/functional/l/logging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/functional/l/logging_format_interpolation.py b/tests/functional/l/logging/logging_format_interpolation.py similarity index 100% rename from tests/functional/l/logging_format_interpolation.py rename to tests/functional/l/logging/logging_format_interpolation.py diff --git a/tests/functional/l/logging_format_interpolation.txt b/tests/functional/l/logging/logging_format_interpolation.txt similarity index 100% rename from tests/functional/l/logging_format_interpolation.txt rename to tests/functional/l/logging/logging_format_interpolation.txt diff --git a/tests/functional/l/logging_format_interpolation_py36.py b/tests/functional/l/logging/logging_format_interpolation_py36.py similarity index 100% rename from tests/functional/l/logging_format_interpolation_py36.py rename to tests/functional/l/logging/logging_format_interpolation_py36.py diff --git a/tests/functional/l/logging_format_interpolation_py36.rc b/tests/functional/l/logging/logging_format_interpolation_py36.rc similarity index 100% rename from tests/functional/l/logging_format_interpolation_py36.rc rename to tests/functional/l/logging/logging_format_interpolation_py36.rc diff --git a/tests/functional/l/logging_format_interpolation_py36.txt b/tests/functional/l/logging/logging_format_interpolation_py36.txt similarity index 100% rename from tests/functional/l/logging_format_interpolation_py36.txt rename to tests/functional/l/logging/logging_format_interpolation_py36.txt diff --git a/tests/functional/l/logging_format_interpolation_style.py b/tests/functional/l/logging/logging_format_interpolation_style.py similarity index 100% rename from tests/functional/l/logging_format_interpolation_style.py rename to tests/functional/l/logging/logging_format_interpolation_style.py diff --git a/tests/functional/l/logging_format_interpolation_style.rc b/tests/functional/l/logging/logging_format_interpolation_style.rc similarity index 100% rename from tests/functional/l/logging_format_interpolation_style.rc rename to tests/functional/l/logging/logging_format_interpolation_style.rc diff --git a/tests/functional/l/logging_fstring_interpolation_py36.py b/tests/functional/l/logging/logging_fstring_interpolation_py36.py similarity index 100% rename from tests/functional/l/logging_fstring_interpolation_py36.py rename to tests/functional/l/logging/logging_fstring_interpolation_py36.py diff --git a/tests/functional/l/logging_fstring_interpolation_py36.rc b/tests/functional/l/logging/logging_fstring_interpolation_py36.rc similarity index 100% rename from tests/functional/l/logging_fstring_interpolation_py36.rc rename to tests/functional/l/logging/logging_fstring_interpolation_py36.rc diff --git a/tests/functional/l/logging_fstring_interpolation_py36.txt b/tests/functional/l/logging/logging_fstring_interpolation_py36.txt similarity index 100% rename from tests/functional/l/logging_fstring_interpolation_py36.txt rename to tests/functional/l/logging/logging_fstring_interpolation_py36.txt diff --git a/tests/functional/l/logging/logging_fstring_interpolation_py37.py b/tests/functional/l/logging/logging_fstring_interpolation_py37.py new file mode 100644 index 0000000000..d08424eb04 --- /dev/null +++ b/tests/functional/l/logging/logging_fstring_interpolation_py37.py @@ -0,0 +1,11 @@ +"""Tests for logging-fstring-interpolation with f-strings""" +import logging + +VAR = "string" +logging.error(f"{VAR}") # [logging-fstring-interpolation] + +WORLD = "world" +logging.error(f'Hello {WORLD}') # [logging-fstring-interpolation] + +logging.error(f'Hello %s', 'World!') # [f-string-without-interpolation] +logging.error(f'Hello %d', 1) # [f-string-without-interpolation] diff --git a/tests/functional/l/logging_fstring_interpolation_py37.rc b/tests/functional/l/logging/logging_fstring_interpolation_py37.rc similarity index 100% rename from tests/functional/l/logging_fstring_interpolation_py37.rc rename to tests/functional/l/logging/logging_fstring_interpolation_py37.rc diff --git a/tests/functional/l/logging/logging_fstring_interpolation_py37.txt b/tests/functional/l/logging/logging_fstring_interpolation_py37.txt new file mode 100644 index 0000000000..d8b84d674d --- /dev/null +++ b/tests/functional/l/logging/logging_fstring_interpolation_py37.txt @@ -0,0 +1,4 @@ +logging-fstring-interpolation:5:0:5:23::Use lazy % formatting in logging functions:UNDEFINED +logging-fstring-interpolation:8:0:8:31::Use lazy % formatting in logging functions:UNDEFINED +f-string-without-interpolation:10:14:10:25::Using an f-string that does not have any interpolated variables:UNDEFINED +f-string-without-interpolation:11:14:11:25::Using an f-string that does not have any interpolated variables:UNDEFINED diff --git a/tests/functional/l/logging_not_lazy.py b/tests/functional/l/logging/logging_not_lazy.py similarity index 100% rename from tests/functional/l/logging_not_lazy.py rename to tests/functional/l/logging/logging_not_lazy.py diff --git a/tests/functional/l/logging_not_lazy.txt b/tests/functional/l/logging/logging_not_lazy.txt similarity index 100% rename from tests/functional/l/logging_not_lazy.txt rename to tests/functional/l/logging/logging_not_lazy.txt diff --git a/tests/functional/l/logging_not_lazy_module.py b/tests/functional/l/logging/logging_not_lazy_module.py similarity index 100% rename from tests/functional/l/logging_not_lazy_module.py rename to tests/functional/l/logging/logging_not_lazy_module.py diff --git a/tests/functional/l/logging_not_lazy_module.rc b/tests/functional/l/logging/logging_not_lazy_module.rc similarity index 100% rename from tests/functional/l/logging_not_lazy_module.rc rename to tests/functional/l/logging/logging_not_lazy_module.rc diff --git a/tests/functional/l/logging_not_lazy_module.txt b/tests/functional/l/logging/logging_not_lazy_module.txt similarity index 100% rename from tests/functional/l/logging_not_lazy_module.txt rename to tests/functional/l/logging/logging_not_lazy_module.txt diff --git a/tests/functional/l/logging_not_lazy_with_logger.py b/tests/functional/l/logging/logging_not_lazy_with_logger.py similarity index 95% rename from tests/functional/l/logging_not_lazy_with_logger.py rename to tests/functional/l/logging/logging_not_lazy_with_logger.py index 69d0e9bd41..ad94d32e10 100644 --- a/tests/functional/l/logging_not_lazy_with_logger.py +++ b/tests/functional/l/logging/logging_not_lazy_with_logger.py @@ -3,7 +3,6 @@ from __future__ import absolute_import import logging -__revision__ = '' LOG = logging.getLogger("domain") LOG.debug("%s" % "junk") # [logging-not-lazy] diff --git a/tests/functional/l/logging_not_lazy_with_logger.rc b/tests/functional/l/logging/logging_not_lazy_with_logger.rc similarity index 100% rename from tests/functional/l/logging_not_lazy_with_logger.rc rename to tests/functional/l/logging/logging_not_lazy_with_logger.rc diff --git a/tests/functional/l/logging/logging_not_lazy_with_logger.txt b/tests/functional/l/logging/logging_not_lazy_with_logger.txt new file mode 100644 index 0000000000..6496c6e52f --- /dev/null +++ b/tests/functional/l/logging/logging_not_lazy_with_logger.txt @@ -0,0 +1,4 @@ +logging-not-lazy:8:0:8:24::Use lazy % formatting in logging functions:UNDEFINED +logging-not-lazy:9:0:9:37::Use lazy % formatting in logging functions:UNDEFINED +logging-not-lazy:11:0:11:19::Use lazy % formatting in logging functions:UNDEFINED +logging-not-lazy:13:0:13:48::Use lazy % formatting in logging functions:UNDEFINED diff --git a/tests/functional/l/logging_too_few_args.py b/tests/functional/l/logging/logging_too_few_args.py similarity index 100% rename from tests/functional/l/logging_too_few_args.py rename to tests/functional/l/logging/logging_too_few_args.py diff --git a/tests/functional/l/logging_too_few_args.rc b/tests/functional/l/logging/logging_too_few_args.rc similarity index 100% rename from tests/functional/l/logging_too_few_args.rc rename to tests/functional/l/logging/logging_too_few_args.rc diff --git a/tests/functional/l/logging_too_few_args.txt b/tests/functional/l/logging/logging_too_few_args.txt similarity index 100% rename from tests/functional/l/logging_too_few_args.txt rename to tests/functional/l/logging/logging_too_few_args.txt diff --git a/tests/functional/l/logging_too_many_args.py b/tests/functional/l/logging/logging_too_many_args.py similarity index 100% rename from tests/functional/l/logging_too_many_args.py rename to tests/functional/l/logging/logging_too_many_args.py diff --git a/tests/functional/l/logging_too_many_args.rc b/tests/functional/l/logging/logging_too_many_args.rc similarity index 100% rename from tests/functional/l/logging_too_many_args.rc rename to tests/functional/l/logging/logging_too_many_args.rc diff --git a/tests/functional/l/logging_too_many_args.txt b/tests/functional/l/logging/logging_too_many_args.txt similarity index 100% rename from tests/functional/l/logging_too_many_args.txt rename to tests/functional/l/logging/logging_too_many_args.txt diff --git a/tests/functional/l/logging_fstring_interpolation_py37.py b/tests/functional/l/logging_fstring_interpolation_py37.py deleted file mode 100644 index 963b2ce8ce..0000000000 --- a/tests/functional/l/logging_fstring_interpolation_py37.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Tests for logging-fstring-interpolation with f-strings""" -import logging - -VAR = "string" -logging.error(f"{VAR}") # [logging-fstring-interpolation] diff --git a/tests/functional/l/logging_fstring_interpolation_py37.txt b/tests/functional/l/logging_fstring_interpolation_py37.txt deleted file mode 100644 index a41c3e1450..0000000000 --- a/tests/functional/l/logging_fstring_interpolation_py37.txt +++ /dev/null @@ -1 +0,0 @@ -logging-fstring-interpolation:5:0:5:23::Use lazy % formatting in logging functions:UNDEFINED diff --git a/tests/functional/l/logging_not_lazy_with_logger.txt b/tests/functional/l/logging_not_lazy_with_logger.txt deleted file mode 100644 index 908d02b651..0000000000 --- a/tests/functional/l/logging_not_lazy_with_logger.txt +++ /dev/null @@ -1,4 +0,0 @@ -logging-not-lazy:9:0:9:24::Use lazy % formatting in logging functions:UNDEFINED -logging-not-lazy:10:0:10:37::Use lazy % formatting in logging functions:UNDEFINED -logging-not-lazy:12:0:12:19::Use lazy % formatting in logging functions:UNDEFINED -logging-not-lazy:14:0:14:48::Use lazy % formatting in logging functions:UNDEFINED diff --git a/tests/functional/l/loopvar_in_dict_comp.py b/tests/functional/l/loopvar_in_dict_comp.py index 072ea1bf52..cc204c93f9 100644 --- a/tests/functional/l/loopvar_in_dict_comp.py +++ b/tests/functional/l/loopvar_in_dict_comp.py @@ -1,7 +1,5 @@ """Tests for loopvar-in-closure.""" -__revision__ = 0 - def bad_case(): """Loop variable from dict comprehension.""" diff --git a/tests/functional/l/loopvar_in_dict_comp.txt b/tests/functional/l/loopvar_in_dict_comp.txt index b8fab5f9c1..881a49ccc1 100644 --- a/tests/functional/l/loopvar_in_dict_comp.txt +++ b/tests/functional/l/loopvar_in_dict_comp.txt @@ -1 +1 @@ -cell-var-from-loop:8:23:8:24:bad_case.:Cell variable x defined in loop:UNDEFINED +cell-var-from-loop:6:23:6:24:bad_case.:Cell variable x defined in loop:UNDEFINED diff --git a/tests/functional/m/mapping_context.py b/tests/functional/m/mapping_context.py index 5064776946..8dc6b3b72d 100644 --- a/tests/functional/m/mapping_context.py +++ b/tests/functional/m/mapping_context.py @@ -1,8 +1,7 @@ """ Checks that only valid values are used in a mapping context. """ -# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,wrong-import-position, useless-object-inheritance -from __future__ import print_function +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,wrong-import-position,use-dict-literal def test(**kwargs): @@ -18,7 +17,7 @@ def test(**kwargs): # in order to be used in kwargs custom mapping class should define # __iter__(), __getitem__(key) and keys(). -class CustomMapping(object): +class CustomMapping: def __init__(self): self.data = dict(a=1, b=2, c=3, d=4, e=5) @@ -31,13 +30,13 @@ def keys(self): test(**CustomMapping()) test(**CustomMapping) # [not-a-mapping] -class NotMapping(object): +class NotMapping: pass test(**NotMapping()) # [not-a-mapping] # skip checks if statement is inside mixin/base/abstract class -class SomeMixin(object): +class SomeMixin: kwargs = None def get_kwargs(self): @@ -50,7 +49,7 @@ def dispatch(self): kws = self.get_kwargs() self.run(**kws) -class AbstractThing(object): +class AbstractThing: kwargs = None def get_kwargs(self): @@ -63,7 +62,7 @@ def dispatch(self): kws = self.get_kwargs() self.run(**kws) -class BaseThing(object): +class BaseThing: kwargs = None def get_kwargs(self): @@ -77,7 +76,7 @@ def dispatch(self): self.run(**kws) # abstract class -class Thing(object): +class Thing: def get_kwargs(self): raise NotImplementedError @@ -97,7 +96,7 @@ class MyClass(Mapping): test(**MyClass()) -class HasDynamicGetattr(object): +class HasDynamicGetattr: def __init__(self): self._obj = {} diff --git a/tests/functional/m/mapping_context.txt b/tests/functional/m/mapping_context.txt index 7391d67652..ce144dc312 100644 --- a/tests/functional/m/mapping_context.txt +++ b/tests/functional/m/mapping_context.txt @@ -1,2 +1,2 @@ -not-a-mapping:32:7:32:20::Non-mapping value CustomMapping is used in a mapping context:UNDEFINED -not-a-mapping:37:7:37:19::Non-mapping value NotMapping() is used in a mapping context:UNDEFINED +not-a-mapping:31:7:31:20::Non-mapping value CustomMapping is used in a mapping context:UNDEFINED +not-a-mapping:36:7:36:19::Non-mapping value NotMapping() is used in a mapping context:UNDEFINED diff --git a/tests/functional/m/mapping_context_py3.py b/tests/functional/m/mapping_context_py3.py index 3646910d85..4865fbb774 100644 --- a/tests/functional/m/mapping_context_py3.py +++ b/tests/functional/m/mapping_context_py3.py @@ -1,5 +1,5 @@ # pylint: disable=missing-docstring,invalid-name,too-few-public-methods -from __future__ import print_function + def test(**kwargs): print(kwargs) diff --git a/tests/functional/m/member/member_checks.py b/tests/functional/m/member/member_checks.py index 22b853d735..ff109d659a 100644 --- a/tests/functional/m/member/member_checks.py +++ b/tests/functional/m/member/member_checks.py @@ -1,7 +1,7 @@ -# pylint: disable=missing-docstring,too-few-public-methods,bare-except,broad-except, useless-object-inheritance, unused-private-member +# pylint: disable=missing-docstring,too-few-public-methods,bare-except,broad-except, unused-private-member # pylint: disable=using-constant-test,expression-not-assigned, assigning-non-slot, unused-variable,pointless-statement, wrong-import-order, wrong-import-position,import-outside-toplevel -from __future__ import print_function -class Provider(object): + +class Provider: """provide some attributes and method""" cattr = 4 def __init__(self): @@ -14,7 +14,7 @@ def hophop(self): print('hop hop hop', self) -class Client(object): +class Client: """use provider class""" def __init__(self): @@ -64,17 +64,17 @@ def test_no_false_positives(self): super().misssing() # [no-member] -class Mixin(object): +class Mixin: """No no-member should be emitted for mixins.""" -class Getattr(object): +class Getattr: """no-member shouldn't be emitted for classes with dunder getattr.""" def __getattr__(self, attr): return self.__dict__[attr] -class Getattribute(object): +class Getattribute: """no-member shouldn't be emitted for classes with dunder getattribute.""" def __getattribute__(self, attr): @@ -164,13 +164,13 @@ def no_conjugate_member(magic_flag): return something.conjugate() -class NoDunderNameInInstance(object): +class NoDunderNameInInstance: """Emit a warning when accessing __name__ from an instance.""" def __init__(self): self.var = self.__name__ # [no-member] -class InvalidAccessBySlots(object): +class InvalidAccessBySlots: __slots__ = ('a', ) def __init__(self): var = self.teta # [no-member] @@ -183,13 +183,13 @@ def __getattr__(cls, attr): return attr -class SomeClass(object, metaclass=MetaWithDynamicGetattr): +class SomeClass(metaclass=MetaWithDynamicGetattr): pass SomeClass.does_not_exist -class ClassWithMangledAttribute(object): +class ClassWithMangledAttribute: def __init__(self): self.name = 'Bug1643' def __bar(self): diff --git a/tests/functional/m/member/member_checks_hints.py b/tests/functional/m/member/member_checks_hints.py index 3925f4741e..f8f5dfcfc4 100644 --- a/tests/functional/m/member/member_checks_hints.py +++ b/tests/functional/m/member/member_checks_hints.py @@ -1,7 +1,7 @@ -# pylint: disable=missing-docstring, too-few-public-methods, pointless-statement, useless-object-inheritance +# pylint: disable=missing-docstring, too-few-public-methods, pointless-statement -class Parent(object): +class Parent: def __init__(self): self._parent = 42 diff --git a/tests/functional/m/member/member_checks_no_hints.py b/tests/functional/m/member/member_checks_no_hints.py index 3925f4741e..f8f5dfcfc4 100644 --- a/tests/functional/m/member/member_checks_no_hints.py +++ b/tests/functional/m/member/member_checks_no_hints.py @@ -1,7 +1,7 @@ -# pylint: disable=missing-docstring, too-few-public-methods, pointless-statement, useless-object-inheritance +# pylint: disable=missing-docstring, too-few-public-methods, pointless-statement -class Parent(object): +class Parent: def __init__(self): self._parent = 42 diff --git a/tests/functional/m/membership_protocol.py b/tests/functional/m/membership_protocol.py index 152c277046..10bec9603e 100644 --- a/tests/functional/m/membership_protocol.py +++ b/tests/functional/m/membership_protocol.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,too-few-public-methods,import-error,wrong-import-position,no-else-return, comparison-with-itself, useless-object-inheritance, redundant-u-string-prefix comparison-of-constants +# pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,too-few-public-methods,import-error,wrong-import-position,no-else-return, comparison-with-itself, redundant-u-string-prefix comparison-of-constants # standard types 1 in [1, 2, 3] @@ -30,20 +30,20 @@ def count(upto=float("inf")): 10 in count(upto=10) # custom instance -class UniversalContainer(object): +class UniversalContainer: def __contains__(self, key): return True 42 in UniversalContainer() # custom iterable -class CustomIterable(object): +class CustomIterable: def __iter__(self): return iter((1, 2, 3)) 3 in CustomIterable() # old-style iterable -class OldStyleIterable(object): +class OldStyleIterable: def __getitem__(self, key): if key < 10: return 2 ** key @@ -60,7 +60,7 @@ class MaybeIterable(ImportedClass): 10 in MaybeIterable() # do not emit warning inside mixins/abstract/base classes -class UsefulMixin(object): +class UsefulMixin: stuff = None def get_stuff(self): @@ -71,7 +71,7 @@ def act(self, thing): if thing in stuff: pass -class BaseThing(object): +class BaseThing: valid_values = None def validate(self, value): @@ -81,7 +81,7 @@ def validate(self, value): # error should not be emitted here return value in self.valid_values -class AbstractThing(object): +class AbstractThing: valid_values = None def validate(self, value): @@ -93,7 +93,7 @@ def validate(self, value): # class is not named as abstract # but still is deduceably abstract -class Thing(object): +class Thing: valid_values = None def __init__(self): @@ -114,7 +114,7 @@ def _init_values(self): 42 not in None # [unsupported-membership-test] 42 in 8.5 # [unsupported-membership-test] -class EmptyClass(object): +class EmptyClass: pass 42 not in EmptyClass() # [unsupported-membership-test] diff --git a/tests/functional/m/metaclass_attr_access.py b/tests/functional/m/metaclass_attr_access.py index 50f9712faf..7d1faa5f51 100644 --- a/tests/functional/m/metaclass_attr_access.py +++ b/tests/functional/m/metaclass_attr_access.py @@ -1,8 +1,6 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods """test attribute access on metaclass""" -from __future__ import print_function - class Meta(type): """the meta class""" @@ -12,7 +10,7 @@ def __init__(cls, name, bases, dictionary): delattr(cls, '_meta_args') -class Test(object): +class Test: """metaclassed class""" __metaclass__ = Meta _meta_args = ('foo', 'bar') diff --git a/tests/functional/m/method_cache_max_size_none.py b/tests/functional/m/method_cache_max_size_none.py index a0f5d3ae07..6604a73258 100644 --- a/tests/functional/m/method_cache_max_size_none.py +++ b/tests/functional/m/method_cache_max_size_none.py @@ -6,6 +6,7 @@ import functools as aliased_functools from functools import lru_cache from functools import lru_cache as aliased_cache +from enum import Enum @lru_cache @@ -78,3 +79,11 @@ def my_func(self, param): @lru_cache(maxsize=None) def my_func(param): return param + 1 + + +class Class(Enum): + A = 1 + + @lru_cache(maxsize=None) + def func(self) -> None: + pass diff --git a/tests/functional/m/method_cache_max_size_none.txt b/tests/functional/m/method_cache_max_size_none.txt index 6a12d97ce1..35512db855 100644 --- a/tests/functional/m/method_cache_max_size_none.txt +++ b/tests/functional/m/method_cache_max_size_none.txt @@ -1,7 +1,7 @@ -method-cache-max-size-none:25:5:25:20:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE -method-cache-max-size-none:29:5:29:30:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE -method-cache-max-size-none:33:5:33:38:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE -method-cache-max-size-none:37:5:37:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE -method-cache-max-size-none:42:5:42:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +method-cache-max-size-none:26:5:26:20:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +method-cache-max-size-none:30:5:30:30:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +method-cache-max-size-none:34:5:34:38:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +method-cache-max-size-none:38:5:38:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE method-cache-max-size-none:43:5:43:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE -method-cache-max-size-none:73:5:73:40:MyClassWithMethodsAndMaxSize.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +method-cache-max-size-none:44:5:44:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +method-cache-max-size-none:74:5:74:40:MyClassWithMethodsAndMaxSize.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE diff --git a/tests/functional/m/method_hidden.py b/tests/functional/m/method_hidden.py index e31bc16c4f..ea94b3ca26 100644 --- a/tests/functional/m/method_hidden.py +++ b/tests/functional/m/method_hidden.py @@ -1,11 +1,10 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance,missing-docstring +# pylint: disable=too-few-public-methods,missing-docstring # pylint: disable=unused-private-member """check method hiding ancestor attribute """ -from __future__ import print_function -class Abcd(object): +class Abcd: """dummy""" def __init__(self): diff --git a/tests/functional/m/method_hidden.txt b/tests/functional/m/method_hidden.txt index 23651bd4a0..abce3cd407 100644 --- a/tests/functional/m/method_hidden.txt +++ b/tests/functional/m/method_hidden.txt @@ -1,3 +1,3 @@ -method-hidden:18:4:18:12:Cdef.abcd:An attribute defined in functional.m.method_hidden line 12 hides this method:UNDEFINED -method-hidden:86:4:86:11:One.one:An attribute defined in functional.m.method_hidden line 84 hides this method:UNDEFINED -method-hidden:113:4:113:18:Child._protected:An attribute defined in functional.m.method_hidden line 109 hides this method:UNDEFINED +method-hidden:17:4:17:12:Cdef.abcd:An attribute defined in functional.m.method_hidden line 11 hides this method:UNDEFINED +method-hidden:85:4:85:11:One.one:An attribute defined in functional.m.method_hidden line 83 hides this method:UNDEFINED +method-hidden:112:4:112:18:Child._protected:An attribute defined in functional.m.method_hidden line 108 hides this method:UNDEFINED diff --git a/tests/functional/m/misplaced_bare_raise.py b/tests/functional/m/misplaced_bare_raise.py index 2170e5f36b..b1b399da3a 100644 --- a/tests/functional/m/misplaced_bare_raise.py +++ b/tests/functional/m/misplaced_bare_raise.py @@ -1,5 +1,5 @@ # pylint: disable=missing-docstring, broad-except, unreachable, try-except-raise, raise-missing-from -# pylint: disable=unused-variable, too-few-public-methods, invalid-name, useless-object-inheritance +# pylint: disable=unused-variable, too-few-public-methods, invalid-name # pylint: disable=comparison-of-constants try: @@ -49,7 +49,7 @@ def best(): raise # [misplaced-bare-raise] -class A(object): +class A: try: pass except Exception: @@ -68,7 +68,7 @@ class A(object): raise # [misplaced-bare-raise] # Don't emit if we're in ``__exit__``. -class ContextManager(object): +class ContextManager: def __enter__(self): return self def __exit__(self, *args): diff --git a/tests/functional/m/misplaced_bare_raise.txt b/tests/functional/m/misplaced_bare_raise.txt index d26893e3fb..dbb20c266a 100644 --- a/tests/functional/m/misplaced_bare_raise.txt +++ b/tests/functional/m/misplaced_bare_raise.txt @@ -1,7 +1,7 @@ -misplaced-bare-raise:6:4:6:9::The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:36:16:36:21:test1.best:The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:39:4:39:9:test1:The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:40:0:40:5::The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:49:4:49:9::The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:57:4:57:9:A:The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:68:4:68:9::The raise statement is not inside an except clause:UNDEFINED +misplaced-bare-raise:6:4:6:9::The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:36:16:36:21:test1.best:The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:39:4:39:9:test1:The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:40:0:40:5::The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:49:4:49:9::The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:57:4:57:9:A:The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:68:4:68:9::The raise statement is not inside an except clause:HIGH diff --git a/tests/functional/m/missing/missing_docstring.py b/tests/functional/m/missing/missing_docstring.py index 72d6762aa2..3b7a679df9 100644 --- a/tests/functional/m/missing/missing_docstring.py +++ b/tests/functional/m/missing/missing_docstring.py @@ -1,5 +1,5 @@ # [missing-module-docstring] -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods def public_documented(): """It has a docstring.""" @@ -14,11 +14,11 @@ def _private_documented(): """It has a docstring.""" -class ClassDocumented(object): +class ClassDocumented: """It has a docstring.""" -class ClassUndocumented(object): # [missing-class-docstring] +class ClassUndocumented: # [missing-class-docstring] pass @@ -35,7 +35,7 @@ def __mangled(): pass -class Property(object): +class Property: """Don't warn about setters and deleters.""" def __init__(self): @@ -54,5 +54,5 @@ def test(self): pass -class DocumentedViaDunderDoc(object): +class DocumentedViaDunderDoc: __doc__ = "This one" diff --git a/tests/functional/m/missing/missing_kwoa.py b/tests/functional/m/missing/missing_kwoa.py index b4dc604cd3..15df710bfd 100644 --- a/tests/functional/m/missing/missing_kwoa.py +++ b/tests/functional/m/missing/missing_kwoa.py @@ -1,7 +1,7 @@ # pylint: disable=missing-docstring,unused-argument,too-few-public-methods +import contextlib import typing - def target(pos, *, keyword): return pos + keyword @@ -71,3 +71,22 @@ def __init__( second): super().__init__(first=first, second=second) self._first = first + second + + +@contextlib.contextmanager +def run(*, a): + yield + +def test_context_managers(**kw): + run(**kw) + + with run(**kw): + pass + + with run(**kw), run(**kw): + pass + + 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 c752340b99..fc1694ed20 100644 --- a/tests/functional/m/missing/missing_kwoa.txt +++ b/tests/functional/m/missing/missing_kwoa.txt @@ -1,3 +1,4 @@ -missing-kwoa:21:4:21:17:not_forwarding_kwargs:Missing mandatory keyword argument 'keyword' in function call:UNDEFINED -missing-kwoa:27:0:27:16::Missing mandatory keyword argument 'keyword' in function call:UNDEFINED +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 diff --git a/tests/functional/m/missing/missing_parentheses_for_call_in_test.py b/tests/functional/m/missing/missing_parentheses_for_call_in_test.py index aa1f8eefdf..36c515ed93 100644 --- a/tests/functional/m/missing/missing_parentheses_for_call_in_test.py +++ b/tests/functional/m/missing/missing_parentheses_for_call_in_test.py @@ -1,5 +1,5 @@ """Verify if call to function or method inside tests are missing parentheses.""" -# pylint: disable=using-constant-test, missing-docstring, useless-object-inheritance +# pylint: disable=using-constant-test, missing-docstring # pylint: disable=invalid-name, expression-not-assigned, unnecessary-lambda-assignment import collections @@ -12,7 +12,7 @@ def nonbool_function(): return 42 -class Class(object): +class Class: @staticmethod def bool_method(): diff --git a/tests/functional/m/missing/missing_parentheses_for_call_in_test.txt b/tests/functional/m/missing/missing_parentheses_for_call_in_test.txt index f782a9704a..02566e43ce 100644 --- a/tests/functional/m/missing/missing_parentheses_for_call_in_test.txt +++ b/tests/functional/m/missing/missing_parentheses_for_call_in_test.txt @@ -1,15 +1,15 @@ -missing-parentheses-for-call-in-test:29:3:29:16::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:35:3:35:19::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:43:3:43:23::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:51:5:51:25::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:56:3:56:14::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:64:3:64:17::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:70:17:70:30::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:72:23:72:34::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:73:24:73:38::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:75:26:75:39::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:76:26:76:37::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:79:26:79:40::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:80:26:80:42::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:85:3:85:26::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED -missing-parentheses-for-call-in-test:91:3:91:20::Using a conditional statement with potentially wrong function or method call due to missing parentheses:UNDEFINED +missing-parentheses-for-call-in-test:29:3:29:16::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:35:3:35:19::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:43:3:43:23::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:51:5:51:25::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:56:3:56:14::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:64:3:64:17::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:70:17:70:30::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:72:23:72:34::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:73:24:73:38::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:75:26:75:39::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:76:26:76:37::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:79:26:79:40::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:80:26:80:42::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:85:3:85:26::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE +missing-parentheses-for-call-in-test:91:3:91:20::Using a conditional statement with potentially wrong function or method call due to missing parentheses:INFERENCE diff --git a/tests/functional/m/missing/missing_self_argument.py b/tests/functional/m/missing/missing_self_argument.py index 79ae34879e..e3d3015dd9 100644 --- a/tests/functional/m/missing/missing_self_argument.py +++ b/tests/functional/m/missing/missing_self_argument.py @@ -1,9 +1,8 @@ """Checks that missing self in method defs don't crash Pylint.""" -# pylint: disable=useless-object-inheritance -class MyClass(object): +class MyClass: """A class with some methods missing self args.""" def __init__(self): diff --git a/tests/functional/m/missing/missing_self_argument.txt b/tests/functional/m/missing/missing_self_argument.txt index 14a9a98633..2ed5b6e035 100644 --- a/tests/functional/m/missing/missing_self_argument.txt +++ b/tests/functional/m/missing/missing_self_argument.txt @@ -1,3 +1,3 @@ -no-method-argument:12:4:12:14:MyClass.method:Method has no argument:UNDEFINED -no-method-argument:15:4:15:13:MyClass.setup:Method has no argument:UNDEFINED -undefined-variable:17:8:17:12:MyClass.setup:Undefined variable 'self':UNDEFINED +no-method-argument:11:4:11:14:MyClass.method:Method 'method' has no argument:UNDEFINED +no-method-argument:14:4:14:13:MyClass.setup:Method 'setup' has no argument:UNDEFINED +undefined-variable:16:8:16:12:MyClass.setup:Undefined variable 'self':UNDEFINED diff --git a/tests/functional/m/missing/missing_timeout.py b/tests/functional/m/missing/missing_timeout.py new file mode 100644 index 0000000000..13ff35f862 --- /dev/null +++ b/tests/functional/m/missing/missing_timeout.py @@ -0,0 +1,85 @@ +"""Tests for missing-timeout.""" + +# pylint: disable=consider-using-with,import-error,no-member,no-name-in-module,reimported + +import requests +from requests import ( + delete, + delete as delete_r, + get, + get as get_r, + head, + head as head_r, + options, + options as options_r, + patch, + patch as patch_r, + post, + post as post_r, + put, + put as put_r, + request, + request as request_r, +) + +# requests without timeout +requests.delete("http://localhost") # [missing-timeout] +requests.get("http://localhost") # [missing-timeout] +requests.head("http://localhost") # [missing-timeout] +requests.options("http://localhost") # [missing-timeout] +requests.patch("http://localhost") # [missing-timeout] +requests.post("http://localhost") # [missing-timeout] +requests.put("http://localhost") # [missing-timeout] +requests.request("call", "http://localhost") # [missing-timeout] + +delete_r("http://localhost") # [missing-timeout] +get_r("http://localhost") # [missing-timeout] +head_r("http://localhost") # [missing-timeout] +options_r("http://localhost") # [missing-timeout] +patch_r("http://localhost") # [missing-timeout] +post_r("http://localhost") # [missing-timeout] +put_r("http://localhost") # [missing-timeout] +request_r("call", "http://localhost") # [missing-timeout] + +delete("http://localhost") # [missing-timeout] +get("http://localhost") # [missing-timeout] +head("http://localhost") # [missing-timeout] +options("http://localhost") # [missing-timeout] +patch("http://localhost") # [missing-timeout] +post("http://localhost") # [missing-timeout] +put("http://localhost") # [missing-timeout] +request("call", "http://localhost") # [missing-timeout] + +kwargs_wo_timeout = {} +post("http://localhost", **kwargs_wo_timeout) # [missing-timeout] + +# requests valid cases +requests.delete("http://localhost", timeout=10) +requests.get("http://localhost", timeout=10) +requests.head("http://localhost", timeout=10) +requests.options("http://localhost", timeout=10) +requests.patch("http://localhost", timeout=10) +requests.post("http://localhost", timeout=10) +requests.put("http://localhost", timeout=10) +requests.request("call", "http://localhost", timeout=10) + +delete_r("http://localhost", timeout=10) +get_r("http://localhost", timeout=10) +head_r("http://localhost", timeout=10) +options_r("http://localhost", timeout=10) +patch_r("http://localhost", timeout=10) +post_r("http://localhost", timeout=10) +put_r("http://localhost", timeout=10) +request_r("call", "http://localhost", timeout=10) + +delete("http://localhost", timeout=10) +get("http://localhost", timeout=10) +head("http://localhost", timeout=10) +options("http://localhost", timeout=10) +patch("http://localhost", timeout=10) +post("http://localhost", timeout=10) +put("http://localhost", timeout=10) +request("call", "http://localhost", timeout=10) + +kwargs_timeout = {'timeout': 10} +post("http://localhost", **kwargs_timeout) diff --git a/tests/functional/m/missing/missing_timeout.txt b/tests/functional/m/missing/missing_timeout.txt new file mode 100644 index 0000000000..f3c633c2a1 --- /dev/null +++ b/tests/functional/m/missing/missing_timeout.txt @@ -0,0 +1,25 @@ +missing-timeout:26:0:26:35::Missing timeout argument for method 'requests.delete' can cause your program to hang indefinitely:INFERENCE +missing-timeout:27:0:27:32::Missing timeout argument for method 'requests.get' can cause your program to hang indefinitely:INFERENCE +missing-timeout:28:0:28:33::Missing timeout argument for method 'requests.head' can cause your program to hang indefinitely:INFERENCE +missing-timeout:29:0:29:36::Missing timeout argument for method 'requests.options' can cause your program to hang indefinitely:INFERENCE +missing-timeout:30:0:30:34::Missing timeout argument for method 'requests.patch' can cause your program to hang indefinitely:INFERENCE +missing-timeout:31:0:31:33::Missing timeout argument for method 'requests.post' can cause your program to hang indefinitely:INFERENCE +missing-timeout:32:0:32:32::Missing timeout argument for method 'requests.put' can cause your program to hang indefinitely:INFERENCE +missing-timeout:33:0:33:44::Missing timeout argument for method 'requests.request' can cause your program to hang indefinitely:INFERENCE +missing-timeout:35:0:35:28::Missing timeout argument for method 'delete_r' can cause your program to hang indefinitely:INFERENCE +missing-timeout:36:0:36:25::Missing timeout argument for method 'get_r' can cause your program to hang indefinitely:INFERENCE +missing-timeout:37:0:37:26::Missing timeout argument for method 'head_r' can cause your program to hang indefinitely:INFERENCE +missing-timeout:38:0:38:29::Missing timeout argument for method 'options_r' can cause your program to hang indefinitely:INFERENCE +missing-timeout:39:0:39:27::Missing timeout argument for method 'patch_r' can cause your program to hang indefinitely:INFERENCE +missing-timeout:40:0:40:26::Missing timeout argument for method 'post_r' can cause your program to hang indefinitely:INFERENCE +missing-timeout:41:0:41:25::Missing timeout argument for method 'put_r' can cause your program to hang indefinitely:INFERENCE +missing-timeout:42:0:42:37::Missing timeout argument for method 'request_r' can cause your program to hang indefinitely:INFERENCE +missing-timeout:44:0:44:26::Missing timeout argument for method 'delete' can cause your program to hang indefinitely:INFERENCE +missing-timeout:45:0:45:23::Missing timeout argument for method 'get' can cause your program to hang indefinitely:INFERENCE +missing-timeout:46:0:46:24::Missing timeout argument for method 'head' can cause your program to hang indefinitely:INFERENCE +missing-timeout:47:0:47:27::Missing timeout argument for method 'options' can cause your program to hang indefinitely:INFERENCE +missing-timeout:48:0:48:25::Missing timeout argument for method 'patch' can cause your program to hang indefinitely:INFERENCE +missing-timeout:49:0:49:24::Missing timeout argument for method 'post' can cause your program to hang indefinitely:INFERENCE +missing-timeout:50:0:50:23::Missing timeout argument for method 'put' can cause your program to hang indefinitely:INFERENCE +missing-timeout:51:0:51:35::Missing timeout argument for method 'request' can cause your program to hang indefinitely:INFERENCE +missing-timeout:54:0:54:45::Missing timeout argument for method 'post' can cause your program to hang indefinitely:INFERENCE diff --git a/tests/functional/m/modified_iterating.py b/tests/functional/m/modified_iterating.py index e99ac8d306..f17c34b6ee 100644 --- a/tests/functional/m/modified_iterating.py +++ b/tests/functional/m/modified_iterating.py @@ -1,7 +1,8 @@ """Tests for iterating-modified messages""" -# pylint: disable=not-callable,unnecessary-comprehension +# pylint: disable=not-callable,unnecessary-comprehension,too-few-public-methods,missing-class-docstring,missing-function-docstring import copy +from enum import Enum item_list = [1, 2, 3] for item in item_list: @@ -26,7 +27,7 @@ i = 1 for item in my_dict: item_list[0] = i # for coverage, see reference at /pull/5628#discussion_r792181642 - my_dict[i] = 1 # [modified-iterating-dict] + my_dict[i] = 1 # [modified-iterating-dict] i += 1 i = 1 @@ -47,6 +48,18 @@ item_set.remove(4) # [modified-iterating-set] item_list.remove(1) # [modified-iterating-list] +for item in [1, 2, 3]: + del item # [modified-iterating-list] + +for inner_first, inner_second in [[1, 2], [1, 2]]: + del inner_second # [modified-iterating-list] + +for k in my_dict: + del k # [modified-iterating-dict] + +for element in item_set: + del element # [modified-iterating-set] + # Check for nested for loops and changes to iterators for l in item_list: item_list.append(1) # [modified-iterating-list] @@ -81,3 +94,39 @@ def update_existing_key(): for key in my_dict: new_key = key.lower() my_dict[new_key] = 1 # [modified-iterating-dict] + + +class MyClass: + """Regression test for https://github.com/PyCQA/pylint/issues/7380""" + + def __init__(self) -> None: + self.attribute = [1, 2, 3] + + def my_method(self): + """This should raise as we are deleting.""" + for var in self.attribute: + del var # [modified-iterating-list] + + +class MyClass2: + """Regression test for https://github.com/PyCQA/pylint/issues/7461""" + def __init__(self) -> None: + self.attribute = {} + + def my_method(self): + """This should not raise, as a copy was made.""" + for key in self.attribute: + tmp = self.attribute.copy() + tmp[key] = None + +class MyEnum(Enum): + FOO = 1 + BAR = 2 + +class EnumClass: + ENUM_SET = {MyEnum.FOO, MyEnum.BAR} + + def useless(self): + other_set = set(self.ENUM_SET) + for obj in self.ENUM_SET: + other_set.remove(obj) diff --git a/tests/functional/m/modified_iterating.txt b/tests/functional/m/modified_iterating.txt index 557d933452..e5b57ca328 100644 --- a/tests/functional/m/modified_iterating.txt +++ b/tests/functional/m/modified_iterating.txt @@ -1,11 +1,16 @@ -modified-iterating-list:8:4:8:26::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE -modified-iterating-list:11:4:11:26::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE -modified-iterating-dict:29:4:29:18::Iterated dict 'my_dict' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE -modified-iterating-set:39:4:39:27::Iterated set 'item_set' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE -modified-iterating-list:46:8:46:27::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE -modified-iterating-set:47:8:47:26::Iterated set 'item_set' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE -modified-iterating-list:48:4:48:23::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE -modified-iterating-list:52:4:52:23::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE -modified-iterating-list:55:12:55:31::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE -modified-iterating-list:57:16:57:35::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE -modified-iterating-dict:83:8:83:28:update_existing_key:Iterated dict 'my_dict' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE +modified-iterating-list:9:4:9:26::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-list:12:4:12:26::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-dict:30:4:30:18::Iterated dict 'my_dict' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE +modified-iterating-set:40:4:40:27::Iterated set 'item_set' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE +modified-iterating-list:47:8:47:27::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-set:48:8:48:26::Iterated set 'item_set' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE +modified-iterating-list:49:4:49:23::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-list:52:4:52:12::Iterated list 'list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-list:55:4:55:20::Iterated list 'list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-dict:58:4:58:9::Iterated dict 'my_dict' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE +modified-iterating-set:61:4:61:15::Iterated set 'item_set' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE +modified-iterating-list:65:4:65:23::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-list:68:12:68:31::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-list:70:16:70:35::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE +modified-iterating-dict:96:8:96:28:update_existing_key:Iterated dict 'my_dict' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE +modified-iterating-list:108:12:108:19:MyClass.my_method:Iterated list 'attribute' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE diff --git a/tests/functional/m/module___dict__.py b/tests/functional/m/module___dict__.py index 8b2154c952..2b8aa041fc 100644 --- a/tests/functional/m/module___dict__.py +++ b/tests/functional/m/module___dict__.py @@ -1,6 +1,5 @@ """https://www.logilab.org/ticket/6949.""" -from __future__ import print_function -__revision__ = None + print(__dict__ is not None) # [used-before-assignment] diff --git a/tests/functional/m/module___dict__.txt b/tests/functional/m/module___dict__.txt index a62def231a..aacbf9c30d 100644 --- a/tests/functional/m/module___dict__.txt +++ b/tests/functional/m/module___dict__.txt @@ -1 +1 @@ -used-before-assignment:5:6:5:14::Using variable '__dict__' before assignment:HIGH +used-before-assignment:4:6:4:14::Using variable '__dict__' before assignment:HIGH diff --git a/tests/functional/m/monkeypatch_method.py b/tests/functional/m/monkeypatch_method.py index e8bd03aa7e..ab6d1c97f9 100644 --- a/tests/functional/m/monkeypatch_method.py +++ b/tests/functional/m/monkeypatch_method.py @@ -1,7 +1,7 @@ -# pylint: disable=missing-docstring,too-few-public-methods, useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods '''Test that a function is considered a method when looked up through a class.''' -class Clazz(object): +class Clazz: 'test class' def __init__(self, value): diff --git a/tests/functional/m/multiple_statements.py b/tests/functional/m/multiple_statements.py index 5b55eac424..c3252f797c 100644 --- a/tests/functional/m/multiple_statements.py +++ b/tests/functional/m/multiple_statements.py @@ -27,4 +27,4 @@ class MyError(Exception): a='a'; b='b' # [multiple-statements] @overload def concat2(arg1: str) -> str: ... -def concat2(arg1: str) -> str: ... # [multiple-statements] +def concat2(arg1: str) -> str: ... diff --git a/tests/functional/m/multiple_statements.txt b/tests/functional/m/multiple_statements.txt index 34d80508ed..661314268d 100644 --- a/tests/functional/m/multiple_statements.txt +++ b/tests/functional/m/multiple_statements.txt @@ -3,4 +3,3 @@ multiple-statements:9:9:9:13::More than one statement on a single line:UNDEFINED multiple-statements:13:26:13:30:MyError:More than one statement on a single line:UNDEFINED multiple-statements:15:26:15:31:MyError:More than one statement on a single line:UNDEFINED multiple-statements:17:26:17:31:MyError:More than one statement on a single line:UNDEFINED -multiple-statements:30:31:30:34:concat2:More than one statement on a single line:UNDEFINED diff --git a/tests/functional/m/multiple_statements_single_line.py b/tests/functional/m/multiple_statements_single_line.py index 4a77d992ea..93a470702c 100644 --- a/tests/functional/m/multiple_statements_single_line.py +++ b/tests/functional/m/multiple_statements_single_line.py @@ -27,4 +27,4 @@ class MyError(Exception): a='a'; b='b' # [multiple-statements] @overload def concat2(arg1: str) -> str: ... -def concat2(arg1: str) -> str: ... # [multiple-statements] +def concat2(arg1: str) -> str: ... diff --git a/tests/functional/m/multiple_statements_single_line.txt b/tests/functional/m/multiple_statements_single_line.txt index a28fc96c4d..cac2f7eb2e 100644 --- a/tests/functional/m/multiple_statements_single_line.txt +++ b/tests/functional/m/multiple_statements_single_line.txt @@ -1,3 +1,2 @@ multiple-statements:9:9:9:13::More than one statement on a single line:UNDEFINED multiple-statements:17:26:17:31:MyError:More than one statement on a single line:UNDEFINED -multiple-statements:30:31:30:34:concat2:More than one statement on a single line:UNDEFINED diff --git a/tests/functional/n/name/name_good_bad_names_regex.txt b/tests/functional/n/name/name_good_bad_names_regex.txt index 66c1c99f15..35f4c2592b 100644 --- a/tests/functional/n/name/name_good_bad_names_regex.txt +++ b/tests/functional/n/name/name_good_bad_names_regex.txt @@ -1,3 +1,3 @@ -disallowed-name:5:0:5:26::"Disallowed name ""explicit_bad_some_constant""":UNDEFINED +disallowed-name:5:0:5:26::"Disallowed name ""explicit_bad_some_constant""":HIGH invalid-name:7:0:7:28::"Constant name ""snake_case_bad_SOME_CONSTANT"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]*|__.*__)$' pattern)":HIGH -disallowed-name:19:0:19:27:disallowed_2_snake_case:"Disallowed name ""disallowed_2_snake_case""":UNDEFINED +disallowed-name:19:0:19:27:disallowed_2_snake_case:"Disallowed name ""disallowed_2_snake_case""":HIGH diff --git a/tests/functional/n/name/name_styles.py b/tests/functional/n/name/name_styles.py index 86f395b524..47ad26e797 100644 --- a/tests/functional/n/name/name_styles.py +++ b/tests/functional/n/name/name_styles.py @@ -1,6 +1,6 @@ """Test for the invalid-name warning.""" -# pylint: disable=useless-object-inheritance, unnecessary-pass, unnecessary-comprehension, unused-private-member, unnecessary-lambda-assignment -from __future__ import print_function +# pylint: disable=unnecessary-pass, unnecessary-comprehension, unused-private-member +# pylint: disable=unnecessary-lambda-assignment import abc import collections import typing @@ -27,11 +27,11 @@ def no_nested_args(arg1, arg21, arg22): print(arg1, arg21, arg22) -class bad_class_name(object): # [invalid-name] +class bad_class_name: # [invalid-name] """Class with a bad name.""" -class CorrectClassName(object): +class CorrectClassName: """Class with a good name.""" def __init__(self): @@ -74,7 +74,7 @@ def BadMethodName(self): def class_builder(): """Function returning a class object.""" - class EmbeddedClass(object): + class EmbeddedClass: """Useless class.""" return EmbeddedClass @@ -94,12 +94,12 @@ class EmbeddedClass(object): def test_globals(): """Names in global statements are also checked.""" global NOT_CORRECT - global AlsoCorrect # [invalid-name] + global AlsoCorrect NOT_CORRECT = 1 AlsoCorrect = 2 -class FooClass(object): +class FooClass: """A test case for property names. Since by default, the regex for attributes is the same as the one diff --git a/tests/functional/n/name/name_styles.txt b/tests/functional/n/name/name_styles.txt index ad7dad05fb..720efd3820 100644 --- a/tests/functional/n/name/name_styles.txt +++ b/tests/functional/n/name/name_styles.txt @@ -10,7 +10,6 @@ invalid-name:53:4:53:38:CorrectClassName.__DunDER_IS_not_free_for_all__:"Method invalid-name:83:0:83:18::"Class name ""BAD_NAME_FOR_CLASS"" doesn't conform to PascalCase naming style":HIGH invalid-name:84:0:84:23::"Class name ""NEXT_BAD_NAME_FOR_CLASS"" doesn't conform to PascalCase naming style":HIGH invalid-name:91:0:91:11::"Class name ""NOT_CORRECT"" doesn't conform to PascalCase naming style":HIGH -invalid-name:97:4:97:22:test_globals:"Constant name ""AlsoCorrect"" doesn't conform to UPPER_CASE naming style":HIGH invalid-name:110:4:110:21:FooClass.PROPERTY_NAME:"Attribute name ""PROPERTY_NAME"" doesn't conform to snake_case naming style":INFERENCE invalid-name:116:4:116:30:FooClass.ABSTRACT_PROPERTY_NAME:"Attribute name ""ABSTRACT_PROPERTY_NAME"" doesn't conform to snake_case naming style":INFERENCE invalid-name:121:4:121:28:FooClass.PROPERTY_NAME_SETTER:"Attribute name ""PROPERTY_NAME_SETTER"" doesn't conform to snake_case naming style":INFERENCE diff --git a/tests/functional/n/named_expr_without_context_py38.py b/tests/functional/n/named_expr_without_context_py38.py new file mode 100644 index 0000000000..ee45b84b35 --- /dev/null +++ b/tests/functional/n/named_expr_without_context_py38.py @@ -0,0 +1,6 @@ +# pylint: disable=missing-docstring + +if (a := 2): + pass + +(b := 1) # [named-expr-without-context] diff --git a/tests/functional/u/useless/useless_super_delegation_py38.rc b/tests/functional/n/named_expr_without_context_py38.rc similarity index 100% rename from tests/functional/u/useless/useless_super_delegation_py38.rc rename to tests/functional/n/named_expr_without_context_py38.rc diff --git a/tests/functional/n/named_expr_without_context_py38.txt b/tests/functional/n/named_expr_without_context_py38.txt new file mode 100644 index 0000000000..2ab9a2733c --- /dev/null +++ b/tests/functional/n/named_expr_without_context_py38.txt @@ -0,0 +1 @@ +named-expr-without-context:6:0:6:8::Named expression used without context:HIGH diff --git a/tests/functional/n/namedtuple_member_inference.py b/tests/functional/n/namedtuple_member_inference.py index 4488ceb0ac..09dc6dd445 100644 --- a/tests/functional/n/namedtuple_member_inference.py +++ b/tests/functional/n/namedtuple_member_inference.py @@ -3,10 +3,8 @@ Regression test for: https://bitbucket.org/logilab/pylint/issue/93/pylint-crashes-on-namedtuple-attribute """ -from __future__ import absolute_import, print_function from collections import namedtuple -__revision__ = None Thing = namedtuple('Thing', ()) diff --git a/tests/functional/n/namedtuple_member_inference.txt b/tests/functional/n/namedtuple_member_inference.txt index c4e7783591..9156eeeb7c 100644 --- a/tests/functional/n/namedtuple_member_inference.txt +++ b/tests/functional/n/namedtuple_member_inference.txt @@ -1 +1 @@ -no-member:17:10:17:17:test:Class 'Thing' has no 'x' member:INFERENCE +no-member:15:10:15:17:test:Class 'Thing' has no 'x' member:INFERENCE diff --git a/tests/functional/n/names_in__all__.py b/tests/functional/n/names_in__all__.py index 52c44f7faa..38ed18a2e7 100644 --- a/tests/functional/n/names_in__all__.py +++ b/tests/functional/n/names_in__all__.py @@ -1,4 +1,4 @@ -# pylint: disable=too-few-public-methods, import-error, useless-object-inheritance, unnecessary-pass +# pylint: disable=too-few-public-methods, import-error, unnecessary-pass """Test Pylint's use of __all__. * NonExistant is not defined in this module, and it is listed in @@ -7,7 +7,6 @@ * This module imports path and republished it in __all__. No errors are expected. """ -from __future__ import print_function from os import path from collections import deque from missing import Missing @@ -24,7 +23,7 @@ 'InnerKlass', deque.__name__] # [undefined-all-variable] -class Dummy(object): +class Dummy: """A class defined in this module.""" pass @@ -37,13 +36,13 @@ def function(): function() -class Klass(object): +class Klass: """A klass which contains a function""" def func(self): """A klass method""" inner = None print(inner) - class InnerKlass(object): + class InnerKlass: """An inner klass""" pass diff --git a/tests/functional/n/names_in__all__.txt b/tests/functional/n/names_in__all__.txt index 72a11c50ba..720942df3d 100644 --- a/tests/functional/n/names_in__all__.txt +++ b/tests/functional/n/names_in__all__.txt @@ -1,6 +1,6 @@ -undefined-all-variable:17:4:17:6::Undefined variable name '' in __all__:UNDEFINED -undefined-variable:19:4:19:17::Undefined variable 'SomeUndefined':UNDEFINED -undefined-all-variable:20:4:20:17::Undefined variable name 'NonExistant' in __all__:UNDEFINED -undefined-all-variable:22:4:22:10::Undefined variable name 'func' in __all__:UNDEFINED -undefined-all-variable:23:4:23:11::Undefined variable name 'inner' in __all__:UNDEFINED -undefined-all-variable:24:4:24:16::Undefined variable name 'InnerKlass' in __all__:UNDEFINED +undefined-all-variable:16:4:16:6::Undefined variable name '' in __all__:UNDEFINED +undefined-variable:18:4:18:17::Undefined variable 'SomeUndefined':UNDEFINED +undefined-all-variable:19:4:19:17::Undefined variable name 'NonExistant' in __all__:UNDEFINED +undefined-all-variable:21:4:21:10::Undefined variable name 'func' in __all__:UNDEFINED +undefined-all-variable:22:4:22:11::Undefined variable name 'inner' in __all__:UNDEFINED +undefined-all-variable:23:4:23:16::Undefined variable name 'InnerKlass' in __all__:UNDEFINED diff --git a/tests/functional/n/nested_min_max.py b/tests/functional/n/nested_min_max.py new file mode 100644 index 0000000000..151e035dd1 --- /dev/null +++ b/tests/functional/n/nested_min_max.py @@ -0,0 +1,44 @@ +"""Test detection of redundant nested calls to min/max functions""" + +# pylint: disable=redefined-builtin,unnecessary-lambda-assignment + +min(1, min(2, 3)) # [nested-min-max] +max(1, max(2, 3)) # [nested-min-max] +min(min(1, 2), 3) # [nested-min-max] +min(min(min(1, 2), 3), 4) # [nested-min-max, nested-min-max] +min(1, max(2, 3)) +min(1, 2, 3) +min(min(1, 2), min(3, 4)) # [nested-min-max] +min(len([]), min(len([1]), len([1, 2]))) # [nested-min-max] + +orig_min = min +min = lambda *args: args[0] +min(1, min(2, 3)) +orig_min(1, orig_min(2, 3)) # [nested-min-max] + +# This is too complicated (for now) as there is no clear better way to write it +max(max(i for i in range(10)), 0) +max(max(max(i for i in range(10)), 0), 1) + +# These examples can be improved by splicing +lst = [1, 2] +max(3, max(lst)) # [nested-min-max] +max(3, *lst) + +nums = (1, 2,) +max(3, max(nums)) # [nested-min-max] +max(3, *nums) + +nums = {1, 2} +max(3, max(nums)) # [nested-min-max] +max(3, *nums) + +nums = {1: 2, 7: 10} +max(3, max(nums)) # [nested-min-max] +max(3, *nums) + +max(3, max(nums.values())) # [nested-min-max] +max(3, *nums.values()) + +lst2 = [3, 7, 10] +max(3, max(nums), max(lst2)) # [nested-min-max] diff --git a/tests/functional/n/nested_min_max.txt b/tests/functional/n/nested_min_max.txt new file mode 100644 index 0000000000..c03f1b500c --- /dev/null +++ b/tests/functional/n/nested_min_max.txt @@ -0,0 +1,14 @@ +nested-min-max:5:0:5:17::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE +nested-min-max:6:0:6:17::Do not use nested call of 'max'; it's possible to do 'max(1, 2, 3)' instead:INFERENCE +nested-min-max:7:0:7:17::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE +nested-min-max:8:4:8:21::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE +nested-min-max:8:0:8:25::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3, 4)' instead:INFERENCE +nested-min-max:11:0:11:25::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3, 4)' instead:INFERENCE +nested-min-max:12:0:12:40::Do not use nested call of 'min'; it's possible to do 'min(len([]), len([1]), len([1, 2]))' instead:INFERENCE +nested-min-max:17:0:17:27::Do not use nested call of 'orig_min'; it's possible to do 'orig_min(1, 2, 3)' instead:INFERENCE +nested-min-max:25:0:25:16::Do not use nested call of 'max'; it's possible to do 'max(3, *lst)' instead:INFERENCE +nested-min-max:29:0:29:17::Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead:INFERENCE +nested-min-max:33:0:33:17::Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead:INFERENCE +nested-min-max:37:0:37:17::Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead:INFERENCE +nested-min-max:40:0:40:26::Do not use nested call of 'max'; it's possible to do 'max(3, *nums.values())' instead:INFERENCE +nested-min-max:44:0:44:28::Do not use nested call of 'max'; it's possible to do 'max(3, *nums, *lst2)' instead:INFERENCE diff --git a/tests/functional/n/new_style_class_py_30.py b/tests/functional/n/new_style_class_py_30.py index b70c6e0976..888a6c4e48 100644 --- a/tests/functional/n/new_style_class_py_30.py +++ b/tests/functional/n/new_style_class_py_30.py @@ -2,8 +2,6 @@ bug notified by Pierre Rouleau on 2005-04-24 """ -from __future__ import print_function -__revision__ = None class File(file): # pylint: disable=undefined-variable diff --git a/tests/functional/n/new_style_class_py_30.txt b/tests/functional/n/new_style_class_py_30.txt index ad22100f36..cb65bc04d3 100644 --- a/tests/functional/n/new_style_class_py_30.txt +++ b/tests/functional/n/new_style_class_py_30.txt @@ -1,4 +1,4 @@ -super-with-arguments:15:8:15:25:File.__init__:Consider using Python 3 style super() without arguments:UNDEFINED -super-with-arguments:21:8:21:25:File.write:Consider using Python 3 style super() without arguments:UNDEFINED -super-with-arguments:26:8:26:25:File.writelines:Consider using Python 3 style super() without arguments:UNDEFINED -super-with-arguments:33:8:33:25:File.close:Consider using Python 3 style super() without arguments:UNDEFINED +super-with-arguments:13:8:13:25:File.__init__:Consider using Python 3 style super() without arguments:UNDEFINED +super-with-arguments:19:8:19:25:File.write:Consider using Python 3 style super() without arguments:UNDEFINED +super-with-arguments:24:8:24:25:File.writelines:Consider using Python 3 style super() without arguments:UNDEFINED +super-with-arguments:31:8:31:25:File.close:Consider using Python 3 style super() without arguments:UNDEFINED diff --git a/tests/functional/n/no/no_classmethod_decorator.py b/tests/functional/n/no/no_classmethod_decorator.py index 66cc0b3c0a..096faa7082 100644 --- a/tests/functional/n/no/no_classmethod_decorator.py +++ b/tests/functional/n/no/no_classmethod_decorator.py @@ -2,9 +2,9 @@ scope and if classmethod's argument is a member of the class """ -# pylint: disable=too-few-public-methods, using-constant-test, no-self-argument, useless-object-inheritance +# pylint: disable=too-few-public-methods, using-constant-test, no-self-argument -class MyClass(object): +class MyClass: """Some class""" def __init__(self): pass @@ -30,6 +30,6 @@ def helloworld(): MyClass.new_class_method = classmethod(helloworld) -class MyOtherClass(object): +class MyOtherClass: """Some other class""" _make = classmethod(tuple.__new__) diff --git a/tests/functional/n/no/no_dummy_redefined.py b/tests/functional/n/no/no_dummy_redefined.py index c6b093dad5..5a03ed5e46 100644 --- a/tests/functional/n/no/no_dummy_redefined.py +++ b/tests/functional/n/no/no_dummy_redefined.py @@ -1,5 +1,4 @@ """Make sure warnings about redefinitions do not trigger for dummy variables.""" -from __future__ import print_function _, INTERESTING = 'a=b'.split('=') diff --git a/tests/functional/n/no/no_dummy_redefined.txt b/tests/functional/n/no/no_dummy_redefined.txt index 1cf7c979b0..467116c160 100644 --- a/tests/functional/n/no/no_dummy_redefined.txt +++ b/tests/functional/n/no/no_dummy_redefined.txt @@ -1,2 +1,2 @@ -invalid-name:7:0:7:5::"Constant name ""value"" doesn't conform to UPPER_CASE naming style":HIGH -redefined-outer-name:12:4:12:9:clobbering:Redefining name 'value' from outer scope (line 7):UNDEFINED +invalid-name:6:0:6:5::"Constant name ""value"" doesn't conform to UPPER_CASE naming style":HIGH +redefined-outer-name:11:4:11:9:clobbering:Redefining name 'value' from outer scope (line 6):UNDEFINED diff --git a/tests/functional/n/no/no_else_break.txt b/tests/functional/n/no/no_else_break.txt index cf045f367b..1e5bee1eb2 100644 --- a/tests/functional/n/no/no_else_break.txt +++ b/tests/functional/n/no/no_else_break.txt @@ -1,7 +1,7 @@ -no-else-break:8:8:11:17:foo1:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-break:16:8:21:17:foo2:"Unnecessary ""elif"" after ""break"", remove the leading ""el"" from ""elif""":UNDEFINED -no-else-break:28:12:33:21:foo3:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-break:41:8:48:17:foo4:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-break:54:8:63:17:foo5:"Unnecessary ""elif"" after ""break"", remove the leading ""el"" from ""elif""":UNDEFINED -no-else-break:70:12:74:21:foo6:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-break:110:8:116:21:bar4:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":UNDEFINED +no-else-break:8:8:11:17:foo1:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-break:16:8:21:17:foo2:"Unnecessary ""elif"" after ""break"", remove the leading ""el"" from ""elif""":HIGH +no-else-break:28:12:33:21:foo3:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-break:41:8:48:17:foo4:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-break:54:8:63:17:foo5:"Unnecessary ""elif"" after ""break"", remove the leading ""el"" from ""elif""":HIGH +no-else-break:70:12:74:21:foo6:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-break:110:8:116:21:bar4:"Unnecessary ""else"" after ""break"", remove the ""else"" and de-indent the code inside it":HIGH diff --git a/tests/functional/n/no/no_else_continue.txt b/tests/functional/n/no/no_else_continue.txt index f0e813e405..7b0dde4e9b 100644 --- a/tests/functional/n/no/no_else_continue.txt +++ b/tests/functional/n/no/no_else_continue.txt @@ -1,7 +1,7 @@ -no-else-continue:8:8:11:17:foo1:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-continue:16:8:21:17:foo2:"Unnecessary ""elif"" after ""continue"", remove the leading ""el"" from ""elif""":UNDEFINED -no-else-continue:28:12:33:24:foo3:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-continue:41:8:48:17:foo4:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-continue:54:8:63:17:foo5:"Unnecessary ""elif"" after ""continue"", remove the leading ""el"" from ""elif""":UNDEFINED -no-else-continue:70:12:74:21:foo6:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-continue:110:8:116:24:bar4:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":UNDEFINED +no-else-continue:8:8:11:17:foo1:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-continue:16:8:21:17:foo2:"Unnecessary ""elif"" after ""continue"", remove the leading ""el"" from ""elif""":HIGH +no-else-continue:28:12:33:24:foo3:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-continue:41:8:48:17:foo4:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-continue:54:8:63:17:foo5:"Unnecessary ""elif"" after ""continue"", remove the leading ""el"" from ""elif""":HIGH +no-else-continue:70:12:74:21:foo6:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-continue:110:8:116:24:bar4:"Unnecessary ""else"" after ""continue"", remove the ""else"" and de-indent the code inside it":HIGH diff --git a/tests/functional/n/no/no_else_raise.py b/tests/functional/n/no/no_else_raise.py index 9a54dfc9f3..33a1fb5611 100644 --- a/tests/functional/n/no/no_else_raise.py +++ b/tests/functional/n/no/no_else_raise.py @@ -1,6 +1,6 @@ """ Test that superfluous else raise are detected. """ -# pylint:disable=invalid-name,missing-docstring,unused-variable,raise-missing-from +# pylint:disable=invalid-name,missing-docstring,unused-variable,raise-missing-from,broad-exception-raised def foo1(x, y, z): if x: # [no-else-raise] diff --git a/tests/functional/n/no/no_else_raise.txt b/tests/functional/n/no/no_else_raise.txt index 72f8f83766..9e22f9a43b 100644 --- a/tests/functional/n/no/no_else_raise.txt +++ b/tests/functional/n/no/no_else_raise.txt @@ -1,7 +1,7 @@ -no-else-raise:6:4:11:26:foo1:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-raise:15:4:23:26:foo2:"Unnecessary ""elif"" after ""raise"", remove the leading ""el"" from ""elif""":UNDEFINED -no-else-raise:29:8:34:30:foo3:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-raise:41:4:48:13:foo4:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-raise:53:4:62:13:foo5:"Unnecessary ""elif"" after ""raise"", remove the leading ""el"" from ""elif""":UNDEFINED -no-else-raise:68:8:72:17:foo6:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-raise:104:4:110:33:bar4:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":UNDEFINED +no-else-raise:6:4:11:26:foo1:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-raise:15:4:23:26:foo2:"Unnecessary ""elif"" after ""raise"", remove the leading ""el"" from ""elif""":HIGH +no-else-raise:29:8:34:30:foo3:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-raise:41:4:48:13:foo4:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-raise:53:4:62:13:foo5:"Unnecessary ""elif"" after ""raise"", remove the leading ""el"" from ""elif""":HIGH +no-else-raise:68:8:72:17:foo6:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-raise:104:4:110:33:bar4:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":HIGH diff --git a/tests/functional/n/no/no_else_return.py b/tests/functional/n/no/no_else_return.py index 0a87551faa..eb4b05d87b 100644 --- a/tests/functional/n/no/no_else_return.py +++ b/tests/functional/n/no/no_else_return.py @@ -108,3 +108,87 @@ def bar4(x): return False except ValueError: return None + +# pylint: disable = bare-except +def try_one_except() -> bool: + try: # [no-else-return] + print('asdf') + except: + print("bad") + return False + else: + return True + + +def try_multiple_except() -> bool: + try: # [no-else-return] + print('asdf') + except TypeError: + print("bad") + return False + except ValueError: + print("bad second") + return False + else: + return True + +def try_not_all_except_return() -> bool: # [inconsistent-return-statements] + try: + print('asdf') + except TypeError: + print("bad") + return False + except ValueError: + val = "something" + else: + return True + +# pylint: disable = raise-missing-from +def try_except_raises() -> bool: + try: # [no-else-raise] + print('asdf') + except: + raise ValueError + else: + return True + +def try_except_raises2() -> bool: + try: # [no-else-raise] + print('asdf') + except TypeError: + raise ValueError + except ValueError: + raise TypeError + else: + return True + +def test() -> bool: # [inconsistent-return-statements] + try: + print('asdf') + except RuntimeError: + return False + finally: + print("in finally") + + +def try_finally_return() -> bool: # [inconsistent-return-statements] + try: + print('asdf') + except RuntimeError: + return False + else: + print("inside else") + finally: + print("in finally") + +def try_finally_raise(): + current_tags = {} + try: + yield current_tags + except Exception: + current_tags["result"] = "failure" + raise + else: + current_tags["result"] = "success" + finally: + print("inside finally") diff --git a/tests/functional/n/no/no_else_return.txt b/tests/functional/n/no/no_else_return.txt index c73b177da5..a2c64f73c3 100644 --- a/tests/functional/n/no/no_else_return.txt +++ b/tests/functional/n/no/no_else_return.txt @@ -1,7 +1,14 @@ -no-else-return:6:4:11:16:foo1:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-return:15:4:23:16:foo2:"Unnecessary ""elif"" after ""return"", remove the leading ""el"" from ""elif""":UNDEFINED -no-else-return:29:8:34:20:foo3:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-return:41:4:48:13:foo4:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-return:53:4:62:13:foo5:"Unnecessary ""elif"" after ""return"", remove the leading ""el"" from ""elif""":UNDEFINED -no-else-return:68:8:72:17:foo6:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":UNDEFINED -no-else-return:104:4:110:23:bar4:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":UNDEFINED +no-else-return:6:4:11:16:foo1:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-return:15:4:23:16:foo2:"Unnecessary ""elif"" after ""return"", remove the leading ""el"" from ""elif""":HIGH +no-else-return:29:8:34:20:foo3:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-return:41:4:48:13:foo4:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-return:53:4:62:13:foo5:"Unnecessary ""elif"" after ""return"", remove the leading ""el"" from ""elif""":HIGH +no-else-return:68:8:72:17:foo6:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-return:104:4:110:23:bar4:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-return:114:4:120:19:try_one_except:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-return:124:4:133:19:try_multiple_except:"Unnecessary ""else"" after ""return"", remove the ""else"" and de-indent the code inside it":HIGH +inconsistent-return-statements:135:0:135:29:try_not_all_except_return:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED +no-else-raise:148:4:153:19:try_except_raises:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":HIGH +no-else-raise:156:4:163:19:try_except_raises2:"Unnecessary ""else"" after ""raise"", remove the ""else"" and de-indent the code inside it":HIGH +inconsistent-return-statements:165:0:165:8:test:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED +inconsistent-return-statements:174:0:174:22:try_finally_return:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED diff --git a/tests/functional/n/no/no_member.py b/tests/functional/n/no/no_member.py index 73b2227a63..1db70cde54 100644 --- a/tests/functional/n/no/no_member.py +++ b/tests/functional/n/no/no_member.py @@ -1,5 +1,5 @@ # pylint: disable=missing-docstring, unused-argument, wrong-import-position, invalid-name - +from pathlib import Path # Regression test for https://github.com/PyCQA/pylint/issues/400 class TestListener: @@ -29,3 +29,20 @@ def peer_joined(self, peer): sorted(parse.parse_qsl(parsed_url.query), key=lambda param: param[0])) new_parsed_url = parse.ParseResult._replace(parsed_url, query=sorted_query) new_url = new_parsed_url.geturl() # No error here + + +# Regression test for https://github.com/PyCQA/pylint/issues/3803 +# pylint: disable=too-few-public-methods +class Base: + label: str + + +class Derived(Base): + label = "I exist!" + + +print(Derived.label) + + +# Regression test for https://github.com/PyCQA/pylint/issues/5832 +starter_path = Path(__file__).parents[3].resolve() diff --git a/tests/functional/n/no/no_member_augassign.py b/tests/functional/n/no/no_member_augassign.py new file mode 100644 index 0000000000..1ffd9a1682 --- /dev/null +++ b/tests/functional/n/no/no_member_augassign.py @@ -0,0 +1,25 @@ +"""Tests for no-member in relation to AugAssign operations.""" +# pylint: disable=missing-module-docstring, too-few-public-methods, missing-class-docstring, invalid-name + +# Test for: https://github.com/PyCQA/pylint/issues/4562 +class A: + value: int + +obj_a = A() +obj_a.value += 1 # [no-member] + + +class B: + value: int + +obj_b = B() +obj_b.value = 1 + obj_b.value # [no-member] + + +class C: + value: int + + +obj_c = C() +obj_c.value += 1 # [no-member] +obj_c.value = 1 + obj_c.value # [no-member] diff --git a/tests/functional/n/no/no_member_augassign.txt b/tests/functional/n/no/no_member_augassign.txt new file mode 100644 index 0000000000..68abf0b935 --- /dev/null +++ b/tests/functional/n/no/no_member_augassign.txt @@ -0,0 +1,4 @@ +no-member:9:0:9:11::Instance of 'A' has no 'value' member:INFERENCE +no-member:16:18:16:29::Instance of 'B' has no 'value' member:INFERENCE +no-member:24:0:24:11::Instance of 'C' has no 'value' member:INFERENCE +no-member:25:18:25:29::Instance of 'C' has no 'value' member:INFERENCE diff --git a/tests/functional/n/no/no_member_dataclasses.py b/tests/functional/n/no/no_member_dataclasses.py index 972da9ad91..97528e6984 100644 --- a/tests/functional/n/no/no_member_dataclasses.py +++ b/tests/functional/n/no/no_member_dataclasses.py @@ -1,6 +1,11 @@ -"""Test various regressions for dataclasses and no-member. -""" +"""Test various regressions for dataclasses and no-member.""" + # pylint: disable=missing-docstring, too-few-public-methods + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from abc import ABCMeta, abstractmethod from dataclasses import asdict, dataclass, field from typing import Any, Dict diff --git a/tests/functional/n/no/no_member_dataclasses.txt b/tests/functional/n/no/no_member_dataclasses.txt index ee33c10fbf..bf91fc84f1 100644 --- a/tests/functional/n/no/no_member_dataclasses.txt +++ b/tests/functional/n/no/no_member_dataclasses.txt @@ -1,2 +1,2 @@ -no-member:69:26:69:46:TestClass2.some_func:Instance of 'Field' has no 'items' member:INFERENCE -no-member:81:26:81:46:TestClass3.some_func:Instance of 'Field' has no 'items' member:INFERENCE +no-member:74:26:74:46:TestClass2.some_func:Instance of 'Field' has no 'items' member:INFERENCE +no-member:86:26:86:46:TestClass3.some_func:Instance of 'Field' has no 'items' member:INFERENCE diff --git a/tests/functional/n/no/no_member_subclassed_dataclasses.py b/tests/functional/n/no/no_member_subclassed_dataclasses.py index 918abc5dbc..0dee108407 100644 --- a/tests/functional/n/no/no_member_subclassed_dataclasses.py +++ b/tests/functional/n/no/no_member_subclassed_dataclasses.py @@ -1,3 +1,10 @@ +# pylint: disable=fixme,logging-too-many-args,logging-fstring-interpolation,missing-docstring,no-else-return +# pylint: disable=too-few-public-methods + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from abc import ABCMeta, abstractmethod import dataclasses as dc from typing import Any, Dict diff --git a/tests/functional/n/no/no_member_subclassed_dataclasses.rc b/tests/functional/n/no/no_member_subclassed_dataclasses.rc index d115e39866..a17bb22daf 100644 --- a/tests/functional/n/no/no_member_subclassed_dataclasses.rc +++ b/tests/functional/n/no/no_member_subclassed_dataclasses.rc @@ -1,10 +1,2 @@ [testoptions] min_pyver=3.7 - -[MESSAGES CONTROL] -disable=fixme, - logging-too-many-args, - logging-fstring-interpolation, - missing-docstring, - no-else-return, - too-few-public-methods, diff --git a/tests/functional/n/no/no_method_argument_py38.py b/tests/functional/n/no/no_method_argument_py38.py index c674532882..8f573613cd 100644 --- a/tests/functional/n/no/no_method_argument_py38.py +++ b/tests/functional/n/no/no_method_argument_py38.py @@ -1,6 +1,17 @@ -# pylint: disable=missing-docstring,too-few-public-methods +# pylint: disable=missing-docstring,no-self-argument class Cls: def __init__(self, obj, /): self.obj = obj + + # regression tests for no-method-argument getting reported + # instead of no-self-argument + def varargs(*args): + """A method without a self argument but with *args.""" + + def kwargs(**kwargs): + """A method without a self argument but with **kwargs.""" + + def varargs_and_kwargs(*args, **kwargs): + """A method without a self argument but with *args and **kwargs.""" diff --git a/tests/functional/n/no/no_name_in_module.py b/tests/functional/n/no/no_name_in_module.py index d8e77a5b87..26c0079b6e 100644 --- a/tests/functional/n/no/no_name_in_module.py +++ b/tests/functional/n/no/no_name_in_module.py @@ -1,7 +1,7 @@ # pylint: disable=wildcard-import,unused-import,invalid-name,import-error # pylint: disable=bare-except,broad-except,wrong-import-order,ungrouped-imports,wrong-import-position """check nonexistent names imported are reported""" -from __future__ import print_function + import collections.tutu # [no-name-in-module] from collections import toto # [no-name-in-module] toto.yo() diff --git a/tests/functional/n/no/no_self_argument.py b/tests/functional/n/no/no_self_argument.py index 38f6040b54..9f51c2a9bb 100644 --- a/tests/functional/n/no/no_self_argument.py +++ b/tests/functional/n/no/no_self_argument.py @@ -1,8 +1,15 @@ """Check for method without self as first argument""" -# pylint: disable=useless-object-inheritance -from __future__ import print_function -class NoSelfArgument(object): + +MYSTATICMETHOD = staticmethod + + +def returns_staticmethod(my_function): + """Create a staticmethod from function `my_function`""" + return staticmethod(my_function) + + +class NoSelfArgument: """dummy class""" def __init__(truc): # [no-self-argument] @@ -16,3 +23,27 @@ def abdc(yoo): # [no-self-argument] def edf(self): """just another method""" print('yapudju in', self) + + @staticmethod + def say_hello(): + """A standard staticmethod""" + print("hello!") + + @MYSTATICMETHOD + def say_goodbye(): + """A staticmethod but using a different name""" + print("goodbye!") + + @returns_staticmethod + def concatenate_strings(string1, string2): + """A staticmethod created by `returns_staticmethod` function""" + return string1 + string2 + + def varargs(*args): # [no-self-argument] + """A method without a self argument but with *args.""" + + def kwargs(**kwargs): # [no-self-argument] + """A method without a self argument but with **kwargs.""" + + def varargs_and_kwargs(*args, **kwargs): # [no-self-argument] + """A method without a self argument but with *args and **kwargs.""" diff --git a/tests/functional/n/no/no_self_argument.txt b/tests/functional/n/no/no_self_argument.txt index 45d12cc321..481c41ebba 100644 --- a/tests/functional/n/no/no_self_argument.txt +++ b/tests/functional/n/no/no_self_argument.txt @@ -1,2 +1,5 @@ -no-self-argument:8:4:8:16:NoSelfArgument.__init__:"Method should have ""self"" as first argument":UNDEFINED -no-self-argument:12:4:12:12:NoSelfArgument.abdc:"Method should have ""self"" as first argument":UNDEFINED +no-self-argument:15:4:15:16:NoSelfArgument.__init__:"Method '__init__' should have ""self"" as first argument":UNDEFINED +no-self-argument:19:4:19:12:NoSelfArgument.abdc:"Method 'abdc' should have ""self"" as first argument":UNDEFINED +no-self-argument:42:4:42:15:NoSelfArgument.varargs:"Method 'varargs' should have ""self"" as first argument":UNDEFINED +no-self-argument:45:4:45:14:NoSelfArgument.kwargs:"Method 'kwargs' should have ""self"" as first argument":UNDEFINED +no-self-argument:48:4:48:26:NoSelfArgument.varargs_and_kwargs:"Method 'varargs_and_kwargs' should have ""self"" as first argument":UNDEFINED diff --git a/tests/functional/n/no/no_self_argument_py37.py b/tests/functional/n/no/no_self_argument_py37.py index 8e6d6fc34e..c1b195f078 100644 --- a/tests/functional/n/no/no_self_argument_py37.py +++ b/tests/functional/n/no/no_self_argument_py37.py @@ -1,9 +1,9 @@ """Test detection of self as argument of first method in Python 3.7 and above.""" -# pylint: disable=missing-docstring,too-few-public-methods,useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods -class Toto(object): +class Toto: def __class_getitem__(cls, params): # This is actually a special method which is always a class method. diff --git a/tests/functional/n/no/no_self_argument_py37.txt b/tests/functional/n/no/no_self_argument_py37.txt index 9f5779ab69..c4f95a6689 100644 --- a/tests/functional/n/no/no_self_argument_py37.txt +++ b/tests/functional/n/no/no_self_argument_py37.txt @@ -1 +1 @@ -no-self-argument:13:4:13:23:Toto.__class_other__:"Method should have ""self"" as first argument":UNDEFINED +no-self-argument:13:4:13:23:Toto.__class_other__:"Method '__class_other__' should have ""self"" as first argument":UNDEFINED diff --git a/tests/functional/n/no/no_staticmethod_decorator.py b/tests/functional/n/no/no_staticmethod_decorator.py index d0e0effa40..ce14c839ae 100644 --- a/tests/functional/n/no/no_staticmethod_decorator.py +++ b/tests/functional/n/no/no_staticmethod_decorator.py @@ -2,9 +2,9 @@ scope and if static method's argument is a member of the class """ -# pylint: disable=too-few-public-methods, using-constant-test, no-method-argument, useless-object-inheritance +# pylint: disable=too-few-public-methods, using-constant-test, no-method-argument -class MyClass(object): +class MyClass: """Some class""" def __init__(self): pass @@ -30,6 +30,6 @@ def helloworld(): MyClass.new_static_method = staticmethod(helloworld) -class MyOtherClass(object): +class MyOtherClass: """Some other class""" _make = staticmethod(tuple.__new__) diff --git a/tests/functional/n/no/no_warning_docstring.py b/tests/functional/n/no/no_warning_docstring.py index 315eeeaab0..c0d63df3f1 100644 --- a/tests/functional/n/no/no_warning_docstring.py +++ b/tests/functional/n/no/no_warning_docstring.py @@ -1,8 +1,8 @@ ''' Test for inheritance ''' -from __future__ import print_function + __revision__ = 1 -# pylint: disable=too-few-public-methods, using-constant-test, useless-object-inheritance -class AAAA(object): +# pylint: disable=too-few-public-methods, using-constant-test +class AAAA: ''' class AAAA ''' def __init__(self): diff --git a/tests/functional/n/non/non_init_parent_called.py b/tests/functional/n/non/non_init_parent_called.py index 7ad3f1932e..7a6c94eadd 100644 --- a/tests/functional/n/non/non_init_parent_called.py +++ b/tests/functional/n/non/non_init_parent_called.py @@ -1,20 +1,19 @@ # pylint: disable=protected-access,import-self,too-few-public-methods,line-too-long -# pylint: disable=wrong-import-order, useless-object-inheritance, unnecessary-dunder-call +# pylint: disable=wrong-import-order, unnecessary-dunder-call """test for call to __init__ from a non ancestor class """ -from __future__ import print_function from . import non_init_parent_called import nonexistant # [import-error] -__revision__ = '$Id: non_init_parent_called.py,v 1.2 2004-09-29 08:35:13 syt Exp $' -class AAAA(object): + +class AAAA: """ancestor 1""" def __init__(self): print('init', self) BBBBMixin.__init__(self) # [non-parent-init-called] -class BBBBMixin(object): +class BBBBMixin: """ancestor 2""" def __init__(self): @@ -46,6 +45,6 @@ class Super2(dict): """ Using the same idiom as Super, but without calling the __init__ method. """ - def __init__(self): # [super-init-not-called] + def __init__(self): base = super() base.__woohoo__() # [no-member] diff --git a/tests/functional/n/non/non_init_parent_called.txt b/tests/functional/n/non/non_init_parent_called.txt index 06de9a2443..d6ef10e9f8 100644 --- a/tests/functional/n/non/non_init_parent_called.txt +++ b/tests/functional/n/non/non_init_parent_called.txt @@ -1,6 +1,5 @@ -import-error:7:0:7:18::Unable to import 'nonexistant':UNDEFINED -non-parent-init-called:15:8:15:26:AAAA.__init__:__init__ method from a non direct base class 'BBBBMixin' is called:UNDEFINED -no-member:23:50:23:77:CCC:Module 'functional.n.non.non_init_parent_called' has no 'BBBB' member:INFERENCE -no-member:28:8:28:35:CCC.__init__:Module 'functional.n.non.non_init_parent_called' has no 'BBBB' member:INFERENCE -super-init-not-called:49:4:49:16:Super2.__init__:__init__ method from base class 'dict' is not called:INFERENCE -no-member:51:8:51:23:Super2.__init__:Super of 'Super2' has no '__woohoo__' member:INFERENCE +import-error:6:0:6:18::Unable to import 'nonexistant':UNDEFINED +non-parent-init-called:14:8:14:26:AAAA.__init__:__init__ method from a non direct base class 'BBBBMixin' is called:UNDEFINED +no-member:22:50:22:77:CCC:Module 'functional.n.non.non_init_parent_called' has no 'BBBB' member:INFERENCE +no-member:27:8:27:35:CCC.__init__:Module 'functional.n.non.non_init_parent_called' has no 'BBBB' member:INFERENCE +no-member:50:8:50:23:Super2.__init__:Super of 'Super2' has no '__woohoo__' member:INFERENCE diff --git a/tests/functional/n/non/non_iterator_returned.py b/tests/functional/n/non/non_iterator_returned.py index de83f68a2c..3bc24a23e0 100644 --- a/tests/functional/n/non/non_iterator_returned.py +++ b/tests/functional/n/non/non_iterator_returned.py @@ -1,9 +1,9 @@ """Check non-iterators returned by __iter__ """ -# pylint: disable=too-few-public-methods, missing-docstring, useless-object-inheritance, consider-using-with +# pylint: disable=too-few-public-methods, missing-docstring, consider-using-with, import-error +from uninferable import UNINFERABLE - -class FirstGoodIterator(object): +class FirstGoodIterator: """ yields in iterator. """ def __iter__(self): @@ -11,7 +11,7 @@ def __iter__(self): yield index -class SecondGoodIterator(object): +class SecondGoodIterator: """ __iter__ and next """ def __iter__(self): @@ -26,14 +26,14 @@ def next(self): return 1 -class ThirdGoodIterator(object): +class ThirdGoodIterator: """ Returns other iterator, not the current instance """ def __iter__(self): return SecondGoodIterator() -class FourthGoodIterator(object): +class FourthGoodIterator: """ __iter__ returns iter(...) """ def __iter__(self): @@ -48,18 +48,18 @@ def next(cls): return 2 -class IteratorClass(object, metaclass=IteratorMetaclass): +class IteratorClass(metaclass=IteratorMetaclass): """Iterable through the metaclass.""" -class FifthGoodIterator(object): +class FifthGoodIterator: """__iter__ returns a class which uses an iterator-metaclass.""" def __iter__(self): return IteratorClass -class FileBasedIterator(object): +class FileBasedIterator: def __init__(self, path): self.path = path self.file = None @@ -73,29 +73,35 @@ def __iter__(self): return self.file -class FirstBadIterator(object): +class FirstBadIterator: """ __iter__ returns a list """ def __iter__(self): # [non-iterator-returned] return [] -class SecondBadIterator(object): +class SecondBadIterator: """ __iter__ without next """ def __iter__(self): # [non-iterator-returned] return self -class ThirdBadIterator(object): +class ThirdBadIterator: """ __iter__ returns an instance of another non-iterator """ def __iter__(self): # [non-iterator-returned] return SecondBadIterator() -class FourthBadIterator(object): +class FourthBadIterator: """__iter__ returns a class.""" def __iter__(self): # [non-iterator-returned] return ThirdBadIterator + +class SixthGoodIterator: + """__iter__ returns Uninferable.""" + + def __iter__(self): + return UNINFERABLE diff --git a/tests/functional/n/non_ascii_import/non_ascii_import_as_bad.py b/tests/functional/n/non_ascii_import/non_ascii_import_as_bad.py index df392961f4..b558c1fe13 100644 --- a/tests/functional/n/non_ascii_import/non_ascii_import_as_bad.py +++ b/tests/functional/n/non_ascii_import/non_ascii_import_as_bad.py @@ -3,4 +3,4 @@ # Usage should not raise a second error -foo = łos.join("a", "b") +test = łos.join("a", "b") diff --git a/tests/functional/n/non_ascii_import/non_ascii_import_from_as.py b/tests/functional/n/non_ascii_import/non_ascii_import_from_as.py index 68beba0ec6..d5ed1d6f01 100644 --- a/tests/functional/n/non_ascii_import/non_ascii_import_from_as.py +++ b/tests/functional/n/non_ascii_import/non_ascii_import_from_as.py @@ -3,4 +3,4 @@ # Usage should not raise a second error -foo = łos("a", "b") +test = łos("a", "b") diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_for_loop.py b/tests/functional/n/non_ascii_name/non_ascii_name_for_loop.py index 59585645a7..5577417530 100644 --- a/tests/functional/n/non_ascii_name/non_ascii_name_for_loop.py +++ b/tests/functional/n/non_ascii_name/non_ascii_name_for_loop.py @@ -1,5 +1,5 @@ """invalid ascii char in a for loop""" - +# pylint: disable=consider-using-join import os diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_inline_var.py b/tests/functional/n/non_ascii_name/non_ascii_name_inline_var.py index 2d47e58c2d..3e878933d8 100644 --- a/tests/functional/n/non_ascii_name/non_ascii_name_inline_var.py +++ b/tests/functional/n/non_ascii_name/non_ascii_name_inline_var.py @@ -2,7 +2,7 @@ import os -foo = [ +test = [ f"{łol} " for łol in os.listdir(".") # [non-ascii-name] ] diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_try_except.py b/tests/functional/n/non_ascii_name/non_ascii_name_try_except.py index beccf4b9a9..bf8f041bde 100644 --- a/tests/functional/n/non_ascii_name/non_ascii_name_try_except.py +++ b/tests/functional/n/non_ascii_name/non_ascii_name_try_except.py @@ -8,4 +8,4 @@ # +1: [non-ascii-name] except AttributeError as łol: # Usage should not raise a second error - foo = łol + test = łol diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_try_except.txt b/tests/functional/n/non_ascii_name/non_ascii_name_try_except.txt index f6b0d6c6c4..855de33932 100644 --- a/tests/functional/n/non_ascii_name/non_ascii_name_try_except.txt +++ b/tests/functional/n/non_ascii_name/non_ascii_name_try_except.txt @@ -1 +1 @@ -non-ascii-name:9:0:11:14::"Variable name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH +non-ascii-name:9:0:11:15::"Variable name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH diff --git a/tests/functional/n/nonlocal_without_binding.py b/tests/functional/n/nonlocal_without_binding.py index de4675eca2..c05a012b1c 100644 --- a/tests/functional/n/nonlocal_without_binding.py +++ b/tests/functional/n/nonlocal_without_binding.py @@ -1,31 +1,46 @@ """ Checks that reversed() receive proper argument """ -# pylint: disable=missing-docstring,invalid-name,unused-variable, useless-object-inheritance +# pylint: disable=missing-docstring,invalid-name,unused-variable # pylint: disable=too-few-public-methods + def test(): def parent(): a = 42 + def stuff(): nonlocal a c = 24 + def parent2(): a = 42 + def stuff(): def other_stuff(): nonlocal a nonlocal c + b = 42 + + def func(): def other_func(): nonlocal b # [nonlocal-without-binding] -class SomeClass(object): - nonlocal x # [nonlocal-without-binding] + # Case where `nonlocal-without-binding` was not emitted when + # the nonlocal name was assigned later in the same scope. + # https://github.com/PyCQA/pylint/issues/6883 + def other_func2(): + nonlocal c # [nonlocal-without-binding] + c = 1 + + +class SomeClass: + nonlocal x # [nonlocal-without-binding] def func(self): - nonlocal some_attr # [nonlocal-without-binding] + nonlocal some_attr # [nonlocal-without-binding] def func2(): diff --git a/tests/functional/n/nonlocal_without_binding.txt b/tests/functional/n/nonlocal_without_binding.txt index dc48e7539c..039d07b522 100644 --- a/tests/functional/n/nonlocal_without_binding.txt +++ b/tests/functional/n/nonlocal_without_binding.txt @@ -1,3 +1,4 @@ -nonlocal-without-binding:22:8:22:18:func.other_func:nonlocal name b found without binding:UNDEFINED -nonlocal-without-binding:25:4:25:14:SomeClass:nonlocal name x found without binding:UNDEFINED -nonlocal-without-binding:28:8:28:26:SomeClass.func:nonlocal name some_attr found without binding:UNDEFINED +nonlocal-without-binding:29:8:29:18:func.other_func:nonlocal name b found without binding:HIGH +nonlocal-without-binding:35:8:35:18:func.other_func2:nonlocal name c found without binding:HIGH +nonlocal-without-binding:40:4:40:14:SomeClass:nonlocal name x found without binding:HIGH +nonlocal-without-binding:43:8:43:26:SomeClass.func:nonlocal name some_attr found without binding:HIGH diff --git a/tests/functional/n/not_async_context_manager.py b/tests/functional/n/not_async_context_manager.py index 138d76dfa4..5fb6d6a0d5 100644 --- a/tests/functional/n/not_async_context_manager.py +++ b/tests/functional/n/not_async_context_manager.py @@ -1,5 +1,5 @@ """Test that an async context manager receives a proper object.""" -# pylint: disable=missing-docstring, import-error, too-few-public-methods, useless-object-inheritance +# pylint: disable=missing-docstring, import-error, too-few-public-methods import contextlib from ala import Portocala @@ -10,17 +10,17 @@ def ctx_manager(): yield -class ContextManager(object): +class ContextManager: def __enter__(self): pass def __exit__(self, *args): pass -class PartialAsyncContextManager(object): +class PartialAsyncContextManager: def __aenter__(self): pass -class SecondPartialAsyncContextManager(object): +class SecondPartialAsyncContextManager: def __aexit__(self, *args): pass @@ -29,16 +29,16 @@ def __aenter__(self): pass -class AsyncManagerMixin(object): +class AsyncManagerMixin: pass -class GoodAsyncManager(object): +class GoodAsyncManager: def __aenter__(self): pass def __aexit__(self, *args): pass -class InheritExit(object): +class InheritExit: def __aexit__(self, *args): pass diff --git a/tests/functional/n/not_callable.py b/tests/functional/n/not_callable.py index e4272ff9b9..c5015e65fe 100644 --- a/tests/functional/n/not_callable.py +++ b/tests/functional/n/not_callable.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring,too-few-public-methods,wrong-import-position,useless-object-inheritance,use-dict-literal +# pylint: disable=missing-docstring,too-few-public-methods,wrong-import-position,use-dict-literal # pylint: disable=wrong-import-order, undefined-variable REVISION = None @@ -10,10 +10,10 @@ def correct(): REVISION = correct() -class Correct(object): +class Correct: """callable object""" -class MetaCorrect(object): +class MetaCorrect: """callable object""" def __call__(self): return self @@ -37,7 +37,7 @@ def __call__(self): class MyProperty(property): """ test subclasses """ -class PropertyTest(object): +class PropertyTest: """ class """ def __init__(self): @@ -69,7 +69,7 @@ def custom(self, value): # Safe from not-callable when using properties. -class SafeProperty(object): +class SafeProperty: @property def static(self): return staticmethod @@ -98,7 +98,7 @@ def range_builtin(self): @property def instance(self): - class Empty(object): + class Empty: def __call__(self): return 42 return Empty() @@ -224,3 +224,22 @@ def something(self): obj2 = Klass2() obj2.something() + + +# Regression test for https://github.com/PyCQA/pylint/issues/7109 +instance_or_cls = MyClass # pylint:disable=invalid-name +instance_or_cls = MyClass() +if not isinstance(instance_or_cls, MyClass): + new = MyClass.__new__(instance_or_cls) + new() + + +# Regression test for https://github.com/PyCQA/pylint/issues/5113. +# Do not emit `not-callable`. +ATTRIBUTES = { + 'DOMAIN': ("domain", str), + 'IMAGE': ("image", bytes), +} + +for key, (name, validate) in ATTRIBUTES.items(): + name = validate(1) diff --git a/tests/functional/n/not_context_manager.py b/tests/functional/n/not_context_manager.py index 2678f265e0..b7d0c5a7a2 100644 --- a/tests/functional/n/not_context_manager.py +++ b/tests/functional/n/not_context_manager.py @@ -1,9 +1,9 @@ """Tests that onjects used in a with statement implement context manager protocol""" # pylint: disable=too-few-public-methods, invalid-name, import-error, missing-docstring -# pylint: disable=wrong-import-position, useless-object-inheritance +# pylint: disable=wrong-import-position # Tests no messages for objects that implement the protocol -class Manager(object): +class Manager: def __enter__(self): pass def __exit__(self, type_, value, traceback): @@ -18,7 +18,7 @@ class AnotherManager(Manager): # Tests message for class that doesn't implement the protocol -class NotAManager(object): +class NotAManager: pass with NotAManager(): #[not-context-manager] pass @@ -70,7 +70,7 @@ def wrapper(): # Tests for properties returning managers. -class Property(object): +class Property: @property def ctx(self): @@ -98,7 +98,7 @@ class TestKnownBases(Missing): pass # Ignore mixins. -class ManagerMixin(object): +class ManagerMixin: def test(self): with self: pass diff --git a/tests/functional/n/not_in_loop.py b/tests/functional/n/not_in_loop.py index f7319eef72..7fffba2791 100644 --- a/tests/functional/n/not_in_loop.py +++ b/tests/functional/n/not_in_loop.py @@ -1,6 +1,6 @@ """Test that not-in-loop is detected properly.""" # pylint: disable=missing-docstring, invalid-name, too-few-public-methods -# pylint: disable=useless-else-on-loop, using-constant-test, useless-object-inheritance +# pylint: disable=useless-else-on-loop, using-constant-test # pylint: disable=no-else-continue while True: @@ -16,7 +16,7 @@ def lala(): continue # [not-in-loop] while True: - class A(object): + class A: continue # [not-in-loop] for _ in range(10): diff --git a/tests/functional/o/object_as_class_attribute.py b/tests/functional/o/object_as_class_attribute.py index 2ac729b36b..8e44abcb9d 100644 --- a/tests/functional/o/object_as_class_attribute.py +++ b/tests/functional/o/object_as_class_attribute.py @@ -1,4 +1,4 @@ -# pylint: disable=too-few-public-methods,useless-object-inheritance +# pylint: disable=too-few-public-methods """Test case for the problem described below : - A class extends 'object' - This class defines its own __init__() @@ -9,9 +9,8 @@ object.__init__() """ -__revision__ = None -class Statement(object): +class Statement: """ ... """ def __init__(self): pass diff --git a/tests/functional/o/overloaded_operator.py b/tests/functional/o/overloaded_operator.py index c14fb651f1..d375238dd0 100644 --- a/tests/functional/o/overloaded_operator.py +++ b/tests/functional/o/overloaded_operator.py @@ -1,8 +1,8 @@ -# pylint: disable=missing-docstring,too-few-public-methods,useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods """#3291""" -from __future__ import print_function -class Myarray(object): + +class Myarray: def __init__(self, array): self.array = array diff --git a/tests/functional/o/overridden_final_method_py38.py b/tests/functional/o/overridden_final_method_py38.py index 26498e7771..252ea3c012 100644 --- a/tests/functional/o/overridden_final_method_py38.py +++ b/tests/functional/o/overridden_final_method_py38.py @@ -1,7 +1,7 @@ """Since Python version 3.8, a method decorated with typing.final cannot be overridden""" -# pylint: disable=useless-object-inheritance, missing-docstring, too-few-public-methods +# pylint: disable=missing-docstring, too-few-public-methods from typing import final diff --git a/tests/functional/p/positional_only_arguments_expected.py b/tests/functional/p/positional_only_arguments_expected.py new file mode 100644 index 0000000000..7bde59ab84 --- /dev/null +++ b/tests/functional/p/positional_only_arguments_expected.py @@ -0,0 +1,18 @@ +# pylint: disable=missing-docstring,unused-argument,pointless-statement +# pylint: disable=too-few-public-methods + +class Gateaux: + def nihon(self, a, r, i, /, cheese=False): + return f"{a}{r}{i}gateaux" + " au fromage" if cheese else "" + + +cake = Gateaux() +# Should not emit error +cake.nihon(1, 2, 3) +cake.nihon(1, 2, 3, True) +cake.nihon(1, 2, 3, cheese=True) +# Emits error +cake.nihon(1, 2, i=3) # [positional-only-arguments-expected] +cake.nihon(1, r=2, i=3) # [positional-only-arguments-expected] +cake.nihon(a=1, r=2, i=3) # [positional-only-arguments-expected] +cake.nihon(1, r=2, i=3, cheese=True) # [positional-only-arguments-expected] diff --git a/tests/functional/p/positional_only_arguments_expected.rc b/tests/functional/p/positional_only_arguments_expected.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/p/positional_only_arguments_expected.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/p/positional_only_arguments_expected.txt b/tests/functional/p/positional_only_arguments_expected.txt new file mode 100644 index 0000000000..91e24f87b5 --- /dev/null +++ b/tests/functional/p/positional_only_arguments_expected.txt @@ -0,0 +1,4 @@ +positional-only-arguments-expected:15:0:15:21::"`cake.nihon()` got some positional-only arguments passed as keyword arguments: 'i'":INFERENCE +positional-only-arguments-expected:16:0:16:23::"`cake.nihon()` got some positional-only arguments passed as keyword arguments: 'r', 'i'":INFERENCE +positional-only-arguments-expected:17:0:17:25::"`cake.nihon()` got some positional-only arguments passed as keyword arguments: 'a', 'r', 'i'":INFERENCE +positional-only-arguments-expected:18:0:18:36::"`cake.nihon()` got some positional-only arguments passed as keyword arguments: 'r', 'i'":INFERENCE diff --git a/tests/functional/p/postponed_evaluation_pep585.py b/tests/functional/p/postponed_evaluation_pep585.py index 9537fccefd..1d539126d7 100644 --- a/tests/functional/p/postponed_evaluation_pep585.py +++ b/tests/functional/p/postponed_evaluation_pep585.py @@ -3,7 +3,15 @@ This check requires Python 3.7 or 3.8! Testing with 3.8 only, to support TypedDict. """ -# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods,invalid-name,inherit-non-class,unsupported-binary-operation,wrong-import-position,ungrouped-imports,unused-variable,unnecessary-direct-lambda-call + +# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods,invalid-name +# pylint: disable=inherit-non-class,unsupported-binary-operation,wrong-import-position,ungrouped-imports +# pylint: disable=unused-variable,unnecessary-direct-lambda-call + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from __future__ import annotations import collections import dataclasses diff --git a/tests/functional/p/postponed_evaluation_pep585.txt b/tests/functional/p/postponed_evaluation_pep585.txt index 7e0d0d163c..899dc59774 100644 --- a/tests/functional/p/postponed_evaluation_pep585.txt +++ b/tests/functional/p/postponed_evaluation_pep585.txt @@ -1,19 +1,19 @@ -unsubscriptable-object:15:15:15:19::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:20:25:20:29:CustomIntListError:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:24:28:24:32::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:26:24:26:28::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:28:14:28:18::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:33:36:33:40::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:43:54:43:58::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:45:60:45:64::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:96:15:96:19::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:97:21:97:25::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:98:15:98:19::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:99:19:99:23::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:100:15:100:19::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:101:9:101:13::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:101:14:101:18::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:121:20:121:24:func3:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:123:33:123:37:func3:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:126:14:126:18:func4:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:129:32:129:35:func5:Value 'set' is unsubscriptable:UNDEFINED +unsubscriptable-object:23:15:23:19::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:28:25:28:29:CustomIntListError:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:32:28:32:32::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:34:24:34:28::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:36:14:36:18::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:41:36:41:40::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:51:54:51:58::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:53:60:53:64::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:104:15:104:19::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:105:21:105:25::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:106:15:106:19::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:107:19:107:23::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:108:15:108:19::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:109:9:109:13::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:109:14:109:18::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:129:20:129:24:func3:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:131:33:131:37:func3:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:134:14:134:18:func4:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:137:32:137:35:func5:Value 'set' is unsubscriptable:UNDEFINED diff --git a/tests/functional/p/postponed_evaluation_pep585_error.py b/tests/functional/p/postponed_evaluation_pep585_error.py index 7c117e33e4..19153105ea 100644 --- a/tests/functional/p/postponed_evaluation_pep585_error.py +++ b/tests/functional/p/postponed_evaluation_pep585_error.py @@ -3,7 +3,15 @@ This check requires Python 3.7 or Python 3.8! Testing with 3.8 only, to support TypedDict. """ -# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods,invalid-name,inherit-non-class,unsupported-binary-operation,unused-variable,line-too-long,unnecessary-direct-lambda-call + +# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods +# pylint: disable=invalid-name,inherit-non-class,unsupported-binary-operation +# pylint: disable=unused-variable,line-too-long,unnecessary-direct-lambda-call + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + import collections import dataclasses import typing diff --git a/tests/functional/p/postponed_evaluation_pep585_error.txt b/tests/functional/p/postponed_evaluation_pep585_error.txt index 4fe18301f4..406081dfae 100644 --- a/tests/functional/p/postponed_evaluation_pep585_error.txt +++ b/tests/functional/p/postponed_evaluation_pep585_error.txt @@ -1,49 +1,49 @@ -unsubscriptable-object:14:15:14:19::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:19:25:19:29:CustomIntListError:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:23:28:23:32::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:25:24:25:28::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:27:14:27:18::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:32:36:32:40::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:35:12:35:16:CustomNamedTuple2:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:38:12:38:16:CustomNamedTuple3:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:42:54:42:58::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:44:60:44:64::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:47:12:47:16:CustomTypedDict3:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:50:12:50:16:CustomTypedDict4:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:61:12:61:16:CustomDataClass:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:65:12:65:16:CustomDataClass2:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:69:12:69:16:CustomDataClass3:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:74:12:74:16:CustomDataClass4:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:77:6:77:9::Value 'set' is unsubscriptable:UNDEFINED -unsubscriptable-object:78:6:78:29::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED -unsubscriptable-object:79:6:79:10::Value 'dict' is unsubscriptable:UNDEFINED -unsubscriptable-object:79:16:79:20::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:80:16:80:20::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:81:6:81:10::Value 'dict' is unsubscriptable:UNDEFINED -unsubscriptable-object:81:11:81:16::Value 'tuple' is unsubscriptable:UNDEFINED -unsubscriptable-object:82:11:82:16::Value 'tuple' is unsubscriptable:UNDEFINED -unsubscriptable-object:83:6:83:10::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:83:11:83:15::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:84:12:84:16::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:84:6:84:11::Value 'tuple' is unsubscriptable:UNDEFINED -unsubscriptable-object:85:12:85:16::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:86:13:86:17::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:87:19:87:23::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:89:14:89:18:func:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:92:15:92:19:func2:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:95:15:95:19::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:96:21:96:25::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:97:19:97:23::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:98:15:98:19::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:99:9:99:13::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:99:14:99:18::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:103:20:103:24:func3:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:105:33:105:37:func3:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:106:11:106:15:func3:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:108:14:108:18:func4:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:111:16:111:20:func5:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:111:32:111:35:func5:Value 'set' is unsubscriptable:UNDEFINED -unsubscriptable-object:114:75:114:79:func6:Value 'dict' is unsubscriptable:UNDEFINED -unsubscriptable-object:114:16:114:20:func6:Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:114:55:114:58:func6:Value 'set' is unsubscriptable:UNDEFINED -unsubscriptable-object:114:37:114:42:func6:Value 'tuple' is unsubscriptable:UNDEFINED +unsubscriptable-object:22:15:22:19::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:27:25:27:29:CustomIntListError:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:31:28:31:32::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:33:24:33:28::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:35:14:35:18::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:40:36:40:40::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:43:12:43:16:CustomNamedTuple2:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:46:12:46:16:CustomNamedTuple3:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:50:54:50:58::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:52:60:52:64::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:55:12:55:16:CustomTypedDict3:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:58:12:58:16:CustomTypedDict4:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:69:12:69:16:CustomDataClass:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:73:12:73:16:CustomDataClass2:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:77:12:77:16:CustomDataClass3:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:82:12:82:16:CustomDataClass4:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:85:6:85:9::Value 'set' is unsubscriptable:UNDEFINED +unsubscriptable-object:86:6:86:29::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED +unsubscriptable-object:87:6:87:10::Value 'dict' is unsubscriptable:UNDEFINED +unsubscriptable-object:87:16:87:20::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:88:16:88:20::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:89:6:89:10::Value 'dict' is unsubscriptable:UNDEFINED +unsubscriptable-object:89:11:89:16::Value 'tuple' is unsubscriptable:UNDEFINED +unsubscriptable-object:90:11:90:16::Value 'tuple' is unsubscriptable:UNDEFINED +unsubscriptable-object:91:6:91:10::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:91:11:91:15::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:92:12:92:16::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:92:6:92:11::Value 'tuple' is unsubscriptable:UNDEFINED +unsubscriptable-object:93:12:93:16::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:94:13:94:17::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:95:19:95:23::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:97:14:97:18:func:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:100:15:100:19:func2:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:103:15:103:19::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:104:21:104:25::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:105:19:105:23::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:106:15:106:19::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:107:9:107:13::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:107:14:107:18::Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:111:20:111:24:func3:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:113:33:113:37:func3:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:114:11:114:15:func3:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:116:14:116:18:func4:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:119:16:119:20:func5:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:119:32:119:35:func5:Value 'set' is unsubscriptable:UNDEFINED +unsubscriptable-object:122:75:122:79:func6:Value 'dict' is unsubscriptable:UNDEFINED +unsubscriptable-object:122:16:122:20:func6:Value 'list' is unsubscriptable:UNDEFINED +unsubscriptable-object:122:55:122:58:func6:Value 'set' is unsubscriptable:UNDEFINED +unsubscriptable-object:122:37:122:42:func6:Value 'tuple' is unsubscriptable:UNDEFINED diff --git a/tests/functional/p/property_affectation_py26.py b/tests/functional/p/property_affectation_py26.py index 323cf2d05b..ce13eb2071 100644 --- a/tests/functional/p/property_affectation_py26.py +++ b/tests/functional/p/property_affectation_py26.py @@ -1,11 +1,10 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods """ Simple test case for an annoying behavior in pylint. """ -__revision__ = 'pouet' -class Test(object): +class Test: """Smallest test case for reported issue.""" def __init__(self): diff --git a/tests/functional/p/protocol_classes_abstract.py b/tests/functional/p/protocol_classes_abstract.py new file mode 100644 index 0000000000..ad8ec5cf5c --- /dev/null +++ b/tests/functional/p/protocol_classes_abstract.py @@ -0,0 +1,46 @@ +"""Test that classes inheriting directly from Protocol should not warn about abstract-method.""" + +# pylint: disable=too-few-public-methods,disallowed-name,invalid-name + +from abc import abstractmethod, ABCMeta +from typing import Protocol, Literal + + +class FooProtocol(Protocol): + """Foo Protocol""" + + @abstractmethod + def foo(self) -> Literal["foo"]: + """foo method""" + + def foo_no_abstract(self) -> Literal["foo"]: + """foo not abstract method""" + + +class BarProtocol(Protocol): + """Bar Protocol""" + @abstractmethod + def bar(self) -> Literal["bar"]: + """bar method""" + + +class FooBarProtocol(FooProtocol, BarProtocol, Protocol): + """FooBar Protocol""" + +class BarParent(BarProtocol): # [abstract-method] + """Doesn't subclass typing.Protocol directly""" + +class IndirectProtocol(FooProtocol): # [abstract-method] + """Doesn't subclass typing.Protocol directly""" + +class AbcProtocol(FooProtocol, metaclass=ABCMeta): + """Doesn't subclass typing.Protocol but uses metaclass directly""" + +class FooBar(FooBarProtocol): + """FooBar object""" + + def bar(self) -> Literal["bar"]: + return "bar" + + def foo(self) -> Literal["foo"]: + return "foo" diff --git a/tests/functional/p/protocol_classes_abstract.rc b/tests/functional/p/protocol_classes_abstract.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/p/protocol_classes_abstract.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/p/protocol_classes_abstract.txt b/tests/functional/p/protocol_classes_abstract.txt new file mode 100644 index 0000000000..9b099ceb7f --- /dev/null +++ b/tests/functional/p/protocol_classes_abstract.txt @@ -0,0 +1,2 @@ +abstract-method:30:0:30:15:BarParent:Method 'bar' is abstract in class 'BarProtocol' but is not overridden in child class 'BarParent':INFERENCE +abstract-method:33:0:33:22:IndirectProtocol:Method 'foo' is abstract in class 'FooProtocol' but is not overridden in child class 'IndirectProtocol':INFERENCE diff --git a/tests/functional/r/raise_missing_from.py b/tests/functional/r/raise_missing_from.py index 897a0e9c4f..3b66609c8e 100644 --- a/tests/functional/r/raise_missing_from.py +++ b/tests/functional/r/raise_missing_from.py @@ -1,5 +1,5 @@ # pylint:disable=missing-docstring, unreachable, using-constant-test, invalid-name, bare-except -# pylint:disable=try-except-raise, undefined-variable, too-few-public-methods, superfluous-parens +# pylint:disable=try-except-raise, undefined-variable, too-few-public-methods, superfluous-parens, no-else-raise try: 1 / 0 diff --git a/tests/functional/r/raising/raising_bad_type.txt b/tests/functional/r/raising/raising_bad_type.txt index 28fcfadc62..04ea2d1700 100644 --- a/tests/functional/r/raising/raising_bad_type.txt +++ b/tests/functional/r/raising/raising_bad_type.txt @@ -1 +1 @@ -raising-bad-type:3:0:3:31::Raising tuple while only classes or instances are allowed:UNDEFINED +raising-bad-type:3:0:3:31::Raising tuple while only classes or instances are allowed:INFERENCE diff --git a/tests/functional/r/raising/raising_format_tuple.txt b/tests/functional/r/raising/raising_format_tuple.txt index 5f9283bb1d..a6456bc842 100644 --- a/tests/functional/r/raising/raising_format_tuple.txt +++ b/tests/functional/r/raising/raising_format_tuple.txt @@ -1,7 +1,7 @@ -raising-format-tuple:11:4:11:38:bad_percent:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:19:4:19:53:bad_multiarg:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:27:4:27:40:bad_braces:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:35:4:37:52:bad_multistring:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:41:4:43:53:bad_triplequote:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:47:4:47:36:bad_unicode:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:52:4:52:56:raise_something_without_name:Exception arguments suggest string formatting might be intended:UNDEFINED +raising-format-tuple:11:4:11:38:bad_percent:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:19:4:19:53:bad_multiarg:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:27:4:27:40:bad_braces:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:35:4:37:52:bad_multistring:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:41:4:43:53:bad_triplequote:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:47:4:47:36:bad_unicode:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:52:4:52:56:raise_something_without_name:Exception arguments suggest string formatting might be intended:HIGH diff --git a/tests/functional/r/raising/raising_non_exception.py b/tests/functional/r/raising/raising_non_exception.py index 7961c52206..e3040cdf49 100644 --- a/tests/functional/r/raising/raising_non_exception.py +++ b/tests/functional/r/raising/raising_non_exception.py @@ -1,13 +1,13 @@ """The following code should emit a raising-non-exception. -Previously, it didn't, due to a bug in the check for bad-exception-context, +Previously, it didn't, due to a bug in the check for bad-exception-cause, which prevented further checking on the Raise node. """ -# pylint: disable=import-error, too-few-public-methods, useless-object-inheritance +# pylint: disable=import-error, too-few-public-methods from missing_module import missing -class Exc(object): +class Exc: """Not an actual exception.""" raise Exc from missing # [raising-non-exception] diff --git a/tests/functional/r/raising/raising_non_exception.txt b/tests/functional/r/raising/raising_non_exception.txt index efa816a5f4..5cab168463 100644 --- a/tests/functional/r/raising/raising_non_exception.txt +++ b/tests/functional/r/raising/raising_non_exception.txt @@ -1 +1 @@ -raising-non-exception:13:0:13:22::Raising a new style class which doesn't inherit from BaseException:UNDEFINED +raising-non-exception:13:0:13:22::Raising a new style class which doesn't inherit from BaseException:INFERENCE diff --git a/tests/functional/r/recursion/recursion_error_940.py b/tests/functional/r/recursion/recursion_error_940.py index 4a9f346cb6..cffbe24dca 100644 --- a/tests/functional/r/recursion/recursion_error_940.py +++ b/tests/functional/r/recursion/recursion_error_940.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring, too-few-public-methods, useless-object-inheritance +# pylint: disable=missing-docstring, too-few-public-methods import datetime @@ -8,6 +8,6 @@ def today(cls): return cls(2010, 1, 1) -class Next(object): +class Next: def __init__(self): datetime.date = NewDate diff --git a/tests/functional/r/redefined/redefined_builtin.py b/tests/functional/r/redefined/redefined_builtin.py index 47d4e35ba8..9cb454dd09 100644 --- a/tests/functional/r/redefined/redefined_builtin.py +++ b/tests/functional/r/redefined/redefined_builtin.py @@ -1,7 +1,6 @@ """Tests for redefining builtins.""" # pylint: disable=unused-import, wrong-import-position, reimported, import-error # pylint: disable=redefined-outer-name, import-outside-toplevel, wrong-import-order -from __future__ import print_function def function(): diff --git a/tests/functional/r/redefined/redefined_builtin.txt b/tests/functional/r/redefined/redefined_builtin.txt index eb4b141013..f37135c665 100644 --- a/tests/functional/r/redefined/redefined_builtin.txt +++ b/tests/functional/r/redefined/redefined_builtin.txt @@ -1,3 +1,3 @@ -redefined-builtin:9:4:9:8:function:Redefining built-in 'type':UNDEFINED -redefined-builtin:14:0:14:3::Redefining built-in 'map':UNDEFINED -redefined-builtin:19:0:19:22::Redefining built-in 'open':UNDEFINED +redefined-builtin:8:4:8:8:function:Redefining built-in 'type':UNDEFINED +redefined-builtin:13:0:13:3::Redefining built-in 'map':UNDEFINED +redefined-builtin:18:0:18:22::Redefining built-in 'open':UNDEFINED diff --git a/tests/functional/r/redefined/redefined_except_handler.txt b/tests/functional/r/redefined/redefined_except_handler.txt index 1184bdd816..a0ccc6b9b3 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:HIGH +used-before-assignment:69:14:69:29:func:Using variable 'CustomException' before assignment:CONTROL_FLOW redefined-outer-name:71:4:72:12:func:Redefining name 'CustomException' from outer scope (line 62):UNDEFINED diff --git a/tests/functional/r/redefined/redefined_outer_name_type_checking.py b/tests/functional/r/redefined/redefined_outer_name_type_checking.py index 562f7ee2dd..d452bcdb03 100644 --- a/tests/functional/r/redefined/redefined_outer_name_type_checking.py +++ b/tests/functional/r/redefined/redefined_outer_name_type_checking.py @@ -3,19 +3,27 @@ from __future__ import annotations from typing import TYPE_CHECKING +import typing as t class Cls: - def func(self, stuff: defaultdict): - # This import makes the definition work. + def func(self, stuff: defaultdict, my_deque: deque): + # These imports make the definition work. # pylint: disable=import-outside-toplevel from collections import defaultdict + from collections import deque obj = defaultdict() + obj2 = deque() obj.update(stuff) + obj2.append(my_deque) return obj if TYPE_CHECKING: # This import makes the annotations work. from collections import defaultdict + +if t.TYPE_CHECKING: + # This import makes the annotations work. + from collections import deque diff --git a/tests/functional/r/redundant_unittest_assert.py b/tests/functional/r/redundant_unittest_assert.py index b7efffc4ee..aa41208312 100644 --- a/tests/functional/r/redundant_unittest_assert.py +++ b/tests/functional/r/redundant_unittest_assert.py @@ -1,13 +1,18 @@ -# pylint: disable=missing-docstring,too-few-public-methods """ -https://www.logilab.org/ticket/355 -If you are using assertTrue or assertFalse and the first argument is a -constant(like a string), then the assert will always be true. Therefore, -it should emit a warning message. +If you are using assertTrue or assertFalse and the first argument is a constant +(like a string), then the assert will always be true. Therefore, it should emit +a warning message. """ +# pylint: disable=missing-docstring,too-few-public-methods + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + import unittest + @unittest.skip("don't run this") class Tests(unittest.TestCase): def test_something(self): diff --git a/tests/functional/r/redundant_unittest_assert.txt b/tests/functional/r/redundant_unittest_assert.txt index 4c72539abd..44f9cb5206 100644 --- a/tests/functional/r/redundant_unittest_assert.txt +++ b/tests/functional/r/redundant_unittest_assert.txt @@ -1,6 +1,6 @@ -redundant-unittest-assert:17:8:17:71:Tests.test_something:Redundant use of assertTrue with constant value 'I meant assertEqual not assertTrue':UNDEFINED -redundant-unittest-assert:19:8:19:73:Tests.test_something:Redundant use of assertFalse with constant value 'I meant assertEqual not assertFalse':UNDEFINED -redundant-unittest-assert:21:8:21:39:Tests.test_something:Redundant use of assertTrue with constant value True:UNDEFINED -redundant-unittest-assert:23:8:23:41:Tests.test_something:Redundant use of assertFalse with constant value False:UNDEFINED -redundant-unittest-assert:25:8:25:40:Tests.test_something:Redundant use of assertFalse with constant value None:UNDEFINED -redundant-unittest-assert:27:8:27:36:Tests.test_something:Redundant use of assertTrue with constant value 0:UNDEFINED +redundant-unittest-assert:22:8:22:71:Tests.test_something:Redundant use of assertTrue with constant value 'I meant assertEqual not assertTrue':UNDEFINED +redundant-unittest-assert:24:8:24:73:Tests.test_something:Redundant use of assertFalse with constant value 'I meant assertEqual not assertFalse':UNDEFINED +redundant-unittest-assert:26:8:26:39:Tests.test_something:Redundant use of assertTrue with constant value True:UNDEFINED +redundant-unittest-assert:28:8:28:41:Tests.test_something:Redundant use of assertFalse with constant value False:UNDEFINED +redundant-unittest-assert:30:8:30:40:Tests.test_something:Redundant use of assertFalse with constant value None:UNDEFINED +redundant-unittest-assert:32:8:32:36:Tests.test_something:Redundant use of assertTrue with constant value 0:UNDEFINED diff --git a/tests/functional/r/regression/regression_4439.py b/tests/functional/r/regression/regression_4439.py index 966cc80067..257a20a866 100644 --- a/tests/functional/r/regression/regression_4439.py +++ b/tests/functional/r/regression/regression_4439.py @@ -1,6 +1,10 @@ """AttributeError: 'Subscript' object has no attribute 'name' """ # pylint: disable=missing-docstring +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from typing import Optional from attr import attrib, attrs @@ -10,4 +14,4 @@ class User: name: str = attrib() age: int = attrib() - occupation = Optional[str] = attrib(default=None) # [unsupported-assignment-operation] + occupation = Optional[str] = attrib(default=None) # [unsupported-assignment-operation] diff --git a/tests/functional/r/regression/regression_4439.txt b/tests/functional/r/regression/regression_4439.txt index 4e280a1afd..6db65057f7 100644 --- a/tests/functional/r/regression/regression_4439.txt +++ b/tests/functional/r/regression/regression_4439.txt @@ -1 +1 @@ -unsupported-assignment-operation:13:17:13:25:User:'Optional' does not support item assignment:UNDEFINED +unsupported-assignment-operation:17:17:17:25:User:'Optional' does not support item assignment:UNDEFINED diff --git a/tests/functional/r/regression/regression_4680.py b/tests/functional/r/regression/regression_4680.py index d43472607f..7994c3030b 100644 --- a/tests/functional/r/regression/regression_4680.py +++ b/tests/functional/r/regression/regression_4680.py @@ -1,9 +1,9 @@ # pylint: disable=missing-docstring,too-few-public-methods -import pkg.sub # [import-error] +import foo.sub # [import-error] -class Failed(metaclass=pkg.sub.Metaclass): +class Failed(metaclass=foo.sub.Metaclass): pass @@ -11,8 +11,8 @@ class FailedTwo(metaclass=ab.ABCMeta): # [undefined-variable] pass -class FailedThree(metaclass=pkg.sob.Metaclass): +class FailedThree(metaclass=foo.sob.Metaclass): pass -assert pkg.sub.value is None +assert foo.sub.value is None diff --git a/tests/functional/r/regression/regression_4680.txt b/tests/functional/r/regression/regression_4680.txt index 072e54f394..d1b7819879 100644 --- a/tests/functional/r/regression/regression_4680.txt +++ b/tests/functional/r/regression/regression_4680.txt @@ -1,2 +1,2 @@ -import-error:3:0:3:14::Unable to import 'pkg.sub':UNDEFINED +import-error:3:0:3:14::Unable to import 'foo.sub':UNDEFINED undefined-variable:10:0:10:15:FailedTwo:Undefined variable 'ab':UNDEFINED diff --git a/tests/functional/r/regression/regression_4723.txt b/tests/functional/r/regression/regression_4723.txt index 74d3df5a22..f64667e722 100644 --- a/tests/functional/r/regression/regression_4723.txt +++ b/tests/functional/r/regression/regression_4723.txt @@ -1 +1 @@ -no-method-argument:15:4:15:12:B.play:Method has no argument:UNDEFINED +no-method-argument:15:4:15:12:B.play:Method 'play' has no argument:UNDEFINED diff --git a/tests/functional/r/regression/regression___file___global.py b/tests/functional/r/regression/regression___file___global.py index 9ad2eed026..192ce71531 100644 --- a/tests/functional/r/regression/regression___file___global.py +++ b/tests/functional/r/regression/regression___file___global.py @@ -4,5 +4,3 @@ def func(): """override __file__""" global __file__ # [global-statement, redefined-builtin] __file__ = 'hop' - -__revision__ = 'pouet' diff --git a/tests/functional/r/regression_02/regression_2567.py b/tests/functional/r/regression_02/regression_2567.py new file mode 100644 index 0000000000..fe5c3f25a2 --- /dev/null +++ b/tests/functional/r/regression_02/regression_2567.py @@ -0,0 +1,33 @@ +""" +Regression test for `no-member`. +See: https://github.com/PyCQA/pylint/issues/2567 +""" + +# pylint: disable=missing-docstring,too-few-public-methods + +import contextlib + + +@contextlib.contextmanager +def context_manager(): + try: + yield + finally: + pass + + +cm = context_manager() +cm.__enter__() +cm.__exit__(None, None, None) + + +@contextlib.contextmanager +def other_context_manager(): + try: + yield + finally: + pass + + +with other_context_manager(): # notice the function call + pass diff --git a/tests/functional/r/regression_02/regression_2964.py b/tests/functional/r/regression_02/regression_2964.py new file mode 100644 index 0000000000..66235fc094 --- /dev/null +++ b/tests/functional/r/regression_02/regression_2964.py @@ -0,0 +1,24 @@ +""" +Regression test for `no-member`. +See: https://github.com/PyCQA/pylint/issues/2964 +""" + +# pylint: disable=missing-class-docstring,too-few-public-methods +# pylint: disable=unused-private-member,protected-access + + +class Node: + def __init__(self, name, path=()): + """ + Initialize self with "name" string and the tuple "path" of its parents. + "self" is added to the tuple as its last item. + """ + self.__name = name + self.__path = path + (self,) + + def get_full_name(self): + """ + A `no-member` message was emitted: + nodes.py:17:24: E1101: Instance of 'tuple' has no '__name' member (no-member) + """ + return ".".join(node.__name for node in self.__path) diff --git a/tests/functional/r/regression_02/regression_3866.py b/tests/functional/r/regression_02/regression_3866.py new file mode 100644 index 0000000000..f23a170d10 --- /dev/null +++ b/tests/functional/r/regression_02/regression_3866.py @@ -0,0 +1,47 @@ +# pylint: disable=missing-module-docstring, missing-docstring +# pylint: disable=invalid-name + +# These trigger the bug +def lambda_with_args(): + variable = 1 + return lambda *_, variable=variable: variable + 1 + + +def lambda_with_args_kwargs(): + variable = 1 + return lambda *_, variable=variable, **_kwargs: variable + 1 + + +def lambda_with_args_and_multi_args(): + variable = 1 + return lambda *_, a, variable=variable: variable + a + + +# The rest of these do not trigger the bug +def lambda_with_multi_args(): + variable = 1 + return lambda a, variable=variable: variable + a + + +def lambda_without_args(): + variable = 1 + return lambda variable=variable: variable + 1 + + +def lambda_with_kwargs(): + variable = 1 + return lambda variable=variable, **_: variable + 1 + + +def func_def(): + variable = 1 + + def f(*_, variable=variable): + return variable + 1 + + return f + + +def different_name(): + variable = 1 + return lambda *args, var=variable: var + 1 diff --git a/tests/functional/r/regression_02/regression_3976.py b/tests/functional/r/regression_02/regression_3976.py new file mode 100644 index 0000000000..3610e9e302 --- /dev/null +++ b/tests/functional/r/regression_02/regression_3976.py @@ -0,0 +1,14 @@ +""" +Regression test for https://github.com/PyCQA/pylint/issues/3976 + +E1123: Unexpected keyword argument 'include_extras' in function call (unexpected-keyword-arg) +""" + +import typing_extensions + + +def function(): + """Simple function""" + + +typing_extensions.get_type_hints(function, include_extras=True) diff --git a/tests/functional/r/regression_02/regression_4660.py b/tests/functional/r/regression_02/regression_4660.py new file mode 100644 index 0000000000..872ed6ca28 --- /dev/null +++ b/tests/functional/r/regression_02/regression_4660.py @@ -0,0 +1,45 @@ +"""Regression tests for https://github.com/PyCQA/pylint/issues/4660""" + +# pylint: disable=useless-return, unused-argument +# pylint: disable=missing-docstring, too-few-public-methods, invalid-name + +from __future__ import annotations + +from typing import Union, Any, Literal, overload +from collections.abc import Callable + + +def my_print(*args: Any) -> None: + print(", ".join(str(x) for x in args)) + return + + +# ---- This is OK ---- +class MyClass: + def my_method(self, option: Literal["mandatory"]) -> Callable[..., Any]: + return my_print + + +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]: + ... + + @overload + def my_method( + self, option: Literal["optional", "mandatory"] + ) -> Union[None, Callable[..., Any]]: + ... + + def my_method( + self, option: Literal["optional", "mandatory"] + ) -> Union[None, Callable[..., Any]]: + return my_print + + +d = MyClass1().my_method("mandatory") +d(1, "bar") # [not-callable] diff --git a/tests/functional/r/regression_02/regression_4660.rc b/tests/functional/r/regression_02/regression_4660.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/r/regression_02/regression_4660.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/r/regression_02/regression_4660.txt b/tests/functional/r/regression_02/regression_4660.txt new file mode 100644 index 0000000000..59b48ecacb --- /dev/null +++ b/tests/functional/r/regression_02/regression_4660.txt @@ -0,0 +1 @@ +not-callable:45:0:45:11::d is not callable:UNDEFINED diff --git a/tests/functional/r/regression_02/regression_5048.py b/tests/functional/r/regression_02/regression_5048.py index 5656759af5..08ff55fb2d 100644 --- a/tests/functional/r/regression_02/regression_5048.py +++ b/tests/functional/r/regression_02/regression_5048.py @@ -1,6 +1,6 @@ """Crash regression in astroid on Compare node inference Fixed in https://github.com/PyCQA/astroid/pull/1185""" -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised # Reported at https://github.com/PyCQA/pylint/issues/5048 diff --git a/tests/functional/r/regression_02/regression_5776.py b/tests/functional/r/regression_02/regression_5776.py new file mode 100644 index 0000000000..5e83425112 --- /dev/null +++ b/tests/functional/r/regression_02/regression_5776.py @@ -0,0 +1,15 @@ +"""Test for a regression with Enums not being recognized when imported with an alias. + +Reported in https://github.com/PyCQA/pylint/issues/5776 +""" + +from enum import Enum as PyEnum + + +class MyEnum(PyEnum): + """My enum""" + + ENUM_KEY = "enum_value" + + +print(MyEnum.ENUM_KEY.value) diff --git a/tests/functional/r/regression_02/regression_5801.py b/tests/functional/r/regression_02/regression_5801.py new file mode 100644 index 0000000000..8c08ec6750 --- /dev/null +++ b/tests/functional/r/regression_02/regression_5801.py @@ -0,0 +1,5 @@ +# https://github.com/PyCQA/pylint/issues/5801 +# pylint: disable=missing-docstring + +import struct +struct.unpack('h', b'\x00\x01') diff --git a/tests/functional/r/regression_02/regression_8067.py b/tests/functional/r/regression_02/regression_8067.py new file mode 100644 index 0000000000..f640aed9d1 --- /dev/null +++ b/tests/functional/r/regression_02/regression_8067.py @@ -0,0 +1,17 @@ +"""Regression tests for inferred.qname missing""" + +# pylint: disable=missing-docstring,too-few-public-methods,disallowed-name + +x = slice(42) +x() # [not-callable] + + +class Foo: + def __init__(self, foo=slice(42)): + self.foo = foo + + +def bar(): + i = Foo() + i.foo() + return 100 diff --git a/tests/functional/r/regression_02/regression_8067.txt b/tests/functional/r/regression_02/regression_8067.txt new file mode 100644 index 0000000000..5dba68c456 --- /dev/null +++ b/tests/functional/r/regression_02/regression_8067.txt @@ -0,0 +1 @@ +not-callable:6:0:6:3::x is not callable:UNDEFINED diff --git a/tests/functional/r/regression_02/regression_8207.py b/tests/functional/r/regression_02/regression_8207.py new file mode 100644 index 0000000000..6538018bc1 --- /dev/null +++ b/tests/functional/r/regression_02/regression_8207.py @@ -0,0 +1,14 @@ +"""Regression test for 8207.""" + +# pylint: disable=missing-docstring,too-few-public-methods + +class Example: + def __init__(self): + self.offset = -10 + + def minus_offset(self): + return { + (x, x): value + for x, row in enumerate([(5, 10), (20, 30)]) + for y, value in enumerate(row, -self.offset) + } diff --git a/tests/functional/r/regression_02/regression_enum_1734.py b/tests/functional/r/regression_02/regression_enum_1734.py new file mode 100644 index 0000000000..06759c7d3d --- /dev/null +++ b/tests/functional/r/regression_02/regression_enum_1734.py @@ -0,0 +1,24 @@ +# Regression test for https://github.com/PyCQA/astroid/pull/1734 +# The following should lint just fine +# Fixed in https://github.com/PyCQA/astroid/pull/1743 + +# pylint: disable=missing-docstring,invalid-name + +from enum import Enum + +class Test(Enum): + LOADED = "loaded", True + SETUP_ERROR = "setup_error", True + + _recoverable: bool + + def __new__(cls, value: str, recoverable: bool): + obj = object.__new__(cls) + obj._value_ = value + obj._recoverable = recoverable + return obj + + @property + def recoverable(self) -> bool: + """Get if the state is recoverable.""" + return self._recoverable diff --git a/tests/functional/r/regression_02/regression_no_member_7631.py b/tests/functional/r/regression_02/regression_no_member_7631.py new file mode 100644 index 0000000000..758aad057a --- /dev/null +++ b/tests/functional/r/regression_02/regression_no_member_7631.py @@ -0,0 +1,16 @@ +"""Regression test from https://github.com/PyCQA/pylint/issues/7631 +The following code should NOT raise no-member. +""" +# pylint: disable=missing-docstring,too-few-public-methods + +class Base: + attr: int = 2 + +class Parent(Base): + attr: int + +class Child(Parent): + attr = 2 + + def __init__(self): + self.attr = self.attr | 4 diff --git a/tests/functional/r/regression_02/regression_too_many_arguments_2335.py b/tests/functional/r/regression_02/regression_too_many_arguments_2335.py index d2759adfe8..55aa873080 100644 --- a/tests/functional/r/regression_02/regression_too_many_arguments_2335.py +++ b/tests/functional/r/regression_02/regression_too_many_arguments_2335.py @@ -7,5 +7,5 @@ class NodeCheckMetaClass(ABCMeta): - def __new__(cls, name, bases, namespace, **kwargs): - return ABCMeta.__new__(cls, name, bases, namespace) + def __new__(mcs, name, bases, namespace, **kwargs): + return ABCMeta.__new__(mcs, name, bases, namespace) diff --git a/tests/functional/r/reimport.py b/tests/functional/r/reimport.py index defe51959f..86f273d3b8 100644 --- a/tests/functional/r/reimport.py +++ b/tests/functional/r/reimport.py @@ -1,6 +1,5 @@ """check reimport """ -from __future__ import absolute_import, print_function # pylint: disable=using-constant-test,ungrouped-imports,wrong-import-position,import-outside-toplevel import os diff --git a/tests/functional/r/reimport.txt b/tests/functional/r/reimport.txt index 7825858d2d..d08fe58c69 100644 --- a/tests/functional/r/reimport.txt +++ b/tests/functional/r/reimport.txt @@ -1,4 +1,4 @@ -reimported:8:0:8:9::Reimport 'os' (imported line 6):UNDEFINED -reimported:16:4:16:30::Reimport 'exists' (imported line 7):UNDEFINED -reimported:21:4:21:20:func:Reimport 'os' (imported line 6):UNDEFINED -reimported:23:4:23:13:func:Reimport 're' (imported line 9):UNDEFINED +reimported:7:0:7:9::Reimport 'os' (imported line 5):HIGH +reimported:15:4:15:30::Reimport 'exists' (imported line 6):HIGH +reimported:20:4:20:20:func:Reimport 'os' (imported line 5):HIGH +reimported:22:4:22:13:func:Reimport 're' (imported line 8):HIGH diff --git a/tests/functional/r/reimported.py b/tests/functional/r/reimported.py index 03b07c4d6a..8833817ecd 100644 --- a/tests/functional/r/reimported.py +++ b/tests/functional/r/reimported.py @@ -1,4 +1,6 @@ -# pylint: disable=missing-docstring,unused-import,import-error, wildcard-import,unused-wildcard-import,redefined-builtin,no-name-in-module,ungrouped-imports,wrong-import-order +# pylint: disable=missing-docstring,unused-import,import-error, wildcard-import,unused-wildcard-import +# pylint: disable=redefined-builtin,no-name-in-module,ungrouped-imports,wrong-import-order,wrong-import-position +# pylint: disable=consider-using-from-import from time import sleep, sleep # [reimported] from lala import missing, missing # [reimported] @@ -15,9 +17,6 @@ from itertools import * from os import * -# pylint: disable=misplaced-future -from __future__ import absolute_import, print_function - import sys import xml.etree.ElementTree @@ -27,7 +26,6 @@ import email.encoders # [reimported] import sys # [reimported] #pylint: disable=ungrouped-imports,wrong-import-order -__revision__ = 0 def no_reimport(): """docstring""" @@ -42,3 +40,6 @@ def reimport(): del sys, ElementTree, xml.etree.ElementTree, encoders, email.encoders + +from pandas._libs import algos as libalgos +import pandas._libs.algos as algos # [reimported] diff --git a/tests/functional/r/reimported.txt b/tests/functional/r/reimported.txt index 41b06eba34..bae1f09b7b 100644 --- a/tests/functional/r/reimported.txt +++ b/tests/functional/r/reimported.txt @@ -1,9 +1,10 @@ -reimported:3:0:3:29::Reimport 'sleep' (imported line 3):UNDEFINED -reimported:4:0:4:33::Reimport 'missing' (imported line 4):UNDEFINED -reimported:7:0:7:15::Reimport 'missing1' (imported line 6):UNDEFINED -reimported:10:0:10:27::Reimport 'deque' (imported line 9):UNDEFINED -reimported:24:0:24:33::Reimport 'ElementTree' (imported line 23):UNDEFINED -reimported:27:0:27:21::Reimport 'email.encoders' (imported line 26):UNDEFINED -reimported:29:0:29:10::Reimport 'sys' (imported line 21):UNDEFINED -redefined-outer-name:40:4:40:14:reimport:Redefining name 'sys' from outer scope (line 16):UNDEFINED -reimported:40:4:40:14:reimport:Reimport 'sys' (imported line 21):UNDEFINED +reimported:5:0:5:29::Reimport 'sleep' (imported line 5):UNDEFINED +reimported:6:0:6:33::Reimport 'missing' (imported line 6):UNDEFINED +reimported:9:0:9:15::Reimport 'missing1' (imported line 8):HIGH +reimported:12:0:12:27::Reimport 'deque' (imported line 11):HIGH +reimported:23:0:23:33::Reimport 'ElementTree' (imported line 22):HIGH +reimported:26:0:26:21::Reimport 'email.encoders' (imported line 25):HIGH +reimported:28:0:28:10::Reimport 'sys' (imported line 20):HIGH +redefined-outer-name:38:4:38:14:reimport:Redefining name 'sys' from outer scope (line 18):UNDEFINED +reimported:38:4:38:14:reimport:Reimport 'sys' (imported line 20):HIGH +reimported:45:0:45:34::Reimport 'pandas._libs.algos' (imported line 44):HIGH diff --git a/tests/functional/r/return_in_init.py b/tests/functional/r/return_in_init.py index bf0aa064ff..886ae73cc7 100644 --- a/tests/functional/r/return_in_init.py +++ b/tests/functional/r/return_in_init.py @@ -1,24 +1,24 @@ -# pylint: disable=missing-docstring,too-few-public-methods,useless-return, useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods,useless-return -class MyClass(object): +class MyClass: def __init__(self): # [return-in-init] return 1 -class MyClass2(object): +class MyClass2: """dummy class""" def __init__(self): return -class MyClass3(object): +class MyClass3: """dummy class""" def __init__(self): return None -class MyClass5(object): +class MyClass5: """dummy class""" def __init__(self): diff --git a/tests/functional/s/self/self_cls_assignment.py b/tests/functional/s/self/self_cls_assignment.py index a9da8767cf..820531c618 100644 --- a/tests/functional/s/self/self_cls_assignment.py +++ b/tests/functional/s/self/self_cls_assignment.py @@ -1,8 +1,7 @@ """Warning about assigning self/cls variable.""" -from __future__ import print_function -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods -class Foo(object): +class Foo: """Class with methods that check for self/cls assignment""" # pylint: disable=no-self-argument diff --git a/tests/functional/s/self/self_cls_assignment.txt b/tests/functional/s/self/self_cls_assignment.txt index efc6b5f954..dd3383e38b 100644 --- a/tests/functional/s/self/self_cls_assignment.txt +++ b/tests/functional/s/self/self_cls_assignment.txt @@ -1,5 +1,5 @@ -self-cls-assignment:11:8:11:17:Foo.self_foo:Invalid assignment to bar_ in method:UNDEFINED -self-cls-assignment:15:8:15:19:Foo.self_foofoo:Invalid assignment to self in method:UNDEFINED -self-cls-assignment:16:8:16:27:Foo.self_foofoo:Invalid assignment to self in method:UNDEFINED -self-cls-assignment:22:8:22:20:Foo.cls_foo:Invalid assignment to cls in method:UNDEFINED -self-cls-assignment:45:12:45:24:TestNonLocal.function._set_param:Invalid assignment to self in method:UNDEFINED +self-cls-assignment:10:8:10:17:Foo.self_foo:Invalid assignment to bar_ in method:UNDEFINED +self-cls-assignment:14:8:14:19:Foo.self_foofoo:Invalid assignment to self in method:UNDEFINED +self-cls-assignment:15:8:15:27:Foo.self_foofoo:Invalid assignment to self in method:UNDEFINED +self-cls-assignment:21:8:21:20:Foo.cls_foo:Invalid assignment to cls in method:UNDEFINED +self-cls-assignment:44:12:44:24:TestNonLocal.function._set_param:Invalid assignment to self in method:UNDEFINED diff --git a/tests/functional/s/shadowed_import.py b/tests/functional/s/shadowed_import.py new file mode 100644 index 0000000000..fb913e8d80 --- /dev/null +++ b/tests/functional/s/shadowed_import.py @@ -0,0 +1,17 @@ +# pylint: disable=missing-docstring,unused-import,import-error, consider-using-from-import +# pylint: disable=wrong-import-order + +from pathlib import Path +from some_other_lib import CustomPath as Path # [shadowed-import] + +from pathlib import Path # [reimported] +import FastAPI.Path as Path # [shadowed-import] + +from pandas._libs import algos +import pandas.core.algorithms as algos # [shadowed-import] + +from sklearn._libs import second as libalgos +import sklearn.core.algorithms as second + +import Hello +from goodbye import CustomHello as Hello # [shadowed-import] diff --git a/tests/functional/s/shadowed_import.txt b/tests/functional/s/shadowed_import.txt new file mode 100644 index 0000000000..8ffac7c634 --- /dev/null +++ b/tests/functional/s/shadowed_import.txt @@ -0,0 +1,5 @@ +shadowed-import:5:0:5:45::Shadowed 'Path' (imported line 4):HIGH +reimported:7:0:7:24::Reimport 'Path' (imported line 4):HIGH +shadowed-import:8:0:8:27::Shadowed 'Path' (imported line 4):HIGH +shadowed-import:11:0:11:38::Shadowed 'algos' (imported line 10):HIGH +shadowed-import:17:0:17:40::Shadowed 'Hello' (imported line 16):HIGH diff --git a/tests/functional/s/signature_differs.py b/tests/functional/s/signature_differs.py index 45631bbd63..929921cf38 100644 --- a/tests/functional/s/signature_differs.py +++ b/tests/functional/s/signature_differs.py @@ -1,6 +1,6 @@ -# pylint: disable=too-few-public-methods, missing-docstring, useless-object-inheritance +# pylint: disable=too-few-public-methods, missing-docstring -class Abcd(object): +class Abcd: def __init__(self): self.aarg = False diff --git a/tests/functional/s/simplifiable/simplifiable_if_statement.py b/tests/functional/s/simplifiable/simplifiable_if_statement.py index 4d4c8b5d4c..59251bd040 100644 --- a/tests/functional/s/simplifiable/simplifiable_if_statement.py +++ b/tests/functional/s/simplifiable/simplifiable_if_statement.py @@ -29,6 +29,7 @@ def test_simplifiable_3(arg, arg2): def test_simplifiable_4(arg): + var = False if arg: var = True else: @@ -89,6 +90,7 @@ def test_not_simplifiable_4(arg): def test_not_simplifiable_5(arg): # Different actions in each branch + var = 43 if arg == "any": return True else: diff --git a/tests/functional/s/simplifiable/simplifiable_if_statement.txt b/tests/functional/s/simplifiable/simplifiable_if_statement.txt index d36768ddd0..e0a82ef6ab 100644 --- a/tests/functional/s/simplifiable/simplifiable_if_statement.txt +++ b/tests/functional/s/simplifiable/simplifiable_if_statement.txt @@ -1,4 +1,4 @@ simplifiable-if-statement:8:4:11:20:test_simplifiable_1:The if statement can be replaced with 'return bool(test)':UNDEFINED simplifiable-if-statement:16:4:19:20:test_simplifiable_2:The if statement can be replaced with 'return bool(test)':UNDEFINED simplifiable-if-statement:24:4:27:19:test_simplifiable_3:The if statement can be replaced with 'var = bool(test)':UNDEFINED -simplifiable-if-statement:35:8:38:24:test_simplifiable_4:The if statement can be replaced with 'return bool(test)':UNDEFINED +simplifiable-if-statement:36:8:39:24:test_simplifiable_4:The if statement can be replaced with 'return bool(test)':UNDEFINED diff --git a/tests/functional/s/singledispatch_functions.py b/tests/functional/s/singledispatch_functions.py index cfd4d873c9..931bfd30d6 100644 --- a/tests/functional/s/singledispatch_functions.py +++ b/tests/functional/s/singledispatch_functions.py @@ -1,6 +1,5 @@ # pylint: disable=missing-docstring,import-error,unused-import,assignment-from-no-return -# pylint: disable=invalid-name, too-few-public-methods, useless-object-inheritance -from __future__ import print_function +# pylint: disable=invalid-name, too-few-public-methods from UNINFERABLE import uninferable_func try: @@ -11,7 +10,7 @@ my_single_dispatch = singledispatch -class FakeSingleDispatch(object): +class FakeSingleDispatch: @staticmethod def register(function): diff --git a/tests/functional/s/singledispatch_functions.txt b/tests/functional/s/singledispatch_functions.txt index 88973375f5..93fc304960 100644 --- a/tests/functional/s/singledispatch_functions.txt +++ b/tests/functional/s/singledispatch_functions.txt @@ -1,5 +1,5 @@ -unused-variable:60:4:60:10:_:Unused variable 'unused':UNDEFINED -unused-argument:65:24:65:27:not_single_dispatch:Unused argument 'arg':HIGH -unused-argument:70:24:70:27:bad_single_dispatch:Unused argument 'arg':HIGH -function-redefined:75:0:75:23:bad_single_dispatch:function already defined line 70:UNDEFINED -unused-argument:75:24:75:27:bad_single_dispatch:Unused argument 'arg':HIGH +unused-variable:59:4:59:10:_:Unused variable 'unused':UNDEFINED +unused-argument:64:24:64:27:not_single_dispatch:Unused argument 'arg':HIGH +unused-argument:69:24:69:27:bad_single_dispatch:Unused argument 'arg':HIGH +function-redefined:74:0:74:23:bad_single_dispatch:function already defined line 69:UNDEFINED +unused-argument:74:24:74:27:bad_single_dispatch:Unused argument 'arg':HIGH diff --git a/tests/functional/s/singledispatch_method.txt b/tests/functional/s/singledispatch_method.txt new file mode 100644 index 0000000000..c747fb6a84 --- /dev/null +++ b/tests/functional/s/singledispatch_method.txt @@ -0,0 +1,3 @@ +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_method_py37.py b/tests/functional/s/singledispatch_method_py37.py new file mode 100644 index 0000000000..c9269f7bf1 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.py @@ -0,0 +1,23 @@ +"""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_method_py37.rc b/tests/functional/s/singledispatch_method_py37.rc new file mode 100644 index 0000000000..67a28a36aa --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.rc @@ -0,0 +1,2 @@ +[testoptions] +max_pyver=3.8 diff --git a/tests/functional/s/singledispatch_method_py37.txt b/tests/functional/s/singledispatch_method_py37.txt new file mode 100644 index 0000000000..111bc47225 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.txt @@ -0,0 +1,3 @@ +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_method_py38.py b/tests/functional/s/singledispatch_method_py38.py new file mode 100644 index 0000000000..ad8eea1dd8 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.py @@ -0,0 +1,40 @@ +"""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_method_py38.rc b/tests/functional/s/singledispatch_method_py38.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/s/singledispatch_method_py38.txt b/tests/functional/s/singledispatch_method_py38.txt new file mode 100644 index 0000000000..c747fb6a84 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.txt @@ -0,0 +1,3 @@ +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/singledispatchmethod_function_py38.py b/tests/functional/s/singledispatchmethod_function_py38.py new file mode 100644 index 0000000000..ef44f71c15 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.py @@ -0,0 +1,41 @@ +"""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/singledispatchmethod_function_py38.rc b/tests/functional/s/singledispatchmethod_function_py38.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/s/singledispatchmethod_function_py38.txt b/tests/functional/s/singledispatchmethod_function_py38.txt new file mode 100644 index 0000000000..4c236b3466 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.txt @@ -0,0 +1,3 @@ +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/s/slots_checks.py b/tests/functional/s/slots_checks.py index a465ef5454..2c22e968ed 100644 --- a/tests/functional/s/slots_checks.py +++ b/tests/functional/s/slots_checks.py @@ -1,6 +1,6 @@ """ Checks that classes uses valid __slots__ """ -# pylint: disable=too-few-public-methods, missing-docstring, useless-object-inheritance +# pylint: disable=too-few-public-methods, missing-docstring # pylint: disable=using-constant-test, wrong-import-position, no-else-return, line-too-long, unused-private-member from collections import deque @@ -11,62 +11,85 @@ def func(): return [str(var) for var in range(3)] -class NotIterable(object): +class NotIterable: def __iter_(self): """ do nothing """ -class Good(object): +class Good: __slots__ = () -class SecondGood(object): +class SecondGood: __slots__ = [] -class ThirdGood(object): +class ThirdGood: __slots__ = ['a'] -class FourthGood(object): +class FourthGood: __slots__ = (f'a{i}' for i in range(10)) -class FifthGood(object): +class FifthGood: __slots__ = deque(["a", "b", "c"]) -class SixthGood(object): +class SixthGood: __slots__ = {"a": "b", "c": "d"} -class Bad(object): # [invalid-slots] +class SeventhGood: + """type-annotated __slots__ with no value""" + __slots__: str + +class EigthGood: + """Multiple __slots__ declared in the class""" + x = 1 + if x: + __slots__: str + else: + __slots__ = ("y",) + +class Bad: # [invalid-slots] __slots__ = list -class SecondBad(object): # [invalid-slots] +class SecondBad: # [invalid-slots] __slots__ = 1 -class ThirdBad(object): +class ThirdBad: __slots__ = ('a', 2) # [invalid-slots-object] -class FourthBad(object): # [invalid-slots] +class FourthBad: # [invalid-slots] __slots__ = NotIterable() -class FifthBad(object): +class FifthBad: __slots__ = ("a", "b", "") # [invalid-slots-object] -class SixthBad(object): # [single-string-used-for-slots] +class SixthBad: # [single-string-used-for-slots] __slots__ = "a" -class SeventhBad(object): # [single-string-used-for-slots] - __slots__ = ('foo') +class SeventhBad: # [single-string-used-for-slots] + __slots__ = ('foo') # [superfluous-parens] -class EighthBad(object): # [single-string-used-for-slots] +class EighthBad: # [single-string-used-for-slots] __slots__ = deque.__name__ -class NinthBad(object): +class NinthBad: __slots__ = [str] # [invalid-slots-object] -class TenthBad(object): +class TenthBad: __slots__ = [1 + 2 + 3] # [invalid-slots-object] -class PotentiallyGood(object): +class EleventhBad: # [invalid-slots] + __slots__ = None + +class TwelfthBad: # [invalid-slots] + """One valid & one invalid __slots__ value""" + x = 1 + if x: + __slots__ = ("y",) + else: + __slots__ = None + +class PotentiallyGood: __slots__ = func() -class PotentiallySecondGood(object): +class PotentiallySecondGood: __slots__ = ('a', deque.__name__) @@ -77,17 +100,17 @@ def __iter__(cls): yield str(value) -class IterableClass(object, metaclass=Metaclass): +class IterableClass(metaclass=Metaclass): pass -class PotentiallyThirdGood(object): +class PotentiallyThirdGood: __slots__ = IterableClass -class PotentiallyFourthGood(object): +class PotentiallyFourthGood: __slots__ = Good.__slots__ -class ValueInSlotConflict(object): +class ValueInSlotConflict: __slots__ = ('first', 'second', 'third', 'fourth') # [class-variable-slots-conflict, class-variable-slots-conflict, class-variable-slots-conflict] first = None @@ -99,7 +122,7 @@ def fourth(self): return self.third -class Parent(object): +class Parent: first = 42 diff --git a/tests/functional/s/slots_checks.txt b/tests/functional/s/slots_checks.txt index 49b3149124..d63ad25173 100644 --- a/tests/functional/s/slots_checks.txt +++ b/tests/functional/s/slots_checks.txt @@ -1,13 +1,16 @@ -invalid-slots:36:0:36:9:Bad:Invalid __slots__ object:UNDEFINED -invalid-slots:39:0:39:15:SecondBad:Invalid __slots__ object:UNDEFINED -invalid-slots-object:43:22:43:23:ThirdBad:Invalid object '2' in __slots__, must contain only non empty strings:INFERENCE -invalid-slots:45:0:45:15:FourthBad:Invalid __slots__ object:UNDEFINED -invalid-slots-object:49:27:49:29:FifthBad:"Invalid object ""''"" in __slots__, must contain only non empty strings":INFERENCE -single-string-used-for-slots:51:0:51:14:SixthBad:Class __slots__ should be a non-string iterable:UNDEFINED -single-string-used-for-slots:54:0:54:16:SeventhBad:Class __slots__ should be a non-string iterable:UNDEFINED -single-string-used-for-slots:57:0:57:15:EighthBad:Class __slots__ should be a non-string iterable:UNDEFINED -invalid-slots-object:61:17:61:20:NinthBad:Invalid object 'str' in __slots__, must contain only non empty strings:INFERENCE -invalid-slots-object:64:17:64:26:TenthBad:Invalid object '1 + 2 + 3' in __slots__, must contain only non empty strings:INFERENCE -class-variable-slots-conflict:91:17:91:24:ValueInSlotConflict:Value 'first' in slots conflicts with class variable:UNDEFINED -class-variable-slots-conflict:91:45:91:53:ValueInSlotConflict:Value 'fourth' in slots conflicts with class variable:UNDEFINED -class-variable-slots-conflict:91:36:91:43:ValueInSlotConflict:Value 'third' in slots conflicts with class variable:UNDEFINED +invalid-slots:48:0:48:9:Bad:Invalid __slots__ object:UNDEFINED +invalid-slots:51:0:51:15:SecondBad:Invalid __slots__ object:UNDEFINED +invalid-slots-object:55:22:55:23:ThirdBad:Invalid object '2' in __slots__, must contain only non empty strings:INFERENCE +invalid-slots:57:0:57:15:FourthBad:Invalid __slots__ object:UNDEFINED +invalid-slots-object:61:27:61:29:FifthBad:"Invalid object ""''"" in __slots__, must contain only non empty strings":INFERENCE +single-string-used-for-slots:63:0:63:14:SixthBad:Class __slots__ should be a non-string iterable:UNDEFINED +single-string-used-for-slots:66:0:66:16:SeventhBad:Class __slots__ should be a non-string iterable:UNDEFINED +superfluous-parens:67:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED +single-string-used-for-slots:69:0:69:15:EighthBad:Class __slots__ should be a non-string iterable:UNDEFINED +invalid-slots-object:73:17:73:20:NinthBad:Invalid object 'str' in __slots__, must contain only non empty strings:INFERENCE +invalid-slots-object:76:17:76:26:TenthBad:Invalid object '1 + 2 + 3' in __slots__, must contain only non empty strings:INFERENCE +invalid-slots:78:0:78:17:EleventhBad:Invalid __slots__ object:UNDEFINED +invalid-slots:81:0:81:16:TwelfthBad:Invalid __slots__ object:UNDEFINED +class-variable-slots-conflict:114:17:114:24:ValueInSlotConflict:Value 'first' in slots conflicts with class variable:UNDEFINED +class-variable-slots-conflict:114:45:114:53:ValueInSlotConflict:Value 'fourth' in slots conflicts with class variable:UNDEFINED +class-variable-slots-conflict:114:36:114:43:ValueInSlotConflict:Value 'third' in slots conflicts with class variable:UNDEFINED diff --git a/tests/functional/s/socketerror_import.py b/tests/functional/s/socketerror_import.py index df5de2957d..f9403d9666 100644 --- a/tests/functional/s/socketerror_import.py +++ b/tests/functional/s/socketerror_import.py @@ -1,6 +1,6 @@ """ds""" -from __future__ import absolute_import, print_function + from socket import error -__revision__ = '$Id: socketerror_import.py,v 1.2 2005-12-28 14:58:22 syt Exp $' + print(error) diff --git a/tests/functional/s/star/star_needs_assignment_target_py37.txt b/tests/functional/s/star/star_needs_assignment_target_py37.txt index a4fa2caea1..fb5a5faa6b 100644 --- a/tests/functional/s/star/star_needs_assignment_target_py37.txt +++ b/tests/functional/s/star/star_needs_assignment_target_py37.txt @@ -1 +1 @@ -star-needs-assignment-target:15:37::Can use starred expression only in assignment target +star-needs-assignment-target:15:36:15:46::Can use starred expression only in assignment target:UNDEFINED diff --git a/tests/functional/s/statement_without_effect.py b/tests/functional/s/statement_without_effect.py index 31fc7250f0..c459b242fe 100644 --- a/tests/functional/s/statement_without_effect.py +++ b/tests/functional/s/statement_without_effect.py @@ -1,5 +1,5 @@ """Test for statements without effects.""" -# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-comprehension, unnecessary-ellipsis, use-list-literal +# pylint: disable=too-few-public-methods, unnecessary-comprehension, unnecessary-ellipsis, use-list-literal # +1:[pointless-string-statement] """inline doc string should use a separated message""" @@ -35,9 +35,12 @@ def to_be(): GOOD_ATTRIBUTE_DOCSTRING = 42 """Module level attribute docstring is fine. """ -class ClassLevelAttributeTest(object): +class ClassLevelAttributeTest: """ test attribute docstrings. """ + class ClassLevelException(Exception): + """Exception defined for access as a class attribute.""" + good_attribute_docstring = 24 """ class level attribute docstring is fine either. """ second_good_attribute_docstring = 42 @@ -73,3 +76,24 @@ def ellipsis(): class EllipsisBody: """Test that an Ellipsis as a body does not trigger the error""" ... + + +def assigned_exception(): + """Test that an assigned exception is not flagged as a pointless statement""" + exception = ValueError("one") + return exception, ValueError("two") + + +def raised_exception(): + """Test that a raised exception is not flagged as a pointless statement""" + raise ValueError() + + +def unraised_exception(): + """Test that instantiating but not raising an exception is flagged as a pointless statement""" + ValueError("pointless-statement") # [pointless-exception-statement] + ValueError(to_be()) # [pointless-exception-statement] + ClassLevelAttributeTest.ClassLevelException(to_be()) # [pointless-exception-statement] + ValueError("another-pointless-statement") # [pointless-exception-statement] + instance = ClassLevelAttributeTest() + instance.ClassLevelException(to_be()) # [pointless-exception-statement] diff --git a/tests/functional/s/statement_without_effect.txt b/tests/functional/s/statement_without_effect.txt index 6bebf450bf..4d6e071be8 100644 --- a/tests/functional/s/statement_without_effect.txt +++ b/tests/functional/s/statement_without_effect.txt @@ -8,5 +8,10 @@ expression-not-assigned:23:0:23:18::"Expression ""list() and tuple()"" is assign expression-not-assigned:30:0:30:17::"Expression ""ANSWER == to_be()"" is assigned to nothing":UNDEFINED expression-not-assigned:32:0:32:22::"Expression ""to_be() or not to_be()"" is assigned to nothing":UNDEFINED expression-not-assigned:33:0:33:13::"Expression ""to_be().title"" is assigned to nothing":UNDEFINED -pointless-string-statement:58:8:58:43:ClassLevelAttributeTest.__init__:String statement has no effect:UNDEFINED -pointless-string-statement:65:8:65:55:ClassLevelAttributeTest.test:String statement has no effect:UNDEFINED +pointless-string-statement:61:8:61:43:ClassLevelAttributeTest.__init__:String statement has no effect:UNDEFINED +pointless-string-statement:68:8:68:55:ClassLevelAttributeTest.test:String statement has no effect:UNDEFINED +pointless-exception-statement:94:4:94:37:unraised_exception:Exception statement has no effect:INFERENCE +pointless-exception-statement:95:4:95:23:unraised_exception:Exception statement has no effect:INFERENCE +pointless-exception-statement:96:4:96:56:unraised_exception:Exception statement has no effect:INFERENCE +pointless-exception-statement:97:4:97:45:unraised_exception:Exception statement has no effect:INFERENCE +pointless-exception-statement:99:4:99:41:unraised_exception:Exception statement has no effect:INFERENCE diff --git a/tests/functional/s/statement_without_effect_py36.py b/tests/functional/s/statement_without_effect_py36.py index 59745ce2ba..841ed5bd95 100644 --- a/tests/functional/s/statement_without_effect_py36.py +++ b/tests/functional/s/statement_without_effect_py36.py @@ -1,8 +1,8 @@ """Test for statements without effects.""" -# pylint: disable=too-few-public-methods, useless-object-inheritance +# pylint: disable=too-few-public-methods -class ClassLevelAttributeTest(object): +class ClassLevelAttributeTest: """ test attribute docstrings. """ some_variable: int = 42 diff --git a/tests/functional/s/stop_iteration_inside_generator.py b/tests/functional/s/stop_iteration_inside_generator.py index 8620d958c6..efde61a77a 100644 --- a/tests/functional/s/stop_iteration_inside_generator.py +++ b/tests/functional/s/stop_iteration_inside_generator.py @@ -1,7 +1,8 @@ """ Test that no StopIteration is raised inside a generator """ -# pylint: disable=missing-docstring,invalid-name,import-error, try-except-raise, wrong-import-position,not-callable,raise-missing-from +# pylint: disable=missing-docstring,invalid-name,import-error, try-except-raise, wrong-import-position +# pylint: disable=not-callable,raise-missing-from,broad-exception-raised import asyncio class RebornStopIteration(StopIteration): @@ -9,12 +10,14 @@ class RebornStopIteration(StopIteration): A class inheriting from StopIteration exception """ + # This one is ok def gen_ok(): yield 1 yield 2 yield 3 + # pylint should warn about this one # because of a direct raising of StopIteration inside generator def gen_stopiter(): @@ -23,6 +26,7 @@ def gen_stopiter(): yield 3 raise StopIteration # [stop-iteration-return] + # pylint should warn about this one # because of a direct raising of an exception inheriting from StopIteration inside generator def gen_stopiterchild(): @@ -31,6 +35,7 @@ def gen_stopiterchild(): yield 3 raise RebornStopIteration # [stop-iteration-return] + # pylint should warn here # because of the possibility that next raises a StopIteration exception def gen_next_raises_stopiter(): @@ -38,6 +43,7 @@ def gen_next_raises_stopiter(): while True: yield next(g) # [stop-iteration-return] + # This one is the same as gen_next_raises_stopiter # but is ok because the next function is inside # a try/except block handling StopIteration @@ -49,6 +55,7 @@ def gen_next_inside_try_except(): except StopIteration: return + # This one is the same as gen_next_inside_try_except # but is not ok because the next function is inside # a try/except block that don't handle StopIteration @@ -60,6 +67,7 @@ def gen_next_inside_wrong_try_except(): except ValueError: return + # This one is the same as gen_next_inside_try_except # but is not ok because the next function is inside # a try/except block that handle StopIteration but reraise it @@ -71,11 +79,13 @@ def gen_next_inside_wrong_try_except2(): except StopIteration: raise StopIteration # [stop-iteration-return] + # Those two last are ok def gen_in_for(): for el in gen_ok(): yield el + def gen_yield_from(): yield from gen_ok() @@ -84,7 +94,7 @@ def gen_dont_crash_on_no_exception(): g = gen_ok() while True: try: - yield next(g) # [stop-iteration-return] + yield next(g) # [stop-iteration-return] except ValueError: raise @@ -97,10 +107,10 @@ def gen_dont_crash_on_uninferable(): # https://github.com/PyCQA/pylint/issues/1830 def gen_next_with_sentinel(): - yield next([], 42) # No bad return + yield next([], 42) # No bad return -from itertools import count +from itertools import count, cycle # https://github.com/PyCQA/pylint/issues/2158 def generator_using_next(): @@ -108,11 +118,18 @@ def generator_using_next(): number = next(counter) yield number * 2 +# https://github.com/PyCQA/pylint/issues/7765 +def infinite_iterator_itertools_cycle(): + counter = cycle('ABCD') + val = next(counter) + yield val + # pylint: disable=too-few-public-methods class SomeClassWithNext: def next(self): return iter([1, 2, 3]) + def some_gen(self): for value in self.next(): yield value @@ -122,8 +139,49 @@ def some_gen(self): def something_invalid(): - raise Exception('cannot iterate this') + raise Exception("cannot iterate this") def invalid_object_passed_to_next(): - yield next(something_invalid()) # [stop-iteration-return] + yield next(something_invalid()) # [stop-iteration-return] + + +# pylint: disable=redefined-builtin,too-many-function-args +def safeiter(it): + """Regression test for issue #7610 when ``next`` builtin is redefined""" + + def next(): + while True: + try: + return next(it) + except StopIteration: + raise + + it = iter(it) + while True: + yield next() + +def other_safeiter(it): + """Regression test for issue #7610 when ``next`` builtin is redefined""" + + def next(*things): + print(*things) + while True: + try: + return next(it) + except StopIteration: + raise + + it = iter(it) + while True: + yield next(1, 2) + +def data(filename): + """ + Ensure pylint doesn't crash if `next` is incorrectly called without args + See https://github.com/PyCQA/pylint/issues/7828 + """ + with open(filename, encoding="utf8") as file: + next() # attempt to skip header but this is incorrect code + for line in file: + yield line diff --git a/tests/functional/s/stop_iteration_inside_generator.txt b/tests/functional/s/stop_iteration_inside_generator.txt index 74ddc6d4e1..f20351c5da 100644 --- a/tests/functional/s/stop_iteration_inside_generator.txt +++ b/tests/functional/s/stop_iteration_inside_generator.txt @@ -1,7 +1,7 @@ -stop-iteration-return:24:4:24:23:gen_stopiter:Do not raise StopIteration in generator, use return statement instead:UNDEFINED -stop-iteration-return:32:4:32:29:gen_stopiterchild:Do not raise StopIteration in generator, use return statement instead:UNDEFINED -stop-iteration-return:39:14:39:21:gen_next_raises_stopiter:Do not raise StopIteration in generator, use return statement instead:UNDEFINED -stop-iteration-return:59:18:59:25:gen_next_inside_wrong_try_except:Do not raise StopIteration in generator, use return statement instead:UNDEFINED -stop-iteration-return:72:12:72:31:gen_next_inside_wrong_try_except2:Do not raise StopIteration in generator, use return statement instead:UNDEFINED -stop-iteration-return:87:18:87:25:gen_dont_crash_on_no_exception:Do not raise StopIteration in generator, use return statement instead:UNDEFINED -stop-iteration-return:129:10:129:35:invalid_object_passed_to_next:Do not raise StopIteration in generator, use return statement instead:UNDEFINED +stop-iteration-return:27:4:27:23:gen_stopiter:Do not raise StopIteration in generator, use return statement instead:INFERENCE +stop-iteration-return:36:4:36:29:gen_stopiterchild:Do not raise StopIteration in generator, use return statement instead:INFERENCE +stop-iteration-return:44:14:44:21:gen_next_raises_stopiter:Do not raise StopIteration in generator, use return statement instead:INFERENCE +stop-iteration-return:66:18:66:25:gen_next_inside_wrong_try_except:Do not raise StopIteration in generator, use return statement instead:INFERENCE +stop-iteration-return:80:12:80:31:gen_next_inside_wrong_try_except2:Do not raise StopIteration in generator, use return statement instead:INFERENCE +stop-iteration-return:97:18:97:25:gen_dont_crash_on_no_exception:Do not raise StopIteration in generator, use return statement instead:INFERENCE +stop-iteration-return:146:10:146:35:invalid_object_passed_to_next:Do not raise StopIteration in generator, use return statement instead:INFERENCE diff --git a/tests/functional/s/string/string_formatting.py b/tests/functional/s/string/string_formatting.py index b72e9f6760..cb88680e1a 100644 --- a/tests/functional/s/string/string_formatting.py +++ b/tests/functional/s/string/string_formatting.py @@ -1,32 +1,32 @@ """Test for Python 3 string formatting error""" # pylint: disable=too-few-public-methods, import-error, unused-argument, line-too-long, -# pylint: disable=useless-object-inheritance, consider-using-f-string +# pylint: disable=consider-using-f-string import os import sys import logging from missing import Missing -class Custom(object): +class Custom: """ Has a __getattr__ """ def __getattr__(self, _): return self -class Test(object): +class Test: """ test format attribute access """ custom = Custom() ids = [1, 2, 3, [4, 5, 6]] -class Getitem(object): +class Getitem: """ test custom getitem for lookup access """ def __getitem__(self, index): return 42 -class ReturnYes(object): +class ReturnYes: """ can't be properly inferred """ missing = Missing() @@ -181,7 +181,7 @@ def issue373(): """ Ignore any object coming from an argument. """ - class SomeClass(object): + class SomeClass: """ empty docstring. """ def __init__(self, opts=None): self.opts = opts diff --git a/tests/functional/s/string/string_formatting_error.py b/tests/functional/s/string/string_formatting_error.py index 681fedd560..0e57946ee3 100644 --- a/tests/functional/s/string/string_formatting_error.py +++ b/tests/functional/s/string/string_formatting_error.py @@ -1,6 +1,5 @@ """test string format error""" # pylint: disable=unsupported-binary-operation,line-too-long, consider-using-f-string -from __future__ import print_function PARG_1 = PARG_2 = PARG_3 = 1 diff --git a/tests/functional/s/string/string_formatting_error.txt b/tests/functional/s/string/string_formatting_error.txt index fec5246d1b..19653c617f 100644 --- a/tests/functional/s/string/string_formatting_error.txt +++ b/tests/functional/s/string/string_formatting_error.txt @@ -1,15 +1,15 @@ -too-few-format-args:10:10:10:46:pprint:Not enough arguments for format string:UNDEFINED -too-many-format-args:11:10:11:33:pprint:Too many arguments for format string:UNDEFINED -mixed-format-string:12:10:12:54:pprint:Mixing named and unnamed conversion specifiers in format string:UNDEFINED -missing-format-string-key:13:10:13:49:pprint:Missing key 'PARG_2' in format string dictionary:UNDEFINED -unused-format-string-key:14:10:14:73:pprint:Unused key 'PARG_3' in format string dictionary:UNDEFINED -bad-format-string-key:15:10:15:54:pprint:Format string dictionary key should be a string, not 2:UNDEFINED -missing-format-string-key:15:10:15:54:pprint:Missing key 'PARG_2' in format string dictionary:UNDEFINED -format-needs-mapping:16:10:16:42:pprint:Expected mapping for format string, not Tuple:UNDEFINED -format-needs-mapping:17:10:17:42:pprint:Expected mapping for format string, not List:UNDEFINED -bad-format-character:18:10:18:24:pprint:Unsupported format character 'z' (0x7a) at index 2:UNDEFINED -truncated-format-string:19:10:19:38:pprint:Format string ends in middle of conversion specifier:UNDEFINED -format-string-without-interpolation:21:10:21:27:pprint:Using formatting for a string that does not have any interpolated variables:UNDEFINED +too-few-format-args:9:10:9:46:pprint:Not enough arguments for format string:UNDEFINED +too-many-format-args:10:10:10:33:pprint:Too many arguments for format string:UNDEFINED +mixed-format-string:11:10:11:54:pprint:Mixing named and unnamed conversion specifiers in format string:UNDEFINED +missing-format-string-key:12:10:12:49:pprint:Missing key 'PARG_2' in format string dictionary:UNDEFINED +unused-format-string-key:13:10:13:73:pprint:Unused key 'PARG_3' in format string dictionary:UNDEFINED +bad-format-string-key:14:10:14:54:pprint:Format string dictionary key should be a string, not 2:UNDEFINED +missing-format-string-key:14:10:14:54:pprint:Missing key 'PARG_2' in format string dictionary:UNDEFINED +format-needs-mapping:15:10:15:42:pprint:Expected mapping for format string, not Tuple:UNDEFINED +format-needs-mapping:16:10:16:42:pprint:Expected mapping for format string, not List:UNDEFINED +bad-format-character:17:10:17:24:pprint:Unsupported format character 'z' (0x7a) at index 2:UNDEFINED +truncated-format-string:18:10:18:38:pprint:Format string ends in middle of conversion specifier:UNDEFINED +format-string-without-interpolation:20:10:20:27:pprint:Using formatting for a string that does not have any interpolated variables:UNDEFINED +format-string-without-interpolation:21:10:21:23:pprint:Using formatting for a string that does not have any interpolated variables:UNDEFINED format-string-without-interpolation:22:10:22:23:pprint:Using formatting for a string that does not have any interpolated variables:UNDEFINED -format-string-without-interpolation:23:10:23:23:pprint:Using formatting for a string that does not have any interpolated variables:UNDEFINED -format-string-without-interpolation:24:10:24:25:pprint:Using formatting for a string that does not have any interpolated variables:UNDEFINED +format-string-without-interpolation:23:10:23:25:pprint:Using formatting for a string that does not have any interpolated variables:UNDEFINED diff --git a/tests/functional/s/subclassed_final_class_py38.py b/tests/functional/s/subclassed_final_class_py38.py index 6aca833520..a4621d5324 100644 --- a/tests/functional/s/subclassed_final_class_py38.py +++ b/tests/functional/s/subclassed_final_class_py38.py @@ -1,7 +1,11 @@ """Since Python version 3.8, a class decorated with typing.final cannot be -subclassed """ +subclassed.""" -# pylint: disable=useless-object-inheritance, missing-docstring, too-few-public-methods +# pylint: disable=missing-docstring, too-few-public-methods + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements from typing import final @@ -11,5 +15,5 @@ class Base: pass -class Subclass(Base): # [subclassed-final-class] +class Subclass(Base): # [subclassed-final-class] pass diff --git a/tests/functional/s/subclassed_final_class_py38.txt b/tests/functional/s/subclassed_final_class_py38.txt index 9dec8110e8..4b73d2d986 100644 --- a/tests/functional/s/subclassed_final_class_py38.txt +++ b/tests/functional/s/subclassed_final_class_py38.txt @@ -1 +1 @@ -subclassed-final-class:14:0:14:14:Subclass:"Class 'Subclass' is a subclass of a class decorated with typing.final: 'Base'":UNDEFINED +subclassed-final-class:18:0:18:14:Subclass:"Class 'Subclass' is a subclass of a class decorated with typing.final: 'Base'":UNDEFINED diff --git a/tests/functional/s/subprocess_run_check.txt b/tests/functional/s/subprocess_run_check.txt index 41887464f1..fea72b673e 100644 --- a/tests/functional/s/subprocess_run_check.txt +++ b/tests/functional/s/subprocess_run_check.txt @@ -1 +1 @@ -subprocess-run-check:6:0:6:16::Using subprocess.run without explicitly set `check` is not recommended.:UNDEFINED +subprocess-run-check:6:0:6:16::'subprocess.run' used without explicitly defining the value for 'check'.:INFERENCE diff --git a/tests/functional/s/super/super_checks.py b/tests/functional/s/super/super_checks.py index ca35d52aec..050fd3c81f 100644 --- a/tests/functional/s/super/super_checks.py +++ b/tests/functional/s/super/super_checks.py @@ -1,4 +1,4 @@ -# pylint: disable=too-few-public-methods,import-error, missing-docstring, useless-object-inheritance +# pylint: disable=too-few-public-methods,import-error, missing-docstring # pylint: disable=useless-super-delegation,wrong-import-position,invalid-name, wrong-import-order # pylint: disable=super-with-arguments from unknown import Missing @@ -12,7 +12,7 @@ def hop(self): def __init__(self): super(Aaaa, self).__init__() -class NewAaaa(object): +class NewAaaa: """old style""" def hop(self): """hop""" @@ -36,19 +36,19 @@ class WrongNameRegression(Py3kAaaa): def __init__(self): super(Missing, self).__init__() # [bad-super-call] -class Getattr(object): +class Getattr: """ crash """ name = NewAaaa -class CrashSuper(object): +class CrashSuper: """ test a crash with this checker """ def __init__(self): super(Getattr.name, self).__init__() # [bad-super-call] -class Empty(object): +class Empty: """Just an empty class.""" -class SuperDifferentScope(object): +class SuperDifferentScope: """Don'emit bad-super-call when the super call is in another scope. For reference, see https://bitbucket.org/logilab/pylint/issue/403. """ @@ -72,7 +72,7 @@ def __init__(self): # Test that we are detecting proper super errors. -class BaseClass(object): +class BaseClass: not_a_method = 42 @@ -114,12 +114,12 @@ def __init__(self): super(TimeoutExpired, self).__init__("", returncode) -class SuperWithType(object): +class SuperWithType: """type(self) may lead to recursion loop in derived classes""" def __init__(self): super(type(self), self).__init__() # [bad-super-call] -class SuperWithSelfClass(object): +class SuperWithSelfClass: """self.__class__ may lead to recursion loop in derived classes""" def __init__(self): super(self.__class__, self).__init__() # [bad-super-call] diff --git a/tests/functional/s/super/super_init_not_called.py b/tests/functional/s/super/super_init_not_called.py index 90a884b0b0..f0bfe03290 100644 --- a/tests/functional/s/super/super_init_not_called.py +++ b/tests/functional/s/super/super_init_not_called.py @@ -1,6 +1,7 @@ """Tests for super-init-not-called.""" # pylint: disable=too-few-public-methods, missing-class-docstring +import abc import ctypes @@ -53,5 +54,45 @@ def __init__(self): # [super-init-not-called] # Regression test as reported in # https://github.com/PyCQA/pylint/issues/6027 class MyUnion(ctypes.Union): - def __init__(self): # [super-init-not-called] + def __init__(self): pass + + +# Should not be called on abstract __init__ methods +# https://github.com/PyCQA/pylint/issues/3975 +class Base: + def __init__(self, param: int, param_two: str) -> None: + raise NotImplementedError() + + +class Derived(Base): + def __init__(self, param: int, param_two: str) -> None: + self.param = param + 1 + self.param_two = param_two[::-1] + + +class AbstractBase(abc.ABC): + def __init__(self, param: int) -> None: + self.param = param + 1 + + def abstract_method(self) -> str: + """This needs to be implemented.""" + raise NotImplementedError() + + +class DerivedFromAbstract(AbstractBase): + def __init__(self, param: int) -> None: # [super-init-not-called] + print("Called") + + def abstract_method(self) -> str: + return "Implemented" + + +class DerivedFrom(UnknownParent): # [undefined-variable] + def __init__(self) -> None: + print("Called") + + +class DerivedFromUnknownGrandparent(DerivedFrom): + def __init__(self) -> None: + DerivedFrom.__init__(self) diff --git a/tests/functional/s/super/super_init_not_called.rc b/tests/functional/s/super/super_init_not_called.rc new file mode 100644 index 0000000000..b8621ee577 --- /dev/null +++ b/tests/functional/s/super/super_init_not_called.rc @@ -0,0 +1,4 @@ +[testoptions] +# ctypes has a different implementation in PyPy and does have an inferable +# __init__ method for ctypes.Union. +except_implementations=PyPy diff --git a/tests/functional/s/super/super_init_not_called.txt b/tests/functional/s/super/super_init_not_called.txt index aafaa2023c..002db0d762 100644 --- a/tests/functional/s/super/super_init_not_called.txt +++ b/tests/functional/s/super/super_init_not_called.txt @@ -1,3 +1,4 @@ -undefined-variable:18:23:18:40:UninferableChild:Undefined variable 'UninferableParent':UNDEFINED -super-init-not-called:49:4:49:16:ChildThree.__init__:__init__ method from base class 'ParentWithoutInit' is not called:INFERENCE -super-init-not-called:56:4:56:16:MyUnion.__init__:__init__ method from base class 'Union' is not called:INFERENCE +undefined-variable:19:23:19:40:UninferableChild:Undefined variable 'UninferableParent':UNDEFINED +super-init-not-called:50:4:50:16:ChildThree.__init__:__init__ method from base class 'ParentWithoutInit' is not called:INFERENCE +super-init-not-called:84:4:84:16:DerivedFromAbstract.__init__:__init__ method from base class 'AbstractBase' is not called:INFERENCE +undefined-variable:91:18:91:31:DerivedFrom:Undefined variable 'UnknownParent':UNDEFINED diff --git a/tests/functional/s/superfluous_parens.py b/tests/functional/s/superfluous_parens.py index db9349ccea..35acfd5d31 100644 --- a/tests/functional/s/superfluous_parens.py +++ b/tests/functional/s/superfluous_parens.py @@ -47,13 +47,6 @@ def function_B(var): def function_C(first, second): return (first or second) in (0, 1) -# TODO: Test string combinations, see https://github.com/PyCQA/pylint/issues/4792 -# The lines with "+" should raise the superfluous-parens message -J = "TestString" -K = ("Test " + "String") -L = ("Test " + "String") in I -assert "" + ("Version " + "String") in I - # Test numpy def function_numpy_A(var_1: int, var_2: int) -> np.ndarray: result = (((var_1 & var_2)) > 0) @@ -72,6 +65,19 @@ def __iter__(self): if (A == 2) is not (B == 2): pass +K = ("Test " + "String") # [superfluous-parens] M = A is not (A <= H) M = True is not (M == K) M = True is not (True is not False) # pylint: disable=comparison-of-constants + +Z = "TestString" +X = ("Test " + "String") # [superfluous-parens] +Y = ("Test " + "String") in Z # [superfluous-parens] +assert ("Test " + "String") in "hello" # [superfluous-parens] +assert ("Version " + "String") in ("Version " + "String") # [superfluous-parens] + +hi = ("CONST") # [superfluous-parens] +hi = ("CONST",) + +#TODO: maybe get this line to report [superfluous-parens] without causing other false positives. +assert "" + ("Version " + "String") in Z diff --git a/tests/functional/s/superfluous_parens.txt b/tests/functional/s/superfluous_parens.txt index f830922bc6..08b2dd3904 100644 --- a/tests/functional/s/superfluous_parens.txt +++ b/tests/functional/s/superfluous_parens.txt @@ -4,3 +4,9 @@ superfluous-parens:12:0:None:None::Unnecessary parens after 'for' keyword:UNDEFI superfluous-parens:14:0:None:None::Unnecessary parens after 'if' keyword:UNDEFINED superfluous-parens:19:0:None:None::Unnecessary parens after 'del' keyword:UNDEFINED superfluous-parens:31:0:None:None::Unnecessary parens after 'assert' keyword:UNDEFINED +superfluous-parens:68:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED +superfluous-parens:74:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED +superfluous-parens:75:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED +superfluous-parens:76:0:None:None::Unnecessary parens after 'assert' keyword:UNDEFINED +superfluous-parens:77:0:None:None::Unnecessary parens after 'assert' keyword:UNDEFINED +superfluous-parens:79:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED diff --git a/tests/functional/s/superfluous_parens_walrus_py38.py b/tests/functional/s/superfluous_parens_walrus_py38.py index cf155954e1..7922e4613f 100644 --- a/tests/functional/s/superfluous_parens_walrus_py38.py +++ b/tests/functional/s/superfluous_parens_walrus_py38.py @@ -1,5 +1,5 @@ """Test the superfluous-parens warning with python 3.8 functionality (walrus operator)""" -# pylint: disable=missing-function-docstring, invalid-name, missing-class-docstring, import-error +# pylint: disable=missing-function-docstring, invalid-name, missing-class-docstring, import-error, pointless-statement,named-expr-without-context import numpy # Test parens in if statements @@ -49,3 +49,25 @@ def function_B(cls): @classmethod def function_C(cls): yield (1 + 1) # [superfluous-parens] + + +if (x := "Test " + "String"): + print(x) + +if (x := ("Test " + "String")): # [superfluous-parens] + print(x) + +if not (foo := "Test " + "String" in "hello"): + print(foo) + +if not (foo := ("Test " + "String") in "hello"): # [superfluous-parens] + print(foo) + +assert (ret := "Test " + "String") +assert (ret := ("Test " + "String")) # [superfluous-parens] + +(walrus := False) +(walrus := (False)) # [superfluous-parens] + +(hi := ("CONST")) # [superfluous-parens] +(hi := ("CONST",)) diff --git a/tests/functional/s/superfluous_parens_walrus_py38.txt b/tests/functional/s/superfluous_parens_walrus_py38.txt index 58097f5201..da8f1b9994 100644 --- a/tests/functional/s/superfluous_parens_walrus_py38.txt +++ b/tests/functional/s/superfluous_parens_walrus_py38.txt @@ -3,3 +3,8 @@ superfluous-parens:19:0:None:None::Unnecessary parens after 'if' keyword:UNDEFIN superfluous-parens:22:0:None:None::Unnecessary parens after 'not' keyword:UNDEFINED superfluous-parens:25:0:None:None::Unnecessary parens after 'not' keyword:UNDEFINED superfluous-parens:51:0:None:None::Unnecessary parens after 'yield' keyword:UNDEFINED +superfluous-parens:57:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED +superfluous-parens:63:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED +superfluous-parens:67:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED +superfluous-parens:70:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED +superfluous-parens:72:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED diff --git a/tests/functional/s/suspicious_str_strip_call.py b/tests/functional/s/suspicious_str_strip_call.py index 40684cc1df..1c49ca5341 100644 --- a/tests/functional/s/suspicious_str_strip_call.py +++ b/tests/functional/s/suspicious_str_strip_call.py @@ -1,6 +1,6 @@ """Suspicious str.strip calls.""" # pylint: disable=redundant-u-string-prefix -__revision__ = 1 + ''.strip('yo') ''.strip() diff --git a/tests/functional/s/syntax/syntax_error.py b/tests/functional/s/syntax/syntax_error.py index c93df6b05d..a520401125 100644 --- a/tests/functional/s/syntax/syntax_error.py +++ b/tests/functional/s/syntax/syntax_error.py @@ -1 +1 @@ -def toto # [syntax-error] +for # [syntax-error] diff --git a/tests/functional/s/syntax/syntax_error.txt b/tests/functional/s/syntax/syntax_error.txt index 2dafd9eb35..3640717728 100644 --- a/tests/functional/s/syntax/syntax_error.txt +++ b/tests/functional/s/syntax/syntax_error.txt @@ -1 +1 @@ -syntax-error:1:10:None:None::invalid syntax (, line 1):UNDEFINED +syntax-error:1:5:None:None::"Parsing failed: 'invalid syntax (, line 1)'":HIGH diff --git a/tests/functional/t/ternary.py b/tests/functional/t/ternary.py index 58171942fb..48f97ffd97 100644 --- a/tests/functional/t/ternary.py +++ b/tests/functional/t/ternary.py @@ -1,18 +1,23 @@ """Test for old ternary constructs""" -from UNINFERABLE import condition, true_value, false_value, some_callable # pylint: disable=import-error +from UNINFERABLE import condition, some_callable, maybe_true, maybe_false # pylint: disable=import-error -SOME_VALUE1 = true_value if condition else false_value -SOME_VALUE2 = condition and true_value or false_value # [consider-using-ternary] +TRUE_VALUE = True +FALSE_VALUE = False + +SOME_VALUE1 = TRUE_VALUE if condition else FALSE_VALUE +SOME_VALUE2 = condition and TRUE_VALUE or FALSE_VALUE # [consider-using-ternary] +NOT_SIMPLIFIABLE_1 = maybe_true if condition else maybe_false +NOT_SIMPLIFIABLE_2 = condition and maybe_true or maybe_false SOME_VALUE3 = condition def func1(): """Ternary return value correct""" - return true_value if condition else false_value + return TRUE_VALUE if condition else FALSE_VALUE def func2(): """Ternary return value incorrect""" - return condition and true_value or false_value # [consider-using-ternary] + return condition and TRUE_VALUE or FALSE_VALUE # [consider-using-ternary] SOME_VALUE4 = some_callable(condition) and 'ERROR' or 'SUCCESS' # [consider-using-ternary] @@ -30,10 +35,23 @@ def func2(): def func4(): """"Using a Name as a condition but still emits""" truth_value = 42 - return condition and truth_value or false_value # [consider-using-ternary] + return condition and truth_value or FALSE_VALUE # [consider-using-ternary] def func5(): """"Using a Name that infers to False as a condition does not emit""" falsy_value = False - return condition and falsy_value or false_value # [simplify-boolean-expression] + return condition and falsy_value or FALSE_VALUE # [simplify-boolean-expression] + + +def func_control_flow(): + """Redefining variables should invalidate simplify-boolean-expression.""" + flag_a = False + flag_b = False + for num in range(2): + if num == 1: + flag_a = True + else: + flag_b = True + multiple = (flag_a and flag_b) or func5() + return multiple diff --git a/tests/functional/t/ternary.txt b/tests/functional/t/ternary.txt index bdec7bcc43..ca93acd2fa 100644 --- a/tests/functional/t/ternary.txt +++ b/tests/functional/t/ternary.txt @@ -1,8 +1,8 @@ -consider-using-ternary:5:0:5:53::Consider using ternary (true_value if condition else false_value):UNDEFINED -consider-using-ternary:15:4:15:50:func2:Consider using ternary (true_value if condition else false_value):UNDEFINED -consider-using-ternary:18:0:18:63::Consider using ternary ('ERROR' if some_callable(condition) else 'SUCCESS'):UNDEFINED -consider-using-ternary:19:0:19:60::Consider using ternary ('greater' if SOME_VALUE1 > 3 else 'not greater'):UNDEFINED -consider-using-ternary:20:0:20:67::Consider using ternary ('both' if SOME_VALUE2 > 4 and SOME_VALUE3 else 'not'):UNDEFINED -simplify-boolean-expression:23:0:23:50::Boolean expression may be simplified to SOME_VALUE2:UNDEFINED -consider-using-ternary:33:4:33:51:func4:Consider using ternary (truth_value if condition else false_value):UNDEFINED -simplify-boolean-expression:39:4:39:51:func5:Boolean expression may be simplified to false_value:UNDEFINED +consider-using-ternary:8:0:8:53::Consider using ternary (TRUE_VALUE if condition else FALSE_VALUE):INFERENCE +consider-using-ternary:20:4:20:50:func2:Consider using ternary (TRUE_VALUE if condition else FALSE_VALUE):INFERENCE +consider-using-ternary:23:0:23:63::Consider using ternary ('ERROR' if some_callable(condition) else 'SUCCESS'):INFERENCE +consider-using-ternary:24:0:24:60::Consider using ternary ('greater' if SOME_VALUE1 > 3 else 'not greater'):INFERENCE +consider-using-ternary:25:0:25:67::Consider using ternary ('both' if SOME_VALUE2 > 4 and SOME_VALUE3 else 'not'):INFERENCE +simplify-boolean-expression:28:0:28:50::Boolean expression may be simplified to SOME_VALUE2:INFERENCE +consider-using-ternary:38:4:38:51:func4:Consider using ternary (truth_value if condition else FALSE_VALUE):INFERENCE +simplify-boolean-expression:44:4:44:51:func5:Boolean expression may be simplified to FALSE_VALUE:INFERENCE diff --git a/tests/functional/t/test_compile.py b/tests/functional/t/test_compile.py index d4a4ac2aaa..78f0870114 100644 --- a/tests/functional/t/test_compile.py +++ b/tests/functional/t/test_compile.py @@ -1,6 +1,6 @@ -# pylint: disable=missing-docstring, unused-variable, pointless-statement, too-few-public-methods, useless-object-inheritance +# pylint: disable=missing-docstring, unused-variable, pointless-statement, too-few-public-methods -class WrapperClass(object): +class WrapperClass: def method(self): var = +4294967296 self.method.__code__.co_consts diff --git a/tests/functional/t/too/too_few_public_methods.py b/tests/functional/t/too/too_few_public_methods.py index 5ba528f798..0a822231ae 100644 --- a/tests/functional/t/too/too_few_public_methods.py +++ b/tests/functional/t/too/too_few_public_methods.py @@ -1,11 +1,10 @@ -# pylint: disable=missing-docstring, useless-object-inheritance -from __future__ import print_function +# pylint: disable=missing-docstring from enum import Enum -class Aaaa(object): # [too-few-public-methods] +class Aaaa: # [too-few-public-methods] def __init__(self): pass @@ -18,7 +17,7 @@ def _dontcount(self): # Don't emit for these cases. -class Klass(object): +class Klass: """docstring""" def meth1(self): diff --git a/tests/functional/t/too/too_few_public_methods.txt b/tests/functional/t/too/too_few_public_methods.txt index 24f46d08f9..fc355abf73 100644 --- a/tests/functional/t/too/too_few_public_methods.txt +++ b/tests/functional/t/too/too_few_public_methods.txt @@ -1 +1 @@ -too-few-public-methods:8:0:8:10:Aaaa:Too few public methods (1/2):UNDEFINED +too-few-public-methods:7:0:7:10:Aaaa:Too few public methods (1/2):UNDEFINED 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 55fc544840..3b63a8fecf 100644 --- a/tests/functional/t/too/too_few_public_methods_37.py +++ b/tests/functional/t/too/too_few_public_methods_37.py @@ -1,4 +1,9 @@ # pylint: disable=missing-docstring + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + import dataclasses import typing from dataclasses import dataclass diff --git a/tests/functional/t/too/too_many_ancestors.py b/tests/functional/t/too/too_many_ancestors.py index 2c105ba283..a460e4f16e 100644 --- a/tests/functional/t/too/too_many_ancestors.py +++ b/tests/functional/t/too/too_many_ancestors.py @@ -1,21 +1,21 @@ -# pylint: disable=missing-docstring, too-few-public-methods, useless-object-inheritance, arguments-differ +# pylint: disable=missing-docstring, too-few-public-methods, arguments-differ from collections.abc import MutableSequence -class Aaaa(object): +class Aaaa: pass -class Bbbb(object): +class Bbbb: pass -class Cccc(object): +class Cccc: pass -class Dddd(object): +class Dddd: pass -class Eeee(object): +class Eeee: pass -class Ffff(object): +class Ffff: pass -class Gggg(object): +class Gggg: pass -class Hhhh(object): +class Hhhh: pass class Iiii(Aaaa, Bbbb, Cccc, Dddd, Eeee, Ffff, Gggg, Hhhh): # [too-many-ancestors] diff --git a/tests/functional/t/too/too_many_instance_attributes.py b/tests/functional/t/too/too_many_instance_attributes.py index 565b8ec101..3177dc8aa8 100644 --- a/tests/functional/t/too/too_many_instance_attributes.py +++ b/tests/functional/t/too/too_many_instance_attributes.py @@ -1,7 +1,11 @@ -# pylint: disable=missing-docstring, too-few-public-methods, useless-object-inheritance +# pylint: disable=missing-docstring, too-few-public-methods +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements -class Aaaa(object): # [too-many-instance-attributes] + +class Aaaa: # [too-many-instance-attributes] def __init__(self): self.aaaa = 1 diff --git a/tests/functional/t/too/too_many_instance_attributes.txt b/tests/functional/t/too/too_many_instance_attributes.txt index d3f1282228..4c67ec5a30 100644 --- a/tests/functional/t/too/too_many_instance_attributes.txt +++ b/tests/functional/t/too/too_many_instance_attributes.txt @@ -1 +1 @@ -too-many-instance-attributes:4:0:4:10:Aaaa:Too many instance attributes (21/7):UNDEFINED +too-many-instance-attributes:8:0:8:10:Aaaa:Too many instance attributes (21/7):UNDEFINED diff --git a/tests/functional/t/too/too_many_instance_attributes_py37.py b/tests/functional/t/too/too_many_instance_attributes_py37.py index 9044757838..152bb1ca22 100644 --- a/tests/functional/t/too/too_many_instance_attributes_py37.py +++ b/tests/functional/t/too/too_many_instance_attributes_py37.py @@ -1,8 +1,17 @@ -# pylint: disable=missing-docstring, too-few-public-methods, useless-object-inheritance +""" +InitVars should not count as instance attributes (see issue #3754) +Default max_instance_attributes is 7 +""" + +# pylint: disable=missing-docstring, too-few-public-methods + +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from dataclasses import dataclass, InitVar -# InitVars should not count as instance attributes (see issue #3754) -# Default max_instance_attributes is 7 + @dataclass class Hello: a_1: int diff --git a/tests/functional/t/too/too_many_lines.py b/tests/functional/t/too/too_many_lines.py index 78e568c23d..79d657775f 100644 --- a/tests/functional/t/too/too_many_lines.py +++ b/tests/functional/t/too/too_many_lines.py @@ -1,6 +1,6 @@ # pylint: disable=missing-docstring # -1: [too-many-lines] -__revision__ = 0 + diff --git a/tests/functional/t/too/too_many_lines_disabled.py b/tests/functional/t/too/too_many_lines_disabled.py index 1b0866db26..b338c0c874 100644 --- a/tests/functional/t/too/too_many_lines_disabled.py +++ b/tests/functional/t/too/too_many_lines_disabled.py @@ -3,7 +3,7 @@ # pylint: disable=too-many-lines -__revision__ = 0 + diff --git a/tests/functional/t/too/too_many_locals.py b/tests/functional/t/too/too_many_locals.py index 332019a1c5..cd37e53cb0 100644 --- a/tests/functional/t/too/too_many_locals.py +++ b/tests/functional/t/too/too_many_locals.py @@ -1,5 +1,5 @@ # pylint: disable=missing-docstring -from __future__ import print_function + def function(arg1, arg2, arg3, arg4, arg5): # [too-many-locals] arg6, arg7, arg8, arg9 = arg1, arg2, arg3, arg4 diff --git a/tests/functional/t/too/too_many_public_methods.py b/tests/functional/t/too/too_many_public_methods.py index 11b4b367d9..5f994a5531 100644 --- a/tests/functional/t/too/too_many_public_methods.py +++ b/tests/functional/t/too/too_many_public_methods.py @@ -1,6 +1,6 @@ -# pylint: disable=missing-docstring, useless-object-inheritance +# pylint: disable=missing-docstring -class Aaaa(object): # [too-many-public-methods] +class Aaaa: # [too-many-public-methods] def __init__(self): pass diff --git a/tests/functional/t/too/too_many_statements.py b/tests/functional/t/too/too_many_statements.py index 7de72bc2c9..c71f5a3d0a 100644 --- a/tests/functional/t/too/too_many_statements.py +++ b/tests/functional/t/too/too_many_statements.py @@ -1,6 +1,5 @@ # pylint: disable=missing-docstring, invalid-name -from __future__ import print_function def stupid_function(arg): # [too-many-statements] if arg == 1: diff --git a/tests/functional/t/too/too_many_statements.txt b/tests/functional/t/too/too_many_statements.txt index b78c4b9a54..fc2fc1aba7 100644 --- a/tests/functional/t/too/too_many_statements.txt +++ b/tests/functional/t/too/too_many_statements.txt @@ -1,3 +1,3 @@ -too-many-statements:5:0:5:19:stupid_function:Too many statements (55/5):UNDEFINED -too-many-statements:62:0:62:33:long_function_with_inline_def:Too many statements (62/5):UNDEFINED -too-many-statements:128:0:128:20:exmaple_function:Too many statements (6/5):UNDEFINED +too-many-statements:4:0:4:19:stupid_function:Too many statements (55/5):UNDEFINED +too-many-statements:61:0:61:33:long_function_with_inline_def:Too many statements (62/5):UNDEFINED +too-many-statements:127:0:127:20:exmaple_function:Too many statements (6/5):UNDEFINED diff --git a/tests/functional/t/trailing_whitespaces.py b/tests/functional/t/trailing_whitespaces.py index 48de809c61..c88b7ea62c 100644 --- a/tests/functional/t/trailing_whitespaces.py +++ b/tests/functional/t/trailing_whitespaces.py @@ -1,11 +1,41 @@ """Regression test for trailing-whitespace (C0303).""" -# pylint: disable=mixed-line-endings -from __future__ import print_function +# pylint: disable=mixed-line-endings,pointless-string-statement # +1: [trailing-whitespace] print('some trailing whitespace') # +1: [trailing-whitespace] print('trailing whitespace does not count towards the line length limit') -print('windows line ends are ok') -# +1: [trailing-whitespace] -print('but trailing whitespace on win is not') +print('windows line ends are ok') +# +1: [trailing-whitespace] +print('but trailing whitespace on win is not') + +# Regression test for https://github.com/PyCQA/pylint/issues/6936 +# +2: [trailing-whitespace] +""" This module has the Board class. +""" + +# +3: [trailing-whitespace] +""" This module has the Board class. +It's a very nice Board. +""" + +# Regression test for https://github.com/PyCQA/pylint/issues/3822 +def example(*args): + """Example function.""" + print(*args) + + +example( + "bob", """ + foobar + more text +""", +) + +example( + "bob", + """ + foobar2 + more text +""", +) diff --git a/tests/functional/t/trailing_whitespaces.txt b/tests/functional/t/trailing_whitespaces.txt index 766a659515..913e29a5a0 100644 --- a/tests/functional/t/trailing_whitespaces.txt +++ b/tests/functional/t/trailing_whitespaces.txt @@ -1,3 +1,5 @@ -trailing-whitespace:6:33:None:None::Trailing whitespace:UNDEFINED -trailing-whitespace:8:73:None:None::Trailing whitespace:UNDEFINED -trailing-whitespace:11:46:None:None::Trailing whitespace:UNDEFINED +trailing-whitespace:5:33:None:None::Trailing whitespace:HIGH +trailing-whitespace:7:73:None:None::Trailing whitespace:HIGH +trailing-whitespace:10:46:None:None::Trailing whitespace:HIGH +trailing-whitespace:15:3:None:None::Trailing whitespace:HIGH +trailing-whitespace:20:3:None:None::Trailing whitespace:HIGH diff --git a/tests/functional/t/try_except_raise.py b/tests/functional/t/try_except_raise.py index 006a29bf91..b82a4bba15 100644 --- a/tests/functional/t/try_except_raise.py +++ b/tests/functional/t/try_except_raise.py @@ -1,5 +1,5 @@ # pylint:disable=missing-docstring, unreachable, bad-except-order, bare-except, unnecessary-pass -# pylint: disable=undefined-variable, broad-except, raise-missing-from +# pylint: disable=undefined-variable, broad-except, raise-missing-from, too-few-public-methods try: int("9a") except: # [try-except-raise] @@ -81,6 +81,18 @@ def ddd(): except OSError: print("a failure") +class NameSpace: + error1 = FileNotFoundError + error2 = PermissionError + parent_error=OSError + +try: + pass +except (NameSpace.error1, NameSpace.error2): + raise +except NameSpace.parent_error: + print("a failure") + # also consider tuples for subsequent exception handler instead of just bare except handler try: pass diff --git a/tests/functional/t/try_except_raise.txt b/tests/functional/t/try_except_raise.txt index 20181fa7f0..7145e07b72 100644 --- a/tests/functional/t/try_except_raise.txt +++ b/tests/functional/t/try_except_raise.txt @@ -3,4 +3,4 @@ try-except-raise:16:0:18:29::The except handler raises immediately:UNDEFINED try-except-raise:53:4:54:13:ddd:The except handler raises immediately:UNDEFINED try-except-raise:67:0:68:9::The except handler raises immediately:UNDEFINED try-except-raise:72:0:73:9::The except handler raises immediately:UNDEFINED -try-except-raise:94:0:95:9::The except handler raises immediately:UNDEFINED +try-except-raise:106:0:107:9::The except handler raises immediately:UNDEFINED diff --git a/tests/functional/t/typevar_naming_style_default.py b/tests/functional/t/typevar_naming_style_default.py index f060115c2e..de614b9419 100644 --- a/tests/functional/t/typevar_naming_style_default.py +++ b/tests/functional/t/typevar_naming_style_default.py @@ -1,7 +1,7 @@ """Test case for typevar-name-incorrect-variance with default settings""" # pylint: disable=too-few-public-methods,line-too-long - from typing import TypeVar +import typing_extensions as te # PascalCase names with prefix GoodNameT = TypeVar("GoodNameT") @@ -31,11 +31,16 @@ AnyStr = TypeVar("AnyStr") DeviceTypeT = TypeVar("DeviceTypeT") HVACModeT = TypeVar("HVACModeT") +TodoT = TypeVar("TodoT") +TypeT = TypeVar("TypeT") _IPAddress = TypeVar("_IPAddress") CALLABLE_T = TypeVar("CALLABLE_T") # [invalid-name] DeviceType = TypeVar("DeviceType") # [invalid-name] IPAddressU = TypeVar("IPAddressU") # [invalid-name] +# Wrong prefix +TAnyStr = TypeVar("TAnyStr") # [invalid-name] + # camelCase names with prefix badName = TypeVar("badName") # [invalid-name] badName_co = TypeVar("badName_co", covariant=True) # [invalid-name] @@ -50,3 +55,10 @@ "GoodName_co", covariant=True ), TypeVar("a_BadName_contra", contravariant=True) GoodName_co, VAR = TypeVar("GoodName_co", covariant=True), "a string" + + +# -- typing_extensions.TypeVar -- +GoodNameT = te.TypeVar("GoodNameT") +GoodNameT_co = te.TypeVar("GoodNameT_co", covariant=True) +badName = te.TypeVar("badName") # [invalid-name] +T_co = te.TypeVar("T_co", covariant=True, contravariant=True) # [typevar-double-variance,typevar-name-incorrect-variance] diff --git a/tests/functional/t/typevar_naming_style_default.txt b/tests/functional/t/typevar_naming_style_default.txt index 8c4c1159ca..1bddf271ee 100644 --- a/tests/functional/t/typevar_naming_style_default.txt +++ b/tests/functional/t/typevar_naming_style_default.txt @@ -5,13 +5,17 @@ typevar-double-variance:23:0:23:4::TypeVar cannot be both covariant and contrava typevar-name-incorrect-variance:23:0:23:4::Type variable name does not reflect variance:INFERENCE typevar-double-variance:24:0:24:8::TypeVar cannot be both covariant and contravariant:INFERENCE typevar-name-incorrect-variance:24:0:24:8::Type variable name does not reflect variance:INFERENCE -invalid-name:35:0:35:10::"Type variable name ""CALLABLE_T"" doesn't conform to predefined naming style":HIGH -invalid-name:36:0:36:10::"Type variable name ""DeviceType"" doesn't conform to predefined naming style":HIGH -invalid-name:37:0:37:10::"Type variable name ""IPAddressU"" doesn't conform to predefined naming style":HIGH -invalid-name:40:0:40:7::"Type variable name ""badName"" doesn't conform to predefined naming style":HIGH -invalid-name:41:0:41:10::"Type variable name ""badName_co"" doesn't conform to predefined naming style":HIGH -invalid-name:42:0:42:14::"Type variable name ""badName_contra"" doesn't conform to predefined naming style":HIGH -invalid-name:46:4:46:13::"Type variable name ""a_BadName"" doesn't conform to predefined naming style":HIGH -invalid-name:47:4:47:26::"Type variable name ""a_BadNameWithoutContra"" doesn't conform to predefined naming style":HIGH -typevar-name-incorrect-variance:47:4:47:26::"Type variable name does not reflect variance. ""a_BadNameWithoutContra"" is contravariant, use ""a_BadNameWithoutContra_contra"" instead":INFERENCE -invalid-name:49:13:49:29::"Type variable name ""a_BadName_contra"" doesn't conform to predefined naming style":HIGH +invalid-name:37:0:37:10::"Type variable name ""CALLABLE_T"" doesn't conform to predefined naming style":HIGH +invalid-name:38:0:38:10::"Type variable name ""DeviceType"" doesn't conform to predefined naming style":HIGH +invalid-name:39:0:39:10::"Type variable name ""IPAddressU"" doesn't conform to predefined naming style":HIGH +invalid-name:42:0:42:7::"Type variable name ""TAnyStr"" doesn't conform to predefined naming style":HIGH +invalid-name:45:0:45:7::"Type variable name ""badName"" doesn't conform to predefined naming style":HIGH +invalid-name:46:0:46:10::"Type variable name ""badName_co"" doesn't conform to predefined naming style":HIGH +invalid-name:47:0:47:14::"Type variable name ""badName_contra"" doesn't conform to predefined naming style":HIGH +invalid-name:51:4:51:13::"Type variable name ""a_BadName"" doesn't conform to predefined naming style":HIGH +invalid-name:52:4:52:26::"Type variable name ""a_BadNameWithoutContra"" doesn't conform to predefined naming style":HIGH +typevar-name-incorrect-variance:52:4:52:26::"Type variable name does not reflect variance. ""a_BadNameWithoutContra"" is contravariant, use ""a_BadNameWithoutContra_contra"" instead":INFERENCE +invalid-name:54:13:54:29::"Type variable name ""a_BadName_contra"" doesn't conform to predefined naming style":HIGH +invalid-name:63:0:63:7::"Type variable name ""badName"" doesn't conform to predefined naming style":HIGH +typevar-double-variance:64:0:64:4::TypeVar cannot be both covariant and contravariant:INFERENCE +typevar-name-incorrect-variance:64:0:64:4::Type variable name does not reflect variance:INFERENCE diff --git a/tests/functional/t/typevar_naming_style_rgx.py b/tests/functional/t/typevar_naming_style_rgx.py index c08eb9e41d..6751e8d849 100644 --- a/tests/functional/t/typevar_naming_style_rgx.py +++ b/tests/functional/t/typevar_naming_style_rgx.py @@ -1,6 +1,6 @@ """Test case for typevar-name-missing-variance with non-default settings""" - from typing import TypeVar +import typing_extensions as te # Name set by regex pattern TypeVarsShouldBeLikeThis = TypeVar("TypeVarsShouldBeLikeThis") @@ -13,3 +13,9 @@ GoodNameT = TypeVar("GoodNameT") # [invalid-name] GoodNameT_co = TypeVar("GoodNameT_co", covariant=True) # [invalid-name] GoodNameT_contra = TypeVar("GoodNameT_contra", contravariant=True) # [invalid-name] + + +# -- typing_extensions.TypeVar -- +TypeVarsShouldBeLikeThis = te.TypeVar("TypeVarsShouldBeLikeThis") +GoodNameT = te.TypeVar("GoodNameT") # [invalid-name] +GoodNameT_co = te.TypeVar("GoodNameT_co", covariant=True) # [invalid-name] diff --git a/tests/functional/t/typevar_naming_style_rgx.txt b/tests/functional/t/typevar_naming_style_rgx.txt index 0b291bbf37..fb89851e43 100644 --- a/tests/functional/t/typevar_naming_style_rgx.txt +++ b/tests/functional/t/typevar_naming_style_rgx.txt @@ -1,3 +1,5 @@ invalid-name:13:0:13:9::"Type variable name ""GoodNameT"" doesn't conform to 'TypeVarsShouldBeLikeThis(_co(ntra)?)?$' pattern":HIGH invalid-name:14:0:14:12::"Type variable name ""GoodNameT_co"" doesn't conform to 'TypeVarsShouldBeLikeThis(_co(ntra)?)?$' pattern":HIGH invalid-name:15:0:15:16::"Type variable name ""GoodNameT_contra"" doesn't conform to 'TypeVarsShouldBeLikeThis(_co(ntra)?)?$' pattern":HIGH +invalid-name:20:0:20:9::"Type variable name ""GoodNameT"" doesn't conform to 'TypeVarsShouldBeLikeThis(_co(ntra)?)?$' pattern":HIGH +invalid-name:21:0:21:12::"Type variable name ""GoodNameT_co"" doesn't conform to 'TypeVarsShouldBeLikeThis(_co(ntra)?)?$' pattern":HIGH diff --git a/tests/functional/u/unbalanced_dict_unpacking.py b/tests/functional/u/unbalanced_dict_unpacking.py new file mode 100644 index 0000000000..2c4d3b1031 --- /dev/null +++ b/tests/functional/u/unbalanced_dict_unpacking.py @@ -0,0 +1,91 @@ +"""Check possible unbalanced dict unpacking """ +# pylint: disable=missing-function-docstring, invalid-name +# pylint: disable=unused-variable, redefined-outer-name, line-too-long + +def dict_vals(): + a, b, c, d, e, f, g = {1: 2}.values() # [unbalanced-dict-unpacking] + return a, b + +def dict_keys(): + a, b, c, d, e, f, g = {1: 2, "hi": 20}.keys() # [unbalanced-dict-unpacking] + return a, b + + +def dict_items(): + tupe_one, tuple_two = {1: 2, "boo": 3}.items() + tupe_one, tuple_two, tuple_three = {1: 2, "boo": 3}.items() # [unbalanced-dict-unpacking] + return tuple_three + +def all_dict(): + a, b, c, d, e, f, g = {1: 2, 3: 4} # [unbalanced-dict-unpacking] + return a + +for a, b, c, d, e, f, g in {1: 2}.items(): # [unbalanced-dict-unpacking] + pass + +for key, value in {1: 2}: # [unbalanced-dict-unpacking] + pass + +for key, value in {1: 2}.keys(): # [unbalanced-dict-unpacking, consider-iterating-dictionary] + pass + +for key, value in {1: 2}.values(): # [unbalanced-dict-unpacking] + pass + +empty = {} + +# this should not raise unbalanced-dict because it is valid code using `items()` +for key, value in empty.items(): + print(key) + print(value) + +for key, val in {1: 2}.items(): + print(key) + +populated = {2: 1} +for key, val in populated.items(): + print(key) + +key, val = populated.items() # [unbalanced-dict-unpacking] + +for key, val in {1: 2, 3: 4, 5: 6}.items(): + print(key) + +key, val = {1: 2, 3: 4, 5: 6}.items() # [unbalanced-dict-unpacking] + +a, b, c = {} # [unbalanced-dict-unpacking] + +for k in {'key': 'value', 1: 2}.items(): + print(k) + +for k, _ in {'key': 'value'}.items(): + print(k) + +for _, _ in {'key': 'value'}.items(): + print(_) + +for _, val in {'key': 'value'}.values(): # [unbalanced-dict-unpacking] + print(val) + +for key, *val in {'key': 'value', 1: 2}.items(): + print(key) + +for *key, val in {'key': 'value', 1: 2}.items(): + print(key) + + +for key, *val in {'key': 'value', 1: 2, 20: 21}.values(): # [unbalanced-dict-unpacking] + print(key) + +for *key, val in {'key': 'value', 1: 2, 20: 21}.values(): # [unbalanced-dict-unpacking] + print(key) + +one, *others = {1: 2, 3: 4, 5: 6}.items() +one, *others, last = {1: 2, 3: 4, 5: 6}.items() + +one, *others = {1: 2, 3: 4, 5: 6}.values() +one, *others, last = {1: 2, 3: 4, 5: 6}.values() + +_, *others = {1: 2, 3: 4, 5: 6}.items() +_, *others = {1: 2, 3: 4, 5: 6}.values() +_, others = {1: 2, 3: 4, 5: 6}.values() # [unbalanced-dict-unpacking] diff --git a/tests/functional/u/unbalanced_dict_unpacking.txt b/tests/functional/u/unbalanced_dict_unpacking.txt new file mode 100644 index 0000000000..b31d89b401 --- /dev/null +++ b/tests/functional/u/unbalanced_dict_unpacking.txt @@ -0,0 +1,16 @@ +unbalanced-dict-unpacking:6:4:6:41:dict_vals:"Possible unbalanced dict unpacking with {1: 2}.values(): left side has 7 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:10:4:10:49:dict_keys:"Possible unbalanced dict unpacking with {1: 2, 'hi': 20}.keys(): left side has 7 labels, right side has 2 values":INFERENCE +unbalanced-dict-unpacking:16:4:16:63:dict_items:"Possible unbalanced dict unpacking with {1: 2, 'boo': 3}.items(): left side has 3 labels, right side has 2 values":INFERENCE +unbalanced-dict-unpacking:20:4:20:38:all_dict:"Possible unbalanced dict unpacking with {1: 2, 3: 4}: left side has 7 labels, right side has 2 values":INFERENCE +unbalanced-dict-unpacking:23:0:24:8::"Possible unbalanced dict unpacking with {1: 2}.items(): left side has 7 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:26:0:27:8::"Possible unbalanced dict unpacking with {1: 2}: left side has 2 labels, right side has 1 value":INFERENCE +consider-iterating-dictionary:29:18:29:31::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +unbalanced-dict-unpacking:29:0:30:8::"Possible unbalanced dict unpacking with {1: 2}.keys(): left side has 2 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:32:0:33:8::"Possible unbalanced dict unpacking with {1: 2}.values(): left side has 2 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:49:0:49:28::"Possible unbalanced dict unpacking with populated.items(): left side has 2 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:54:0:54:37::"Possible unbalanced dict unpacking with {1: 2, 3: 4, 5: 6}.items(): left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-dict-unpacking:56:0:56:12::"Possible unbalanced dict unpacking with {}: left side has 3 labels, right side has 0 values":INFERENCE +unbalanced-dict-unpacking:67:0:68:14::"Possible unbalanced dict unpacking with {'key': 'value'}.values(): left side has 2 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:77:0:78:14::"Possible unbalanced dict unpacking with {'key': 'value', 1: 2, 20: 21}.values(): left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-dict-unpacking:80:0:81:14::"Possible unbalanced dict unpacking with {'key': 'value', 1: 2, 20: 21}.values(): left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-dict-unpacking:91:0:91:39::"Possible unbalanced dict unpacking with {1: 2, 3: 4, 5: 6}.values(): left side has 2 labels, right side has 3 values":INFERENCE diff --git a/tests/functional/u/unbalanced_tuple_unpacking.py b/tests/functional/u/unbalanced_tuple_unpacking.py index a4b72f8a80..2267489339 100644 --- a/tests/functional/u/unbalanced_tuple_unpacking.py +++ b/tests/functional/u/unbalanced_tuple_unpacking.py @@ -3,7 +3,7 @@ from typing import NamedTuple from functional.u.unpacking.unpacking import unpack -# pylint: disable=missing-class-docstring, missing-function-docstring, using-constant-test, useless-object-inheritance,import-outside-toplevel +# pylint: disable=missing-class-docstring, missing-function-docstring, using-constant-test, import-outside-toplevel def do_stuff(): @@ -83,7 +83,7 @@ def do_stuff9(): return first + second -class UnbalancedUnpacking(object): +class UnbalancedUnpacking: """Test unbalanced tuple unpacking in instance attributes.""" # pylint: disable=attribute-defined-outside-init, invalid-name, too-few-public-methods diff --git a/tests/functional/u/unbalanced_tuple_unpacking.txt b/tests/functional/u/unbalanced_tuple_unpacking.txt index e320698477..651e098403 100644 --- a/tests/functional/u/unbalanced_tuple_unpacking.txt +++ b/tests/functional/u/unbalanced_tuple_unpacking.txt @@ -1,9 +1,9 @@ -unbalanced-tuple-unpacking:11:4:11:27:do_stuff:"Possible unbalanced tuple unpacking with sequence (1, 2, 3): left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:17:4:17:29:do_stuff1:"Possible unbalanced tuple unpacking with sequence [1, 2, 3]: left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:23:4:23:29:do_stuff2:"Possible unbalanced tuple unpacking with sequence (1, 2, 3): left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:82:4:82:28:do_stuff9:"Possible unbalanced tuple unpacking with sequence defined at line 7 of functional.u.unpacking.unpacking: left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:96:8:96:33:UnbalancedUnpacking.test:"Possible unbalanced tuple unpacking with sequence defined at line 7 of functional.u.unpacking.unpacking: left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:140:8:140:43:MyClass.sum_unpack_3_into_4:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 4 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:145:8:145:28:MyClass.sum_unpack_3_into_2:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:157:0:157:24::"Possible unbalanced tuple unpacking with sequence defined at line 151: left side has 2 label(s), right side has 0 value(s)":UNDEFINED -unbalanced-tuple-unpacking:162:0:162:16::"Possible unbalanced tuple unpacking with sequence (1, 2): left side has 3 label(s), right side has 2 value(s)":UNDEFINED +unbalanced-tuple-unpacking:11:4:11:27:do_stuff:"Possible unbalanced tuple unpacking with sequence '(1, 2, 3)': left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:17:4:17:29:do_stuff1:"Possible unbalanced tuple unpacking with sequence '[1, 2, 3]': left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:23:4:23:29:do_stuff2:"Possible unbalanced tuple unpacking with sequence '(1, 2, 3)': left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:82:4:82:28:do_stuff9:"Possible unbalanced tuple unpacking with sequence defined at line 7 of functional.u.unpacking.unpacking: left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:96:8:96:33:UnbalancedUnpacking.test:"Possible unbalanced tuple unpacking with sequence defined at line 7 of functional.u.unpacking.unpacking: left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:140:8:140:43:MyClass.sum_unpack_3_into_4:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 4 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:145:8:145:28:MyClass.sum_unpack_3_into_2:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:157:0:157:24::"Possible unbalanced tuple unpacking with sequence defined at line 151: left side has 2 labels, right side has 0 values":INFERENCE +unbalanced-tuple-unpacking:162:0:162:16::"Possible unbalanced tuple unpacking with sequence '(1, 2)': left side has 3 labels, right side has 2 values":INFERENCE diff --git a/tests/functional/u/unbalanced_tuple_unpacking_py30.py b/tests/functional/u/unbalanced_tuple_unpacking_py30.py index 68f5fb79a5..c45cccdd1d 100644 --- a/tests/functional/u/unbalanced_tuple_unpacking_py30.py +++ b/tests/functional/u/unbalanced_tuple_unpacking_py30.py @@ -1,11 +1,11 @@ """ Test that using starred nodes in unpacking does not trigger a false positive on Python 3. """ - -__revision__ = 1 +# pylint: disable=unused-variable def test(): """ Test that starred expressions don't give false positives. """ first, second, *last = (1, 2, 3, 4) + one, two, three, *four = (1, 2, 3, 4) *last, = (1, 2) return (first, second, last) diff --git a/tests/functional/u/undefined/undefined_loop_variable.py b/tests/functional/u/undefined/undefined_loop_variable.py index c18d227552..9d5cf4111b 100644 --- a/tests/functional/u/undefined/undefined_loop_variable.py +++ b/tests/functional/u/undefined/undefined_loop_variable.py @@ -1,4 +1,12 @@ -# pylint: disable=missing-docstring,redefined-builtin, consider-using-f-string, unnecessary-direct-lambda-call +# pylint: disable=missing-docstring,redefined-builtin, consider-using-f-string, unnecessary-direct-lambda-call, broad-exception-raised + +import sys + +if sys.version_info >= (3, 8): + from typing import NoReturn +else: + from typing_extensions import NoReturn + def do_stuff(some_random_list): for var in some_random_list: @@ -107,6 +115,36 @@ def for_else_raises(iterable): print(thing) +def for_else_break(iterable): + while True: + for thing in iterable: + break + else: + break + print(thing) + + +def for_else_continue(iterable): + while True: + for thing in iterable: + break + else: + continue + print(thing) + + +def for_else_no_return(iterable): + def fail() -> NoReturn: + ... + + while True: + for thing in iterable: + break + else: + fail() + print(thing) + + lst = [] lst2 = [1, 2, 3] diff --git a/tests/functional/u/undefined/undefined_loop_variable.txt b/tests/functional/u/undefined/undefined_loop_variable.txt index 9cee5085d7..e10c9e0021 100644 --- a/tests/functional/u/undefined/undefined_loop_variable.txt +++ b/tests/functional/u/undefined/undefined_loop_variable.txt @@ -1,4 +1,4 @@ -undefined-loop-variable:6:11:6:14:do_stuff:Using possibly undefined loop variable 'var':UNDEFINED -undefined-loop-variable:25:7:25:11::Using possibly undefined loop variable 'var1':UNDEFINED -undefined-loop-variable:75:11:75:14:do_stuff_with_redefined_range:Using possibly undefined loop variable 'var':UNDEFINED -undefined-loop-variable:163:11:163:20:find_even_number:Using possibly undefined loop variable 'something':UNDEFINED +undefined-loop-variable:14:11:14:14:do_stuff:Using possibly undefined loop variable 'var':UNDEFINED +undefined-loop-variable:33:7:33:11::Using possibly undefined loop variable 'var1':UNDEFINED +undefined-loop-variable:83:11:83:14:do_stuff_with_redefined_range:Using possibly undefined loop variable 'var':UNDEFINED +undefined-loop-variable:201:11:201:20:find_even_number:Using possibly undefined loop variable 'something':UNDEFINED diff --git a/tests/functional/u/undefined/undefined_loop_variable_py311.py b/tests/functional/u/undefined/undefined_loop_variable_py311.py new file mode 100644 index 0000000000..93b43a5468 --- /dev/null +++ b/tests/functional/u/undefined/undefined_loop_variable_py311.py @@ -0,0 +1,17 @@ +"""Tests for undefined-loop-variable using Python 3.11 syntax.""" + +from typing import Never + + +def for_else_never(iterable): + """Test for-else with Never type.""" + + def idontreturn() -> Never: + """This function never returns.""" + + while True: + for thing in iterable: + break + else: + idontreturn() + print(thing) diff --git a/tests/functional/u/undefined/undefined_loop_variable_py311.rc b/tests/functional/u/undefined/undefined_loop_variable_py311.rc new file mode 100644 index 0000000000..56e6770585 --- /dev/null +++ b/tests/functional/u/undefined/undefined_loop_variable_py311.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.11 diff --git a/tests/functional/u/undefined/undefined_loop_variable_py38.py b/tests/functional/u/undefined/undefined_loop_variable_py38.py new file mode 100644 index 0000000000..5778df7d29 --- /dev/null +++ b/tests/functional/u/undefined/undefined_loop_variable_py38.py @@ -0,0 +1,8 @@ +"""Tests for undefined-loop-variable with assignment expressions""" + + +def walrus_in_comprehension_test(container): + """https://github.com/PyCQA/pylint/issues/7222""" + for something in container: + print(something) + print([my_test for something in container if (my_test := something)]) diff --git a/tests/functional/u/undefined/undefined_loop_variable_py38.rc b/tests/functional/u/undefined/undefined_loop_variable_py38.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/u/undefined/undefined_loop_variable_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/u/undefined/undefined_variable.py b/tests/functional/u/undefined/undefined_variable.py index c8fb541100..78a32dacc4 100644 --- a/tests/functional/u/undefined/undefined_variable.py +++ b/tests/functional/u/undefined/undefined_variable.py @@ -1,11 +1,9 @@ -# pylint: disable=missing-docstring, multiple-statements, useless-object-inheritance, import-outside-toplevel +# pylint: disable=missing-docstring, multiple-statements, import-outside-toplevel # pylint: disable=too-few-public-methods, bare-except, broad-except # pylint: disable=using-constant-test, import-error, global-variable-not-assigned, unnecessary-comprehension # pylint: disable=unnecessary-lambda-assignment -from __future__ import print_function -# pylint: disable=wrong-import-position from typing import TYPE_CHECKING DEFINED = 1 @@ -106,7 +104,7 @@ def test1(self): """ class UsingBeforeDefinition(Empty): # [used-before-assignment] """ uses Empty before definition """ - class Empty(object): + class Empty: """ no op """ return UsingBeforeDefinition @@ -116,11 +114,11 @@ class MissingAncestor1(Ancestor): """ no op """ return MissingAncestor1 -class Self(object): +class Self: """ Detect when using the same name inside the class scope. """ obj = Self # [undefined-variable] -class Self1(object): +class Self1: """ No error should be raised here. """ def test(self): @@ -128,17 +126,17 @@ def test(self): return Self1 -class Ancestor(object): +class Ancestor: """ No op """ -class Ancestor1(object): +class Ancestor1: """ No op """ NANA = BAT # [undefined-variable] del BAT # [undefined-variable] -class KeywordArgument(object): +class KeywordArgument: """Test keyword arguments.""" enable = True @@ -186,11 +184,11 @@ def test_conditional_comprehension(): return my_methods -class MyError(object): +class MyError: pass -class MyClass(object): +class MyClass: class MyError(MyError): pass @@ -269,15 +267,24 @@ def func_should_fail(_dt: datetime): # [used-before-assignment] if TYPE_CHECKING: from collections import Counter from collections import OrderedDict + from collections import defaultdict + from collections import UserDict else: Counter = object OrderedDict = object + def defaultdict(): + return {} + class UserDict(dict): + pass def tick(counter: Counter, name: str, dictionary: OrderedDict) -> OrderedDict: counter[name] += 1 return dictionary +defaultdict() + +UserDict() # pylint: disable=unused-argument def not_using_loop_variable_accordingly(iterator): diff --git a/tests/functional/u/undefined/undefined_variable.txt b/tests/functional/u/undefined/undefined_variable.txt index 1066dbda2c..ab1a004209 100644 --- a/tests/functional/u/undefined/undefined_variable.txt +++ b/tests/functional/u/undefined/undefined_variable.txt @@ -1,39 +1,39 @@ -undefined-variable:14:19:14:26::Undefined variable 'unknown':UNDEFINED -undefined-variable:20:10:20:21:in_method:Undefined variable 'nomoreknown':UNDEFINED -undefined-variable:23:19:23:31::Undefined variable '__revision__':UNDEFINED -undefined-variable:25:8:25:20::Undefined variable '__revision__':UNDEFINED -undefined-variable:29:29:29:37:bad_default:Undefined variable 'unknown2':UNDEFINED -undefined-variable:32:10:32:14:bad_default:Undefined variable 'xxxx':UNDEFINED -undefined-variable:33:4:33:10:bad_default:Undefined variable 'augvar':UNDEFINED -undefined-variable:34:8:34:14:bad_default:Undefined variable 'vardel':UNDEFINED -undefined-variable:36:19:36:31::Undefined variable 'doesnotexist':UNDEFINED -undefined-variable:37:23:37:24::Undefined variable 'z':UNDEFINED -used-before-assignment:40:4:40:9::Using variable 'POUET' before assignment:CONTROL_FLOW -used-before-assignment:45:4:45:10::Using variable 'POUETT' before assignment:CONTROL_FLOW -used-before-assignment:50:4:50:11::Using variable 'POUETTT' before assignment:CONTROL_FLOW -used-before-assignment:58:4:58:9::Using variable 'PLOUF' before assignment:CONTROL_FLOW -used-before-assignment:67:11:67:14:if_branch_test:Using variable 'xxx' before assignment:HIGH -used-before-assignment:93:23:93:32:test_arguments:Using variable 'TestClass' before assignment:HIGH -used-before-assignment:97:16:97:24:TestClass:Using variable 'Ancestor' before assignment:HIGH -used-before-assignment:100:26:100:35:TestClass.MissingAncestor:Using variable 'Ancestor1' before assignment:HIGH -used-before-assignment:107:36:107:41:TestClass.test1.UsingBeforeDefinition:Using variable 'Empty' before assignment:HIGH -undefined-variable:121:10:121:14:Self:Undefined variable 'Self':UNDEFINED -undefined-variable:137:7:137:10::Undefined variable 'BAT':UNDEFINED -undefined-variable:138:4:138:7::Undefined variable 'BAT':UNDEFINED -used-before-assignment:148:31:148:38:KeywordArgument.test1:Using variable 'enabled' before assignment:HIGH -undefined-variable:151:32:151:40:KeywordArgument.test2:Undefined variable 'disabled':UNDEFINED -undefined-variable:156:22:156:25:KeywordArgument.:Undefined variable 'arg':UNDEFINED -undefined-variable:168:4:168:13::Undefined variable 'unicode_2':UNDEFINED -undefined-variable:173:4:173:13::Undefined variable 'unicode_3':UNDEFINED -undefined-variable:228:25:228:37:LambdaClass4.:Undefined variable 'LambdaClass4':UNDEFINED -undefined-variable:236:25:236:37:LambdaClass5.:Undefined variable 'LambdaClass5':UNDEFINED -used-before-assignment:257:26:257:34:func_should_fail:Using variable 'datetime' before assignment:HIGH -undefined-variable:284:18:284:24:not_using_loop_variable_accordingly:Undefined variable 'iteree':UNDEFINED -undefined-variable:301:27:301:28:undefined_annotation:Undefined variable 'x':UNDEFINED -used-before-assignment:302:7:302:8:undefined_annotation:Using variable 'x' before assignment:HIGH -undefined-variable:332:11:332:12:decorated3:Undefined variable 'x':UNDEFINED -undefined-variable:337:19:337:20:decorated4:Undefined variable 'y':UNDEFINED -undefined-variable:358:10:358:20:global_var_mixed_assignment:Undefined variable 'GLOBAL_VAR':HIGH -undefined-variable:370:19:370:44:RepeatedReturnAnnotations.x:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED -undefined-variable:372:19:372:44:RepeatedReturnAnnotations.y:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED -undefined-variable:374:19:374:44:RepeatedReturnAnnotations.z:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED +undefined-variable:12:19:12:26::Undefined variable 'unknown':UNDEFINED +undefined-variable:18:10:18:21:in_method:Undefined variable 'nomoreknown':UNDEFINED +undefined-variable:21:19:21:31::Undefined variable '__revision__':UNDEFINED +undefined-variable:23:8:23:20::Undefined variable '__revision__':UNDEFINED +undefined-variable:27:29:27:37:bad_default:Undefined variable 'unknown2':UNDEFINED +undefined-variable:30:10:30:14:bad_default:Undefined variable 'xxxx':UNDEFINED +undefined-variable:31:4:31:10:bad_default:Undefined variable 'augvar':UNDEFINED +undefined-variable:32:8:32:14:bad_default:Undefined variable 'vardel':UNDEFINED +undefined-variable:34:19:34:31::Undefined variable 'doesnotexist':UNDEFINED +undefined-variable:35:23:35:24::Undefined variable 'z':UNDEFINED +used-before-assignment:38:4:38:9::Using variable 'POUET' before assignment:CONTROL_FLOW +used-before-assignment:43:4:43:10::Using variable 'POUETT' before assignment:CONTROL_FLOW +used-before-assignment:48:4:48:11::Using variable 'POUETTT' before assignment:CONTROL_FLOW +used-before-assignment:56:4:56:9::Using variable 'PLOUF' before assignment:CONTROL_FLOW +used-before-assignment:65:11:65:14:if_branch_test:Using variable 'xxx' before assignment:HIGH +used-before-assignment:91:23:91:32:test_arguments:Using variable 'TestClass' before assignment:HIGH +used-before-assignment:95:16:95:24:TestClass:Using variable 'Ancestor' before assignment:HIGH +used-before-assignment:98:26:98:35:TestClass.MissingAncestor:Using variable 'Ancestor1' before assignment:HIGH +used-before-assignment:105:36:105:41:TestClass.test1.UsingBeforeDefinition:Using variable 'Empty' before assignment:HIGH +undefined-variable:119:10:119:14:Self:Undefined variable 'Self':UNDEFINED +undefined-variable:135:7:135:10::Undefined variable 'BAT':UNDEFINED +undefined-variable:136:4:136:7::Undefined variable 'BAT':UNDEFINED +used-before-assignment:146:31:146:38:KeywordArgument.test1:Using variable 'enabled' before assignment:HIGH +undefined-variable:149:32:149:40:KeywordArgument.test2:Undefined variable 'disabled':UNDEFINED +undefined-variable:154:22:154:25:KeywordArgument.:Undefined variable 'arg':UNDEFINED +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:HIGH +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 +undefined-variable:339:11:339:12:decorated3:Undefined variable 'x':UNDEFINED +undefined-variable:344:19:344:20:decorated4:Undefined variable 'y':UNDEFINED +undefined-variable:365:10:365:20:global_var_mixed_assignment:Undefined variable 'GLOBAL_VAR':HIGH +undefined-variable:377:19:377:44:RepeatedReturnAnnotations.x:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED +undefined-variable:379:19:379:44:RepeatedReturnAnnotations.y:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED +undefined-variable:381:19:381:44:RepeatedReturnAnnotations.z:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED diff --git a/tests/functional/u/undefined/undefined_variable_py30.py b/tests/functional/u/undefined/undefined_variable_py30.py index a0beef00e7..ff77aaf8e8 100644 --- a/tests/functional/u/undefined/undefined_variable_py30.py +++ b/tests/functional/u/undefined/undefined_variable_py30.py @@ -1,7 +1,7 @@ """Test warnings about access to undefined variables for various Python 3 constructs. """ # pylint: disable=too-few-public-methods, import-error -# pylint: disable=wrong-import-position, invalid-metaclass, useless-object-inheritance +# pylint: disable=wrong-import-position, invalid-metaclass class Undefined: """ test various annotation problems. """ @@ -37,7 +37,7 @@ def test1(self)->ABC: # [undefined-variable] """ Triggers undefined-variable. """ -class FalsePositive342(object): +class FalsePositive342: # pylint: disable=line-too-long """ Fix some false positives found in https://bitbucket.org/logilab/pylint/issue/342/spurious-undefined-variable-for-class @@ -89,9 +89,9 @@ def used_before_assignment(*, arg): return arg + 1 # Test for #4021 # https://github.com/PyCQA/pylint/issues/4021 class MetaClass(type): - def __new__(cls, *args, parameter=None, **kwargs): + def __new__(mcs, *args, parameter=None, **kwargs): print(parameter) - return super().__new__(cls, *args, **kwargs) + return super().__new__(mcs, *args, **kwargs) class InheritingClass(metaclass=MetaClass, parameter=variable): # [undefined-variable] diff --git a/tests/functional/u/undefined/undefined_variable_py38.py b/tests/functional/u/undefined/undefined_variable_py38.py index 93ba1eb2ba..6fb543e800 100644 --- a/tests/functional/u/undefined/undefined_variable_py38.py +++ b/tests/functional/u/undefined/undefined_variable_py38.py @@ -3,6 +3,7 @@ # Tests for annotation of variables and potentially undefinition +from typing import TYPE_CHECKING def typing_and_assignment_expression(): """The variable gets assigned in an assignment expression""" @@ -14,7 +15,13 @@ def typing_and_assignment_expression(): def typing_and_self_referencing_assignment_expression(): """The variable gets assigned in an assignment expression that references itself""" var: int - if (var := var ** 2): # false negative: https://github.com/PyCQA/pylint/issues/5653 + if (var := var ** 2): # [used-before-assignment] + print(var) + + +def self_referencing_assignment_expression(): + """An invalid self-referencing assignment expression""" + if (var := var()): # [used-before-assignment] print(var) @@ -166,6 +173,44 @@ def expression_in_ternary_operator_inside_container_tuple(): return [(val3, val3) if (val3 := 'something') else 'anything'] -def expression_in_ternary_operator_inside_container_wrong_position(): - """2-element list where named expression comes too late""" - return [val3, val3 if (val3 := 'something') else 'anything'] # [used-before-assignment] +def expression_in_ternary_operator_inside_container_later_position(): + """ + Named expression follows unrelated item in container. + + If 23 is replaced with `val3`, there is currently a false negative, + but the false positive here is more important and likely to occur.""" + return [23, val3 if (val3 := 'something') else 'anything'] + + +# Self-referencing +if (z := z): # [used-before-assignment] + z = z + 1 + + +if (defined := False): + NEVER_DEFINED = 1 +print(defined) +print(NEVER_DEFINED) # [used-before-assignment] + +if (still_defined := False) == 1: + NEVER_DEFINED_EITHER = 1 +print(still_defined) + + +if TYPE_CHECKING: + import enum + import weakref +elif input(): + if input() + 1: + pass + elif (enum := None): + pass + else: + print(None if (weakref := '') else True) +else: + pass + +def defined_by_walrus_in_type_checking() -> weakref: + """Usage of variables defined in TYPE_CHECKING blocks""" + print(enum) + return weakref diff --git a/tests/functional/u/undefined/undefined_variable_py38.txt b/tests/functional/u/undefined/undefined_variable_py38.txt index e912cbbf98..1674707a57 100644 --- a/tests/functional/u/undefined/undefined_variable_py38.txt +++ b/tests/functional/u/undefined/undefined_variable_py38.txt @@ -1,7 +1,10 @@ -undefined-variable:42:6:42:16::Undefined variable 'no_default':UNDEFINED -undefined-variable:50:6:50:22::Undefined variable 'again_no_default':UNDEFINED -undefined-variable:76:6:76:19::Undefined variable 'else_assign_1':INFERENCE -undefined-variable:99:6:99:19::Undefined variable 'else_assign_2':INFERENCE -used-before-assignment:134:10:134:16:type_annotation_used_improperly_after_comprehension:Using variable 'my_int' before assignment:HIGH -used-before-assignment:141:10:141:16:type_annotation_used_improperly_after_comprehension_2:Using variable 'my_int' before assignment:HIGH -used-before-assignment:171:12:171:16:expression_in_ternary_operator_inside_container_wrong_position:Using variable 'val3' before assignment:HIGH +used-before-assignment:18:15:18:18:typing_and_self_referencing_assignment_expression:Using variable 'var' before assignment:HIGH +used-before-assignment:24:15:24:18:self_referencing_assignment_expression:Using variable 'var' before assignment:HIGH +undefined-variable:49:6:49:16::Undefined variable 'no_default':UNDEFINED +undefined-variable:57:6:57:22::Undefined variable 'again_no_default':UNDEFINED +undefined-variable:83:6:83:19::Undefined variable 'else_assign_1':INFERENCE +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 diff --git a/tests/functional/u/unexpected_special_method_signature.py b/tests/functional/u/unexpected_special_method_signature.py index a731e8f314..e2ae338573 100644 --- a/tests/functional/u/unexpected_special_method_signature.py +++ b/tests/functional/u/unexpected_special_method_signature.py @@ -1,9 +1,9 @@ """Test for special methods implemented incorrectly.""" # pylint: disable=missing-docstring, unused-argument, too-few-public-methods -# pylint: disable=invalid-name,too-many-arguments,bad-staticmethod-argument, useless-object-inheritance +# pylint: disable=invalid-name,too-many-arguments,bad-staticmethod-argument -class Invalid(object): +class Invalid: def __enter__(self, other): # [unexpected-special-method-signature] pass @@ -34,19 +34,19 @@ def __subclasses__(self, blabla): # [unexpected-special-method-signature] pass -class FirstBadContextManager(object): +class FirstBadContextManager: def __enter__(self): return self def __exit__(self, exc_type): # [unexpected-special-method-signature] pass -class SecondBadContextManager(object): +class SecondBadContextManager: def __enter__(self): return self def __exit__(self, exc_type, value, tb, stack): # [unexpected-special-method-signature] pass -class ThirdBadContextManager(object): +class ThirdBadContextManager: def __enter__(self): return self @@ -55,7 +55,7 @@ def __exit__(self, exc_type, value, tb, stack, *args): pass -class Async(object): +class Async: def __aiter__(self, extra): # [unexpected-special-method-signature] pass @@ -69,7 +69,7 @@ def __aexit__(self): # [unexpected-special-method-signature] pass -class Valid(object): +class Valid: def __new__(cls, test, multiple, args): pass @@ -108,19 +108,19 @@ def __init_subclass__(cls, blabla): pass -class FirstGoodContextManager(object): +class FirstGoodContextManager: def __enter__(self): return self def __exit__(self, exc_type, value, tb): pass -class SecondGoodContextManager(object): +class SecondGoodContextManager: def __enter__(self): return self def __exit__(self, exc_type=None, value=None, tb=None): pass -class ThirdGoodContextManager(object): +class ThirdGoodContextManager: def __enter__(self): return self def __exit__(self, exc_type, *args): diff --git a/tests/functional/u/unexpected_special_method_signature.txt b/tests/functional/u/unexpected_special_method_signature.txt index 1ae7f38887..9a4c6e6ff6 100644 --- a/tests/functional/u/unexpected_special_method_signature.txt +++ b/tests/functional/u/unexpected_special_method_signature.txt @@ -4,7 +4,7 @@ unexpected-special-method-signature:14:4:14:18:Invalid.__format__:The special me unexpected-special-method-signature:17:4:17:19:Invalid.__setattr__:The special method '__setattr__' expects 2 param(s), 0 was given:UNDEFINED unexpected-special-method-signature:20:4:20:17:Invalid.__round__:The special method '__round__' expects between 0 or 1 param(s), 2 were given:UNDEFINED unexpected-special-method-signature:23:4:23:20:Invalid.__deepcopy__:The special method '__deepcopy__' expects 1 param(s), 2 were given:UNDEFINED -no-method-argument:26:4:26:16:Invalid.__iter__:Method has no argument:UNDEFINED +no-method-argument:26:4:26:16:Invalid.__iter__:Method '__iter__' has no argument:UNDEFINED unexpected-special-method-signature:30:4:30:19:Invalid.__getattr__:The special method '__getattr__' expects 1 param(s), 2 were given:UNDEFINED unexpected-special-method-signature:33:4:33:22:Invalid.__subclasses__:The special method '__subclasses__' expects 0 param(s), 1 was given:UNDEFINED unexpected-special-method-signature:40:4:40:16:FirstBadContextManager.__exit__:The special method '__exit__' expects 3 param(s), 1 was given:UNDEFINED diff --git a/tests/functional/u/unhashable_dict_key.py b/tests/functional/u/unhashable_dict_key.py deleted file mode 100644 index fe6142ff44..0000000000 --- a/tests/functional/u/unhashable_dict_key.py +++ /dev/null @@ -1,12 +0,0 @@ -# pylint: disable=missing-docstring,expression-not-assigned,too-few-public-methods,pointless-statement, useless-object-inheritance - - -class Unhashable(object): - __hash__ = list.__hash__ - -{}[[1, 2, 3]] # [unhashable-dict-key] -{}[{}] # [unhashable-dict-key] -{}[Unhashable()] # [unhashable-dict-key] -{}[1:2] # [unhashable-dict-key] -{'foo': 'bar'}['foo'] -{'foo': 'bar'}[42] diff --git a/tests/functional/u/unhashable_dict_key.txt b/tests/functional/u/unhashable_dict_key.txt deleted file mode 100644 index 87c74ab57a..0000000000 --- a/tests/functional/u/unhashable_dict_key.txt +++ /dev/null @@ -1,4 +0,0 @@ -unhashable-dict-key:7:0:7:2::Dict key is unhashable:UNDEFINED -unhashable-dict-key:8:0:8:2::Dict key is unhashable:UNDEFINED -unhashable-dict-key:9:0:9:2::Dict key is unhashable:UNDEFINED -unhashable-dict-key:10:0:10:2::Dict key is unhashable:UNDEFINED diff --git a/tests/functional/u/unhashable_member.py b/tests/functional/u/unhashable_member.py new file mode 100644 index 0000000000..c788668c47 --- /dev/null +++ b/tests/functional/u/unhashable_member.py @@ -0,0 +1,30 @@ +# pylint: disable=missing-docstring,expression-not-assigned,too-few-public-methods,pointless-statement + + +class Unhashable: + __hash__ = list.__hash__ + +# Subscripts +{}[[1, 2, 3]] # [unhashable-member] +{}[{}] # [unhashable-member] +{}[Unhashable()] # [unhashable-member] +{}[1:2] # [unhashable-member] +{'foo': 'bar'}['foo'] +{'foo': 'bar'}[42] + +# Keys +{[1, 2, 3]: "tomato"} # [unhashable-member] +{ + [1, 2, 3]: "tomato", # [unhashable-member] + [4, 5, 6]: "celeriac", # [unhashable-member] +} +{[1, 2, 3]} # [unhashable-member] +{"tomato": "tomahto"} +{dict: {}} +{lambda x: x: "tomato"} # pylint: disable=unnecessary-lambda + + +class FromDict(dict): + ... + +{FromDict: 1} diff --git a/tests/functional/u/unhashable_member.txt b/tests/functional/u/unhashable_member.txt new file mode 100644 index 0000000000..cbde3f8d67 --- /dev/null +++ b/tests/functional/u/unhashable_member.txt @@ -0,0 +1,8 @@ +unhashable-member:8:0:8:2::'[1, 2, 3]' is unhashable and can't be used as a key in a dict:INFERENCE +unhashable-member:9:0:9:2::'{}' is unhashable and can't be used as a key in a dict:INFERENCE +unhashable-member:10:0:10:2::'Unhashable()' is unhashable and can't be used as a key in a dict:INFERENCE +unhashable-member:11:0:11:2::"'1:2' is unhashable and can't be used as a key in a dict":INFERENCE +unhashable-member:16:1:16:10::'[1, 2, 3]' is unhashable and can't be used as a key in a dict:INFERENCE +unhashable-member:18:4:18:13::'[1, 2, 3]' is unhashable and can't be used as a key in a dict:INFERENCE +unhashable-member:19:4:19:13::'[4, 5, 6]' is unhashable and can't be used as a key in a dict:INFERENCE +unhashable-member:21:1:21:10::'[1, 2, 3]' is unhashable and can't be used as a member in a set:INFERENCE diff --git a/tests/functional/u/unidiomatic_typecheck.py b/tests/functional/u/unidiomatic_typecheck.py index 1e7642046b..2a1957d75e 100644 --- a/tests/functional/u/unidiomatic_typecheck.py +++ b/tests/functional/u/unidiomatic_typecheck.py @@ -1,5 +1,5 @@ """Warnings for using type(x) == Y or type(x) is Y instead of isinstance(x, Y).""" -# pylint: disable=missing-docstring,expression-not-assigned,redefined-builtin,invalid-name,unnecessary-lambda-assignment +# pylint: disable=missing-docstring,expression-not-assigned,redefined-builtin,invalid-name,unnecessary-lambda-assignment,use-dict-literal def simple_positives(): type(42) is int # [unidiomatic-typecheck] diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call.py b/tests/functional/u/unnecessary/unnecessary_dunder_call.py index e1986a9374..18e4ef855c 100644 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call.py +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call.py @@ -1,5 +1,5 @@ """Checks for unnecessary-dunder-call.""" -# pylint: disable=too-few-public-methods, undefined-variable, useless-object-inheritance +# pylint: disable=too-few-public-methods, undefined-variable # pylint: disable=missing-class-docstring, missing-function-docstring from collections import OrderedDict from typing import Any @@ -26,19 +26,19 @@ def is_bigger_than_two(val): # Test dunder methods don't raise lint # if within a dunder method definition. -class Foo1(object): +class Foo1: def __init__(self): object.__init__(self) -class Foo2(object): +class Foo2: def __init__(self): super().__init__(self) -class Bar1(object): +class Bar1: def __new__(cls): object.__new__(cls) -class Bar2(object): +class Bar2: def __new__(cls): super().__new__(cls) @@ -77,7 +77,7 @@ def __contains__(self, item): print("do some special checks") return super().__contains__(item) -class PluginBase(object): +class PluginBase: subclasses = [] def __init_subclass__(cls, **kwargs): @@ -122,3 +122,9 @@ def get_first_subclass(cls): # since we can't apply alternate operators/functions here. a = [1, 2, 3] assert super(type(a), a).__str__() == "[1, 2, 3]" + +class MyString(str): + """Custom str implementation""" + def rjust(self, width, fillchar= ' '): + """Acceptable call to __index__""" + width = width.__index__() diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.py b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.py new file mode 100644 index 0000000000..c2ab58a57b --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.py @@ -0,0 +1,15 @@ +"""Checks for unnecessary-dunder-call on __aiter__/__anext__ with py-version=3.10.""" + + +class MyClass: + """A class implementing __aiter__ and __anext__.""" + + def __aiter__(self): + ... + + async def __anext__(self): + ... + + +MyClass().__aiter__() # [unnecessary-dunder-call] +MyClass().__anext__() # [unnecessary-dunder-call] diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.rc b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.rc new file mode 100644 index 0000000000..7d7b7aa0c1 --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.rc @@ -0,0 +1,2 @@ +[master] +py-version=3.10 diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.txt b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.txt new file mode 100644 index 0000000000..bcb5647f94 --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py310.txt @@ -0,0 +1,2 @@ +unnecessary-dunder-call:14:0:14:21::Unnecessarily calls dunder method __aiter__. Use aiter built-in function.:HIGH +unnecessary-dunder-call:15:0:15:21::Unnecessarily calls dunder method __anext__. Use anext built-in function.:HIGH diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py39.py b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py39.py new file mode 100644 index 0000000000..589524170f --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py39.py @@ -0,0 +1,15 @@ +"""Checks for unnecessary-dunder-call on __aiter__/__anext__ with py-version=3.9.""" + + +class MyClass: + """A class implementing __aiter__ and __anext__.""" + + def __aiter__(self): + ... + + async def __anext__(self): + ... + + +MyClass().__aiter__() +MyClass().__anext__() diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py39.rc b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py39.rc new file mode 100644 index 0000000000..aed012f734 --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call_async_py39.rc @@ -0,0 +1,2 @@ +[MAIN] +py-version=3.9 diff --git a/tests/functional/u/unnecessary/unnecessary_lambda.py b/tests/functional/u/unnecessary/unnecessary_lambda.py index 8484f63989..3e5ece2b12 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda.py +++ b/tests/functional/u/unnecessary/unnecessary_lambda.py @@ -1,8 +1,7 @@ -# pylint: disable=undefined-variable, use-list-literal, unnecessary-lambda-assignment +# pylint: disable=undefined-variable, use-list-literal, unnecessary-lambda-assignment, use-dict-literal """test suspicious lambda expressions """ -__revision__ = '' # Some simple examples of the most commonly encountered forms. # +1: [unnecessary-lambda] diff --git a/tests/functional/u/unnecessary/unnecessary_lambda.txt b/tests/functional/u/unnecessary/unnecessary_lambda.txt index 68b675ae45..1cfb149dfd 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda.txt +++ b/tests/functional/u/unnecessary/unnecessary_lambda.txt @@ -1,7 +1,7 @@ -unnecessary-lambda:9:4:9:18::Lambda may not be necessary:UNDEFINED -unnecessary-lambda:11:4:11:21::Lambda may not be necessary:UNDEFINED -unnecessary-lambda:13:4:13:26::Lambda may not be necessary:UNDEFINED -unnecessary-lambda:20:4:20:33::Lambda may not be necessary:UNDEFINED -unnecessary-lambda:22:4:22:39::Lambda may not be necessary:UNDEFINED -unnecessary-lambda:24:4:24:53::Lambda may not be necessary:UNDEFINED -unnecessary-lambda:26:4:26:71::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:8:4:8:18::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:10:4:10:21::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:12:4:12:26::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:19:4:19:33::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:21:4:21:39::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:23:4:23:53::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:25:4:25:71::Lambda may not be necessary:UNDEFINED diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py index c9aad6d9a2..ec5ee22c20 100644 --- a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py @@ -74,3 +74,78 @@ def process_list_again(data): print(updated_list[idx]) # [unnecessary-list-index-lookup] updated_list[idx] -= 1 print(updated_list[idx]) + +# Regression test for https://github.com/PyCQA/pylint/issues/6896 +parts = ["a", "b", "c", "d"] +for i, part in enumerate(parts): + if i == 3: # more complex condition actually + parts.insert(i, "X") + print(part, parts[i]) + +# regression tests for https://github.com/PyCQA/pylint/issues/7682 +series = [1, 2, 3, 4, 5] +output_list = [ + (item, series[index]) + for index, item in enumerate(series, start=1) + if index < len(series) +] + +output_list = [ + (item, series[index]) + for index, item in enumerate(series, 1) + if index < len(series) +] + +for idx, val in enumerate(series, start=2): + print(series[idx]) + +for idx, val in enumerate(series, 2): + print(series[idx]) + +for idx, val in enumerate(series, start=-2): + print(series[idx]) + +for idx, val in enumerate(series, -2): + print(series[idx]) + +for idx, val in enumerate(series, start=0): + print(series[idx]) # [unnecessary-list-index-lookup] + +for idx, val in enumerate(series, 0): + print(series[idx]) # [unnecessary-list-index-lookup] + +START = 0 +for idx, val in enumerate(series, start=START): + print(series[idx]) # [unnecessary-list-index-lookup] + +for idx, val in enumerate(series, START): + print(series[idx]) # [unnecessary-list-index-lookup] + +START = [1, 2, 3] +for i, k in enumerate(series, len(START)): + print(series[idx]) + +def return_start(start): + return start + +for i, k in enumerate(series, return_start(20)): + print(series[idx]) + +for idx, val in enumerate(iterable=series, start=0): + print(series[idx]) # [unnecessary-list-index-lookup] + +result = [my_list[idx] for idx, val in enumerate(iterable=my_list)] # [unnecessary-list-index-lookup] + +for idx, val in enumerate(): + print(my_list[idx]) + +class Command: + def _get_extra_attrs(self, extra_columns): + self.extra_rows_start = 8 # pylint: disable=attribute-defined-outside-init + for index, column in enumerate(extra_columns, start=self.extra_rows_start): + pass + +Y_START = 2 +nums = list(range(20)) +for y, x in enumerate(nums, start=Y_START + 1): + pass diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt index 9a894e139e..da658a20d0 100644 --- a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt @@ -2,3 +2,9 @@ unnecessary-list-index-lookup:8:10:8:22::Unnecessary list index lookup, use 'val unnecessary-list-index-lookup:43:52:43:64::Unnecessary list index lookup, use 'val' instead:HIGH unnecessary-list-index-lookup:46:10:46:22::Unnecessary list index lookup, use 'val' instead:HIGH unnecessary-list-index-lookup:74:10:74:27::Unnecessary list index lookup, use 'val' instead:HIGH +unnecessary-list-index-lookup:112:10:112:21::Unnecessary list index lookup, use 'val' instead:HIGH +unnecessary-list-index-lookup:115:10:115:21::Unnecessary list index lookup, use 'val' instead:HIGH +unnecessary-list-index-lookup:119:10:119:21::Unnecessary list index lookup, use 'val' instead:INFERENCE +unnecessary-list-index-lookup:122:10:122:21::Unnecessary list index lookup, use 'val' instead:INFERENCE +unnecessary-list-index-lookup:135:10:135:21::Unnecessary list index lookup, use 'val' instead:HIGH +unnecessary-list-index-lookup:137:10:137:22::Unnecessary list index lookup, use 'val' instead:HIGH diff --git a/tests/functional/u/unnecessary/unnecessary_not.py b/tests/functional/u/unnecessary/unnecessary_not.py index 6b6b51369e..52c1f00452 100644 --- a/tests/functional/u/unnecessary/unnecessary_not.py +++ b/tests/functional/u/unnecessary/unnecessary_not.py @@ -1,7 +1,7 @@ """Check exceeding negations in boolean expressions trigger warnings""" # pylint: disable=singleton-comparison,too-many-branches,too-few-public-methods,undefined-variable -# pylint: disable=literal-comparison, comparison-with-itself, useless-object-inheritance, comparison-of-constants +# pylint: disable=literal-comparison, comparison-with-itself, comparison-of-constants def unneeded_not(): """This is not ok """ @@ -57,7 +57,7 @@ def tolerated_statements(): pass -class Klass(object): +class Klass: """This is also ok""" def __ne__(self, other): return not self == other diff --git a/tests/functional/u/unpacking/unpacking_non_sequence.py b/tests/functional/u/unpacking/unpacking_non_sequence.py index a201a75cb6..feb465ecbe 100644 --- a/tests/functional/u/unpacking/unpacking_non_sequence.py +++ b/tests/functional/u/unpacking/unpacking_non_sequence.py @@ -1,16 +1,15 @@ """Check unpacking non-sequences in assignments. """ # pylint: disable=too-few-public-methods, invalid-name, attribute-defined-outside-init, unused-variable -# pylint: disable=using-constant-test, missing-docstring, wrong-import-order,wrong-import-position,no-else-return, useless-object-inheritance +# pylint: disable=using-constant-test, missing-docstring, wrong-import-order,wrong-import-position,no-else-return from os import rename as nonseq_func from functional.u.unpacking.unpacking import nonseq from typing import NamedTuple -__revision__ = 0 # Working -class Seq(object): +class Seq: """ sequence """ def __init__(self): self.items = range(2) @@ -21,7 +20,7 @@ def __getitem__(self, item): def __len__(self): return len(self.items) -class Iter(object): +class Iter: """ Iterator """ def __iter__(self): for number in range(2): @@ -46,7 +45,7 @@ def __iter__(cls): class IterClass(metaclass=MetaIter): "class that is iterable (and unpackable)" -class AbstrClass(object): +class AbstrClass: "abstract class" pair = None @@ -72,7 +71,7 @@ def __init__(self): a, b = IterClass # Not working -class NonSeq(object): +class NonSeq: """ does nothing """ a, b = NonSeq() # [unpacking-non-sequence] @@ -83,7 +82,7 @@ class NonSeq(object): a, b = nonseq() # [unpacking-non-sequence] a, b = nonseq_func # [unpacking-non-sequence] -class ClassUnpacking(object): +class ClassUnpacking: """ Check unpacking as instance attributes. """ def test(self): @@ -100,7 +99,7 @@ def test(self): self.a, self.b = ValueError # [unpacking-non-sequence] self.a, c = nonseq_func # [unpacking-non-sequence] -class TestBase(object): +class TestBase: 'base class with `test` method implementation' @staticmethod def test(data): diff --git a/tests/functional/u/unpacking/unpacking_non_sequence.txt b/tests/functional/u/unpacking/unpacking_non_sequence.txt index c3c65050cb..473acde6f9 100644 --- a/tests/functional/u/unpacking/unpacking_non_sequence.txt +++ b/tests/functional/u/unpacking/unpacking_non_sequence.txt @@ -1,10 +1,10 @@ -unpacking-non-sequence:78:0:78:15::Attempting to unpack a non-sequence defined at line 75:UNDEFINED -unpacking-non-sequence:79:0:79:17::Attempting to unpack a non-sequence:UNDEFINED -unpacking-non-sequence:80:0:80:11::Attempting to unpack a non-sequence None:UNDEFINED -unpacking-non-sequence:81:0:81:8::Attempting to unpack a non-sequence 1:UNDEFINED -unpacking-non-sequence:82:0:82:13::Attempting to unpack a non-sequence defined at line 9 of functional.u.unpacking.unpacking:UNDEFINED -unpacking-non-sequence:83:0:83:15::Attempting to unpack a non-sequence defined at line 11 of functional.u.unpacking.unpacking:UNDEFINED -unpacking-non-sequence:84:0:84:18::Attempting to unpack a non-sequence:UNDEFINED -unpacking-non-sequence:99:8:99:33:ClassUnpacking.test:Attempting to unpack a non-sequence defined at line 75:UNDEFINED -unpacking-non-sequence:100:8:100:35:ClassUnpacking.test:Attempting to unpack a non-sequence:UNDEFINED -unpacking-non-sequence:101:8:101:31:ClassUnpacking.test:Attempting to unpack a non-sequence:UNDEFINED +unpacking-non-sequence:77:0:77:15::Attempting to unpack a non-sequence defined at line 74:UNDEFINED +unpacking-non-sequence:78:0:78:17::Attempting to unpack a non-sequence:UNDEFINED +unpacking-non-sequence:79:0:79:11::Attempting to unpack a non-sequence 'None':UNDEFINED +unpacking-non-sequence:80:0:80:8::Attempting to unpack a non-sequence '1':UNDEFINED +unpacking-non-sequence:81:0:81:13::Attempting to unpack a non-sequence defined at line 9 of functional.u.unpacking.unpacking:UNDEFINED +unpacking-non-sequence:82:0:82:15::Attempting to unpack a non-sequence defined at line 11 of functional.u.unpacking.unpacking:UNDEFINED +unpacking-non-sequence:83:0:83:18::Attempting to unpack a non-sequence:UNDEFINED +unpacking-non-sequence:98:8:98:33:ClassUnpacking.test:Attempting to unpack a non-sequence defined at line 74:UNDEFINED +unpacking-non-sequence:99:8:99:35:ClassUnpacking.test:Attempting to unpack a non-sequence:UNDEFINED +unpacking-non-sequence:100:8:100:31:ClassUnpacking.test:Attempting to unpack a non-sequence:UNDEFINED diff --git a/tests/functional/u/unpacking/unpacking_non_sequence_py37.py b/tests/functional/u/unpacking/unpacking_non_sequence_py37.py index dd8af1136c..13ab35b9a9 100644 --- a/tests/functional/u/unpacking/unpacking_non_sequence_py37.py +++ b/tests/functional/u/unpacking/unpacking_non_sequence_py37.py @@ -1,6 +1,13 @@ +""" +https://github.com/PyCQA/pylint/issues/4895 +""" + # pylint: disable=missing-docstring -# https://github.com/PyCQA/pylint/issues/4895 +# Disabled because of a bug with pypy 3.8 see +# https://github.com/PyCQA/pylint/pull/7918#issuecomment-1352737369 +# pylint: disable=multiple-statements + from __future__ import annotations from collections.abc import Callable diff --git a/tests/functional/u/unreachable.py b/tests/functional/u/unreachable.py index 2c71f6cf44..0211a61366 100644 --- a/tests/functional/u/unreachable.py +++ b/tests/functional/u/unreachable.py @@ -1,6 +1,9 @@ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised, too-few-public-methods, redefined-outer-name +# pylint: disable=consider-using-sys-exit, protected-access -from __future__ import print_function +import os +import signal +import sys def func1(): return 1 @@ -33,3 +36,46 @@ def func6(): return yield print("unreachable") # [unreachable] + +def func7(): + sys.exit(1) + var = 2 + 2 # [unreachable] + print(var) + +def func8(): + signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) + try: + print(1) + except KeyboardInterrupt: + pass + +class FalseExit: + def exit(self, number): + print(f"False positive this is not sys.exit({number})") + +def func_false_exit(): + sys = FalseExit() + sys.exit(1) + var = 2 + 2 + print(var) + +def func9(): + os._exit() + var = 2 + 2 # [unreachable] + print(var) + +def func10(): + exit() + var = 2 + 2 # [unreachable] + print(var) + +def func11(): + quit() + var = 2 + 2 # [unreachable] + print(var) + +incognito_function = sys.exit +def func12(): + incognito_function() + var = 2 + 2 # [unreachable] + print(var) diff --git a/tests/functional/u/unreachable.txt b/tests/functional/u/unreachable.txt index 21905a17e6..82f9797aa1 100644 --- a/tests/functional/u/unreachable.txt +++ b/tests/functional/u/unreachable.txt @@ -1,5 +1,10 @@ -unreachable:7:4:7:24:func1:Unreachable code:UNDEFINED -unreachable:12:8:12:28:func2:Unreachable code:UNDEFINED -unreachable:18:8:18:28:func3:Unreachable code:UNDEFINED -unreachable:22:4:22:16:func4:Unreachable code:UNDEFINED -unreachable:35:4:35:24:func6:Unreachable code:UNDEFINED +unreachable:10:4:10:24:func1:Unreachable code:HIGH +unreachable:15:8:15:28:func2:Unreachable code:HIGH +unreachable:21:8:21:28:func3:Unreachable code:HIGH +unreachable:25:4:25:16:func4:Unreachable code:HIGH +unreachable:38:4:38:24:func6:Unreachable code:HIGH +unreachable:42:4:42:15:func7:Unreachable code:INFERENCE +unreachable:64:4:64:15:func9:Unreachable code:INFERENCE +unreachable:69:4:69:15:func10:Unreachable code:INFERENCE +unreachable:74:4:74:15:func11:Unreachable code:INFERENCE +unreachable:80:4:80:15:func12:Unreachable code:INFERENCE diff --git a/tests/functional/u/unsubscriptable_value.py b/tests/functional/u/unsubscriptable_value.py index 6a97cc4c84..79e17903b7 100644 --- a/tests/functional/u/unsubscriptable_value.py +++ b/tests/functional/u/unsubscriptable_value.py @@ -3,8 +3,8 @@ (i.e. defines __getitem__ method). """ # pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,wrong-import-position, unnecessary-comprehension -# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order, useless-object-inheritance, redundant-u-string-prefix -import six +# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order, redundant-u-string-prefix +# pylint: disable=use-dict-literal # primitives numbers = [1, 2, 3] @@ -22,10 +22,10 @@ # instances -class NonSubscriptable(object): +class NonSubscriptable: pass -class Subscriptable(object): +class Subscriptable: def __getitem__(self, key): return key + key @@ -70,7 +70,7 @@ class MetaSubscriptable(type): def __getitem__(cls, key): return key + key -class SubscriptableClass(six.with_metaclass(MetaSubscriptable, object)): +class SubscriptableClass(metaclass=MetaSubscriptable): pass SubscriptableClass[0] @@ -90,7 +90,7 @@ def test(*args, **kwargs): deq[0] -class AbstractClass(object): +class AbstractClass: def __init__(self): self.ala = {i for i in range(10)} @@ -102,7 +102,7 @@ def test_unsubscriptable(self): self.portocala[0] -class ClassMixin(object): +class ClassMixin: def __init__(self): self.ala = {i for i in range(10)} diff --git a/tests/functional/u/unsupported/unsupported_assignment_operation.py b/tests/functional/u/unsupported/unsupported_assignment_operation.py index c0b58668a9..93e84c0206 100644 --- a/tests/functional/u/unsupported/unsupported_assignment_operation.py +++ b/tests/functional/u/unsupported/unsupported_assignment_operation.py @@ -3,8 +3,7 @@ (i.e. defines __setitem__ method). """ # pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,wrong-import-position,unnecessary-comprehension -# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order, useless-object-inheritance -import six +# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order,use-dict-literal # primitives numbers = [1, 2, 3] @@ -21,10 +20,10 @@ # instances -class NonSubscriptable(object): +class NonSubscriptable: pass -class Subscriptable(object): +class Subscriptable: def __setitem__(self, key, value): return key + value @@ -69,7 +68,7 @@ class MetaSubscriptable(type): def __setitem__(cls, key, value): return key + value -class SubscriptableClass(six.with_metaclass(MetaSubscriptable, object)): +class SubscriptableClass(metaclass=MetaSubscriptable): pass SubscriptableClass[0] = 24 diff --git a/tests/functional/u/unsupported/unsupported_assignment_operation.txt b/tests/functional/u/unsupported/unsupported_assignment_operation.txt index de3fb3f1b5..3177fa1996 100644 --- a/tests/functional/u/unsupported/unsupported_assignment_operation.txt +++ b/tests/functional/u/unsupported/unsupported_assignment_operation.txt @@ -1,17 +1,17 @@ -unsupported-assignment-operation:16:0:16:9::'(1, 2, 3)' does not support item assignment:UNDEFINED -unsupported-assignment-operation:31:0:31:18::'NonSubscriptable()' does not support item assignment:UNDEFINED -unsupported-assignment-operation:32:0:32:16::'NonSubscriptable' does not support item assignment:UNDEFINED -unsupported-assignment-operation:34:0:34:13::'Subscriptable' does not support item assignment:UNDEFINED -unsupported-assignment-operation:43:0:43:15::'powers_of_two()' does not support item assignment:UNDEFINED -unsupported-assignment-operation:44:0:44:13::'powers_of_two' does not support item assignment:UNDEFINED -unsupported-assignment-operation:48:0:48:4::'True' does not support item assignment:UNDEFINED -unsupported-assignment-operation:49:0:49:4::'None' does not support item assignment:UNDEFINED -unsupported-assignment-operation:50:0:50:3::'8.5' does not support item assignment:UNDEFINED -unsupported-assignment-operation:51:0:51:2::'10' does not support item assignment:UNDEFINED -unsupported-assignment-operation:54:0:54:27::'{x**2 for x in range(10)}' does not support item assignment:UNDEFINED -unsupported-assignment-operation:55:0:55:12::'set(numbers)' does not support item assignment:UNDEFINED -unsupported-assignment-operation:56:0:56:18::'frozenset(numbers)' does not support item assignment:UNDEFINED -unsupported-assignment-operation:76:0:76:20::'SubscriptableClass()' does not support item assignment:UNDEFINED -unsupported-assignment-operation:82:0:82:6::'test()' does not support item assignment:UNDEFINED -unsupported-assignment-operation:83:0:83:4::'test' does not support item assignment:UNDEFINED -unsupported-assignment-operation:94:12:94:32::'SubscriptableClass()' does not support item assignment:UNDEFINED +unsupported-assignment-operation:15:0:15:9::'(1, 2, 3)' does not support item assignment:UNDEFINED +unsupported-assignment-operation:30:0:30:18::'NonSubscriptable()' does not support item assignment:UNDEFINED +unsupported-assignment-operation:31:0:31:16::'NonSubscriptable' does not support item assignment:UNDEFINED +unsupported-assignment-operation:33:0:33:13::'Subscriptable' does not support item assignment:UNDEFINED +unsupported-assignment-operation:42:0:42:15::'powers_of_two()' does not support item assignment:UNDEFINED +unsupported-assignment-operation:43:0:43:13::'powers_of_two' does not support item assignment:UNDEFINED +unsupported-assignment-operation:47:0:47:4::'True' does not support item assignment:UNDEFINED +unsupported-assignment-operation:48:0:48:4::'None' does not support item assignment:UNDEFINED +unsupported-assignment-operation:49:0:49:3::'8.5' does not support item assignment:UNDEFINED +unsupported-assignment-operation:50:0:50:2::'10' does not support item assignment:UNDEFINED +unsupported-assignment-operation:53:0:53:27::'{x**2 for x in range(10)}' does not support item assignment:UNDEFINED +unsupported-assignment-operation:54:0:54:12::'set(numbers)' does not support item assignment:UNDEFINED +unsupported-assignment-operation:55:0:55:18::'frozenset(numbers)' does not support item assignment:UNDEFINED +unsupported-assignment-operation:75:0:75:20::'SubscriptableClass()' does not support item assignment:UNDEFINED +unsupported-assignment-operation:81:0:81:6::'test()' does not support item assignment:UNDEFINED +unsupported-assignment-operation:82:0:82:4::'test' does not support item assignment:UNDEFINED +unsupported-assignment-operation:93:12:93:32::'SubscriptableClass()' does not support item assignment:UNDEFINED diff --git a/tests/functional/u/unsupported/unsupported_binary_operation.py b/tests/functional/u/unsupported/unsupported_binary_operation.py index 0bfe1d16a1..00db8cdfe5 100644 --- a/tests/functional/u/unsupported/unsupported_binary_operation.py +++ b/tests/functional/u/unsupported/unsupported_binary_operation.py @@ -1,6 +1,6 @@ """Test for unsupported-binary-operation.""" # pylint: disable=missing-docstring,too-few-public-methods,pointless-statement -# pylint: disable=expression-not-assigned, invalid-name, useless-object-inheritance +# pylint: disable=expression-not-assigned, invalid-name import collections @@ -20,32 +20,32 @@ [] * 2.0 # [unsupported-binary-operation] () * 2.0 # [unsupported-binary-operation] 2.0 >> 2.0 # [unsupported-binary-operation] -class A(object): +class A: pass -class B(object): +class B: pass A() + B() # [unsupported-binary-operation] -class A1(object): +class A1: def __add__(self, other): return NotImplemented A1() + A1() # [unsupported-binary-operation] -class A2(object): +class A2: def __add__(self, other): return NotImplemented -class B2(object): +class B2: def __radd__(self, other): return NotImplemented A2() + B2() # [unsupported-binary-operation] -class Parent(object): +class Parent: pass class Child(Parent): def __add__(self, other): return NotImplemented Child() + Parent() # [unsupported-binary-operation] -class A3(object): +class A3: def __add__(self, other): return NotImplemented class B3(A3): diff --git a/tests/functional/u/unsupported/unsupported_delete_operation.py b/tests/functional/u/unsupported/unsupported_delete_operation.py index 3c2882a64f..c33a6eb891 100644 --- a/tests/functional/u/unsupported/unsupported_delete_operation.py +++ b/tests/functional/u/unsupported/unsupported_delete_operation.py @@ -3,8 +3,7 @@ (i.e. defines __delitem__ method). """ # pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,wrong-import-position,unnecessary-comprehension -# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order, useless-object-inheritance -import six +# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order,use-dict-literal # primitives numbers = [1, 2, 3] @@ -21,10 +20,10 @@ # instances -class NonSubscriptable(object): +class NonSubscriptable: pass -class Subscriptable(object): +class Subscriptable: def __delitem__(self, key): pass @@ -69,7 +68,7 @@ class MetaSubscriptable(type): def __delitem__(cls, key): pass -class SubscriptableClass(six.with_metaclass(MetaSubscriptable, object)): +class SubscriptableClass(metaclass=MetaSubscriptable): pass del SubscriptableClass[0] diff --git a/tests/functional/u/unsupported/unsupported_delete_operation.txt b/tests/functional/u/unsupported/unsupported_delete_operation.txt index d3363f6b8e..4ee05ab207 100644 --- a/tests/functional/u/unsupported/unsupported_delete_operation.txt +++ b/tests/functional/u/unsupported/unsupported_delete_operation.txt @@ -1,17 +1,17 @@ -unsupported-delete-operation:16:4:16:13::'(1, 2, 3)' does not support item deletion:UNDEFINED -unsupported-delete-operation:31:4:31:22::'NonSubscriptable()' does not support item deletion:UNDEFINED -unsupported-delete-operation:32:4:32:20::'NonSubscriptable' does not support item deletion:UNDEFINED -unsupported-delete-operation:34:4:34:17::'Subscriptable' does not support item deletion:UNDEFINED -unsupported-delete-operation:43:4:43:19::'powers_of_two()' does not support item deletion:UNDEFINED -unsupported-delete-operation:44:4:44:17::'powers_of_two' does not support item deletion:UNDEFINED -unsupported-delete-operation:48:4:48:8::'True' does not support item deletion:UNDEFINED -unsupported-delete-operation:49:4:49:8::'None' does not support item deletion:UNDEFINED -unsupported-delete-operation:50:4:50:7::'8.5' does not support item deletion:UNDEFINED -unsupported-delete-operation:51:4:51:6::'10' does not support item deletion:UNDEFINED -unsupported-delete-operation:54:4:54:31::'{x**2 for x in range(10)}' does not support item deletion:UNDEFINED -unsupported-delete-operation:55:4:55:16::'set(numbers)' does not support item deletion:UNDEFINED -unsupported-delete-operation:56:4:56:22::'frozenset(numbers)' does not support item deletion:UNDEFINED -unsupported-delete-operation:76:4:76:24::'SubscriptableClass()' does not support item deletion:UNDEFINED -unsupported-delete-operation:82:4:82:10::'test()' does not support item deletion:UNDEFINED -unsupported-delete-operation:83:4:83:8::'test' does not support item deletion:UNDEFINED -unsupported-delete-operation:94:16:94:36::'SubscriptableClass()' does not support item deletion:UNDEFINED +unsupported-delete-operation:15:4:15:13::'(1, 2, 3)' does not support item deletion:UNDEFINED +unsupported-delete-operation:30:4:30:22::'NonSubscriptable()' does not support item deletion:UNDEFINED +unsupported-delete-operation:31:4:31:20::'NonSubscriptable' does not support item deletion:UNDEFINED +unsupported-delete-operation:33:4:33:17::'Subscriptable' does not support item deletion:UNDEFINED +unsupported-delete-operation:42:4:42:19::'powers_of_two()' does not support item deletion:UNDEFINED +unsupported-delete-operation:43:4:43:17::'powers_of_two' does not support item deletion:UNDEFINED +unsupported-delete-operation:47:4:47:8::'True' does not support item deletion:UNDEFINED +unsupported-delete-operation:48:4:48:8::'None' does not support item deletion:UNDEFINED +unsupported-delete-operation:49:4:49:7::'8.5' does not support item deletion:UNDEFINED +unsupported-delete-operation:50:4:50:6::'10' does not support item deletion:UNDEFINED +unsupported-delete-operation:53:4:53:31::'{x**2 for x in range(10)}' does not support item deletion:UNDEFINED +unsupported-delete-operation:54:4:54:16::'set(numbers)' does not support item deletion:UNDEFINED +unsupported-delete-operation:55:4:55:22::'frozenset(numbers)' does not support item deletion:UNDEFINED +unsupported-delete-operation:75:4:75:24::'SubscriptableClass()' does not support item deletion:UNDEFINED +unsupported-delete-operation:81:4:81:10::'test()' does not support item deletion:UNDEFINED +unsupported-delete-operation:82:4:82:8::'test' does not support item deletion:UNDEFINED +unsupported-delete-operation:93:16:93:36::'SubscriptableClass()' does not support item deletion:UNDEFINED diff --git a/tests/functional/u/unused/unused_argument.py b/tests/functional/u/unused/unused_argument.py index 8ce9bd7d45..b46c1e4d75 100644 --- a/tests/functional/u/unused/unused_argument.py +++ b/tests/functional/u/unused/unused_argument.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring,too-few-public-methods, useless-object-inheritance +# pylint: disable=missing-docstring,too-few-public-methods def test_unused(first, second, _not_used): # [unused-argument, unused-argument] pass @@ -17,7 +17,7 @@ def test_prefixed_with_unused(first, unused_second): # use the arguments (e.g. Sub2) -class Base(object): +class Base: "parent" def inherited(self, aaa, aab, aac): "abstract method" @@ -53,15 +53,14 @@ def metadata_from_dict_2(key): return {key: (a, b) for key, (a, b) in key.items()} -# pylint: disable=too-few-public-methods, misplaced-future,wrong-import-position -from __future__ import print_function +# pylint: disable=too-few-public-methods, wrong-import-position def function(arg=1): # [unused-argument] """ignore arg""" -class AAAA(object): +class AAAA: """dummy class""" def method(self, arg): # [unused-argument] @@ -87,7 +86,7 @@ def inner(row, col=0, etype=etype, req=self, rset=rset): # pylint: disable = attribute-defined-outside-init rset.get_entity = inner -class BBBB(object): +class BBBB: """dummy class""" def __init__(self, arg): # [unused-argument] diff --git a/tests/functional/u/unused/unused_argument.txt b/tests/functional/u/unused/unused_argument.txt index 92795058ef..19c4393046 100644 --- a/tests/functional/u/unused/unused_argument.txt +++ b/tests/functional/u/unused/unused_argument.txt @@ -1,9 +1,9 @@ unused-argument:3:16:3:21:test_unused:Unused argument 'first':HIGH unused-argument:3:23:3:29:test_unused:Unused argument 'second':HIGH unused-argument:32:29:32:32:Sub.newmethod:Unused argument 'aay':INFERENCE -unused-argument:60:13:60:16:function:Unused argument 'arg':HIGH -unused-argument:67:21:67:24:AAAA.method:Unused argument 'arg':INFERENCE -unused-argument:74:0:None:None:AAAA.selected:Unused argument 'args':INFERENCE -unused-argument:74:0:None:None:AAAA.selected:Unused argument 'kwargs':INFERENCE -unused-argument:93:23:93:26:BBBB.__init__:Unused argument 'arg':INFERENCE -unused-argument:104:34:104:39:Ancestor.set_thing:Unused argument 'other':INFERENCE +unused-argument:59:13:59:16:function:Unused argument 'arg':HIGH +unused-argument:66:21:66:24:AAAA.method:Unused argument 'arg':INFERENCE +unused-argument:73:0:None:None:AAAA.selected:Unused argument 'args':INFERENCE +unused-argument:73:0:None:None:AAAA.selected:Unused argument 'kwargs':INFERENCE +unused-argument:92:23:92:26:BBBB.__init__:Unused argument 'arg':INFERENCE +unused-argument:103:34:103:39:Ancestor.set_thing:Unused argument 'other':INFERENCE diff --git a/tests/functional/u/unused/unused_import.py b/tests/functional/u/unused/unused_import.py index 143ee7cf3a..3534cd0cf3 100644 --- a/tests/functional/u/unused/unused_import.py +++ b/tests/functional/u/unused/unused_import.py @@ -1,22 +1,29 @@ """unused import""" -# pylint: disable=undefined-all-variable, import-error, too-few-public-methods, missing-docstring,wrong-import-position, useless-object-inheritance, multiple-imports +# pylint: disable=undefined-all-variable, import-error, too-few-public-methods, missing-docstring,wrong-import-position, multiple-imports import xml.etree # [unused-import] import xml.sax # [unused-import] import os.path as test # [unused-import] +from abc import ABCMeta from sys import argv as test2 # [unused-import] from sys import flags # [unused-import] + # +1:[unused-import,unused-import] from collections import deque, OrderedDict, Counter import re, html.parser # [unused-import] + DATA = Counter() # pylint: disable=self-assigning-variable from fake import SomeName, SomeOtherName # [unused-import] -class SomeClass(object): - SomeName = SomeName # https://bitbucket.org/logilab/pylint/issue/475 + + +class SomeClass: + SomeName = SomeName # https://bitbucket.org/logilab/pylint/issue/475 SomeOtherName = 1 SomeOtherName = SomeOtherName + from never import __all__ + # pylint: disable=wrong-import-order,ungrouped-imports,reimported import typing from typing import TYPE_CHECKING @@ -31,27 +38,27 @@ class SomeClass(object): import xml -def get_ordered_dict() -> 'collections.OrderedDict': +def get_ordered_dict() -> "collections.OrderedDict": return [] -def get_itertools_obj() -> 'itertools.count': +def get_itertools_obj() -> "itertools.count": return [] -def use_html_parser() -> 'html.parser.HTMLParser': - return html.parser.HTMLParser -# pylint: disable=misplaced-future +def use_html_parser() -> "html.parser.HTMLParser": + return html.parser.HTMLParser -from __future__ import print_function import os # [unused-import] import sys -class NonRegr(object): + +class NonRegr: """???""" + def __init__(self): - print('initialized') + print("initialized") def sys(self): """should not get sys from there...""" @@ -63,7 +70,8 @@ def dummy(self, truc): def blop(self): """yo""" - print(self, 'blip') + print(self, "blip") + if TYPE_CHECKING: if sys.version_info >= (3, 6, 2): @@ -72,7 +80,8 @@ def blop(self): # Pathological cases from io import TYPE_CHECKING # pylint: disable=no-name-in-module import trace as t -import astroid as typing +import astroid as typing # pylint: disable=shadowed-import + TYPE_CHECKING = "red herring" if TYPE_CHECKING: @@ -87,3 +96,17 @@ def blop(self): TYPE_CHECKING = False if TYPE_CHECKING: import zoneinfo + + +class WithMetaclass(metaclass=ABCMeta): + pass + + +# Regression test for https://github.com/PyCQA/pylint/issues/3765 +# `unused-import` should not be emitted when a type annotation uses quotation marks +from typing import List + + +class Bee: + def get_all_classes(self) -> "List[Bee]": + pass diff --git a/tests/functional/u/unused/unused_import.txt b/tests/functional/u/unused/unused_import.txt index 059405388e..f242bcb23d 100644 --- a/tests/functional/u/unused/unused_import.txt +++ b/tests/functional/u/unused/unused_import.txt @@ -1,14 +1,14 @@ unused-import:3:0:3:16::Unused import xml.etree:UNDEFINED unused-import:4:0:4:14::Unused import xml.sax:UNDEFINED unused-import:5:0:5:22::Unused os.path imported as test:UNDEFINED -unused-import:6:0:6:29::Unused argv imported from sys as test2:UNDEFINED -unused-import:7:0:7:21::Unused flags imported from sys:UNDEFINED -unused-import:9:0:9:51::Unused OrderedDict imported from collections:UNDEFINED -unused-import:9:0:9:51::Unused deque imported from collections:UNDEFINED -unused-import:10:0:10:22::Unused import re:UNDEFINED -unused-import:13:0:13:40::Unused SomeOtherName imported from fake:UNDEFINED -unused-import:48:0:48:9::Unused import os:UNDEFINED -unused-import:79:4:79:19::Unused import unittest:UNDEFINED -unused-import:81:4:81:15::Unused import uuid:UNDEFINED -unused-import:83:4:83:19::Unused import warnings:UNDEFINED -unused-import:85:4:85:21::Unused import compileall:UNDEFINED +unused-import:7:0:7:29::Unused argv imported from sys as test2:UNDEFINED +unused-import:8:0:8:21::Unused flags imported from sys:UNDEFINED +unused-import:11:0:11:51::Unused OrderedDict imported from collections:UNDEFINED +unused-import:11:0:11:51::Unused deque imported from collections:UNDEFINED +unused-import:12:0:12:22::Unused import re:UNDEFINED +unused-import:16:0:16:40::Unused SomeOtherName imported from fake:UNDEFINED +unused-import:53:0:53:9::Unused import os:UNDEFINED +unused-import:88:4:88:19::Unused import unittest:UNDEFINED +unused-import:90:4:90:15::Unused import uuid:UNDEFINED +unused-import:92:4:92:19::Unused import warnings:UNDEFINED +unused-import:94:4:94:21::Unused import compileall:UNDEFINED diff --git a/tests/functional/u/unused/unused_import_assigned_to.py b/tests/functional/u/unused/unused_import_assigned_to.py index fb339bce78..21b8e97c9a 100644 --- a/tests/functional/u/unused/unused_import_assigned_to.py +++ b/tests/functional/u/unused/unused_import_assigned_to.py @@ -1,5 +1,5 @@ # pylint: disable=missing-docstring, import-error, invalid-name -# pylint: disable=too-few-public-methods, disallowed-name, no-member, useless-object-inheritance +# pylint: disable=too-few-public-methods, disallowed-name, no-member import uuid @@ -9,7 +9,7 @@ from .a import x -class Y(object): +class Y: x = x[0] @@ -17,9 +17,9 @@ def test(default=None): return default -class BaseModel(object): +class BaseModel: uuid = test(default=uuid.uuid4) -class bar(object): +class bar: foo = foo.baz diff --git a/tests/functional/u/unused/unused_import_py30.py b/tests/functional/u/unused/unused_import_py30.py index 95fed20550..2e79b57956 100644 --- a/tests/functional/u/unused/unused_import_py30.py +++ b/tests/functional/u/unused/unused_import_py30.py @@ -1,6 +1,6 @@ """check unused import for metaclasses""" # pylint: disable=too-few-public-methods,wrong-import-position,ungrouped-imports -__revision__ = 1 + import abc import sys from abc import ABCMeta diff --git a/tests/functional/u/unused/unused_import_py30.txt b/tests/functional/u/unused/unused_import_py30.txt index 355e812ecb..69c2e293db 100644 --- a/tests/functional/u/unused/unused_import_py30.txt +++ b/tests/functional/u/unused/unused_import_py30.txt @@ -1 +1 @@ -reimported:7:0:7:40::Reimport 'ABCMeta' (imported line 6):UNDEFINED +reimported:7:0:7:40::Reimport 'ABCMeta' (imported line 6):HIGH diff --git a/tests/functional/u/unused/unused_import_py39.py b/tests/functional/u/unused/unused_import_py39.py new file mode 100644 index 0000000000..2a897b1741 --- /dev/null +++ b/tests/functional/u/unused/unused_import_py39.py @@ -0,0 +1,10 @@ +""" +Test that a constant parameter of `typing.Annotated` does not emit `unused-import`. +`typing.Annotated` was introduced in Python version 3.9 +""" + +from pathlib import Path # [unused-import] +import typing as t + + +example: t.Annotated[str, "Path"] = "/foo/bar" diff --git a/tests/functional/u/unused/unused_import_py39.rc b/tests/functional/u/unused/unused_import_py39.rc new file mode 100644 index 0000000000..16b75eea75 --- /dev/null +++ b/tests/functional/u/unused/unused_import_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.9 diff --git a/tests/functional/u/unused/unused_import_py39.txt b/tests/functional/u/unused/unused_import_py39.txt new file mode 100644 index 0000000000..50e5ad5a96 --- /dev/null +++ b/tests/functional/u/unused/unused_import_py39.txt @@ -0,0 +1 @@ +unused-import:6:0:6:24::Unused Path imported from pathlib:UNDEFINED diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation.py b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation.py new file mode 100644 index 0000000000..400e7725e0 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation.py @@ -0,0 +1,31 @@ +"""Test if pylint sees names inside string literal type annotations. #3299""" +# pylint: disable=too-few-public-methods + +from argparse import ArgumentParser, Namespace +import os +from os import PathLike +from pathlib import Path +from typing import NoReturn, Set + +# unused-import shouldn't be emitted for Path +example1: Set["Path"] = set() + +def example2(_: "ArgumentParser") -> "NoReturn": + """unused-import shouldn't be emitted for ArgumentParser or NoReturn.""" + while True: + pass + +def example3(_: "os.PathLike[str]") -> None: + """unused-import shouldn't be emitted for os.""" + +def example4(_: "PathLike[str]") -> None: + """unused-import shouldn't be emitted for PathLike.""" + +# pylint shouldn't crash with the following strings in a type annotation context +example5: Set[""] +example6: Set[" "] +example7: Set["?"] + +class Class: + """unused-import shouldn't be emitted for Namespace""" + cls: "Namespace" diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.py b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.py new file mode 100644 index 0000000000..00bf5799fc --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.py @@ -0,0 +1,9 @@ +# pylint: disable=missing-docstring + +from typing import TypeAlias + +def unused_variable_should_not_be_emitted(): + """unused-variable shouldn't be emitted for Example.""" + Example: TypeAlias = int + result: set["Example"] = set() + return result diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.rc b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.rc new file mode 100644 index 0000000000..68a8c8ef15 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.py b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.py new file mode 100644 index 0000000000..96658ae369 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.py @@ -0,0 +1,27 @@ +# pylint: disable=missing-docstring + +from argparse import ArgumentParser # [unused-import] +from argparse import Namespace # [unused-import] +import http # [unused-import] +from http import HTTPStatus +import typing as t +from typing import Literal as Lit + +# str inside Literal shouldn't be treated as names +example1: t.Literal["ArgumentParser", Lit["Namespace", "ArgumentParser"]] + + +def unused_variable_example(): + hello = "hello" # [unused-variable] + world = "world" # [unused-variable] + example2: Lit["hello", "world"] = "hello" + return example2 + + +# pylint shouldn't crash with the following strings in a type annotation context +example3: Lit["", " ", "?"] = "?" + + +# See https://peps.python.org/pep-0586/#literals-enums-and-forward-references +example4: t.Literal["http.HTTPStatus.OK", "http.HTTPStatus.NOT_FOUND"] +example5: "t.Literal[HTTPStatus.OK, HTTPStatus.NOT_FOUND]" diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.rc b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.txt b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.txt new file mode 100644 index 0000000000..6f8c709bf2 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.txt @@ -0,0 +1,5 @@ +unused-import:3:0:3:35::Unused ArgumentParser imported from argparse:UNDEFINED +unused-import:4:0:4:30::Unused Namespace imported from argparse:UNDEFINED +unused-import:5:0:5:11::Unused import http:UNDEFINED +unused-variable:15:4:15:9:unused_variable_example:Unused variable 'hello':UNDEFINED +unused-variable:16:4:16:9:unused_variable_example:Unused variable 'world':UNDEFINED diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.py b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.py new file mode 100644 index 0000000000..1258844cd2 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.py @@ -0,0 +1,11 @@ +# pylint: disable=missing-docstring + +import graphlib +from graphlib import TopologicalSorter + +def example( + sorter1: "graphlib.TopologicalSorter[int]", + sorter2: "TopologicalSorter[str]", +) -> None: + """unused-import shouldn't be emitted for graphlib or TopologicalSorter.""" + print(sorter1, sorter2) diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc new file mode 100644 index 0000000000..16b75eea75 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.9 diff --git a/tests/functional/u/unused/unused_private_member.py b/tests/functional/u/unused/unused_private_member.py index 8693c1dd6b..1696fb691b 100644 --- a/tests/functional/u/unused/unused_private_member.py +++ b/tests/functional/u/unused/unused_private_member.py @@ -320,8 +320,8 @@ class FalsePositive4756a: def __bar(self, x): print(x) fizz = partialmethod(__bar, 'fizz') -foo = FalsePositive4756a() -foo.fizz() +test = FalsePositive4756a() +test.fizz() class FalsePositive4756b: def __get_prop(self): diff --git a/tests/functional/u/unused/unused_variable.py b/tests/functional/u/unused/unused_variable.py index d59043e22d..0058516c94 100644 --- a/tests/functional/u/unused/unused_variable.py +++ b/tests/functional/u/unused/unused_variable.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring, invalid-name, too-few-public-methods, useless-object-inheritance,import-outside-toplevel, fixme, line-too-long +# pylint: disable=missing-docstring, invalid-name, too-few-public-methods, import-outside-toplevel, fixme, line-too-long, broad-exception-raised def test_regression_737(): import xml # [unused-import] @@ -22,7 +22,7 @@ def test_local_field_prefixed_with_unused_or_ignored(): ignored_local_field = 42 -class HasUnusedDunderClass(object): +class HasUnusedDunderClass: def test(self): __class__ = 42 # [unused-variable] @@ -66,8 +66,7 @@ def hello(arg): return True raise Exception -# pylint: disable=wrong-import-position,misplaced-future -from __future__ import print_function +# pylint: disable=wrong-import-position PATH = OS = collections = deque = None @@ -95,7 +94,7 @@ def test_global(): variables through imports. """ # pylint: disable=redefined-outer-name - global PATH, OS, collections, deque # [global-variable-not-assigned, global-variable-not-assigned] + global PATH, OS, collections, deque # [global-statement] from os import path as PATH import os as OS import collections @@ -187,3 +186,16 @@ def sibling_except_handlers(): pass except ValueError as e: print(e) + +def func6(): + a = 1 + + def nonlocal_writer(): + nonlocal a + + for a in range(10): + pass + + nonlocal_writer() + + assert a == 9, a diff --git a/tests/functional/u/unused/unused_variable.txt b/tests/functional/u/unused/unused_variable.txt index ee9798e670..7b1fa834db 100644 --- a/tests/functional/u/unused/unused_variable.txt +++ b/tests/functional/u/unused/unused_variable.txt @@ -12,18 +12,17 @@ unused-import:54:4:54:38:unused_import_from:Unused wraps imported from functools unused-import:55:4:55:38:unused_import_from:Unused namedtuple imported from collections:UNDEFINED unused-import:59:4:59:40:unused_import_in_function:Unused hexdigits imported from string:UNDEFINED unused-variable:64:4:64:10:hello:Unused variable 'my_var':UNDEFINED -unused-variable:76:4:76:8:function:Unused variable 'aaaa':UNDEFINED -global-variable-not-assigned:98:4:98:39:test_global:Using global for 'PATH' but no assignment is done:UNDEFINED -global-variable-not-assigned:98:4:98:39:test_global:Using global for 'deque' but no assignment is done:UNDEFINED -unused-import:104:4:104:28:test_global:Unused platform imported from sys:UNDEFINED -unused-import:105:4:105:38:test_global:Unused version imported from sys as VERSION:UNDEFINED -unused-import:106:4:106:15:test_global:Unused import this:UNDEFINED -unused-import:107:4:107:19:test_global:Unused re imported as RE:UNDEFINED -unused-variable:111:4:111:10:function2:Unused variable 'unused':UNDEFINED -redefined-outer-name:117:8:118:42:function2:Redefining name 'error' from outer scope (line 114):UNDEFINED -redefined-outer-name:145:8:146:28:func3:Redefining name 'error' from outer scope (line 141):UNDEFINED -unused-variable:145:8:146:28:func3:Unused variable 'error':UNDEFINED -unused-variable:151:4:155:26:func4:Unused variable 'error':UNDEFINED -redefined-outer-name:154:8:155:26:func4:Redefining name 'error' from outer scope (line 151):UNDEFINED -unused-variable:162:4:163:12:main:Unused variable 'e':UNDEFINED -undefined-loop-variable:169:10:169:11:main:Using possibly undefined loop variable 'e':UNDEFINED +unused-variable:75:4:75:8:function:Unused variable 'aaaa':UNDEFINED +global-statement:97:4:97:39:test_global:Using the global statement:UNDEFINED +unused-import:103:4:103:28:test_global:Unused platform imported from sys:UNDEFINED +unused-import:104:4:104:38:test_global:Unused version imported from sys as VERSION:UNDEFINED +unused-import:105:4:105:15:test_global:Unused import this:UNDEFINED +unused-import:106:4:106:19:test_global:Unused re imported as RE:UNDEFINED +unused-variable:110:4:110:10:function2:Unused variable 'unused':UNDEFINED +redefined-outer-name:116:8:117:42:function2:Redefining name 'error' from outer scope (line 113):UNDEFINED +redefined-outer-name:144:8:145:28:func3:Redefining name 'error' from outer scope (line 140):UNDEFINED +unused-variable:144:8:145:28:func3:Unused variable 'error':UNDEFINED +unused-variable:150:4:154:26:func4:Unused variable 'error':UNDEFINED +redefined-outer-name:153:8:154:26:func4:Redefining name 'error' from outer scope (line 150):UNDEFINED +unused-variable:161:4:162:12:main:Unused variable 'e':UNDEFINED +undefined-loop-variable:168:10:168:11:main:Using possibly undefined loop variable 'e':UNDEFINED diff --git a/tests/functional/u/use/use_implicit_booleaness_not_comparison.txt b/tests/functional/u/use/use_implicit_booleaness_not_comparison.txt index d316d5acd3..2ace15d7e2 100644 --- a/tests/functional/u/use/use_implicit_booleaness_not_comparison.txt +++ b/tests/functional/u/use/use_implicit_booleaness_not_comparison.txt @@ -1,32 +1,32 @@ -use-implicit-booleaness-not-comparison:14:7:14:21:github_issue_4774:'bad_list == []' can be simplified to 'not bad_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:22:3:22:20::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:25:3:25:19::'empty_list == []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:28:3:28:19::'empty_dict == {}' can be simplified to 'not empty_dict' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:31:3:31:20::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:34:3:34:19::'empty_list == []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:37:3:37:19::'empty_dict == {}' can be simplified to 'not empty_dict' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:42:11:42:18:bad_tuple_return:'t == ()' can be simplified to 'not t' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:46:11:46:18:bad_list_return:'b == []' can be simplified to 'not b' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:50:11:50:18:bad_dict_return:'c == {}' can be simplified to 'not c' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:52:7:52:24::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:53:7:53:23::'empty_list == []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:54:7:54:23::'empty_dict != {}' can be simplified to 'empty_dict' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:55:7:55:23::'empty_tuple < ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:56:7:56:23::'empty_list <= []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:57:7:57:23::'empty_tuple > ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:58:7:58:23::'empty_list >= []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:83:3:83:10::'a == []' can be simplified to 'not a' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:95:3:95:10::'e == []' can be simplified to 'not e' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:95:15:95:22::'f == {}' can be simplified to 'not f' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:133:3:133:14::'A.lst == []' can be simplified to 'not A.lst' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:137:3:137:14::'A.lst == []' can be simplified to 'not A.lst' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:141:3:141:20::'A.test(...) == []' can be simplified to 'not A.test(...)' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:149:3:149:24::'test_function(...) == []' can be simplified to 'not test_function(...)' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:156:3:156:20::'numpy_array == []' can be simplified to 'not numpy_array' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:158:3:158:20::'numpy_array != []' can be simplified to 'numpy_array' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:160:3:160:20::'numpy_array >= ()' can be simplified to 'not numpy_array' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:185:3:185:13::'data == {}' can be simplified to 'not data' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:187:3:187:13::'data != {}' can be simplified to 'data' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:195:3:195:26::'long_test == {}' can be simplified to 'not long_test' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:233:11:233:41:test_func:'my_class.parent_function == {}' can be simplified to 'not my_class.parent_function' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:234:11:234:37:test_func:'my_class.my_property == {}' can be simplified to 'not my_class.my_property' as an empty sequence is falsey:UNDEFINED +use-implicit-booleaness-not-comparison:14:7:14:21:github_issue_4774:'bad_list == []' can be simplified to 'not bad_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:22:3:22:20::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:25:3:25:19::'empty_list == []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:28:3:28:19::'empty_dict == {}' can be simplified to 'not empty_dict' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:31:3:31:20::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:34:3:34:19::'empty_list == []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:37:3:37:19::'empty_dict == {}' can be simplified to 'not empty_dict' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:42:11:42:18:bad_tuple_return:'t == ()' can be simplified to 'not t' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:46:11:46:18:bad_list_return:'b == []' can be simplified to 'not b' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:50:11:50:18:bad_dict_return:'c == {}' can be simplified to 'not c' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:52:7:52:24::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:53:7:53:23::'empty_list == []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:54:7:54:23::'empty_dict != {}' can be simplified to 'empty_dict' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:55:7:55:23::'empty_tuple < ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:56:7:56:23::'empty_list <= []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:57:7:57:23::'empty_tuple > ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:58:7:58:23::'empty_list >= []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:83:3:83:10::'a == []' can be simplified to 'not a' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:95:3:95:10::'e == []' can be simplified to 'not e' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:95:15:95:22::'f == {}' can be simplified to 'not f' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:133:3:133:14::'A.lst == []' can be simplified to 'not A.lst' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:137:3:137:14::'A.lst == []' can be simplified to 'not A.lst' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:141:3:141:20::'A.test(...) == []' can be simplified to 'not A.test(...)' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:149:3:149:24::'test_function(...) == []' can be simplified to 'not test_function(...)' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:156:3:156:20::'numpy_array == []' can be simplified to 'not numpy_array' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:158:3:158:20::'numpy_array != []' can be simplified to 'numpy_array' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:160:3:160:20::'numpy_array >= ()' can be simplified to 'not numpy_array' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:185:3:185:13::'data == {}' can be simplified to 'not data' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:187:3:187:13::'data != {}' can be simplified to 'data' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:195:3:195:26::'long_test == {}' can be simplified to 'not long_test' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:233:11:233:41:test_func:'my_class.parent_function == {}' can be simplified to 'not my_class.parent_function' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:234:11:234:37:test_func:'my_class.my_property == {}' can be simplified to 'not my_class.my_property' as an empty dict is falsey:HIGH diff --git a/tests/functional/u/use/use_implicit_booleaness_not_len.txt b/tests/functional/u/use/use_implicit_booleaness_not_len.txt index 11412f5b28..85917de828 100644 --- a/tests/functional/u/use/use_implicit_booleaness_not_len.txt +++ b/tests/functional/u/use/use_implicit_booleaness_not_len.txt @@ -1,26 +1,26 @@ -use-implicit-booleaness-not-len:4:3:4:14::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:7:3:7:18::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:11:9:11:34::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:14:11:14:22::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED +use-implicit-booleaness-not-len:4:3:4:14::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:7:3:7:18::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:11:9:11:34::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:14:11:14:22::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE comparison-of-constants:39:3:39:28::"Comparison between constants: '0 < 1' has a constant value":HIGH -use-implicit-booleaness-not-len:56:5:56:16::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:61:5:61:20::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:64:6:64:17::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:67:6:67:21::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:70:12:70:23::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:73:6:73:21::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:96:11:96:20:github_issue_1331_v2:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:99:11:99:20:github_issue_1331_v3:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:102:17:102:26:github_issue_1331_v4:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:104:9:104:15::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:105:9:105:20::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:124:11:124:34:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:125:11:125:39:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:126:11:126:24:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:127:11:127:35:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:128:11:128:33:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:129:11:129:41:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:130:11:130:43:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:171:11:171:42:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED +use-implicit-booleaness-not-len:56:5:56:16::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:61:5:61:20::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:64:6:64:17::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:67:6:67:21::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:70:12:70:23::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:73:6:73:21::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:96:11:96:20:github_issue_1331_v2:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:99:11:99:20:github_issue_1331_v3:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:102:17:102:26:github_issue_1331_v4:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:104:9:104:15::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:105:9:105:20::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:124:11:124:34:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:125:11:125:39:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:126:11:126:24:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:127:11:127:35:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:128:11:128:33:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:129:11:129:41:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:130:11:130:43:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:171:11:171:42:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE undefined-variable:183:11:183:24:github_issue_4215:Undefined variable 'undefined_var':UNDEFINED undefined-variable:185:11:185:25:github_issue_4215:Undefined variable 'undefined_var2':UNDEFINED diff --git a/tests/functional/u/use/use_literal_dict.py b/tests/functional/u/use/use_literal_dict.py index 3377b4e630..598b382bdf 100644 --- a/tests/functional/u/use/use_literal_dict.py +++ b/tests/functional/u/use/use_literal_dict.py @@ -1,7 +1,46 @@ -# pylint: disable=missing-docstring, invalid-name +# pylint: disable=missing-docstring, invalid-name, disallowed-name, unused-argument, too-few-public-methods x = dict() # [use-dict-literal] -x = dict(a="1", b=None, c=3) +x = dict(a="1", b=None, c=3) # [use-dict-literal] x = dict(zip(["a", "b", "c"], [1, 2, 3])) x = {} x = {"a": 1, "b": 2, "c": 3} +x = dict(**x) # [use-dict-literal] + +def bar(boo: bool = False): + return 1 + +x = dict(foo=bar()) # [use-dict-literal] + +baz = {"e": 9, "f": 1} + +dict( # [use-dict-literal] + **baz, + suggestions=list( + bar( + boo=True, + ) + ), +) + +class SomeClass: + prop: dict = {"a": 1} + +inst = SomeClass() + +dict( # [use-dict-literal] + url="/foo", + **inst.prop, +) + +dict( # [use-dict-literal] + Lorem="ipsum", + dolor="sit", + amet="consectetur", + adipiscing="elit", + sed="do", + eiusmod="tempor", + incididunt="ut", + labore="et", + dolore="magna", +) diff --git a/tests/functional/u/use/use_literal_dict.txt b/tests/functional/u/use/use_literal_dict.txt index cbcb83f24e..1457664796 100644 --- a/tests/functional/u/use/use_literal_dict.txt +++ b/tests/functional/u/use/use_literal_dict.txt @@ -1 +1,7 @@ -use-dict-literal:3:4:3:10::Consider using {} instead of dict():UNDEFINED +use-dict-literal:3:4:3:10::Consider using '{}' instead of a call to 'dict'.:INFERENCE +use-dict-literal:4:4:4:28::"Consider using '{""a"": '1', ""b"": None, ""c"": 3}' instead of a call to 'dict'.":INFERENCE +use-dict-literal:8:4:8:13::Consider using '{**x}' instead of a call to 'dict'.:INFERENCE +use-dict-literal:13:4:13:19::"Consider using '{""foo"": bar()}' instead of a call to 'dict'.":INFERENCE +use-dict-literal:17:0:24:1::"Consider using '{""suggestions"": list(bar(boo=True)), **baz}' instead of a call to 'dict'.":INFERENCE +use-dict-literal:31:0:34:1::"Consider using '{""url"": '/foo', **inst.prop}' instead of a call to 'dict'.":INFERENCE +use-dict-literal:36:0:46:1::"Consider using '{""Lorem"": 'ipsum', ""dolor"": 'sit', ""amet"": 'consectetur', ""adipiscing"": 'elit', ... }' instead of a call to 'dict'.":INFERENCE diff --git a/tests/functional/u/use/use_maxsplit_arg.py b/tests/functional/u/use/use_maxsplit_arg.py index d0d43c2b98..449457a0c3 100644 --- a/tests/functional/u/use/use_maxsplit_arg.py +++ b/tests/functional/u/use/use_maxsplit_arg.py @@ -52,12 +52,12 @@ def get_string(self) -> str: # Test with accessors -bar = Foo() -get_first = bar.get_string().split(',')[0] # [use-maxsplit-arg] -get_last = bar.get_string().split(',')[-1] # [use-maxsplit-arg] +test = Foo() +get_first = test.get_string().split(',')[0] # [use-maxsplit-arg] +get_last = test.get_string().split(',')[-1] # [use-maxsplit-arg] -get_mid = bar.get_string().split(',')[1] -get_mid = bar.get_string().split(',')[-2] +get_mid = test.get_string().split(',')[1] +get_mid = test.get_string().split(',')[-2] # Test with iterating over strings @@ -94,3 +94,11 @@ class Bar(): # Test for crash when sep is given by keyword # https://github.com/PyCQA/pylint/issues/5737 get_last = SEQ.split(sep=None)[-1] # [use-maxsplit-arg] + + +class FalsePositive4857: + def split(self, point): + return point + +obj = FalsePositive4857() +obj = obj.split((0, 0))[0] diff --git a/tests/functional/u/use/use_maxsplit_arg.txt b/tests/functional/u/use/use_maxsplit_arg.txt index a583dfccaf..b8f254004b 100644 --- a/tests/functional/u/use/use_maxsplit_arg.txt +++ b/tests/functional/u/use/use_maxsplit_arg.txt @@ -8,8 +8,8 @@ use-maxsplit-arg:45:12:45:36::Use Foo.class_str.split(',', maxsplit=1)[0] instea use-maxsplit-arg:46:11:46:35::Use Foo.class_str.rsplit(',', maxsplit=1)[-1] instead:UNDEFINED use-maxsplit-arg:47:12:47:37::Use Foo.class_str.split(',', maxsplit=1)[0] instead:UNDEFINED use-maxsplit-arg:48:11:48:36::Use Foo.class_str.rsplit(',', maxsplit=1)[-1] instead:UNDEFINED -use-maxsplit-arg:56:12:56:39::Use bar.get_string().split(',', maxsplit=1)[0] instead:UNDEFINED -use-maxsplit-arg:57:11:57:38::Use bar.get_string().rsplit(',', maxsplit=1)[-1] instead:UNDEFINED +use-maxsplit-arg:56:12:56:40::Use test.get_string().split(',', maxsplit=1)[0] instead:UNDEFINED +use-maxsplit-arg:57:11:57:39::Use test.get_string().rsplit(',', maxsplit=1)[-1] instead:UNDEFINED use-maxsplit-arg:66:10:66:22::Use s.split(' ', maxsplit=1)[0] instead:UNDEFINED use-maxsplit-arg:67:10:67:22::Use s.rsplit(' ', maxsplit=1)[-1] instead:UNDEFINED use-maxsplit-arg:76:6:76:26::Use Bar.split.split(',', maxsplit=1)[0] instead:UNDEFINED diff --git a/tests/functional/u/use/use_sequence_for_iteration.py b/tests/functional/u/use/use_sequence_for_iteration.py index 2dd1feb187..264e6e7b9d 100644 --- a/tests/functional/u/use/use_sequence_for_iteration.py +++ b/tests/functional/u/use/use_sequence_for_iteration.py @@ -13,4 +13,16 @@ [x for x in var] [x for x in {1, 2, 3}] # [use-sequence-for-iteration] -[x for x in {*var, 4}] # [use-sequence-for-iteration] +[x for x in {*var, 4}] + +def deduplicate(list_in): + for thing in {*list_in}: + print(thing) + +def deduplicate_two_lists(input1, input2): + for thing in {*input1, *input2}: + print(thing) + +def deduplicate_nested_sets(input1, input2, input3, input4): + for thing in {{*input1, *input2}, {*input3, *input4}}: + print(thing) diff --git a/tests/functional/u/use/use_sequence_for_iteration.txt b/tests/functional/u/use/use_sequence_for_iteration.txt index beb23a4dfd..3787b7a0eb 100644 --- a/tests/functional/u/use/use_sequence_for_iteration.txt +++ b/tests/functional/u/use/use_sequence_for_iteration.txt @@ -1,4 +1,3 @@ -use-sequence-for-iteration:7:9:7:18::Use a sequence type when iterating over values:UNDEFINED -use-sequence-for-iteration:11:12:11:21::Use a sequence type when iterating over values:UNDEFINED -use-sequence-for-iteration:14:12:14:21::Use a sequence type when iterating over values:UNDEFINED -use-sequence-for-iteration:16:12:16:21::Use a sequence type when iterating over values:UNDEFINED +use-sequence-for-iteration:7:9:7:18::Use a sequence type when iterating over values:HIGH +use-sequence-for-iteration:11:12:11:21::Use a sequence type when iterating over values:HIGH +use-sequence-for-iteration:14:12:14:21::Use a sequence type when iterating over values:HIGH diff --git a/tests/functional/u/used/used_before_assignment.py b/tests/functional/u/used/used_before_assignment.py index 5b469041ea..d36b2fd8d5 100644 --- a/tests/functional/u/used/used_before_assignment.py +++ b/tests/functional/u/used/used_before_assignment.py @@ -1,8 +1,118 @@ -"""pylint doesn't see the NameError in this module""" +"""Miscellaneous used-before-assignment cases""" # pylint: disable=consider-using-f-string, missing-function-docstring -__revision__ = None MSG = "hello %s" % MSG # [used-before-assignment] MSG2 = "hello %s" % MSG2 # [used-before-assignment] + +def outer(): + inner() # [used-before-assignment] + def inner(): + pass + +outer() + + +# pylint: disable=unused-import, wrong-import-position, import-outside-toplevel, reimported, redefined-outer-name, global-statement +import time +def redefine_time_import(): + print(time.time()) # [used-before-assignment] + import time + + +def redefine_time_import_with_global(): + global time # pylint: disable=invalid-name + print(time.time()) + import time + + +# Control flow cases +FALSE = False +if FALSE: + VAR2 = True +if VAR2: # [used-before-assignment] + pass + +if FALSE: # pylint: disable=simplifiable-if-statement + VAR3 = True +elif VAR2: + VAR3 = True +else: + VAR3 = False +if VAR3: + pass + +if FALSE: + VAR4 = True +elif VAR2: + pass +else: + VAR4 = False +if VAR4: # [used-before-assignment] + pass + +if FALSE: + VAR5 = True +elif VAR2: + if FALSE: # pylint: disable=simplifiable-if-statement + VAR5 = True + else: + VAR5 = True +if VAR5: + pass + +if FALSE: + VAR6 = False +if VAR6: # [used-before-assignment] + pass + + +# Nested try +if FALSE: + try: + VAR7 = True + except ValueError: + pass +else: + VAR7 = False +if VAR7: + pass + +if FALSE: + try: + VAR8 = True + except ValueError as ve: + print(ve) + raise +else: + VAR8 = False +if VAR8: + pass + +if FALSE: + for i in range(5): + VAR9 = i + break +print(VAR9) + +if FALSE: + with open(__name__, encoding='utf-8') as f: + VAR10 = __name__ +print(VAR10) # [used-before-assignment] + +for num in [0, 1]: + VAR11 = num + if VAR11: + VAR12 = False +print(VAR12) + +def turn_on2(**kwargs): + """https://github.com/PyCQA/pylint/issues/7873""" + if "brightness" in kwargs: + brightness = kwargs["brightness"] + var, *args = (1, "set_dimmer_state", brightness) + else: + var, *args = (1, "restore_dimmer_state") + + print(var, *args) diff --git a/tests/functional/u/used/used_before_assignment.txt b/tests/functional/u/used/used_before_assignment.txt index 64fee9e556..70153f39ac 100644 --- a/tests/functional/u/used/used_before_assignment.txt +++ b/tests/functional/u/used/used_before_assignment.txt @@ -1,2 +1,8 @@ -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: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:20:10:20:14:redefine_time_import:Using variable 'time' before assignment:HIGH +used-before-assignment:34:3:34:7::Using variable 'VAR2' before assignment:CONTROL_FLOW +used-before-assignment:52:3:52:7::Using variable 'VAR4' before assignment:CONTROL_FLOW +used-before-assignment:67:3:67:7::Using variable 'VAR6' before assignment:CONTROL_FLOW +used-before-assignment:102:6:102:11::Using variable 'VAR10' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/used/used_before_assignment_comprehension_homonyms.py b/tests/functional/u/used/used_before_assignment_comprehension_homonyms.py index feae58dbe5..2321afed74 100644 --- a/tests/functional/u/used/used_before_assignment_comprehension_homonyms.py +++ b/tests/functional/u/used/used_before_assignment_comprehension_homonyms.py @@ -1,4 +1,5 @@ """Homonym between filtered comprehension and assignment in except block.""" +# pylint: disable=broad-exception-raised def func(): """https://github.com/PyCQA/pylint/issues/5586""" diff --git a/tests/functional/u/used/used_before_assignment_conditional.py b/tests/functional/u/used/used_before_assignment_conditional.py new file mode 100644 index 0000000000..b024d28982 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_conditional.py @@ -0,0 +1,7 @@ +"""used-before-assignment cases involving IF conditions""" + +if 1 + 1 == 2: + x = x + 1 # [used-before-assignment] + +if y: # [used-before-assignment] + y = y + 1 diff --git a/tests/functional/u/used/used_before_assignment_conditional.txt b/tests/functional/u/used/used_before_assignment_conditional.txt new file mode 100644 index 0000000000..a65f9f7387 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_conditional.txt @@ -0,0 +1,2 @@ +used-before-assignment:4:8:4:9::Using variable 'x' before assignment:HIGH +used-before-assignment:6:3:6:4::Using variable 'y' before assignment:HIGH 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 a5dc5c23b5..a7e58bb61f 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,6 @@ """If the else block returns, it is generally safe to rely on assignments in the except.""" - +# pylint: disable=missing-function-docstring, invalid-name +import sys def valid(): """https://github.com/PyCQA/pylint/issues/6790""" @@ -59,3 +60,15 @@ def invalid_4(): else: print(error) # [used-before-assignment] return + +def valid_exit(): + try: + pass + except SystemExit as e: + lint_result = e.code + else: + sys.exit("Bad") + if lint_result != 0: + sys.exit("Error is 0.") + + print(lint_result) diff --git a/tests/functional/u/used/used_before_assignment_else_return.txt b/tests/functional/u/used/used_before_assignment_else_return.txt index d7d1835f49..5ef28cfd68 100644 --- a/tests/functional/u/used/used_before_assignment_else_return.txt +++ b/tests/functional/u/used/used_before_assignment_else_return.txt @@ -1,4 +1,4 @@ -used-before-assignment:25:14:25:19:invalid:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:38:14:38:19:invalid_2:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:50:14:50:19:invalid_3:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:60:14:60:19:invalid_4:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:26:14:26:19:invalid:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:39:14:39:19:invalid_2:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:51:14:51:19:invalid_3:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:61:14:61:19:invalid_4:Using variable 'error' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.py b/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.py index 086ad0554d..c83a484739 100644 --- a/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.py +++ b/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.py @@ -2,7 +2,7 @@ try blocks with return statements. See: https://github.com/PyCQA/pylint/issues/5500. """ -# pylint: disable=inconsistent-return-statements +# pylint: disable=inconsistent-return-statements,broad-exception-raised def function(): @@ -77,7 +77,7 @@ def func_ok5(var): def func_ok6(var): """Define 'msg' in one handler nested under if block.""" - err_message = False + err_message = "Division by 0" try: return 1 / var.some_other_func() except ZeroDivisionError: diff --git a/tests/functional/u/used/used_before_assignment_issue1081.py b/tests/functional/u/used/used_before_assignment_issue1081.py index 0c59ccebe0..d478bdeecc 100644 --- a/tests/functional/u/used/used_before_assignment_issue1081.py +++ b/tests/functional/u/used/used_before_assignment_issue1081.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring,invalid-name,too-few-public-methods, useless-object-inheritance +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods x = 24 @@ -36,5 +36,5 @@ def func(something): return something ** 3 -class FalsePositive(object): +class FalsePositive: x = func(x) diff --git a/tests/functional/u/used/used_before_assignment_issue626.txt b/tests/functional/u/used/used_before_assignment_issue626.txt index 1ee575ba3e..3d0e572463 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:HIGH +used-before-assignment:8:10:8:11:main1:Using variable 'e' before assignment:CONTROL_FLOW 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:HIGH +used-before-assignment:44:10:44:11:main4:Using variable 'e' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/used/used_before_assignment_nonlocal.py b/tests/functional/u/used/used_before_assignment_nonlocal.py index d651bcf11a..18e16177d0 100644 --- a/tests/functional/u/used/used_before_assignment_nonlocal.py +++ b/tests/functional/u/used/used_before_assignment_nonlocal.py @@ -1,7 +1,6 @@ """Check for nonlocal and used-before-assignment""" # pylint: disable=missing-docstring, unused-variable, too-few-public-methods -__revision__ = 0 def test_ok(): """ uses nonlocal """ @@ -89,3 +88,21 @@ def inner(): nonlocal some_num inner() print(some_num) # [used-before-assignment] + + +def inner_function_lacks_access_to_outer_args(args): + """Check homonym between inner function and outer function names""" + def inner(): + print(args) # [used-before-assignment] + args = [] + inner() + print(args) + + +def inner_function_ok(args): + """Explicitly redefined homonym defined before is OK.""" + def inner(): + args = [] + print(args) + inner() + print(args) diff --git a/tests/functional/u/used/used_before_assignment_nonlocal.txt b/tests/functional/u/used/used_before_assignment_nonlocal.txt index f3e873315d..2bdbf2fe1e 100644 --- a/tests/functional/u/used/used_before_assignment_nonlocal.txt +++ b/tests/functional/u/used/used_before_assignment_nonlocal.txt @@ -1,7 +1,8 @@ -used-before-assignment:18:14:18:17:test_fail.wrap:Using variable 'cnt' before assignment:HIGH -used-before-assignment:27:14:27:17:test_fail2.wrap:Using variable 'cnt' before assignment:HIGH -used-before-assignment:30:20:30:30:test_fail3:Using variable 'test_fail4' before assignment:HIGH -used-before-assignment:34:22:34:32:test_fail4:Using variable 'test_fail5' before assignment:HIGH -used-before-assignment:34:44:34:53:test_fail4:Using variable 'undefined' before assignment:HIGH -used-before-assignment:40:18:40:28:test_fail5:Using variable 'undefined1' before assignment:HIGH -used-before-assignment:91:10:91:18:type_annotation_never_gets_value_despite_nonlocal:Using variable 'some_num' before assignment:HIGH +used-before-assignment:17:14:17:17:test_fail.wrap:Using variable 'cnt' before assignment:HIGH +used-before-assignment:26:14:26:17:test_fail2.wrap:Using variable 'cnt' before assignment:HIGH +used-before-assignment:29:20:29:30:test_fail3:Using variable 'test_fail4' before assignment:HIGH +used-before-assignment:33:22:33:32:test_fail4:Using variable 'test_fail5' before assignment:HIGH +used-before-assignment:33:44:33:53:test_fail4:Using variable 'undefined' before assignment:HIGH +used-before-assignment:39:18:39:28:test_fail5:Using variable 'undefined1' before assignment:HIGH +used-before-assignment:90:10:90:18:type_annotation_never_gets_value_despite_nonlocal:Using variable 'some_num' before assignment:HIGH +used-before-assignment:96:14:96:18:inner_function_lacks_access_to_outer_args.inner:Using variable 'args' before assignment:HIGH diff --git a/tests/functional/u/used/used_before_assignment_py310.py b/tests/functional/u/used/used_before_assignment_py310.py new file mode 100644 index 0000000000..14f46b61e9 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_py310.py @@ -0,0 +1,7 @@ +"""Tests for used-before-assignment with python 3.10's pattern matching""" + +match ("example", "one"): + case (x, y) if x == "example": + print("x used to cause used-before-assignment!") + case _: + print("good thing it doesn't now!") diff --git a/tests/functional/u/used/used_before_assignment_py310.rc b/tests/functional/u/used/used_before_assignment_py310.rc new file mode 100644 index 0000000000..68a8c8ef15 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_py310.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/u/used/used_before_assignment_ternary.py b/tests/functional/u/used/used_before_assignment_ternary.py new file mode 100644 index 0000000000..72012f4243 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_ternary.py @@ -0,0 +1,54 @@ +"""Tests for used-before-assignment false positive from ternary expression with walrus operator""" +# pylint: disable=unnecessary-lambda-assignment, unused-variable, disallowed-name, invalid-name + +def invalid(): + """invalid cases that will trigger used-before-assignment""" + var = foo(a, '', '') # [used-before-assignment] + print(str(1 if (a:=-1) else 0)) + var = bar(b) # [used-before-assignment] + var = c*c # [used-before-assignment] + var = 1 if (b:=-1) else 0 + var = 1 if (c:=-1) else 0 + +def attribute_call_valid(): + """assignment with attribute calls""" + var = (a if (a:='a') else '').lower() + var = ('' if (b:='b') else b).lower() + var = (c if (c:='c') else c).upper().lower().replace('', '').strip() + var = ''.strip().replace('', '' + (e if (e:='e') else '').lower()) + +def function_call_arg_valid(): + """assignment as function call arguments""" + var = str(a if (a:='a') else '') + var = str('' if (b:='b') else b) + var = foo(1, c if (c:=1) else 0, 1) + print(foo('', '', foo('', str(int(d if (d:='1') else '')), ''))) + +def function_call_keyword_valid(): + """assignment as function call keywords""" + var = foo(x=a if (a:='1') else '', y='', z='') + var = foo(x='', y=foo(x='', y='', z=b if (b:='1') else ''), z='') + +def dictionary_items_valid(): + """assignment as dictionary keys/values""" + var = { + 0: w if (w:=input()) else "", + } + var = { + x if (x:=input()) else "": 0, + } + var = { + 0: y if (y:=input()) else "", + z if (z:=input()) else "": 0, + } + +def complex_valid(): + """assignment within complex call expression""" + var = str(bar(bar(a if (a:=1) else 0))).lower().upper() + print(foo(x=foo(''.replace('', str(b if (b:=1) else 0).upper()), '', z=''), y='', z='')) + +def foo(x, y, z): + """helper function for tests""" + return x+y+z + +bar = lambda x : x diff --git a/tests/functional/u/used/used_before_assignment_ternary.rc b/tests/functional/u/used/used_before_assignment_ternary.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_ternary.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/u/used/used_before_assignment_ternary.txt b/tests/functional/u/used/used_before_assignment_ternary.txt new file mode 100644 index 0000000000..d991970e4c --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_ternary.txt @@ -0,0 +1,3 @@ +used-before-assignment:6:14:6:15:invalid:Using variable 'a' before assignment:HIGH +used-before-assignment:8:14:8:15:invalid:Using variable 'b' before assignment:HIGH +used-before-assignment:9:10:9:11:invalid:Using variable 'c' before assignment:HIGH diff --git a/tests/functional/u/used/used_before_assignment_typing.py b/tests/functional/u/used/used_before_assignment_typing.py index 9be01d7706..a685bdabc8 100644 --- a/tests/functional/u/used/used_before_assignment_typing.py +++ b/tests/functional/u/used/used_before_assignment_typing.py @@ -1,9 +1,65 @@ """Tests for used-before-assignment for typing related issues""" -# pylint: disable=missing-function-docstring - - -from typing import List, Optional - +# pylint: disable=missing-function-docstring,ungrouped-imports,invalid-name + + +from typing import List, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + if True: # pylint: disable=using-constant-test + import math + from urllib.request import urlopen + import array + import base64 + import binascii + import bisect + import calendar + import collections + import copy + import datetime + import email + import heapq + import json + import mailbox + import mimetypes + import numbers + import pprint + import types + import zoneinfo +elif input(): + import calendar, bisect # pylint: disable=multiple-imports + if input() + 1: + import heapq + else: + import heapq +elif input(): + try: + numbers = None if input() else 1 + import array + except Exception as e: # pylint: disable=broad-exception-caught + import types + finally: + copy = None +elif input(): + for i in range(1,2): + email = None + else: # pylint: disable=useless-else-on-loop + json = None + while input(): + import mailbox + else: # pylint: disable=useless-else-on-loop + mimetypes = None +elif input(): + with input() as base64: + pass + with input() as temp: + import binascii +else: + from urllib.request import urlopen + zoneinfo: str = '' + def pprint(): + pass + class collections: # pylint: disable=too-few-public-methods,missing-class-docstring + pass class MyClass: """Type annotation or default values for first level methods can't refer to their own class""" @@ -74,3 +130,68 @@ def function(self, var: int) -> None: def other_function(self) -> None: _x: MyThirdClass = self + + +class MyFourthClass: # pylint: disable=too-few-public-methods + """Class to test conditional imports guarded by TYPE_CHECKING two levels + up then used in function annotation. See https://github.com/PyCQA/pylint/issues/7539""" + + def is_close(self, comparator: math.isclose, first, second): # [used-before-assignment] + """Conditional imports guarded are only valid for variable annotations.""" + comparator(first, second) + + +class VariableAnnotationsGuardedByTypeChecking: # pylint: disable=too-few-public-methods + """Class to test conditional imports guarded by TYPE_CHECKING then used in + local (function) variable annotations, which are not evaluated at runtime. + + See: https://github.com/PyCQA/pylint/issues/7609 + and https://github.com/PyCQA/pylint/issues/7882 + """ + + still_an_error: datetime.date # [used-before-assignment] + + def print_date(self, date) -> None: + date: datetime.date = date + print(date) + + import datetime # pylint: disable=import-outside-toplevel + + +class ConditionalImportGuardedWhenUsed: # pylint: disable=too-few-public-methods + """Conditional imports also guarded by TYPE_CHECKING when used.""" + if TYPE_CHECKING: + print(urlopen) + + +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) + return calendar.Calendar() + + def defined_in_else_branch(self) -> urlopen: + print(zoneinfo) + print(pprint()) + print(collections()) + return urlopen + + def defined_in_nested_if_else(self) -> heapq: + print(heapq) + return heapq + + def defined_in_try_except(self) -> array: + print(types) + print(copy) + print(numbers) + return array + + def defined_in_loops(self) -> json: + print(email) + print(mailbox) + print(mimetypes) + return json + + def defined_in_with(self) -> base64: + print(binascii) + 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 ae05b23f38..c0a31fae08 100644 --- a/tests/functional/u/used/used_before_assignment_typing.txt +++ b/tests/functional/u/used/used_before_assignment_typing.txt @@ -1,3 +1,5 @@ -undefined-variable:12:21:12:28:MyClass.incorrect_typing_method:Undefined variable 'MyClass':UNDEFINED -undefined-variable:17:26:17:33:MyClass.incorrect_nested_typing_method:Undefined variable 'MyClass':UNDEFINED -undefined-variable:22:20:22:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED +undefined-variable:68:21:68:28:MyClass.incorrect_typing_method:Undefined variable 'MyClass':UNDEFINED +undefined-variable:73:26:73:33:MyClass.incorrect_nested_typing_method:Undefined variable 'MyClass':UNDEFINED +undefined-variable:78:20:78:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED +used-before-assignment:139:35:139:39:MyFourthClass.is_close:Using variable 'math' before assignment:HIGH +used-before-assignment:152:20:152:28:VariableAnnotationsGuardedByTypeChecking:Using variable 'datetime' before assignment:HIGH diff --git a/tests/functional/u/useless/useless_else_on_loop.py b/tests/functional/u/useless/useless_else_on_loop.py index 3431513cda..20354cad08 100644 --- a/tests/functional/u/useless/useless_else_on_loop.py +++ b/tests/functional/u/useless/useless_else_on_loop.py @@ -1,6 +1,5 @@ """Check for else branches on loops with break and return only.""" -from __future__ import print_function -__revision__ = 0 + def test_return_for(): """else + return is not acceptable.""" diff --git a/tests/functional/u/useless/useless_else_on_loop.txt b/tests/functional/u/useless/useless_else_on_loop.txt index 938f24cc51..067b6435d2 100644 --- a/tests/functional/u/useless/useless_else_on_loop.txt +++ b/tests/functional/u/useless/useless_else_on_loop.txt @@ -1,6 +1,6 @@ -useless-else-on-loop:10:4:11:31:test_return_for:Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED -useless-else-on-loop:18:4:19:31:test_return_while:Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED -useless-else-on-loop:28:0:29:21::Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED -useless-else-on-loop:35:0:36:21::Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED -useless-else-on-loop:40:0:43:13::Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED -useless-else-on-loop:87:4:88:19:test_break_in_orelse_deep2:Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED +useless-else-on-loop:9:4:10:31:test_return_for:Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED +useless-else-on-loop:17:4:18:31:test_return_while:Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED +useless-else-on-loop:27:0:28:21::Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED +useless-else-on-loop:34:0:35:21::Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED +useless-else-on-loop:39:0:42:13::Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED +useless-else-on-loop:86:4:87:19:test_break_in_orelse_deep2:Else clause on loop without a break statement, remove the else and de-indent all the code inside it:UNDEFINED diff --git a/tests/functional/u/useless/useless_super_delegation.py b/tests/functional/u/useless/useless_parent_delegation.py similarity index 73% rename from tests/functional/u/useless/useless_super_delegation.py rename to tests/functional/u/useless/useless_parent_delegation.py index cc7d51b65e..ce645e31f8 100644 --- a/tests/functional/u/useless/useless_super_delegation.py +++ b/tests/functional/u/useless/useless_parent_delegation.py @@ -1,14 +1,20 @@ # pylint: disable=missing-docstring, no-member, bad-super-call # pylint: disable=too-few-public-methods, unused-argument, invalid-name, too-many-public-methods -# pylint: disable=line-too-long, useless-object-inheritance, arguments-out-of-order +# pylint: disable=line-too-long, arguments-out-of-order # pylint: disable=super-with-arguments, dangerous-default-value +# pylint: disable=too-many-function-args, no-method-argument + +import random +from typing import Any, List default_var = 1 + + def not_a_method(param, param2): return super(None, None).not_a_method(param, param2) -class SuperBase(object): +class SuperBase: def with_default_arg(self, first, default_arg="only_in_super_base"): pass @@ -65,8 +71,8 @@ def with_default_arg_quad(self, first, default_arg="has_been_changed"): def with_default_unhandled(self, first, default_arg=lambda: True): super().with_default_arg_quad(first, default_arg) -class NotUselessSuper(Base): +class NotUselessSuper(Base): def multiple_statements(self): first = 42 * 24 return super().multiple_statements() + first @@ -117,14 +123,13 @@ def not_passing_default(self, first, second=None): return super(NotUselessSuper, self).not_passing_default(first) def passing_only_a_handful(self, first, second, third, fourth): - return super(NotUselessSuper, self).passing_only_a_handful( - first, second) + return super(NotUselessSuper, self).passing_only_a_handful(first, second) def not_the_same_order(self, first, second, third): return super(NotUselessSuper, self).not_the_same_order(third, first, second) def no_kwargs_in_signature(self, key=None): - values = {'key': 'something'} + values = {"key": "something"} return super(NotUselessSuper, self).no_kwargs_in_signature(**values) def no_args_in_signature(self, first, second): @@ -133,18 +138,16 @@ def no_args_in_signature(self, first, second): def variadics_with_multiple_keyword_arguments(self, **kwargs): return super(NotUselessSuper, self).variadics_with_multiple_keyword_arguments( - first=None, - second=None, - **kwargs) + first=None, second=None, **kwargs + ) def extraneous_keyword_params(self, none_ok=False): super(NotUselessSuper, self).extraneous_keyword_params( - none_ok, - valid_values=[23, 42]) + none_ok, valid_values=[23, 42] + ) def extraneous_positional_args(self, **args): - super(NotUselessSuper, self).extraneous_positional_args( - 1, 2, **args) + super(NotUselessSuper, self).extraneous_positional_args(1, 2, **args) def with_default_argument(self, first, default_arg="other"): # Not useless because the default_arg is different from the one in the base class @@ -154,7 +157,7 @@ def without_default_argument(self, first, second=True): # Not useless because in the base class there is not default value for second argument super(NotUselessSuper, self).without_default_argument(first, second) - def with_default_argument_none(self, first, default_arg='NotNone'): + def with_default_argument_none(self, first, default_arg="NotNone"): # Not useless because the default_arg is different from the one in the base class super(NotUselessSuper, self).with_default_argument_none(first, default_arg) @@ -170,11 +173,12 @@ def with_default_argument_tuple(self, first, default_arg=("42", "a")): # Not useless because the default_arg is different from the one in the base class super(NotUselessSuper, self).with_default_argument_tuple(first, default_arg) - def with_default_argument_dict(self, first, default_arg={'foo': 'bar'}): + def with_default_argument_dict(self, first, default_arg={"foo": "bar"}): # Not useless because the default_arg is different from the one in the base class super(NotUselessSuper, self).with_default_argument_dict(first, default_arg) default_var = 2 + def with_default_argument_var(self, first, default_arg=default_var): # Not useless because the default_arg refers to a different variable from the one in the base class super(NotUselessSuper, self).with_default_argument_var(first, default_arg) @@ -182,7 +186,9 @@ def with_default_argument_var(self, first, default_arg=default_var): def with_default_argument_bis(self, first, default_arg="default"): # Although the default_arg is the same as in the base class, the call signature # differs. Thus it is not useless. - super(NotUselessSuper, self).with_default_argument_bis(default_arg + "_argument") + super(NotUselessSuper, self).with_default_argument_bis( + default_arg + "_argument" + ) def fake_method(self, param2="other"): super(NotUselessSuper, self).fake_method(param2) @@ -202,7 +208,9 @@ def with_default_arg_ter(self, first, default_arg="has_been_changed_again"): def with_default_arg_quad(self, first, default_arg="has_been_changed"): # Not useless because the default value is the same as in the base but the # call is different from the signature - super(NotUselessSuper, self).with_default_arg_quad(first, default_arg + "_and_modified") + super(NotUselessSuper, self).with_default_arg_quad( + first, default_arg + "_and_modified" + ) def with_default_unhandled(self, first, default_arg=lambda: True): # Not useless because the default value type is not explicitly handled (Lambda), so assume they are different @@ -210,64 +218,63 @@ def with_default_unhandled(self, first, default_arg=lambda: True): class UselessSuper(Base): - - def equivalent_params(self): # [useless-super-delegation] + def equivalent_params(self): # [useless-parent-delegation] return super(UselessSuper, self).equivalent_params() - def equivalent_params_1(self, first): # [useless-super-delegation] + def equivalent_params_1(self, first): # [useless-parent-delegation] return super(UselessSuper, self).equivalent_params_1(first) - def equivalent_params_2(self, *args): # [useless-super-delegation] + def equivalent_params_2(self, *args): # [useless-parent-delegation] return super(UselessSuper, self).equivalent_params_2(*args) - def equivalent_params_3(self, *args, **kwargs): # [useless-super-delegation] + def equivalent_params_3(self, *args, **kwargs): # [useless-parent-delegation] return super(UselessSuper, self).equivalent_params_3(*args, **kwargs) - def equivalent_params_4(self, first): # [useless-super-delegation] + def equivalent_params_4(self, first): # [useless-parent-delegation] super(UselessSuper, self).equivalent_params_4(first) - def equivalent_params_5(self, first, *args): # [useless-super-delegation] + def equivalent_params_5(self, first, *args): # [useless-parent-delegation] super(UselessSuper, self).equivalent_params_5(first, *args) - def equivalent_params_6(self, first, *args, **kwargs): # [useless-super-delegation] + def equivalent_params_6(self, first, *args, **kwargs): # [useless-parent-delegation] return super(UselessSuper, self).equivalent_params_6(first, *args, **kwargs) - def with_default_argument(self, first, default_arg="default"): # [useless-super-delegation] + def with_default_argument(self, first, default_arg="default"): # [useless-parent-delegation] # useless because the default value here is the same as in the base class return super(UselessSuper, self).with_default_argument(first, default_arg) - def without_default_argument(self, first, second): # [useless-super-delegation] + def without_default_argument(self, first, second): # [useless-parent-delegation] return super(UselessSuper, self).without_default_argument(first, second) - def with_default_argument_none(self, first, default_arg=None): # [useless-super-delegation] + def with_default_argument_none(self, first, default_arg=None): # [useless-parent-delegation] # useless because the default value here is the same as in the base class super(UselessSuper, self).with_default_argument_none(first, default_arg) - def with_default_argument_int(self, first, default_arg=42): # [useless-super-delegation] + def with_default_argument_int(self, first, default_arg=42): # [useless-parent-delegation] super(UselessSuper, self).with_default_argument_int(first, default_arg) - def with_default_argument_tuple(self, first, default_arg=()): # [useless-super-delegation] + def with_default_argument_tuple(self, first, default_arg=()): # [useless-parent-delegation] super(UselessSuper, self).with_default_argument_tuple(first, default_arg) - def with_default_argument_dict(self, first, default_arg={}): # [useless-super-delegation] + def with_default_argument_dict(self, first, default_arg={}): # [useless-parent-delegation] super(UselessSuper, self).with_default_argument_dict(first, default_arg) - def with_default_argument_var(self, first, default_arg=default_var): # [useless-super-delegation] + def with_default_argument_var(self, first, default_arg=default_var): # [useless-parent-delegation] super(UselessSuper, self).with_default_argument_var(first, default_arg) - def __init__(self): # [useless-super-delegation] + def __init__(self): # [useless-parent-delegation] super(UselessSuper, self).__init__() - def with_default_arg(self, first, default_arg="only_in_super_base"): # [useless-super-delegation] + def with_default_arg(self, first, default_arg="only_in_super_base"): # [useless-parent-delegation] super(UselessSuper, self).with_default_arg(first, default_arg) - def with_default_arg_bis(self, first, default_arg="only_in_super_base"): # [useless-super-delegation] + def with_default_arg_bis(self, first, default_arg="only_in_super_base"): # [useless-parent-delegation] super(UselessSuper, self).with_default_arg_bis(first, default_arg) - def with_default_arg_ter(self, first, default_arg="has_been_changed"): # [useless-super-delegation] + def with_default_arg_ter(self, first, default_arg="has_been_changed"): # [useless-parent-delegation] super(UselessSuper, self).with_default_arg_ter(first, default_arg) - def with_default_arg_quad(self, first, default_arg="has_been_changed"): # [useless-super-delegation] + def with_default_arg_quad(self, first, default_arg="has_been_changed"): # [useless-parent-delegation] super(UselessSuper, self).with_default_arg_quad(first, default_arg) @@ -276,7 +283,7 @@ def trigger_something(value_to_trigger): class NotUselessSuperDecorators(Base): - @trigger_something('value1') + @trigger_something("value1") def method_decorated(self): super(NotUselessSuperDecorators, self).method_decorated() @@ -299,9 +306,9 @@ def __hash__(self): class DecoratedList(MyList): def __str__(self): - return f'List -> {super().__str__()}' + return f"List -> {super().__str__()}" - def __hash__(self): # [useless-super-delegation] + def __hash__(self): # [useless-parent-delegation] return super().__hash__() @@ -327,10 +334,99 @@ def __init__(self, a, *args): class SubTwoOne(SuperTwo): - def __init__(self, a, *args): # [useless-super-delegation] + def __init__(self, a, *args): # [useless-parent-delegation] super().__init__(a, *args) class SubTwoTwo(SuperTwo): def __init__(self, a, b, *args): super().__init__(a, b, *args) + + +class NotUselessSuperPy3: + def not_passing_keyword_only(self, first, *, second): + return super().not_passing_keyword_only(first) + + def passing_keyword_only_with_modifications(self, first, *, second): + return super().passing_keyword_only_with_modifications(first, second + 1) + + +class AlsoNotUselessSuperPy3(NotUselessSuperPy3): + def not_passing_keyword_only(self, first, *, second="second"): + return super().not_passing_keyword_only(first, second=second) + + +class UselessSuperPy3: + def useless(self, *, first): # [useless-parent-delegation] + super().useless(first=first) + + +class Egg(): + def __init__(self, thing: object) -> None: + pass + + +class Spam(Egg): + def __init__(self, thing: int) -> None: + super().__init__(thing) + + +class Ham(Egg): + def __init__(self, thing: object) -> None: # [useless-parent-delegation] + super().__init__(thing) + + +class Test: + def __init__(self, _arg: List[int]) -> None: + super().__init__() + + +class ReturnTypeAny: + choices = ["a", 1, (2, 3)] + + def draw(self) -> Any: + return random.choice(self.choices) + + +class ReturnTypeNarrowed(ReturnTypeAny): + choices = [1, 2, 3] + + def draw(self) -> int: + return super().draw() + + +class NoReturnType: + choices = ["a", 1, (2, 3)] + + def draw(self): + return random.choice(self.choices) + + +class ReturnTypeSpecified(NoReturnType): + choices = ["a", "b"] + + def draw(self) -> str: # [useless-parent-delegation] + return super().draw() + + +class ReturnTypeSame(ReturnTypeAny): + choices = ["a", "b"] + + def draw(self) -> Any: # [useless-parent-delegation] + return super().draw() + + +# Any number of positional arguments followed by one keyword argument with a default value +class Fruit: + def __init__(*, tastes_bitter=None): + ... + + +class Lemon(Fruit): + def __init__(*, tastes_bitter=True): + super().__init__(tastes_bitter=tastes_bitter) + + +class CustomError(Exception): + def __init__(self, message="default"): + super().__init__(message) diff --git a/tests/functional/u/useless/useless_parent_delegation.txt b/tests/functional/u/useless/useless_parent_delegation.txt new file mode 100644 index 0000000000..0917021739 --- /dev/null +++ b/tests/functional/u/useless/useless_parent_delegation.txt @@ -0,0 +1,25 @@ +useless-parent-delegation:221:4:221:25:UselessSuper.equivalent_params:Useless parent or super() delegation in method 'equivalent_params':INFERENCE +useless-parent-delegation:224:4:224:27:UselessSuper.equivalent_params_1:Useless parent or super() delegation in method 'equivalent_params_1':INFERENCE +useless-parent-delegation:227:4:227:27:UselessSuper.equivalent_params_2:Useless parent or super() delegation in method 'equivalent_params_2':INFERENCE +useless-parent-delegation:230:4:230:27:UselessSuper.equivalent_params_3:Useless parent or super() delegation in method 'equivalent_params_3':INFERENCE +useless-parent-delegation:233:4:233:27:UselessSuper.equivalent_params_4:Useless parent or super() delegation in method 'equivalent_params_4':INFERENCE +useless-parent-delegation:236:4:236:27:UselessSuper.equivalent_params_5:Useless parent or super() delegation in method 'equivalent_params_5':INFERENCE +useless-parent-delegation:239:4:239:27:UselessSuper.equivalent_params_6:Useless parent or super() delegation in method 'equivalent_params_6':INFERENCE +useless-parent-delegation:242:4:242:29:UselessSuper.with_default_argument:Useless parent or super() delegation in method 'with_default_argument':INFERENCE +useless-parent-delegation:246:4:246:32:UselessSuper.without_default_argument:Useless parent or super() delegation in method 'without_default_argument':INFERENCE +useless-parent-delegation:249:4:249:34:UselessSuper.with_default_argument_none:Useless parent or super() delegation in method 'with_default_argument_none':INFERENCE +useless-parent-delegation:253:4:253:33:UselessSuper.with_default_argument_int:Useless parent or super() delegation in method 'with_default_argument_int':INFERENCE +useless-parent-delegation:256:4:256:35:UselessSuper.with_default_argument_tuple:Useless parent or super() delegation in method 'with_default_argument_tuple':INFERENCE +useless-parent-delegation:259:4:259:34:UselessSuper.with_default_argument_dict:Useless parent or super() delegation in method 'with_default_argument_dict':INFERENCE +useless-parent-delegation:262:4:262:33:UselessSuper.with_default_argument_var:Useless parent or super() delegation in method 'with_default_argument_var':INFERENCE +useless-parent-delegation:265:4:265:16:UselessSuper.__init__:Useless parent or super() delegation in method '__init__':INFERENCE +useless-parent-delegation:268:4:268:24:UselessSuper.with_default_arg:Useless parent or super() delegation in method 'with_default_arg':INFERENCE +useless-parent-delegation:271:4:271:28:UselessSuper.with_default_arg_bis:Useless parent or super() delegation in method 'with_default_arg_bis':INFERENCE +useless-parent-delegation:274:4:274:28:UselessSuper.with_default_arg_ter:Useless parent or super() delegation in method 'with_default_arg_ter':INFERENCE +useless-parent-delegation:277:4:277:29:UselessSuper.with_default_arg_quad:Useless parent or super() delegation in method 'with_default_arg_quad':INFERENCE +useless-parent-delegation:311:4:311:16:DecoratedList.__hash__:Useless parent or super() delegation in method '__hash__':INFERENCE +useless-parent-delegation:337:4:337:16:SubTwoOne.__init__:Useless parent or super() delegation in method '__init__':INFERENCE +useless-parent-delegation:360:4:360:15:UselessSuperPy3.useless:Useless parent or super() delegation in method 'useless':INFERENCE +useless-parent-delegation:375:4:375:16:Ham.__init__:Useless parent or super() delegation in method '__init__':INFERENCE +useless-parent-delegation:408:4:408:12:ReturnTypeSpecified.draw:Useless parent or super() delegation in method 'draw':INFERENCE +useless-parent-delegation:415:4:415:12:ReturnTypeSame.draw:Useless parent or super() delegation in method 'draw':INFERENCE diff --git a/tests/functional/u/useless/useless_super_delegation_py38.py b/tests/functional/u/useless/useless_parent_delegation_py38.py similarity index 93% rename from tests/functional/u/useless/useless_super_delegation_py38.py rename to tests/functional/u/useless/useless_parent_delegation_py38.py index ec402299f7..dded3b2cb8 100644 --- a/tests/functional/u/useless/useless_super_delegation_py38.py +++ b/tests/functional/u/useless/useless_parent_delegation_py38.py @@ -13,5 +13,5 @@ def __init__(self, first: float, /, second: float) -> None: class Ham(Egg): - def __init__(self, first: Any, /, second: Any) -> None: # [useless-super-delegation] + def __init__(self, first: Any, /, second: Any) -> None: # [useless-parent-delegation] super().__init__(first, second) diff --git a/tests/functional/u/useless/useless_parent_delegation_py38.rc b/tests/functional/u/useless/useless_parent_delegation_py38.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/u/useless/useless_parent_delegation_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/u/useless/useless_parent_delegation_py38.txt b/tests/functional/u/useless/useless_parent_delegation_py38.txt new file mode 100644 index 0000000000..a42a822149 --- /dev/null +++ b/tests/functional/u/useless/useless_parent_delegation_py38.txt @@ -0,0 +1 @@ +useless-parent-delegation:16:4:16:16:Ham.__init__:Useless parent or super() delegation in method '__init__':INFERENCE diff --git a/tests/functional/u/useless/useless_return.py b/tests/functional/u/useless/useless_return.py index c0cffaf5a9..e7537353ef 100644 --- a/tests/functional/u/useless/useless_return.py +++ b/tests/functional/u/useless/useless_return.py @@ -1,11 +1,11 @@ -# pylint: disable=missing-docstring,too-few-public-methods,bad-option-value,useless-object-inheritance -from __future__ import print_function +# pylint: disable=missing-docstring,too-few-public-methods,bad-option-value + def myfunc(): # [useless-return] print('---- testing ---') return -class SomeClass(object): +class SomeClass: def mymethod(self): # [useless-return] print('---- testing ---') return None diff --git a/tests/functional/u/useless/useless_super_delegation.txt b/tests/functional/u/useless/useless_super_delegation.txt deleted file mode 100644 index 9cf3115732..0000000000 --- a/tests/functional/u/useless/useless_super_delegation.txt +++ /dev/null @@ -1,21 +0,0 @@ -useless-super-delegation:214:4:214:25:UselessSuper.equivalent_params:Useless super delegation in method 'equivalent_params':UNDEFINED -useless-super-delegation:217:4:217:27:UselessSuper.equivalent_params_1:Useless super delegation in method 'equivalent_params_1':UNDEFINED -useless-super-delegation:220:4:220:27:UselessSuper.equivalent_params_2:Useless super delegation in method 'equivalent_params_2':UNDEFINED -useless-super-delegation:223:4:223:27:UselessSuper.equivalent_params_3:Useless super delegation in method 'equivalent_params_3':UNDEFINED -useless-super-delegation:226:4:226:27:UselessSuper.equivalent_params_4:Useless super delegation in method 'equivalent_params_4':UNDEFINED -useless-super-delegation:229:4:229:27:UselessSuper.equivalent_params_5:Useless super delegation in method 'equivalent_params_5':UNDEFINED -useless-super-delegation:232:4:232:27:UselessSuper.equivalent_params_6:Useless super delegation in method 'equivalent_params_6':UNDEFINED -useless-super-delegation:235:4:235:29:UselessSuper.with_default_argument:Useless super delegation in method 'with_default_argument':UNDEFINED -useless-super-delegation:239:4:239:32:UselessSuper.without_default_argument:Useless super delegation in method 'without_default_argument':UNDEFINED -useless-super-delegation:242:4:242:34:UselessSuper.with_default_argument_none:Useless super delegation in method 'with_default_argument_none':UNDEFINED -useless-super-delegation:246:4:246:33:UselessSuper.with_default_argument_int:Useless super delegation in method 'with_default_argument_int':UNDEFINED -useless-super-delegation:249:4:249:35:UselessSuper.with_default_argument_tuple:Useless super delegation in method 'with_default_argument_tuple':UNDEFINED -useless-super-delegation:252:4:252:34:UselessSuper.with_default_argument_dict:Useless super delegation in method 'with_default_argument_dict':UNDEFINED -useless-super-delegation:255:4:255:33:UselessSuper.with_default_argument_var:Useless super delegation in method 'with_default_argument_var':UNDEFINED -useless-super-delegation:258:4:258:16:UselessSuper.__init__:Useless super delegation in method '__init__':UNDEFINED -useless-super-delegation:261:4:261:24:UselessSuper.with_default_arg:Useless super delegation in method 'with_default_arg':UNDEFINED -useless-super-delegation:264:4:264:28:UselessSuper.with_default_arg_bis:Useless super delegation in method 'with_default_arg_bis':UNDEFINED -useless-super-delegation:267:4:267:28:UselessSuper.with_default_arg_ter:Useless super delegation in method 'with_default_arg_ter':UNDEFINED -useless-super-delegation:270:4:270:29:UselessSuper.with_default_arg_quad:Useless super delegation in method 'with_default_arg_quad':UNDEFINED -useless-super-delegation:304:4:304:16:DecoratedList.__hash__:Useless super delegation in method '__hash__':UNDEFINED -useless-super-delegation:330:4:330:16:SubTwoOne.__init__:Useless super delegation in method '__init__':UNDEFINED diff --git a/tests/functional/u/useless/useless_super_delegation_py3.py b/tests/functional/u/useless/useless_super_delegation_py3.py deleted file mode 100644 index b32ced873b..0000000000 --- a/tests/functional/u/useless/useless_super_delegation_py3.py +++ /dev/null @@ -1,43 +0,0 @@ -# pylint: disable=missing-docstring, no-member, unused-argument, invalid-name,unused-variable -# pylint: disable=too-few-public-methods,wrong-import-position, useless-object-inheritance - -class NotUselessSuper(object): - - def not_passing_keyword_only(self, first, *, second): - return super().not_passing_keyword_only(first) - - def passing_keyword_only_with_modifications(self, first, *, second): - return super().passing_keyword_only_with_modifications( - first, second + 1) - - -class AlsoNotUselessSuper(NotUselessSuper): - def not_passing_keyword_only(self, first, *, second="second"): - return super().not_passing_keyword_only(first, second=second) - - -class UselessSuper(object): - - def useless(self, *, first): # [useless-super-delegation] - super().useless(first=first) - - -class Egg(): - def __init__(self, thing: object) -> None: - pass - -class Spam(Egg): - def __init__(self, thing: int) -> None: - super().__init__(thing) - -class Ham(Egg): - def __init__(self, thing: object) -> None: # [useless-super-delegation] - super().__init__(thing) - - -from typing import List - - -class Test: - def __init__(self, _arg: List[int]) -> None: - super().__init__() diff --git a/tests/functional/u/useless/useless_super_delegation_py3.txt b/tests/functional/u/useless/useless_super_delegation_py3.txt deleted file mode 100644 index 2497504a06..0000000000 --- a/tests/functional/u/useless/useless_super_delegation_py3.txt +++ /dev/null @@ -1,2 +0,0 @@ -useless-super-delegation:21:4:21:15:UselessSuper.useless:Useless super delegation in method 'useless':UNDEFINED -useless-super-delegation:34:4:34:16:Ham.__init__:Useless super delegation in method '__init__':UNDEFINED diff --git a/tests/functional/u/useless/useless_super_delegation_py35.py b/tests/functional/u/useless/useless_super_delegation_py35.py deleted file mode 100644 index 789ae55d3d..0000000000 --- a/tests/functional/u/useless/useless_super_delegation_py35.py +++ /dev/null @@ -1,46 +0,0 @@ -# pylint: disable=missing-docstring,too-few-public-methods,no-member,unused-argument, useless-object-inheritance - -class NotUselessSuper(object): - - def not_passing_all_params(self, first, *args, second=None, **kwargs): - return super().not_passing_all_params(*args, second, **kwargs) - - -class UselessSuper(object): - - def useless(self, first, *, second=None, **kwargs): # [useless-super-delegation] - return super().useless(first, second=second, **kwargs) - -# pylint: disable=wrong-import-position -import random -from typing import Any - -class ReturnTypeAny: - choices = ['a', 1, (2, 3)] - - def draw(self) -> Any: - return random.choice(self.choices) - -class ReturnTypeNarrowed(ReturnTypeAny): - choices = [1, 2, 3] - - def draw(self) -> int: - return super().draw() - -class NoReturnType: - choices = ['a', 1, (2, 3)] - - def draw(self): - return random.choice(self.choices) - -class ReturnTypeSpecified(NoReturnType): - choices = ['a', 'b'] - - def draw(self) -> str: # [useless-super-delegation] - return super().draw() - -class ReturnTypeSame(ReturnTypeAny): - choices = ['a', 'b'] - - def draw(self) -> Any: # [useless-super-delegation] - return super().draw() diff --git a/tests/functional/u/useless/useless_super_delegation_py35.txt b/tests/functional/u/useless/useless_super_delegation_py35.txt deleted file mode 100644 index 58c98b0f99..0000000000 --- a/tests/functional/u/useless/useless_super_delegation_py35.txt +++ /dev/null @@ -1,3 +0,0 @@ -useless-super-delegation:11:4:11:15:UselessSuper.useless:Useless super delegation in method 'useless':UNDEFINED -useless-super-delegation:39:4:39:12:ReturnTypeSpecified.draw:Useless super delegation in method 'draw':UNDEFINED -useless-super-delegation:45:4:45:12:ReturnTypeSame.draw:Useless super delegation in method 'draw':UNDEFINED diff --git a/tests/functional/u/useless/useless_super_delegation_py38.txt b/tests/functional/u/useless/useless_super_delegation_py38.txt deleted file mode 100644 index 1c4631c938..0000000000 --- a/tests/functional/u/useless/useless_super_delegation_py38.txt +++ /dev/null @@ -1 +0,0 @@ -useless-super-delegation:16:4:16:16:Ham.__init__:Useless super delegation in method '__init__':UNDEFINED diff --git a/tests/functional/u/using_constant_test.py b/tests/functional/u/using_constant_test.py index 78974681ee..4586150b17 100644 --- a/tests/functional/u/using_constant_test.py +++ b/tests/functional/u/using_constant_test.py @@ -1,6 +1,6 @@ """Verify if constant tests are used inside if statements.""" # pylint: disable=invalid-name, missing-docstring,too-few-public-methods -# pylint: disable=expression-not-assigned, useless-object-inheritance +# pylint: disable=expression-not-assigned # pylint: disable=missing-parentheses-for-call-in-test, unnecessary-comprehension, condition-evals-to-constant # pylint: disable=use-list-literal, use-dict-literal @@ -11,7 +11,7 @@ def function(): yield -class Class(object): +class Class: def method(self): pass @@ -147,3 +147,32 @@ def test_good_comprehension_checks(): {data for data in range(100) if abs(data)} {data: 1 for data in range(100) if data} {data: 1 for data in range(100)} + + +# Calls to functions returning generator expressions are always truthy +def get_generator(): + return (x for x in range(0)) + +if get_generator(): # [using-constant-test] + pass + +def maybe_get_generator(arg): + if arg: + return (x for x in range(0)) + return None + +if maybe_get_generator(None): + pass + +y = (a for a in range(10)) +if y: # [using-constant-test] + pass + +z = (a for a in range(10)) +z = "red herring" +if z: + pass + +gen = get_generator() +if gen: # [using-constant-test] + pass diff --git a/tests/functional/u/using_constant_test.txt b/tests/functional/u/using_constant_test.txt index e311cae55f..033bad0b0f 100644 --- a/tests/functional/u/using_constant_test.txt +++ b/tests/functional/u/using_constant_test.txt @@ -1,27 +1,30 @@ -using-constant-test:22:3:22:14::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:26:3:26:31::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:29:3:29:15::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:32:3:32:11::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:35:3:35:8::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:38:3:38:4::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:41:3:41:7::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:44:3:44:5::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:47:3:47:6::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:50:3:50:6::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:53:3:53:5::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:56:3:56:12::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:59:3:59:12::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:62:3:62:5::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:65:3:65:12::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:68:3:68:5::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:73:3:73:12::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:76:8:76:9::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:80:36:80:39:test_comprehensions:Using a conditional statement with a constant value:UNDEFINED -using-constant-test:81:36:81:37:test_comprehensions:Using a conditional statement with a constant value:UNDEFINED -using-constant-test:82:36:82:39:test_comprehensions:Using a conditional statement with a constant value:UNDEFINED -using-constant-test:83:36:83:37:test_comprehensions:Using a conditional statement with a constant value:UNDEFINED -using-constant-test:84:36:84:39:test_comprehensions:Using a conditional statement with a constant value:UNDEFINED -using-constant-test:85:39:85:42:test_comprehensions:Using a conditional statement with a constant value:UNDEFINED -using-constant-test:89:3:89:15::Using a conditional statement with a constant value:UNDEFINED -using-constant-test:93:3:93:18::Using a conditional statement with a constant value:UNDEFINED +using-constant-test:22:3:22:14::Using a conditional statement with a constant value:INFERENCE +using-constant-test:26:3:26:31::Using a conditional statement with a constant value:INFERENCE +using-constant-test:29:3:29:15::Using a conditional statement with a constant value:INFERENCE +using-constant-test:32:3:32:11::Using a conditional statement with a constant value:INFERENCE +using-constant-test:35:3:35:8::Using a conditional statement with a constant value:INFERENCE +using-constant-test:38:3:38:4::Using a conditional statement with a constant value:INFERENCE +using-constant-test:41:3:41:7::Using a conditional statement with a constant value:INFERENCE +using-constant-test:44:3:44:5::Using a conditional statement with a constant value:INFERENCE +using-constant-test:47:3:47:6::Using a conditional statement with a constant value:INFERENCE +using-constant-test:50:3:50:6::Using a conditional statement with a constant value:INFERENCE +using-constant-test:53:3:53:5::Using a conditional statement with a constant value:INFERENCE +using-constant-test:56:3:56:12::Using a conditional statement with a constant value:INFERENCE +using-constant-test:59:3:59:12::Using a conditional statement with a constant value:INFERENCE +using-constant-test:62:3:62:5::Using a conditional statement with a constant value:INFERENCE +using-constant-test:65:3:65:12::Using a conditional statement with a constant value:INFERENCE +using-constant-test:68:3:68:5::Using a conditional statement with a constant value:INFERENCE +using-constant-test:73:3:73:12::Using a conditional statement with a constant value:INFERENCE +using-constant-test:76:8:76:9::Using a conditional statement with a constant value:INFERENCE +using-constant-test:80:36:80:39:test_comprehensions:Using a conditional statement with a constant value:INFERENCE +using-constant-test:81:36:81:37:test_comprehensions:Using a conditional statement with a constant value:INFERENCE +using-constant-test:82:36:82:39:test_comprehensions:Using a conditional statement with a constant value:INFERENCE +using-constant-test:83:36:83:37:test_comprehensions:Using a conditional statement with a constant value:INFERENCE +using-constant-test:84:36:84:39:test_comprehensions:Using a conditional statement with a constant value:INFERENCE +using-constant-test:85:39:85:42:test_comprehensions:Using a conditional statement with a constant value:INFERENCE +using-constant-test:89:3:89:15::Using a conditional statement with a constant value:INFERENCE +using-constant-test:93:3:93:18::Using a conditional statement with a constant value:INFERENCE comparison-of-constants:117:3:117:8::"Comparison between constants: '2 < 3' has a constant value":HIGH +using-constant-test:156:0:157:8::Using a conditional statement with a constant value:INFERENCE +using-constant-test:168:3:168:4::Using a conditional statement with a constant value:INFERENCE +using-constant-test:177:0:178:8::Using a conditional statement with a constant value:INFERENCE diff --git a/tests/functional/w/with_using_generator.py b/tests/functional/w/with_using_generator.py index 187bdcfeaa..9f557363fe 100644 --- a/tests/functional/w/with_using_generator.py +++ b/tests/functional/w/with_using_generator.py @@ -1,7 +1,6 @@ """ Testing with statements that use generators. This should not crash. """ -# pylint: disable=useless-object-inheritance -class Base(object): +class Base: """ Base class. """ val = 0 diff --git a/tests/functional/w/with_using_generator.txt b/tests/functional/w/with_using_generator.txt index f8d80d02d3..12e3ad06af 100644 --- a/tests/functional/w/with_using_generator.txt +++ b/tests/functional/w/with_using_generator.txt @@ -1 +1 @@ -not-context-manager:14:8:15:16:Base.fun:Context manager 'generator' doesn't implement __enter__ and __exit__.:UNDEFINED +not-context-manager:13:8:14:16:Base.fun:Context manager 'generator' doesn't implement __enter__ and __exit__.:UNDEFINED diff --git a/tests/functional/w/wrong_exception_operation.py b/tests/functional/w/wrong_exception_operation.py index 1c3c4e3803..8078573c41 100644 --- a/tests/functional/w/wrong_exception_operation.py +++ b/tests/functional/w/wrong_exception_operation.py @@ -3,7 +3,7 @@ try: 1/0 -except (ValueError | TypeError): # [wrong-exception-operation] +except (ValueError | TypeError): # [catching-non-exception,wrong-exception-operation] pass try: diff --git a/tests/functional/w/wrong_exception_operation.rc b/tests/functional/w/wrong_exception_operation.rc new file mode 100644 index 0000000000..68a8c8ef15 --- /dev/null +++ b/tests/functional/w/wrong_exception_operation.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/w/wrong_exception_operation.txt b/tests/functional/w/wrong_exception_operation.txt index c92fcc2a2b..dc3c213462 100644 --- a/tests/functional/w/wrong_exception_operation.txt +++ b/tests/functional/w/wrong_exception_operation.txt @@ -1,3 +1,4 @@ +catching-non-exception:6:8:6:30::"Catching an exception which doesn't inherit from Exception: ValueError | TypeError":UNDEFINED wrong-exception-operation:6:8:6:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED wrong-exception-operation:11:8:11:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED wrong-exception-operation:17:8:17:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED diff --git a/tests/functional/w/wrong_exception_operation_py37.py b/tests/functional/w/wrong_exception_operation_py37.py new file mode 100644 index 0000000000..1c3c4e3803 --- /dev/null +++ b/tests/functional/w/wrong_exception_operation_py37.py @@ -0,0 +1,18 @@ +# pylint: disable=missing-docstring, superfluous-parens + + +try: + 1/0 +except (ValueError | TypeError): # [wrong-exception-operation] + pass + +try: + 1/0 +except (ValueError + TypeError): # [wrong-exception-operation] + pass + + +try: + 1/0 +except (ValueError < TypeError): # [wrong-exception-operation] + pass diff --git a/tests/functional/w/wrong_exception_operation_py37.rc b/tests/functional/w/wrong_exception_operation_py37.rc new file mode 100644 index 0000000000..dd83cc9536 --- /dev/null +++ b/tests/functional/w/wrong_exception_operation_py37.rc @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=3.7 +max_pyver=3.10 diff --git a/tests/functional/w/wrong_exception_operation_py37.txt b/tests/functional/w/wrong_exception_operation_py37.txt new file mode 100644 index 0000000000..c92fcc2a2b --- /dev/null +++ b/tests/functional/w/wrong_exception_operation_py37.txt @@ -0,0 +1,3 @@ +wrong-exception-operation:6:8:6:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED +wrong-exception-operation:11:8:11:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED +wrong-exception-operation:17:8:17:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED diff --git a/tests/functional/w/wrong_import_position.py b/tests/functional/w/wrong_import_position.py index c06f9da1fb..7d1fddfa3b 100644 --- a/tests/functional/w/wrong_import_position.py +++ b/tests/functional/w/wrong_import_position.py @@ -1,6 +1,6 @@ """Checks import order rule""" # pylint: disable=unused-import,ungrouped-imports,wrong-import-order -# pylint: disable=import-error, too-few-public-methods, missing-docstring,using-constant-test, useless-object-inheritance +# pylint: disable=import-error, too-few-public-methods, missing-docstring,using-constant-test import os.path if True: @@ -8,13 +8,13 @@ try: import sys except ImportError: - class Myclass(object): + class Myclass: """docstring""" if sys.version_info[0] >= 3: from collections import OrderedDict else: - class OrderedDict(object): + class OrderedDict: """Nothing to see here.""" def some_func(self): pass diff --git a/tests/functional/y/yield_assign.py b/tests/functional/y/yield_assign.py index 6a5ae00b20..e7a938c692 100644 --- a/tests/functional/y/yield_assign.py +++ b/tests/functional/y/yield_assign.py @@ -1,6 +1,5 @@ """https://www.logilab.org/ticket/8771""" -from __future__ import print_function def generator(): """yield as assignment""" diff --git a/tests/functional/y/yield_return_mix.py b/tests/functional/y/yield_return_mix.py index 8e050f0f0f..a69a669d65 100644 --- a/tests/functional/y/yield_return_mix.py +++ b/tests/functional/y/yield_return_mix.py @@ -1,6 +1,6 @@ """ module doc """ # pylint: disable=useless-return -__revision__ = None + def somegen(): """this kind of mix is OK""" diff --git a/tests/lint/test_pylinter.py b/tests/lint/test_pylinter.py index f982787d7c..1d0f438194 100644 --- a/tests/lint/test_pylinter.py +++ b/tests/lint/test_pylinter.py @@ -2,12 +2,14 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +import os +from pathlib import Path from typing import Any, NoReturn +from unittest import mock from unittest.mock import patch import pytest -from astroid import AstroidBuildingError -from py._path.local import LocalPath # type: ignore[import] +from _pytest.recwarn import WarningsRecorder from pytest import CaptureFixture from pylint.lint.pylinter import PyLinter @@ -15,29 +17,44 @@ def raise_exception(*args: Any, **kwargs: Any) -> NoReturn: - raise AstroidBuildingError(modname="spam") + raise ValueError @patch.object(FileState, "iter_spurious_suppression_messages", raise_exception) def test_crash_in_file( - linter: PyLinter, capsys: CaptureFixture, tmpdir: LocalPath + linter: PyLinter, capsys: CaptureFixture[str], tmp_path: Path ) -> None: with pytest.warns(DeprecationWarning): args = linter.load_command_line_configuration([__file__]) - linter.crash_file_path = str(tmpdir / "pylint-crash-%Y") + linter.crash_file_path = str(tmp_path / "pylint-crash-%Y") linter.check(args) out, err = capsys.readouterr() assert not out assert not err - files = tmpdir.listdir() + files = os.listdir(tmp_path) assert len(files) == 1 assert "pylint-crash-20" in str(files[0]) - with open(files[0], encoding="utf8") as f: - content = f.read() - assert "Failed to import module spam." in content + assert any(m.symbol == "fatal" for m in linter.reporter.messages) -def test_check_deprecation(linter: PyLinter, recwarn): +def test_check_deprecation(linter: PyLinter, recwarn: WarningsRecorder) -> None: linter.check("myfile.py") msg = recwarn.pop() assert "check function will only accept sequence" in str(msg) + + +def test_crash_during_linting( + linter: PyLinter, capsys: CaptureFixture[str], tmp_path: Path +) -> None: + with mock.patch( + "pylint.lint.PyLinter.check_astroid_module", side_effect=RuntimeError + ): + linter.crash_file_path = str(tmp_path / "pylint-crash-%Y") + linter.check([__file__]) + out, err = capsys.readouterr() + assert not out + assert not err + files = os.listdir(tmp_path) + assert len(files) == 1 + assert "pylint-crash-20" in str(files[0]) + assert any(m.symbol == "astroid-error" for m in linter.reporter.messages) diff --git a/tests/lint/test_run_pylint.py b/tests/lint/test_run_pylint.py new file mode 100644 index 0000000000..73dc26331b --- /dev/null +++ b/tests/lint/test_run_pylint.py @@ -0,0 +1,36 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from pathlib import Path + +import pytest +from _pytest.capture import CaptureFixture + +from pylint import run_pylint + + +def test_run_pylint_with_invalid_argument(capsys: CaptureFixture[str]) -> None: + """Check that appropriate exit code is used with invalid argument.""" + with pytest.raises(SystemExit) as ex: + run_pylint(["--never-use-this"]) + captured = capsys.readouterr() + assert captured.err.startswith("usage: pylint [options]") + assert ex.value.code == 32 + + +def test_run_pylint_with_invalid_argument_in_config( + capsys: CaptureFixture[str], tmp_path: Path +) -> None: + """Check that appropriate exit code is used with an ambiguous + argument in a config file. + """ + test_file = tmp_path / "testpylintrc" + with open(test_file, "w", encoding="utf-8") as f: + f.write("[MASTER]\nno=") + + with pytest.raises(SystemExit) as ex: + run_pylint(["--rcfile", f"{test_file}"]) + captured = capsys.readouterr() + assert captured.err.startswith("usage: pylint [options]") + assert ex.value.code == 32 diff --git a/tests/lint/test_utils.py b/tests/lint/test_utils.py index 6cc79f18ba..872919f725 100644 --- a/tests/lint/test_utils.py +++ b/tests/lint/test_utils.py @@ -18,7 +18,7 @@ def test_prepare_crash_report(tmp_path: PosixPath) -> None: with open(python_file, "w", encoding="utf8") as f: f.write(python_content) try: - raise Exception(exception_content) + raise Exception(exception_content) # pylint: disable=broad-exception-raised except Exception as ex: # pylint: disable=broad-except template_path = prepare_crash_report( ex, str(python_file), str(tmp_path / "pylint-crash-%Y.txt") diff --git a/tests/lint/unittest_expand_modules.py b/tests/lint/unittest_expand_modules.py index 15f72d0c52..1c3f23b00c 100644 --- a/tests/lint/unittest_expand_modules.py +++ b/tests/lint/unittest_expand_modules.py @@ -12,7 +12,7 @@ from pylint.checkers import BaseChecker from pylint.lint.expand_modules import _is_in_ignore_list_re, expand_modules from pylint.testutils import CheckerTestCase, set_config -from pylint.typing import MessageDefinitionTuple +from pylint.typing import MessageDefinitionTuple, ModuleDescriptionDict def test__is_in_ignore_list_re_match() -> None: @@ -45,6 +45,14 @@ def test__is_in_ignore_list_re_match() -> None: "path": EXPAND_MODULES, } +this_file_from_init_deduplicated = { + "basename": "lint", + "basepath": INIT_PATH, + "isarg": True, + "name": "lint.unittest_expand_modules", + "path": EXPAND_MODULES, +} + unittest_lint = { "basename": "lint", "basepath": INIT_PATH, @@ -61,6 +69,14 @@ def test__is_in_ignore_list_re_match() -> None: "path": str(TEST_DIRECTORY / "lint/test_utils.py"), } +test_run_pylint = { + "basename": "lint", + "basepath": INIT_PATH, + "isarg": False, + "name": "lint.test_run_pylint", + "path": str(TEST_DIRECTORY / "lint/test_run_pylint.py"), +} + test_pylinter = { "basename": "lint", "basepath": INIT_PATH, @@ -77,7 +93,6 @@ def test__is_in_ignore_list_re_match() -> None: "path": str(TEST_DIRECTORY / "lint/test_caching.py"), } - init_of_package = { "basename": "lint", "basepath": INIT_PATH, @@ -87,6 +102,21 @@ def test__is_in_ignore_list_re_match() -> None: } +def _list_expected_package_modules( + deduplicating: bool = False, +) -> tuple[dict[str, object], ...]: + """Generates reusable list of modules for our package.""" + return ( + init_of_package, + test_caching, + test_pylinter, + test_run_pylint, + test_utils, + this_file_from_init_deduplicated if deduplicating else this_file_from_init, + unittest_lint, + ) + + class TestExpandModules(CheckerTestCase): """Test the expand_modules function while allowing options to be set.""" @@ -102,56 +132,86 @@ class Checker(BaseChecker): @pytest.mark.parametrize( "files_or_modules,expected", [ - ([__file__], [this_file]), + ([__file__], {this_file["path"]: this_file}), ( [str(Path(__file__).parent)], - [ - init_of_package, - test_caching, - test_pylinter, - test_utils, - this_file_from_init, - unittest_lint, - ], + { + module["path"]: module # pylint: disable=unsubscriptable-object + for module in _list_expected_package_modules() + }, ), ], ) @set_config(ignore_paths="") - def test_expand_modules(self, files_or_modules, expected): + def test_expand_modules( + self, files_or_modules: list[str], expected: dict[str, ModuleDescriptionDict] + ) -> None: """Test expand_modules with the default value of ignore-paths.""" - ignore_list, ignore_list_re = [], [] + ignore_list: list[str] = [] + ignore_list_re: list[re.Pattern[str]] = [] + modules, errors = expand_modules( + files_or_modules, + ignore_list, + ignore_list_re, + self.linter.config.ignore_paths, + ) + assert modules == expected + assert not errors + + @pytest.mark.parametrize( + "files_or_modules,expected", + [ + ([__file__, __file__], {this_file["path"]: this_file}), + ( + [EXPAND_MODULES, str(Path(__file__).parent), EXPAND_MODULES], + { + module["path"]: module # pylint: disable=unsubscriptable-object + for module in _list_expected_package_modules(deduplicating=True) + }, + ), + ], + ) + @set_config(ignore_paths="") + def test_expand_modules_deduplication( + self, files_or_modules: list[str], expected: dict[str, ModuleDescriptionDict] + ) -> None: + """Test expand_modules deduplication.""" + ignore_list: list[str] = [] + ignore_list_re: list[re.Pattern[str]] = [] modules, errors = expand_modules( files_or_modules, ignore_list, ignore_list_re, self.linter.config.ignore_paths, ) - modules.sort(key=lambda d: d["name"]) assert modules == expected assert not errors @pytest.mark.parametrize( "files_or_modules,expected", [ - ([__file__], []), + ([__file__], {}), ( [str(Path(__file__).parent)], - [ - init_of_package, - ], + { + module["path"]: module # pylint: disable=unsubscriptable-object + for module in (init_of_package,) + }, ), ], ) @set_config(ignore_paths=".*/lint/.*") - def test_expand_modules_with_ignore(self, files_or_modules, expected): + def test_expand_modules_with_ignore( + self, files_or_modules: list[str], expected: dict[str, ModuleDescriptionDict] + ) -> None: """Test expand_modules with a non-default value of ignore-paths.""" - ignore_list, ignore_list_re = [], [] + ignore_list: list[str] = [] + ignore_list_re: list[re.Pattern[str]] = [] modules, errors = expand_modules( files_or_modules, ignore_list, ignore_list_re, self.linter.config.ignore_paths, ) - modules.sort(key=lambda d: d["name"]) assert modules == expected assert not errors diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py index 3dbde39cec..619c5fe123 100644 --- a/tests/lint/unittest_lint.py +++ b/tests/lint/unittest_lint.py @@ -12,17 +12,18 @@ import re import sys import tempfile -from collections.abc import Iterable, Iterator +from collections.abc import Iterator from contextlib import contextmanager from importlib import reload from io import StringIO from os import chdir, getcwd from os.path import abspath, dirname, join, sep from pathlib import Path -from shutil import rmtree +from shutil import copy, rmtree import platformdirs import pytest +from astroid import nodes from pytest import CaptureFixture from pylint import checkers, config, exceptions, interfaces, lint, testutils @@ -59,12 +60,12 @@ @contextmanager -def fake_home() -> Iterator: +def fake_home() -> Iterator[str]: folder = tempfile.mkdtemp("fake-home") old_home = os.environ.get(HOME) try: os.environ[HOME] = folder - yield + yield folder finally: os.environ.pop("PYLINTRC", "") if old_home is None: @@ -74,7 +75,7 @@ def fake_home() -> Iterator: rmtree(folder, ignore_errors=True) -def remove(file): +def remove(file: str) -> None: try: os.remove(file) except OSError: @@ -108,15 +109,15 @@ def tempdir() -> Iterator[str]: @pytest.fixture -def fake_path() -> Iterator[Iterable[str]]: +def fake_path() -> Iterator[list[str]]: orig = list(sys.path) - fake: Iterable[str] = ["1", "2", "3"] + fake = ["1", "2", "3"] sys.path[:] = fake yield fake sys.path[:] = orig -def test_no_args(fake_path: list[int]) -> None: +def test_no_args(fake_path: list[str]) -> None: with lint.fix_import_path([]): assert sys.path == fake_path assert sys.path == fake_path @@ -145,7 +146,7 @@ def test_one_arg(fake_path: list[str], case: list[str]) -> None: ["a", "a/c/__init__.py"], ], ) -def test_two_similar_args(fake_path, case): +def test_two_similar_args(fake_path: list[str], case: list[str]) -> None: with tempdir() as chroot: create_files(["a/b/__init__.py", "a/c/__init__.py"]) expected = [join(chroot, "a")] + fake_path @@ -164,7 +165,7 @@ def test_two_similar_args(fake_path, case): ["a/b/c", "a", "a/b/c", "a/e", "a"], ], ) -def test_more_args(fake_path, case): +def test_more_args(fake_path: list[str], case: list[str]) -> None: with tempdir() as chroot: create_files(["a/b/c/__init__.py", "a/d/__init__.py", "a/e/f.py"]) expected = [ @@ -179,12 +180,12 @@ def test_more_args(fake_path, case): @pytest.fixture(scope="module") -def disable(): +def disable() -> list[str]: return ["I"] @pytest.fixture(scope="module") -def reporter(): +def reporter() -> type[testutils.GenericTestReporter]: return testutils.GenericTestReporter @@ -208,7 +209,7 @@ class CustomChecker(checkers.BaseChecker): msgs = {"W9999": ("", "custom", "")} @only_required_for_messages("custom") - def visit_class(self, _): + def visit_class(self, _: nodes.ClassDef) -> None: pass linter.register_checker(CustomChecker(linter)) @@ -524,6 +525,280 @@ def test_load_plugin_command_line() -> None: sys.path.remove(dummy_plugin_path) +@pytest.mark.usefixtures("pop_pylintrc") +def test_load_plugin_path_manipulation_case_6() -> None: + """Case 6 refers to GitHub issue #7264. + + This is where we supply a plugin we want to load on both the CLI and + config file, but that plugin is only loadable after the ``init-hook`` in + the config file has run. This is not supported, and was previously a silent + failure. This test ensures a ``bad-plugin-value`` message is emitted. + """ + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") + ) + with fake_home() as home_path: + # construct a basic rc file that just modifies the path + pylintrc_file = join(home_path, "pylintrc") + with open(pylintrc_file, "w", encoding="utf8") as out: + out.writelines( + [ + "[MASTER]\n", + f"init-hook=\"import sys; sys.path.append(r'{home_path}')\"\n", + "load-plugins=copy_dummy\n", + ] + ) + + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) + + # To confirm we won't load this module _without_ the init hook running. + assert home_path not in sys.path + + run = Run( + [ + "--rcfile", + pylintrc_file, + "--load-plugins", + "copy_dummy", + join(REGRTEST_DATA_DIR, "empty.py"), + ], + reporter=testutils.GenericTestReporter(), + exit=False, + ) + assert run._rcfile == pylintrc_file + assert home_path in sys.path + # The module should not be loaded + assert not any(ch.name == "dummy_plugin" for ch in run.linter.get_checkers()) + + # There should be a bad-plugin-message for this module + assert len(run.linter.reporter.messages) == 1 + assert run.linter.reporter.messages[0] == Message( + msg_id="E0013", + symbol="bad-plugin-value", + msg="Plugin 'copy_dummy' is impossible to load, is it installed ? ('No module named 'copy_dummy'')", + confidence=interfaces.Confidence( + name="UNDEFINED", + description="Warning without any associated confidence level.", + ), + location=MessageLocationTuple( + abspath="Command line or configuration file", + path="Command line or configuration file", + module="Command line or configuration file", + obj="", + line=1, + column=0, + end_line=None, + end_column=None, + ), + ) + + # Necessary as the executed init-hook modifies sys.path + sys.path.remove(home_path) + + +@pytest.mark.usefixtures("pop_pylintrc") +def test_load_plugin_path_manipulation_case_3() -> None: + """Case 3 refers to GitHub issue #7264. + + This is where we supply a plugin we want to load on the CLI only, + but that plugin is only loadable after the ``init-hook`` in + the config file has run. This is not supported, and was previously a silent + failure. This test ensures a ``bad-plugin-value`` message is emitted. + """ + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") + ) + with fake_home() as home_path: + # construct a basic rc file that just modifies the path + pylintrc_file = join(home_path, "pylintrc") + with open(pylintrc_file, "w", encoding="utf8") as out: + out.writelines( + [ + "[MASTER]\n", + f"init-hook=\"import sys; sys.path.append(r'{home_path}')\"\n", + ] + ) + + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) + + # To confirm we won't load this module _without_ the init hook running. + assert home_path not in sys.path + + run = Run( + [ + "--rcfile", + pylintrc_file, + "--load-plugins", + "copy_dummy", + join(REGRTEST_DATA_DIR, "empty.py"), + ], + reporter=testutils.GenericTestReporter(), + exit=False, + ) + assert run._rcfile == pylintrc_file + assert home_path in sys.path + # The module should not be loaded + assert not any(ch.name == "dummy_plugin" for ch in run.linter.get_checkers()) + + # There should be a bad-plugin-message for this module + assert len(run.linter.reporter.messages) == 1 + assert run.linter.reporter.messages[0] == Message( + msg_id="E0013", + symbol="bad-plugin-value", + msg="Plugin 'copy_dummy' is impossible to load, is it installed ? ('No module named 'copy_dummy'')", + confidence=interfaces.Confidence( + name="UNDEFINED", + description="Warning without any associated confidence level.", + ), + location=MessageLocationTuple( + abspath="Command line or configuration file", + path="Command line or configuration file", + module="Command line or configuration file", + obj="", + line=1, + column=0, + end_line=None, + end_column=None, + ), + ) + + # Necessary as the executed init-hook modifies sys.path + sys.path.remove(home_path) + + +@pytest.mark.usefixtures("pop_pylintrc") +def test_load_plugin_pylintrc_order_independent() -> None: + """Test that the init-hook is called independent of the order in a config file. + + We want to ensure that any path manipulation in init hook + that means a plugin can load (as per GitHub Issue #7264 Cases 4+7) + runs before the load call, regardless of the order of lines in the + pylintrc file. + """ + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") + ) + + with fake_home() as home_path: + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) + # construct a basic rc file that just modifies the path + pylintrc_file_before = join(home_path, "pylintrc_before") + with open(pylintrc_file_before, "w", encoding="utf8") as out: + out.writelines( + [ + "[MASTER]\n", + f"init-hook=\"import sys; sys.path.append(r'{home_path}')\"\n", + "load-plugins=copy_dummy\n", + ] + ) + pylintrc_file_after = join(home_path, "pylintrc_after") + with open(pylintrc_file_after, "w", encoding="utf8") as out: + out.writelines( + [ + "[MASTER]\n", + "load-plugins=copy_dummy\n" + f"init-hook=\"import sys; sys.path.append(r'{home_path}')\"\n", + ] + ) + for rcfile in (pylintrc_file_before, pylintrc_file_after): + # To confirm we won't load this module _without_ the init hook running. + assert home_path not in sys.path + run = Run( + [ + "--rcfile", + rcfile, + join(REGRTEST_DATA_DIR, "empty.py"), + ], + exit=False, + ) + assert ( + len( + [ + ch.name + for ch in run.linter.get_checkers() + if ch.name == "dummy_plugin" + ] + ) + == 2 + ) + assert run._rcfile == rcfile + assert home_path in sys.path + + # Necessary as the executed init-hook modifies sys.path + sys.path.remove(home_path) + + +def test_load_plugin_command_line_before_init_hook() -> None: + """Check that the order of 'load-plugins' and 'init-hook' doesn't affect execution.""" + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") + ) + + with fake_home() as home_path: + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) + # construct a basic rc file that just modifies the path + assert home_path not in sys.path + run = Run( + [ + "--load-plugins", + "copy_dummy", + "--init-hook", + f'import sys; sys.path.append(r"{home_path}")', + join(REGRTEST_DATA_DIR, "empty.py"), + ], + exit=False, + ) + assert home_path in sys.path + assert ( + len( + [ + ch.name + for ch in run.linter.get_checkers() + if ch.name == "dummy_plugin" + ] + ) + == 2 + ) + + # Necessary as the executed init-hook modifies sys.path + sys.path.remove(home_path) + + +def test_load_plugin_command_line_with_init_hook_command_line() -> None: + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") + ) + + with fake_home() as home_path: + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) + # construct a basic rc file that just modifies the path + assert home_path not in sys.path + run = Run( + [ + "--init-hook", + f'import sys; sys.path.append(r"{home_path}")', + "--load-plugins", + "copy_dummy", + join(REGRTEST_DATA_DIR, "empty.py"), + ], + exit=False, + ) + assert ( + len( + [ + ch.name + for ch in run.linter.get_checkers() + if ch.name == "dummy_plugin" + ] + ) + == 2 + ) + assert home_path in sys.path + + # Necessary as the executed init-hook modifies sys.path + sys.path.remove(home_path) + + def test_load_plugin_config_file() -> None: dummy_plugin_path = join(REGRTEST_DATA_DIR, "dummy_plugin") sys.path.append(dummy_plugin_path) @@ -555,6 +830,8 @@ def test_load_plugin_configuration() -> None: ], exit=False, ) + + sys.path.remove(dummy_plugin_path) assert run.linter.config.ignore == ["foo", "bar", "bin"] @@ -613,7 +890,7 @@ def test_full_documentation(linter: PyLinter) -> None: def test_list_msgs_enabled( - initialized_linter: PyLinter, capsys: CaptureFixture + initialized_linter: PyLinter, capsys: CaptureFixture[str] ) -> None: linter = initialized_linter linter.enable("W0101", scope="package") @@ -667,7 +944,7 @@ def test_pylint_home_from_environ() -> None: del os.environ["PYLINTHOME"] -def test_warn_about_old_home(capsys: CaptureFixture) -> None: +def test_warn_about_old_home(capsys: CaptureFixture[str]) -> None: """Test that we correctly warn about old_home.""" # Create old home old_home = Path(USER_HOME) / OLD_DEFAULT_PYLINT_HOME @@ -702,6 +979,7 @@ def test_pylintrc() -> None: with fake_home(): current_dir = getcwd() chdir(os.path.dirname(os.path.abspath(sys.executable))) + # pylint: disable = too-many-try-statements try: with pytest.warns(DeprecationWarning): assert config.find_pylintrc() is None @@ -719,7 +997,6 @@ def test_pylintrc() -> None: @pytest.mark.usefixtures("pop_pylintrc") def test_pylintrc_parentdir() -> None: with tempdir() as chroot: - create_files( [ "a/pylintrc", @@ -857,7 +1134,6 @@ def test_by_module_statement_value(initialized_linter: PyLinter) -> None: by_module_stats = linter.stats.by_module for module, module_stats in by_module_stats.items(): - linter2 = initialized_linter if module == "data": linter2.check([os.path.join(os.path.dirname(__file__), "data/__init__.py")]) @@ -880,7 +1156,7 @@ def test_by_module_statement_value(initialized_linter: PyLinter) -> None: ("--ignore-paths", ".*ignored.*/failing.*"), ], ) -def test_recursive_ignore(ignore_parameter, ignore_parameter_value) -> None: +def test_recursive_ignore(ignore_parameter: str, ignore_parameter_value: str) -> None: run = Run( [ "--recursive", @@ -912,6 +1188,38 @@ def test_recursive_ignore(ignore_parameter, ignore_parameter_value) -> None: assert module in linted_file_paths +def test_relative_imports(initialized_linter: PyLinter) -> None: + """Regression test for https://github.com/PyCQA/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) + with open("x/y/__init__.py", "w", encoding="utf-8") as f: + f.write( + """ +\"\"\"Module x.y\"\"\" +from .one import ONE +from .two import TWO +""" + ) + with open("x/y/one.py", "w", encoding="utf-8") as f: + f.write( + """ +\"\"\"Module x.y.one\"\"\" +ONE = 1 +""" + ) + with open("x/y/two.py", "w", encoding="utf-8") as f: + f.write( + """ +\"\"\"Module x.y.two\"\"\" +from .one import ONE +TWO = ONE + ONE +""" + ) + linter.check(["x/y"]) + assert not linter.stats.by_msg + + 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`.""" @@ -931,3 +1239,24 @@ def test_import_sibling_module_from_namespace(initialized_linter: PyLinter) -> N with fix_import_path([tmpdir]): linter.check(["submodule2.py"]) assert not linter.stats.by_msg + + +def test_lint_namespace_package_under_dir(initialized_linter: PyLinter) -> None: + """Regression test for https://github.com/PyCQA/pylint/issues/1667""" + linter = initialized_linter + with tempdir(): + create_files(["outer/namespace/__init__.py", "outer/namespace/module.py"]) + linter.check(["outer.namespace"]) + assert not linter.stats.by_msg + + +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.""" + linter = initialized_linter + with tempdir() as tmpdir: + create_files(["namespace_on_path/submodule1.py"]) + os.chdir(tmpdir) + with fix_import_path([tmpdir]): + linter.check(["namespace_on_path"]) + assert linter.file_state.base_name == "namespace_on_path" diff --git a/tests/message/unittest_message.py b/tests/message/unittest_message.py index d0805e337d..edb803daf7 100644 --- a/tests/message/unittest_message.py +++ b/tests/message/unittest_message.py @@ -55,5 +55,7 @@ def build_message( ) e1234 = build_message(e1234_message_definition, e1234_location_values) w1234 = build_message(w1234_message_definition, w1234_location_values) + assert e1234.location == e1234_location_values + assert w1234.location == w1234_location_values assert e1234.format(template) == expected assert w1234.format(template) == "8:11:12: W1234: message (msg-symbol)" diff --git a/tests/message/unittest_message_definition.py b/tests/message/unittest_message_definition.py index 2a3fd7a6a8..aebd1bc6b0 100644 --- a/tests/message/unittest_message_definition.py +++ b/tests/message/unittest_message_definition.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + import sys from unittest import mock @@ -21,7 +23,7 @@ ("W12345", "Invalid message id 'W12345'"), ], ) -def test_create_invalid_message_type(msgid, expected): +def test_create_invalid_message_type(msgid: str, expected: str) -> None: checker_mock = mock.Mock(name="Checker") checker_mock.name = "checker" @@ -51,20 +53,23 @@ def __init__(self) -> None: class TestMessagesDefinition: @staticmethod - def assert_with_fail_msg(msg: MessageDefinition, expected: bool = True) -> None: + def assert_with_fail_msg( + msg: MessageDefinition, + expected: bool = True, + py_version: tuple[int, ...] | sys._version_info = sys.version_info, + ) -> None: fail_msg = ( f"With minversion='{msg.minversion}' and maxversion='{msg.maxversion}'," - f" and the python interpreter being {sys.version_info} " + f" and the py-version option being {py_version} " "the message should{}be emitable" ) if expected: - assert msg.may_be_emitted(), fail_msg.format(" ") + assert msg.may_be_emitted(py_version), fail_msg.format(" ") else: - assert not msg.may_be_emitted(), fail_msg.format(" not ") + assert not msg.may_be_emitted(py_version), fail_msg.format(" not ") @staticmethod def get_message_definition() -> MessageDefinition: - kwargs = {"minversion": None, "maxversion": None} return MessageDefinition( FalseChecker(), "W1234", @@ -72,10 +77,9 @@ def get_message_definition() -> MessageDefinition: "description", "msg-symbol", WarningScope.NODE, - **kwargs, ) - def test_may_be_emitted(self) -> None: + def test_may_be_emitted_default(self) -> None: major = sys.version_info.major minor = sys.version_info.minor msg = self.get_message_definition() @@ -90,6 +94,21 @@ def test_may_be_emitted(self) -> None: msg.maxversion = (major, minor - 1) self.assert_with_fail_msg(msg, expected=False) + def test_may_be_emitted_py_version(self) -> None: + msg = self.get_message_definition() + self.assert_with_fail_msg(msg, expected=True, py_version=(3, 2)) + + msg.maxversion = (3, 5) + self.assert_with_fail_msg(msg, expected=True, py_version=(3, 2)) + self.assert_with_fail_msg(msg, expected=False, py_version=(3, 5)) + self.assert_with_fail_msg(msg, expected=False, py_version=(3, 6)) + + msg.maxversion = None + msg.minversion = (3, 9) + self.assert_with_fail_msg(msg, expected=True, py_version=(3, 9)) + self.assert_with_fail_msg(msg, expected=True, py_version=(3, 10)) + self.assert_with_fail_msg(msg, expected=False, py_version=(3, 8)) + def test_repr(self) -> None: msg = self.get_message_definition() repr_str = str([msg, msg]) diff --git a/tests/message/unittest_message_definition_store.py b/tests/message/unittest_message_definition_store.py index 6a7914334b..d36b1b42a9 100644 --- a/tests/message/unittest_message_definition_store.py +++ b/tests/message/unittest_message_definition_store.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + from contextlib import redirect_stdout from io import StringIO @@ -13,6 +15,7 @@ from pylint.lint.pylinter import PyLinter from pylint.message import MessageDefinition from pylint.message.message_definition_store import MessageDefinitionStore +from pylint.typing import MessageDefinitionTuple @pytest.mark.parametrize( @@ -120,7 +123,11 @@ ), ], ) -def test_register_error(empty_store, messages, expected): +def test_register_error( + empty_store: MessageDefinitionStore, + messages: dict[str, MessageDefinitionTuple], + expected: str, +) -> None: class Checker(BaseChecker): def __init__(self) -> None: super().__init__(PyLinter()) diff --git a/tests/message/unittest_message_id_store.py b/tests/message/unittest_message_id_store.py index f300b4f195..9dcf774e5c 100644 --- a/tests/message/unittest_message_id_store.py +++ b/tests/message/unittest_message_id_store.py @@ -127,10 +127,12 @@ def test_exclusivity_of_msgids() -> None: "07": ("exceptions", "broad_try_clause", "overlap-except"), "12": ("design", "logging"), "17": ("async", "refactoring"), - "20": ("compare-to-zero", "refactoring"), + "20": ("compare-to-zero", "empty-comment", "magic-value"), } for msgid, definition in runner.linter.msgs_store._messages_definitions.items(): + if definition.shared: + continue if msgid[1:3] in checker_id_pairs: assert ( definition.checker_name in checker_id_pairs[msgid[1:3]] diff --git a/tests/primer/__main__.py b/tests/primer/__main__.py new file mode 100644 index 0000000000..1e99ca2346 --- /dev/null +++ b/tests/primer/__main__.py @@ -0,0 +1,17 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +from pathlib import Path + +from pylint.testutils._primer.primer import Primer + +PRIMER_DIRECTORY = Path(__file__).parent.parent / ".pylint_primer_tests/" +PACKAGES_TO_PRIME_PATH = Path(__file__).parent / "packages_to_prime.json" + + +if __name__ == "__main__": + primer = Primer(PRIMER_DIRECTORY, PACKAGES_TO_PRIME_PATH) + primer.run() diff --git a/tests/primer/packages_to_lint_batch_one.json b/tests/primer/packages_to_lint_batch_one.json index 0d1d2ba368..6520e2bd12 100644 --- a/tests/primer/packages_to_lint_batch_one.json +++ b/tests/primer/packages_to_lint_batch_one.json @@ -1,18 +1,7 @@ { - "django": { - "branch": "main", - "directories": ["django"], - "url": "https://github.com/django/django.git" - }, "keras": { "branch": "master", "directories": ["keras"], "url": "https://github.com/keras-team/keras.git" - }, - "music21": { - "branch": "master", - "directories": ["music21"], - "pylintrc_relpath": ".pylintrc", - "url": "https://github.com/cuthbertLab/music21" } } diff --git a/tests/primer/packages_to_prime.json b/tests/primer/packages_to_prime.json index 06e456eff9..a1fd74b4d7 100644 --- a/tests/primer/packages_to_prime.json +++ b/tests/primer/packages_to_prime.json @@ -10,11 +10,23 @@ "directories": ["src/black/", "src/blackd/", "src/blib2to3/"], "url": "https://github.com/psf/black" }, + "django": { + "branch": "main", + "directories": ["django"], + "url": "https://github.com/django/django" + }, "flask": { "branch": "main", "directories": ["src/flask"], "url": "https://github.com/pallets/flask" }, + "music21": { + "branch": "master", + "directories": ["music21"], + "pylintrc_relpath": ".pylintrc", + "minimum_python": "3.10", + "url": "https://github.com/cuthbertLab/music21" + }, "pandas": { "branch": "main", "directories": ["pandas"], @@ -40,5 +52,11 @@ "branch": "master", "directories": ["src/sentry"], "url": "https://github.com/getsentry/sentry" + }, + "coverage": { + "branch": "master", + "directories": ["coverage"], + "url": "https://github.com/nedbat/coveragepy", + "pylintrc_relpath": "pylintrc" } } diff --git a/tests/primer/primer_tool.py b/tests/primer/primer_tool.py deleted file mode 100644 index 1da737b77c..0000000000 --- a/tests/primer/primer_tool.py +++ /dev/null @@ -1,294 +0,0 @@ -# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -from __future__ import annotations - -import argparse -import json -import sys -import warnings -from io import StringIO -from itertools import chain -from pathlib import Path -from typing import Dict, List, Union - -import git - -from pylint.lint import Run -from pylint.reporters import JSONReporter -from pylint.testutils.primer import PackageToLint - -TESTS_DIR = Path(__file__).parent.parent -PRIMER_DIRECTORY = TESTS_DIR / ".pylint_primer_tests/" -PACKAGES_TO_PRIME_PATH = Path(__file__).parent / "packages_to_prime.json" - -PackageMessages = Dict[str, List[Dict[str, Union[str, int]]]] - -GITHUB_CRASH_TEMPLATE_LOCATION = "/home/runner/.cache" -CRASH_TEMPLATE_INTRO = "There is a pre-filled template" - - -class Primer: - """Main class to handle priming of packages.""" - - def __init__(self, json_path: Path) -> None: - # Preparing arguments - self._argument_parser = argparse.ArgumentParser(prog="Pylint Primer") - self._subparsers = self._argument_parser.add_subparsers(dest="command") - - # All arguments for the prepare parser - prepare_parser = self._subparsers.add_parser("prepare") - prepare_parser.add_argument( - "--clone", help="Clone all packages.", action="store_true", default=False - ) - prepare_parser.add_argument( - "--check", - help="Check consistencies and commits of all packages.", - action="store_true", - default=False, - ) - prepare_parser.add_argument( - "--make-commit-string", - help="Get latest commit string.", - action="store_true", - default=False, - ) - prepare_parser.add_argument( - "--read-commit-string", - help="Print latest commit string.", - action="store_true", - default=False, - ) - - # All arguments for the run parser - run_parser = self._subparsers.add_parser("run") - run_parser.add_argument( - "--type", choices=["main", "pr"], required=True, help="Type of primer run." - ) - - # All arguments for the compare parser - compare_parser = self._subparsers.add_parser("compare") - compare_parser.add_argument( - "--base-file", - required=True, - help="Location of output file of the base run.", - ) - compare_parser.add_argument( - "--new-file", - required=True, - help="Location of output file of the new run.", - ) - compare_parser.add_argument( - "--commit", - required=True, - help="Commit hash of the PR commit being checked.", - ) - - # Storing arguments - self.config = self._argument_parser.parse_args() - - self.packages = self._get_packages_to_lint_from_json(json_path) - """All packages to prime.""" - - def run(self) -> None: - if self.config.command == "prepare": - self._handle_prepare_command() - if self.config.command == "run": - self._handle_run_command() - if self.config.command == "compare": - self._handle_compare_command() - - def _handle_prepare_command(self) -> None: - commit_string = "" - if self.config.clone: - for package, data in self.packages.items(): - local_commit = data.lazy_clone() - print(f"Cloned '{package}' at commit '{local_commit}'.") - commit_string += local_commit + "_" - elif self.config.check: - for package, data in self.packages.items(): - local_commit = git.Repo(data.clone_directory).head.object.hexsha - print(f"Found '{package}' at commit '{local_commit}'.") - commit_string += local_commit + "_" - elif self.config.make_commit_string: - for package, data in self.packages.items(): - remote_sha1_commit = ( - git.cmd.Git().ls_remote(data.url, data.branch).split("\t")[0] - ) - print(f"'{package}' remote is at commit '{remote_sha1_commit}'.") - commit_string += remote_sha1_commit + "_" - elif self.config.read_commit_string: - with open(PRIMER_DIRECTORY / "commit_string.txt", encoding="utf-8") as f: - print(f.read()) - - if commit_string: - with open( - PRIMER_DIRECTORY / "commit_string.txt", "w", encoding="utf-8" - ) as f: - f.write(commit_string) - - def _handle_run_command(self) -> None: - packages: PackageMessages = {} - - for package, data in self.packages.items(): - output = self._lint_package(data) - packages[package] = output - print(f"Successfully primed {package}.") - - astroid_errors = [] - other_fatal_msgs = [] - for msg in chain.from_iterable(packages.values()): - if msg["type"] == "fatal": - # Remove the crash template location if we're running on GitHub. - # We were falsely getting "new" errors when the timestamp changed. - assert isinstance(msg["message"], str) - if GITHUB_CRASH_TEMPLATE_LOCATION in msg["message"]: - msg["message"] = msg["message"].rsplit(CRASH_TEMPLATE_INTRO)[0] - if msg["symbol"] == "astroid-error": - astroid_errors.append(msg) - else: - other_fatal_msgs.append(msg) - - with open( - PRIMER_DIRECTORY - / f"output_{'.'.join(str(i) for i in sys.version_info[:3])}_{self.config.type}.txt", - "w", - encoding="utf-8", - ) as f: - json.dump(packages, f) - - # Fail loudly (and fail CI pipelines) if any fatal errors are found, - # unless they are astroid-errors, in which case just warn. - # This is to avoid introducing a dependency on bleeding-edge astroid - # for pylint CI pipelines generally, even though we want to use astroid main - # for the purpose of diffing emitted messages and generating PR comments. - if astroid_errors: - warnings.warn(f"Fatal errors traced to astroid: {astroid_errors}") - assert not other_fatal_msgs, other_fatal_msgs - - def _handle_compare_command(self) -> None: - with open(self.config.base_file, encoding="utf-8") as f: - main_dict: PackageMessages = json.load(f) - with open(self.config.new_file, encoding="utf-8") as f: - new_dict: PackageMessages = json.load(f) - - final_main_dict: PackageMessages = {} - for package, messages in main_dict.items(): - final_main_dict[package] = [] - for message in messages: - try: - new_dict[package].remove(message) - except ValueError: - final_main_dict[package].append(message) - - self._create_comment(final_main_dict, new_dict) - - def _create_comment( - self, all_missing_messages: PackageMessages, all_new_messages: PackageMessages - ) -> None: - comment = "" - for package, missing_messages in all_missing_messages.items(): - new_messages = all_new_messages[package] - package_data = self.packages[package] - - if not missing_messages and not new_messages: - continue - - comment += f"\n\n**Effect on [{package}]({self.packages[package].url}):**\n" - - # Create comment for new messages - count = 1 - astroid_errors = 0 - new_non_astroid_messages = "" - if new_messages: - print("Now emitted:") - for message in new_messages: - filepath = str(message["path"]).replace( - str(package_data.clone_directory), "" - ) - # Existing astroid errors may still show up as "new" because the timestamp - # in the message is slightly different. - if message["symbol"] == "astroid-error": - astroid_errors += 1 - else: - new_non_astroid_messages += ( - f"{count}) {message['symbol']}:\n*{message['message']}*\n" - f"{package_data.url}/blob/{package_data.branch}{filepath}#L{message['line']}\n" - ) - print(message) - count += 1 - - if astroid_errors: - comment += ( - f"{astroid_errors} error(s) were found stemming from the `astroid` library. " - "This is unlikely to have been caused by your changes. " - "A GitHub Actions warning links directly to the crash report template. " - "Please open an issue against `astroid` if one does not exist already. \n\n" - ) - if new_non_astroid_messages: - comment += ( - "The following messages are now emitted:\n\n
\n\n" - + new_non_astroid_messages - + "\n
\n\n" - ) - - # Create comment for missing messages - count = 1 - if missing_messages: - comment += ( - "The following messages are no longer emitted:\n\n
\n\n" - ) - print("No longer emitted:") - for message in missing_messages: - comment += f"{count}) {message['symbol']}:\n*{message['message']}*\n" - filepath = str(message["path"]).replace( - str(package_data.clone_directory), "" - ) - assert not package_data.url.endswith( - ".git" - ), "You don't need the .git at the end of the github url." - comment += f"{package_data.url}/blob/{package_data.branch}{filepath}#L{message['line']}\n" - count += 1 - print(message) - if missing_messages: - comment += "\n
\n\n" - - if comment == "": - comment = "🤖 According to the primer, this change has **no effect** on the checked open source code. 🤖🎉" - else: - comment = ( - "🤖 **Effect of this PR on checked open source code:** 🤖\n\n" + comment - ) - - comment += f"*This comment was generated for commit {self.config.commit}*" - - with open(PRIMER_DIRECTORY / "comment.txt", "w", encoding="utf-8") as f: - f.write(comment) - - def _lint_package(self, data: PackageToLint) -> list[dict[str, str | int]]: - # We want to test all the code we can - enables = ["--enable-all-extensions", "--enable=all"] - # Duplicate code takes too long and is relatively safe - # TODO: Find a way to allow cyclic-import and compare output correctly - disables = ["--disable=duplicate-code,cyclic-import"] - arguments = data.pylint_args + enables + disables - if data.pylintrc_relpath: - arguments += [f"--rcfile={data.pylintrc_relpath}"] - output = StringIO() - reporter = JSONReporter(output) - Run(arguments, reporter=reporter, exit=False) - return json.loads(output.getvalue()) - - @staticmethod - def _get_packages_to_lint_from_json(json_path: Path) -> dict[str, PackageToLint]: - with open(json_path, encoding="utf8") as f: - return { - name: PackageToLint(**package_data) - for name, package_data in json.load(f).items() - } - - -if __name__ == "__main__": - primer = Primer(PACKAGES_TO_PRIME_PATH) - primer.run() diff --git a/tests/primer/test_primer_external.py b/tests/primer/test_primer_external.py index b2b07499a0..01b1367b1d 100644 --- a/tests/primer/test_primer_external.py +++ b/tests/primer/test_primer_external.py @@ -12,7 +12,7 @@ import pytest from pytest import LogCaptureFixture -from pylint.testutils.primer import PackageToLint +from pylint.testutils._primer import PackageToLint PRIMER_DIRECTORY = (Path("tests") / ".pylint_primer_tests/").resolve() diff --git a/tests/primer/test_primer_stdlib.py b/tests/primer/test_primer_stdlib.py index 6cae6fd367..c2d879764a 100644 --- a/tests/primer/test_primer_stdlib.py +++ b/tests/primer/test_primer_stdlib.py @@ -2,10 +2,14 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + import contextlib import io import os import sys +import warnings +from collections.abc import Iterator import pytest from pytest import CaptureFixture @@ -22,7 +26,7 @@ def is_package(filename: str, location: str) -> bool: @contextlib.contextmanager -def _patch_stdout(out): +def _patch_stdout(out: io.StringIO) -> Iterator[None]: sys.stdout = out try: yield @@ -57,11 +61,13 @@ def test_primer_stdlib_no_crash( # Duplicate code takes too long and is relatively safe # We don't want to lint the test directory which are repetitive disables = ["--disable=duplicate-code", "--ignore=test"] - Run([test_module_name] + enables + disables) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=UserWarning) + Run([test_module_name] + enables + disables) except SystemExit as ex: out, err = capsys.readouterr() assert not err, err assert not out msg = f"Encountered {{}} during primer stlib test for {test_module_name}" assert ex.code != 32, msg.format("a crash") - assert ex.code % 2 == 0, msg.format("a message of category 'fatal'") + assert ex.code % 2 == 0, msg.format("a message of category 'fatal'") # type: ignore[operator] diff --git a/tests/profile/test_profile_against_externals.py b/tests/profile/test_profile_against_externals.py index 579a5bc9ca..3ee7564f0d 100644 --- a/tests/profile/test_profile_against_externals.py +++ b/tests/profile/test_profile_against_externals.py @@ -6,19 +6,23 @@ # pylint: disable=missing-function-docstring +from __future__ import annotations + import os import pprint +from pathlib import Path import pytest +from git.repo import Repo from pylint.testutils import GenericTestReporter as Reporter from pylint.testutils._run import _Run as Run -def _get_py_files(scanpath): +def _get_py_files(scanpath: str) -> list[str]: assert os.path.exists(scanpath), f"Dir not found {scanpath}" - filepaths = [] + filepaths: list[str] = [] for dirpath, dirnames, filenames in os.walk(scanpath): dirnames[:] = [dirname for dirname in dirnames if dirname != "__pycache__"] filepaths.extend( @@ -38,11 +42,11 @@ def _get_py_files(scanpath): @pytest.mark.parametrize( "name,git_repo", [("numpy", "https://github.com/numpy/numpy.git")] ) -def test_run(tmp_path, name, git_repo): +def test_run(tmp_path: Path, name: str, git_repo: str) -> None: """Runs pylint against external sources.""" checkoutdir = tmp_path / name checkoutdir.mkdir() - os.system(f"git clone --depth=1 {git_repo} {checkoutdir}") + Repo.clone_from(url=git_repo, to_path=checkoutdir, depth=1) filepaths = _get_py_files(scanpath=str(checkoutdir)) print(f"Have {len(filepaths)} files") diff --git a/tests/pyreverse/conftest.py b/tests/pyreverse/conftest.py index d8b6ea9f44..a37e4bde1a 100644 --- a/tests/pyreverse/conftest.py +++ b/tests/pyreverse/conftest.py @@ -12,6 +12,7 @@ from pylint.lint import fix_import_path from pylint.pyreverse.inspector import Project, project_from_files from pylint.testutils.pyreverse import PyreverseConfig +from pylint.typing import GetProjectCallable @pytest.fixture() @@ -66,11 +67,11 @@ def html_config() -> PyreverseConfig: @pytest.fixture(scope="session") -def get_project() -> Callable: +def get_project() -> GetProjectCallable: def _get_project(module: str, name: str | None = "No Name") -> Project: """Return an astroid project representation.""" - def _astroid_wrapper(func: Callable, modname: str) -> Module: + def _astroid_wrapper(func: Callable[[str], Module], modname: str) -> Module: return func(modname) with fix_import_path([module]): diff --git a/tests/pyreverse/data/classes_No_Name.dot b/tests/pyreverse/data/classes_No_Name.dot index 1f3f705e78..a598ab6d9a 100644 --- a/tests/pyreverse/data/classes_No_Name.dot +++ b/tests/pyreverse/data/classes_No_Name.dot @@ -1,17 +1,17 @@ digraph "classes_No_Name" { rankdir=BT charset="utf-8" -"data.clientmodule_test.Ancestor" [color="black", fontcolor="black", label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value(value)\l}", shape="record", style="solid"]; -"data.suppliermodule_test.CustomException" [color="black", fontcolor="red", label="{CustomException|\l|}", shape="record", style="solid"]; -"data.suppliermodule_test.DoNothing" [color="black", fontcolor="black", label="{DoNothing|\l|}", shape="record", style="solid"]; -"data.suppliermodule_test.DoNothing2" [color="black", fontcolor="black", label="{DoNothing2|\l|}", shape="record", style="solid"]; -"data.suppliermodule_test.DoSomething" [color="black", fontcolor="black", label="{DoSomething|my_int : Optional[int]\lmy_int_2 : Optional[int]\lmy_string : str\l|do_it(new_int: int): int\l}", shape="record", style="solid"]; -"data.suppliermodule_test.Interface" [color="black", fontcolor="black", label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record", style="solid"]; -"data.property_pattern.PropertyPatterns" [color="black", fontcolor="black", label="{PropertyPatterns|prop1\lprop2\l|}", shape="record", style="solid"]; -"data.clientmodule_test.Specialization" [color="black", fontcolor="black", label="{Specialization|TYPE : str\lrelation\lrelation2\ltop : str\l|from_value(value: int)\lincrement_value(): None\ltransform_value(value: int): int\l}", shape="record", style="solid"]; +"data.clientmodule_test.Ancestor" [color="black", fontcolor="black", label=<{Ancestor|attr : str
cls_member
|get_value()
set_value(value)
}>, shape="record", style="solid"]; +"data.suppliermodule_test.CustomException" [color="black", fontcolor="red", label=<{CustomException|
|}>, shape="record", style="solid"]; +"data.suppliermodule_test.DoNothing" [color="black", fontcolor="black", label=<{DoNothing|
|}>, shape="record", style="solid"]; +"data.suppliermodule_test.DoNothing2" [color="black", fontcolor="black", label=<{DoNothing2|
|}>, shape="record", style="solid"]; +"data.suppliermodule_test.DoSomething" [color="black", fontcolor="black", label=<{DoSomething|my_int : Optional[int]
my_int_2 : Optional[int]
my_string : str
|do_it(new_int: int): int
}>, shape="record", style="solid"]; +"data.suppliermodule_test.Interface" [color="black", fontcolor="black", label=<{Interface|
|get_value()
set_value(value)
}>, shape="record", style="solid"]; +"data.property_pattern.PropertyPatterns" [color="black", fontcolor="black", label=<{PropertyPatterns|prop1
prop2
|}>, shape="record", style="solid"]; +"data.clientmodule_test.Specialization" [color="black", fontcolor="black", label=<{Specialization|TYPE : str
relation
relation2
top : str
|from_value(value: int)
increment_value(): None
transform_value(value: int): int
}>, shape="record", style="solid"]; "data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"]; "data.clientmodule_test.Ancestor" -> "data.suppliermodule_test.Interface" [arrowhead="empty", arrowtail="node", style="dashed"]; "data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Ancestor" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"]; "data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"]; -"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"]; +"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="odiamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"]; } diff --git a/tests/pyreverse/data/classes_No_Name.html b/tests/pyreverse/data/classes_No_Name.html index 956758223e..602f2e3b7a 100644 --- a/tests/pyreverse/data/classes_No_Name.html +++ b/tests/pyreverse/data/classes_No_Name.html @@ -23,8 +23,8 @@ do_it(new_int: int) int } class Interface { - get_value() - set_value(value) + get_value()* + set_value(value)* } class PropertyPatterns { prop1 @@ -43,7 +43,7 @@ Ancestor ..|> Interface DoNothing --* Ancestor : cls_member DoNothing --* Specialization : relation - DoNothing2 --* Specialization : relation2 + DoNothing2 --o Specialization : relation2 diff --git a/tests/pyreverse/data/classes_No_Name.mmd b/tests/pyreverse/data/classes_No_Name.mmd index 4daa91c244..1db88b2ae8 100644 --- a/tests/pyreverse/data/classes_No_Name.mmd +++ b/tests/pyreverse/data/classes_No_Name.mmd @@ -18,8 +18,8 @@ classDiagram do_it(new_int: int) int } class Interface { - get_value() - set_value(value) + get_value()* + set_value(value)* } class PropertyPatterns { prop1 @@ -38,4 +38,4 @@ classDiagram Ancestor ..|> Interface DoNothing --* Ancestor : cls_member DoNothing --* Specialization : relation - DoNothing2 --* Specialization : relation2 + DoNothing2 --o Specialization : relation2 diff --git a/tests/pyreverse/data/classes_No_Name.puml b/tests/pyreverse/data/classes_No_Name.puml index 37767b321c..837e6865c3 100644 --- a/tests/pyreverse/data/classes_No_Name.puml +++ b/tests/pyreverse/data/classes_No_Name.puml @@ -19,8 +19,8 @@ class "DoSomething" as data.suppliermodule_test.DoSomething { do_it(new_int: int) -> int } class "Interface" as data.suppliermodule_test.Interface { - get_value() - set_value(value) + {abstract}get_value() + {abstract}set_value(value) } class "PropertyPatterns" as data.property_pattern.PropertyPatterns { prop1 @@ -39,5 +39,5 @@ data.clientmodule_test.Specialization --|> data.clientmodule_test.Ancestor data.clientmodule_test.Ancestor ..|> data.suppliermodule_test.Interface data.suppliermodule_test.DoNothing --* data.clientmodule_test.Ancestor : cls_member data.suppliermodule_test.DoNothing --* data.clientmodule_test.Specialization : relation -data.suppliermodule_test.DoNothing2 --* data.clientmodule_test.Specialization : relation2 +data.suppliermodule_test.DoNothing2 --o data.clientmodule_test.Specialization : relation2 @enduml diff --git a/tests/pyreverse/data/classes_colorized.dot b/tests/pyreverse/data/classes_colorized.dot index 72f30658db..4ff12a8191 100644 --- a/tests/pyreverse/data/classes_colorized.dot +++ b/tests/pyreverse/data/classes_colorized.dot @@ -1,17 +1,17 @@ digraph "classes_colorized" { rankdir=BT charset="utf-8" -"data.clientmodule_test.Ancestor" [color="aliceblue", fontcolor="black", label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value(value)\l}", shape="record", style="filled"]; -"data.suppliermodule_test.CustomException" [color="aliceblue", fontcolor="red", label="{CustomException|\l|}", shape="record", style="filled"]; -"data.suppliermodule_test.DoNothing" [color="aliceblue", fontcolor="black", label="{DoNothing|\l|}", shape="record", style="filled"]; -"data.suppliermodule_test.DoNothing2" [color="aliceblue", fontcolor="black", label="{DoNothing2|\l|}", shape="record", style="filled"]; -"data.suppliermodule_test.DoSomething" [color="aliceblue", fontcolor="black", label="{DoSomething|my_int : Optional[int]\lmy_int_2 : Optional[int]\lmy_string : str\l|do_it(new_int: int): int\l}", shape="record", style="filled"]; -"data.suppliermodule_test.Interface" [color="aliceblue", fontcolor="black", label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record", style="filled"]; -"data.property_pattern.PropertyPatterns" [color="aliceblue", fontcolor="black", label="{PropertyPatterns|prop1\lprop2\l|}", shape="record", style="filled"]; -"data.clientmodule_test.Specialization" [color="aliceblue", fontcolor="black", label="{Specialization|TYPE : str\lrelation\lrelation2\ltop : str\l|from_value(value: int)\lincrement_value(): None\ltransform_value(value: int): int\l}", shape="record", style="filled"]; +"data.clientmodule_test.Ancestor" [color="aliceblue", fontcolor="black", label=<{Ancestor|attr : str
cls_member
|get_value()
set_value(value)
}>, shape="record", style="filled"]; +"data.suppliermodule_test.CustomException" [color="aliceblue", fontcolor="red", label=<{CustomException|
|}>, shape="record", style="filled"]; +"data.suppliermodule_test.DoNothing" [color="aliceblue", fontcolor="black", label=<{DoNothing|
|}>, shape="record", style="filled"]; +"data.suppliermodule_test.DoNothing2" [color="aliceblue", fontcolor="black", label=<{DoNothing2|
|}>, shape="record", style="filled"]; +"data.suppliermodule_test.DoSomething" [color="aliceblue", fontcolor="black", label=<{DoSomething|my_int : Optional[int]
my_int_2 : Optional[int]
my_string : str
|do_it(new_int: int): int
}>, shape="record", style="filled"]; +"data.suppliermodule_test.Interface" [color="aliceblue", fontcolor="black", label=<{Interface|
|get_value()
set_value(value)
}>, shape="record", style="filled"]; +"data.property_pattern.PropertyPatterns" [color="aliceblue", fontcolor="black", label=<{PropertyPatterns|prop1
prop2
|}>, shape="record", style="filled"]; +"data.clientmodule_test.Specialization" [color="aliceblue", fontcolor="black", label=<{Specialization|TYPE : str
relation
relation2
top : str
|from_value(value: int)
increment_value(): None
transform_value(value: int): int
}>, shape="record", style="filled"]; "data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"]; "data.clientmodule_test.Ancestor" -> "data.suppliermodule_test.Interface" [arrowhead="empty", arrowtail="node", style="dashed"]; "data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Ancestor" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"]; "data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"]; -"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"]; +"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="odiamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"]; } diff --git a/tests/pyreverse/data/classes_colorized.puml b/tests/pyreverse/data/classes_colorized.puml index 1226f7b4e6..7398ee60f6 100644 --- a/tests/pyreverse/data/classes_colorized.puml +++ b/tests/pyreverse/data/classes_colorized.puml @@ -19,8 +19,8 @@ class "DoSomething" as data.suppliermodule_test.DoSomething #aliceblue { do_it(new_int: int) -> int } class "Interface" as data.suppliermodule_test.Interface #aliceblue { - get_value() - set_value(value) + {abstract}get_value() + {abstract}set_value(value) } class "PropertyPatterns" as data.property_pattern.PropertyPatterns #aliceblue { prop1 @@ -39,5 +39,5 @@ data.clientmodule_test.Specialization --|> data.clientmodule_test.Ancestor data.clientmodule_test.Ancestor ..|> data.suppliermodule_test.Interface data.suppliermodule_test.DoNothing --* data.clientmodule_test.Ancestor : cls_member data.suppliermodule_test.DoNothing --* data.clientmodule_test.Specialization : relation -data.suppliermodule_test.DoNothing2 --* data.clientmodule_test.Specialization : relation2 +data.suppliermodule_test.DoNothing2 --o data.clientmodule_test.Specialization : relation2 @enduml diff --git a/tests/pyreverse/data/packages_No_Name.dot b/tests/pyreverse/data/packages_No_Name.dot index 461c8f9b46..5421c328c0 100644 --- a/tests/pyreverse/data/packages_No_Name.dot +++ b/tests/pyreverse/data/packages_No_Name.dot @@ -1,9 +1,9 @@ digraph "packages_No_Name" { rankdir=BT charset="utf-8" -"data" [color="black", label="data", shape="box", style="solid"]; -"data.clientmodule_test" [color="black", label="data.clientmodule_test", shape="box", style="solid"]; -"data.property_pattern" [color="black", label="data.property_pattern", shape="box", style="solid"]; -"data.suppliermodule_test" [color="black", label="data.suppliermodule_test", shape="box", style="solid"]; +"data" [color="black", label=, shape="box", style="solid"]; +"data.clientmodule_test" [color="black", label=, shape="box", style="solid"]; +"data.property_pattern" [color="black", label=, shape="box", style="solid"]; +"data.suppliermodule_test" [color="black", label=, shape="box", style="solid"]; "data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"]; } diff --git a/tests/pyreverse/data/packages_colorized.dot b/tests/pyreverse/data/packages_colorized.dot index 1a95d4c97a..10005f26cb 100644 --- a/tests/pyreverse/data/packages_colorized.dot +++ b/tests/pyreverse/data/packages_colorized.dot @@ -1,9 +1,9 @@ digraph "packages_colorized" { rankdir=BT charset="utf-8" -"data" [color="aliceblue", label="data", shape="box", style="filled"]; -"data.clientmodule_test" [color="aliceblue", label="data.clientmodule_test", shape="box", style="filled"]; -"data.property_pattern" [color="aliceblue", label="data.property_pattern", shape="box", style="filled"]; -"data.suppliermodule_test" [color="aliceblue", label="data.suppliermodule_test", shape="box", style="filled"]; +"data" [color="aliceblue", label=, shape="box", style="filled"]; +"data.clientmodule_test" [color="aliceblue", label=, shape="box", style="filled"]; +"data.property_pattern" [color="aliceblue", label=, shape="box", style="filled"]; +"data.suppliermodule_test" [color="aliceblue", label=, shape="box", style="filled"]; "data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"]; } diff --git a/tests/pyreverse/functional/class_diagrams/annotations/attributes_annotation.dot b/tests/pyreverse/functional/class_diagrams/annotations/attributes_annotation.dot index e9b23699b1..94c242ccfc 100644 --- a/tests/pyreverse/functional/class_diagrams/annotations/attributes_annotation.dot +++ b/tests/pyreverse/functional/class_diagrams/annotations/attributes_annotation.dot @@ -1,6 +1,6 @@ digraph "classes" { rankdir=BT charset="utf-8" -"attributes_annotation.Dummy" [color="black", fontcolor="black", label="{Dummy|\l|}", shape="record", style="solid"]; -"attributes_annotation.Dummy2" [color="black", fontcolor="black", label="{Dummy2|alternative_union_syntax : str \| int\lclass_attr : list[Dummy]\loptional : Optional[Dummy]\lparam : str\lunion : Union[int, str]\l|}", shape="record", style="solid"]; +"attributes_annotation.Dummy" [color="black", fontcolor="black", label=<{Dummy|
|}>, shape="record", style="solid"]; +"attributes_annotation.Dummy2" [color="black", fontcolor="black", label=<{Dummy2|alternative_union_syntax : str \| int
class_attr : list[Dummy]
optional : Optional[Dummy]
param : str
union : Union[int, str]
|}>, shape="record", style="solid"]; } diff --git a/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml b/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml index 6a4e356abe..a5ccf11f79 100644 --- a/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml +++ b/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml @@ -16,19 +16,19 @@ class "ExceptionsChecker" as pylint.checkers.exceptions.ExceptionsChecker #aquam msgs : dict name : str options : tuple - open() + open() -> None visit_binop(node: nodes.BinOp) -> None visit_compare(node: nodes.Compare) -> None visit_raise(node: nodes.Raise) -> None visit_tryexcept(node: nodes.TryExcept) -> None } class "StdlibChecker" as pylint.checkers.stdlib.StdlibChecker #aquamarine { - msgs + msgs : dict[str, MessageDefinitionTuple] name : str - deprecated_arguments(method: str) - deprecated_classes(module: str) - deprecated_decorators() -> Iterable - deprecated_methods() + deprecated_arguments(method: str) -> tuple[tuple[int | None, str], ...] + deprecated_classes(module: str) -> Iterable[str] + deprecated_decorators() -> Iterable[str] + deprecated_methods() -> set[str] visit_boolop(node: nodes.BoolOp) -> None visit_call(node: nodes.Call) -> None visit_functiondef(node: nodes.FunctionDef) -> None diff --git a/tests/pyreverse/functional/class_diagrams/regression/regression_8031.mmd b/tests/pyreverse/functional/class_diagrams/regression/regression_8031.mmd new file mode 100644 index 0000000000..7ca9a64627 --- /dev/null +++ b/tests/pyreverse/functional/class_diagrams/regression/regression_8031.mmd @@ -0,0 +1,5 @@ +classDiagram + class MyClass { + a + b + } diff --git a/tests/pyreverse/functional/class_diagrams/regression/regression_8031.py b/tests/pyreverse/functional/class_diagrams/regression/regression_8031.py new file mode 100644 index 0000000000..db56c37af7 --- /dev/null +++ b/tests/pyreverse/functional/class_diagrams/regression/regression_8031.py @@ -0,0 +1,4 @@ +class MyClass: + + def __init__(self, a, b): + self.a, self.b = a, b diff --git a/tests/pyreverse/test_diadefs.py b/tests/pyreverse/test_diadefs.py index 01457802c4..da16eea333 100644 --- a/tests/pyreverse/test_diadefs.py +++ b/tests/pyreverse/test_diadefs.py @@ -9,7 +9,7 @@ from __future__ import annotations import sys -from collections.abc import Callable +from collections.abc import Iterator from pathlib import Path import pytest @@ -24,6 +24,11 @@ from pylint.pyreverse.diagrams import DiagramEntity, Relationship from pylint.pyreverse.inspector import Linker, Project from pylint.testutils.pyreverse import PyreverseConfig +from pylint.testutils.utils import _test_cwd +from pylint.typing import GetProjectCallable + +HERE = Path(__file__) +TESTS = HERE.parent.parent def _process_classes(classes: list[DiagramEntity]) -> list[tuple[bool, str]]: @@ -49,8 +54,9 @@ def HANDLER(default_config: PyreverseConfig) -> DiadefsHandler: @pytest.fixture(scope="module") -def PROJECT(get_project): - return get_project("data") +def PROJECT(get_project: GetProjectCallable) -> Iterator[Project]: + with _test_cwd(TESTS): + yield get_project("data") def test_option_values( @@ -93,24 +99,23 @@ def test_default_values() -> None: class TestDefaultDiadefGenerator: _should_rels = [ + ("aggregation", "DoNothing2", "Specialization"), ("association", "DoNothing", "Ancestor"), ("association", "DoNothing", "Specialization"), - ("association", "DoNothing2", "Specialization"), ("implements", "Ancestor", "Interface"), ("specialization", "Specialization", "Ancestor"), ] - def test_exctract_relations( - self, HANDLER: DiadefsHandler, PROJECT: Project - ) -> None: + def test_extract_relations(self, HANDLER: DiadefsHandler, PROJECT: Project) -> None: """Test extract_relations between classes.""" - cd = DefaultDiadefGenerator(Linker(PROJECT), HANDLER).visit(PROJECT)[1] + with pytest.warns(DeprecationWarning): + cd = DefaultDiadefGenerator(Linker(PROJECT), HANDLER).visit(PROJECT)[1] cd.extract_relationships() relations = _process_relations(cd.relationships) assert relations == self._should_rels def test_functional_relation_extraction( - self, default_config: PyreverseConfig, get_project: Callable + self, default_config: PyreverseConfig, get_project: GetProjectCallable ) -> None: """Functional test of relations extraction; different classes possibly in different modules @@ -154,7 +159,9 @@ def test_known_values1(HANDLER: DiadefsHandler, PROJECT: Project) -> None: ] -def test_known_values2(HANDLER: DiadefsHandler, get_project: Callable) -> None: +def test_known_values2( + HANDLER: DiadefsHandler, get_project: GetProjectCallable +) -> None: project = get_project("data.clientmodule_test") dd = DefaultDiadefGenerator(Linker(project), HANDLER).visit(project) assert len(dd) == 1 @@ -199,7 +206,7 @@ def test_known_values4(HANDLER: DiadefsHandler, PROJECT: Project) -> None: @pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires dataclasses") def test_regression_dataclasses_inference( - HANDLER: DiadefsHandler, get_project: Callable + HANDLER: DiadefsHandler, get_project: GetProjectCallable ) -> None: project_path = Path("regrtest_data") / "dataclasses_pyreverse" path = get_project(str(project_path)) diff --git a/tests/pyreverse/test_diagrams.py b/tests/pyreverse/test_diagrams.py index b4a59a571c..863bcecc95 100644 --- a/tests/pyreverse/test_diagrams.py +++ b/tests/pyreverse/test_diagrams.py @@ -6,15 +6,14 @@ from __future__ import annotations -from collections.abc import Callable - from pylint.pyreverse.diadefslib import DefaultDiadefGenerator, DiadefsHandler from pylint.pyreverse.inspector import Linker from pylint.testutils.pyreverse import PyreverseConfig +from pylint.typing import GetProjectCallable def test_property_handling( - default_config: PyreverseConfig, get_project: Callable + default_config: PyreverseConfig, get_project: GetProjectCallable ) -> None: project = get_project("data.property_pattern") class_diagram = DefaultDiadefGenerator( diff --git a/tests/pyreverse/test_inspector.py b/tests/pyreverse/test_inspector.py index 0fd54e8f81..00cad918f6 100644 --- a/tests/pyreverse/test_inspector.py +++ b/tests/pyreverse/test_inspector.py @@ -9,7 +9,9 @@ from __future__ import annotations import os -from collections.abc import Callable +import warnings +from collections.abc import Generator +from pathlib import Path import astroid import pytest @@ -17,14 +19,22 @@ from pylint.pyreverse import inspector from pylint.pyreverse.inspector import Project +from pylint.testutils.utils import _test_cwd +from pylint.typing import GetProjectCallable + +HERE = Path(__file__) +TESTS = HERE.parent.parent @pytest.fixture -def project(get_project: Callable) -> Project: - project = get_project("data", "data") - linker = inspector.Linker(project) - linker.visit(project) - return project +def project(get_project: GetProjectCallable) -> Generator[Project, None, None]: + with _test_cwd(TESTS): + project = get_project("data", "data") + linker = inspector.Linker(project) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + linker.visit(project) + yield project def test_class_implements(project: Project) -> None: diff --git a/tests/pyreverse/test_main.py b/tests/pyreverse/test_main.py index 69a6ddb266..37398e6263 100644 --- a/tests/pyreverse/test_main.py +++ b/tests/pyreverse/test_main.py @@ -13,6 +13,8 @@ from unittest import mock import pytest +from _pytest.capture import CaptureFixture +from _pytest.fixtures import SubRequest from pylint.lint import fix_import_path from pylint.pyreverse import main @@ -22,13 +24,13 @@ @pytest.fixture(name="mock_subprocess") -def mock_utils_subprocess(): +def mock_utils_subprocess() -> Iterator[mock.MagicMock]: with mock.patch("pylint.pyreverse.utils.subprocess") as mock_subprocess: yield mock_subprocess @pytest.fixture -def mock_graphviz(mock_subprocess): +def mock_graphviz(mock_subprocess: mock.MagicMock) -> Iterator[None]: mock_subprocess.run.return_value = mock.Mock( stderr=( 'Format: "XYZ" not recognized. Use one of: ' @@ -44,7 +46,7 @@ def mock_graphviz(mock_subprocess): @pytest.fixture(params=[PROJECT_ROOT_DIR, TEST_DATA_DIR]) -def setup_path(request) -> Iterator: +def setup_path(request: SubRequest) -> Iterator[None]: current_sys_path = list(sys.path) sys.path[:] = [] current_dir = os.getcwd() @@ -55,7 +57,7 @@ def setup_path(request) -> Iterator: @pytest.mark.usefixtures("setup_path") -def test_project_root_in_sys_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 """ @@ -67,7 +69,9 @@ def test_project_root_in_sys_path(): @mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock()) @mock.patch("pylint.pyreverse.main.writer") @pytest.mark.usefixtures("mock_graphviz") -def test_graphviz_supported_image_format(mock_writer, capsys): +def test_graphviz_supported_image_format( + mock_writer: mock.MagicMock, capsys: CaptureFixture[str] +) -> None: """Test that Graphviz is used if the image format is supported.""" with pytest.raises(SystemExit) as wrapped_sysexit: # we have to catch the SystemExit so the test execution does not stop @@ -87,8 +91,8 @@ def test_graphviz_supported_image_format(mock_writer, capsys): @mock.patch("pylint.pyreverse.main.writer") @pytest.mark.usefixtures("mock_graphviz") def test_graphviz_cant_determine_supported_formats( - mock_writer, mock_subprocess, capsys -): + mock_writer: mock.MagicMock, mock_subprocess: mock.MagicMock, capsys: CaptureFixture +) -> None: """Test that Graphviz is used if the image format is supported.""" mock_subprocess.run.return_value.stderr = "..." with pytest.raises(SystemExit) as wrapped_sysexit: @@ -108,7 +112,7 @@ def test_graphviz_cant_determine_supported_formats( @mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock()) @mock.patch("pylint.pyreverse.main.writer", new=mock.MagicMock()) @pytest.mark.usefixtures("mock_graphviz") -def test_graphviz_unsupported_image_format(capsys): +def test_graphviz_unsupported_image_format(capsys: CaptureFixture) -> None: """Test that Graphviz is used if the image format is supported.""" with pytest.raises(SystemExit) as wrapped_sysexit: # we have to catch the SystemExit so the test execution does not stop @@ -147,7 +151,7 @@ def test_graphviz_unsupported_image_format(capsys): @mock.patch("pylint.pyreverse.main.sys.exit", new=mock.MagicMock()) def test_command_line_arguments_defaults(arg: str, expected_default: Any) -> None: """Test that the default arguments of all options are correct.""" - run = main.Run([TEST_DATA_DIR]) + run = main.Run([TEST_DATA_DIR]) # type: ignore[var-annotated] assert getattr(run.config, arg) == expected_default @@ -174,7 +178,7 @@ def test_class_command( Make sure that we append multiple --class arguments to one option destination. """ - runner = main.Run( + runner = main.Run( # type: ignore[var-annotated] [ "--class", "data.clientmodule_test.Ancestor", @@ -185,3 +189,16 @@ def test_class_command( ) assert "data.clientmodule_test.Ancestor" in runner.config.classes assert "data.property_pattern.PropertyPatterns" in runner.config.classes + + +def test_version_info( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: + """Test that it is possible to display the version information.""" + test_full_version = "1.2.3.4" + monkeypatch.setattr(main.constants, "full_version", test_full_version) # type: ignore[attr-defined] + with pytest.raises(SystemExit): + main.Run(["--version"]) + out, _ = capsys.readouterr() + assert "pyreverse is included in pylint" in out + assert test_full_version in out diff --git a/tests/pyreverse/test_printer.py b/tests/pyreverse/test_printer.py index 3822383db0..4248e8baed 100644 --- a/tests/pyreverse/test_printer.py +++ b/tests/pyreverse/test_printer.py @@ -39,12 +39,12 @@ def test_explicit_layout( "layout, printer_class", [(Layout.BOTTOM_TO_TOP, PlantUmlPrinter), (Layout.RIGHT_TO_LEFT, PlantUmlPrinter)], ) -def test_unsupported_layout(layout: Layout, printer_class: type[Printer]): +def test_unsupported_layout(layout: Layout, printer_class: type[Printer]) -> None: with pytest.raises(ValueError): printer_class(title="unittest", layout=layout) -def test_method_arguments_none(): +def test_method_arguments_none() -> None: func = nodes.FunctionDef() args = nodes.Arguments() args.args = None diff --git a/tests/pyreverse/test_printer_factory.py b/tests/pyreverse/test_printer_factory.py index 97ee1179c8..76406f0a8f 100644 --- a/tests/pyreverse/test_printer_factory.py +++ b/tests/pyreverse/test_printer_factory.py @@ -4,11 +4,14 @@ """Unit tests for pylint.pyreverse.printer_factory.""" +from __future__ import annotations + import pytest from pylint.pyreverse import printer_factory from pylint.pyreverse.dot_printer import DotPrinter from pylint.pyreverse.plantuml_printer import PlantUmlPrinter +from pylint.pyreverse.printer import Printer from pylint.pyreverse.vcg_printer import VCGPrinter @@ -22,5 +25,7 @@ ("png", DotPrinter), ], ) -def test_get_printer_for_filetype(filetype, expected_printer_class): +def test_get_printer_for_filetype( + filetype: str, expected_printer_class: type[Printer] +) -> None: assert printer_factory.get_printer_for_filetype(filetype) == expected_printer_class diff --git a/tests/pyreverse/test_pyreverse_functional.py b/tests/pyreverse/test_pyreverse_functional.py index 2cc880d70a..15fd1978b6 100644 --- a/tests/pyreverse/test_pyreverse_functional.py +++ b/tests/pyreverse/test_pyreverse_functional.py @@ -5,7 +5,6 @@ from pathlib import Path import pytest -from py._path.local import LocalPath # type: ignore[import] from pylint.pyreverse.main import Run from pylint.testutils.pyreverse import ( @@ -23,17 +22,15 @@ CLASS_DIAGRAM_TESTS, ids=CLASS_DIAGRAM_TEST_IDS, ) -def test_class_diagrams( - testfile: FunctionalPyreverseTestfile, tmpdir: LocalPath -) -> None: +def test_class_diagrams(testfile: FunctionalPyreverseTestfile, tmp_path: Path) -> None: input_file = testfile.source for output_format in testfile.options["output_formats"]: with pytest.raises(SystemExit) as sys_exit: - args = ["-o", f"{output_format}", "-d", str(tmpdir)] + args = ["-o", f"{output_format}", "-d", str(tmp_path)] args.extend(testfile.options["command_line_args"]) args += [str(input_file)] Run(args) assert sys_exit.value.code == 0 assert testfile.source.with_suffix(f".{output_format}").read_text( encoding="utf8" - ) == Path(tmpdir / f"classes.{output_format}").read_text(encoding="utf8") + ) == (tmp_path / f"classes.{output_format}").read_text(encoding="utf8") diff --git a/tests/pyreverse/test_utils.py b/tests/pyreverse/test_utils.py index 70d95346f6..d64bf4fa70 100644 --- a/tests/pyreverse/test_utils.py +++ b/tests/pyreverse/test_utils.py @@ -4,6 +4,8 @@ """Tests for pylint.pyreverse.utils.""" +from __future__ import annotations + from typing import Any from unittest.mock import patch @@ -31,7 +33,7 @@ ), ], ) -def test_get_visibility(names, expected): +def test_get_visibility(names: list[str], expected: str) -> None: for name in names: got = get_visibility(name) assert got == expected, f"got {got} instead of {expected} for value {name}" @@ -46,10 +48,12 @@ def test_get_visibility(names, expected): ("a: Optional[str] = None", "Optional[str]"), ], ) -def test_get_annotation_annassign(assign, label): +def test_get_annotation_annassign(assign: str, label: str) -> None: """AnnAssign.""" - node = astroid.extract_node(assign) - got = get_annotation(node.value).name + node: nodes.AnnAssign = astroid.extract_node(assign) + annotation = get_annotation(node.value) + assert annotation is not None + got = annotation.name assert isinstance(node, nodes.AnnAssign) assert got == label, f"got {got} instead of {label} for value {node}" @@ -65,7 +69,7 @@ def test_get_annotation_annassign(assign, label): ("def __init__(self, x: Optional[str] = 'str'): self.x = x", "Optional[str]"), ], ) -def test_get_annotation_assignattr(init_method, label): +def test_get_annotation_assignattr(init_method: str, label: str) -> None: """AssignAttr.""" assign = rf""" class A: @@ -75,7 +79,9 @@ class A: instance_attrs = node.instance_attrs for assign_attrs in instance_attrs.values(): for assign_attr in assign_attrs: - got = get_annotation(assign_attr).name + annotation = get_annotation(assign_attr) + assert annotation is not None + got = annotation.name assert isinstance(assign_attr, nodes.AssignAttr) assert got == label, f"got {got} instead of {label} for value {node}" @@ -98,7 +104,7 @@ def test_get_annotation_label_of_return_type( @patch("pylint.pyreverse.utils.get_annotation") -@patch("astroid.node_classes.NodeNG.infer", side_effect=astroid.InferenceError) +@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 @@ -111,7 +117,7 @@ def test_infer_node_1(mock_infer: Any, mock_get_annotation: Any) -> None: @patch("pylint.pyreverse.utils.get_annotation") -@patch("astroid.node_classes.NodeNG.infer") +@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 diff --git a/tests/pyreverse/test_writer.py b/tests/pyreverse/test_writer.py index a031050924..805f8fab59 100644 --- a/tests/pyreverse/test_writer.py +++ b/tests/pyreverse/test_writer.py @@ -8,7 +8,7 @@ import codecs import os -from collections.abc import Callable, Iterator +from collections.abc import Iterator from difflib import unified_diff from unittest.mock import Mock @@ -18,6 +18,7 @@ from pylint.pyreverse.inspector import Linker, Project from pylint.pyreverse.writer import DiagramWriter from pylint.testutils.pyreverse import PyreverseConfig +from pylint.typing import GetProjectCallable _DEFAULTS = { "all_ancestors": None, @@ -49,7 +50,7 @@ class Config: """Config object for tests.""" - def __init__(self): + def __init__(self) -> None: for attr, value in _DEFAULTS.items(): setattr(self, attr, value) @@ -69,7 +70,9 @@ def _file_lines(path: str) -> list[str]: @pytest.fixture() -def setup_dot(default_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_dot( + default_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(default_config) project = get_project(TEST_DATA_DIR) yield from _setup(project, default_config, writer) @@ -77,22 +80,26 @@ def setup_dot(default_config: PyreverseConfig, get_project: Callable) -> Iterato @pytest.fixture() def setup_colorized_dot( - colorized_dot_config: PyreverseConfig, get_project: Callable -) -> Iterator: + colorized_dot_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(colorized_dot_config) project = get_project(TEST_DATA_DIR, name="colorized") yield from _setup(project, colorized_dot_config, writer) @pytest.fixture() -def setup_vcg(vcg_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_vcg( + vcg_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(vcg_config) project = get_project(TEST_DATA_DIR) yield from _setup(project, vcg_config, writer) @pytest.fixture() -def setup_puml(puml_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_puml( + puml_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(puml_config) project = get_project(TEST_DATA_DIR) yield from _setup(project, puml_config, writer) @@ -100,15 +107,17 @@ def setup_puml(puml_config: PyreverseConfig, get_project: Callable) -> Iterator: @pytest.fixture() def setup_colorized_puml( - colorized_puml_config: PyreverseConfig, get_project: Callable -) -> Iterator: + colorized_puml_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(colorized_puml_config) project = get_project(TEST_DATA_DIR, name="colorized") yield from _setup(project, colorized_puml_config, writer) @pytest.fixture() -def setup_mmd(mmd_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_mmd( + mmd_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(mmd_config) project = get_project(TEST_DATA_DIR) @@ -116,7 +125,9 @@ def setup_mmd(mmd_config: PyreverseConfig, get_project: Callable) -> Iterator: @pytest.fixture() -def setup_html(html_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_html( + html_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(html_config) project = get_project(TEST_DATA_DIR) @@ -125,7 +136,7 @@ def setup_html(html_config: PyreverseConfig, get_project: Callable) -> Iterator: def _setup( project: Project, config: PyreverseConfig, writer: DiagramWriter -) -> Iterator: +) -> Iterator[None]: linker = Linker(project) handler = DiadefsHandler(config) dd = DefaultDiadefGenerator(linker, handler).visit(project) diff --git a/tests/regrtest_data/allow_reexport/__init__.py b/tests/regrtest_data/allow_reexport/__init__.py new file mode 100644 index 0000000000..0ee6e5127f --- /dev/null +++ b/tests/regrtest_data/allow_reexport/__init__.py @@ -0,0 +1 @@ +import os as os diff --git a/tests/regrtest_data/allow_reexport/file.py b/tests/regrtest_data/allow_reexport/file.py new file mode 100644 index 0000000000..a1adccd46a --- /dev/null +++ b/tests/regrtest_data/allow_reexport/file.py @@ -0,0 +1,2 @@ +# pylint: disable=unused-import +import os as os diff --git a/tests/regrtest_data/classdoc_usage.py b/tests/regrtest_data/classdoc_usage.py index 2d9df51cd7..b12bafa72c 100644 --- a/tests/regrtest_data/classdoc_usage.py +++ b/tests/regrtest_data/classdoc_usage.py @@ -1,9 +1,8 @@ """ds""" __revision__ = None -# pylint: disable=useless-object-inheritance -class SomeClass(object): +class SomeClass: """cds""" doc = __doc__ diff --git a/tests/regrtest_data/encoding/bad_missing_num.py b/tests/regrtest_data/encoding/bad_missing_num.py new file mode 100644 index 0000000000..a43139838d --- /dev/null +++ b/tests/regrtest_data/encoding/bad_missing_num.py @@ -0,0 +1 @@ +# -*- encoding: utf -*- diff --git a/tests/regrtest_data/encoding/bad_wrong_num.py b/tests/regrtest_data/encoding/bad_wrong_num.py new file mode 100644 index 0000000000..5c6bfe7868 --- /dev/null +++ b/tests/regrtest_data/encoding/bad_wrong_num.py @@ -0,0 +1 @@ +# -*- encoding: utf-9 -*- diff --git a/tests/regrtest_data/encoding/good.py b/tests/regrtest_data/encoding/good.py new file mode 100644 index 0000000000..dae354a675 --- /dev/null +++ b/tests/regrtest_data/encoding/good.py @@ -0,0 +1 @@ +# -*- encoding: utf-8 -*- diff --git a/tests/regrtest_data/func_block_disable_msg.py b/tests/regrtest_data/func_block_disable_msg.py index 8a94ab4f3a..a1dd9a627c 100644 --- a/tests/regrtest_data/func_block_disable_msg.py +++ b/tests/regrtest_data/func_block_disable_msg.py @@ -1,8 +1,8 @@ -# pylint: disable=C0302,bare-except, useless-object-inheritance +# pylint: disable=C0302,bare-except """pylint option block-disable""" from __future__ import print_function -class Foo(object): +class Foo: """block-disable test""" def __init__(self): @@ -110,7 +110,7 @@ def meth10(self): print(self.blu) -class ClassLevelMessage(object): +class ClassLevelMessage: """shouldn't display to much attributes/not enough methods messages """ # pylint: disable=R0902,R0903 diff --git a/tests/regrtest_data/imported_module_in_typehint/module_a.py b/tests/regrtest_data/imported_module_in_typehint/module_a.py new file mode 100644 index 0000000000..d9754eca4c --- /dev/null +++ b/tests/regrtest_data/imported_module_in_typehint/module_a.py @@ -0,0 +1,5 @@ +import uuid +from typing import Optional + + +ID = None # type: Optional[uuid.UUID] diff --git a/tests/regrtest_data/imported_module_in_typehint/module_b.py b/tests/regrtest_data/imported_module_in_typehint/module_b.py new file mode 100644 index 0000000000..4ab5fc595e --- /dev/null +++ b/tests/regrtest_data/imported_module_in_typehint/module_b.py @@ -0,0 +1 @@ +import uuid diff --git a/tests/regrtest_data/importing_plugin/importing_plugin.py b/tests/regrtest_data/importing_plugin/importing_plugin.py new file mode 100644 index 0000000000..227b82ed33 --- /dev/null +++ b/tests/regrtest_data/importing_plugin/importing_plugin.py @@ -0,0 +1,32 @@ +from importlib import import_module + +from pylint.checkers import BaseChecker +from pylint.lint.pylinter import PyLinter + + +class ImportingChecker(BaseChecker): + options = ( + ( + "settings-module", + { + "default": "settings", + "type": "string", + "metavar": "" + }, + ), + ) + + msgs = { + "E9999": ( + "Importing checker error message", + "importing-checker-error", + "Importing checker error message", + ), + } + + def open(self) -> None: + import_module(self.linter.config.settings_module) + + +def register(linter: "PyLinter") -> None: + linter.register_checker(ImportingChecker(linter)) diff --git a/tests/regrtest_data/invalid_encoding.py b/tests/regrtest_data/invalid_encoding.py new file mode 100644 index 0000000000..d508d19df8 --- /dev/null +++ b/tests/regrtest_data/invalid_encoding.py @@ -0,0 +1 @@ +# -*- coding: lala -*- diff --git a/tests/regrtest_data/line_too_long_no_code.py b/tests/regrtest_data/line_too_long_no_code.py new file mode 100644 index 0000000000..75ab07fc2e --- /dev/null +++ b/tests/regrtest_data/line_too_long_no_code.py @@ -0,0 +1,2 @@ +# ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 # pylint: disable=line-too-long +# See https://github.com/PyCQA/pylint/issues/3368 diff --git a/tests/regrtest_data/pkg_mod_imports/__init__.py b/tests/regrtest_data/pkg_mod_imports/__init__.py new file mode 100644 index 0000000000..3999a48b0f --- /dev/null +++ b/tests/regrtest_data/pkg_mod_imports/__init__.py @@ -0,0 +1,6 @@ +base = [ + 'Exchange', + 'Precise', + 'exchanges', + 'decimal_to_precision', +] diff --git a/tests/regrtest_data/pkg_mod_imports/base/__init__.py b/tests/regrtest_data/pkg_mod_imports/base/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regrtest_data/pkg_mod_imports/base/errors.py b/tests/regrtest_data/pkg_mod_imports/base/errors.py new file mode 100644 index 0000000000..c72119df36 --- /dev/null +++ b/tests/regrtest_data/pkg_mod_imports/base/errors.py @@ -0,0 +1,2 @@ +class SomeError(Exception): + pass diff --git a/tests/regrtest_data/preferred_module/unpreferred_module.py b/tests/regrtest_data/preferred_module/unpreferred_module.py new file mode 100644 index 0000000000..348f0f6e32 --- /dev/null +++ b/tests/regrtest_data/preferred_module/unpreferred_module.py @@ -0,0 +1,3 @@ +import os + +os.path(".") diff --git a/tests/regrtest_data/settings_project/models.py b/tests/regrtest_data/settings_project/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regrtest_data/settings_project/settings.py b/tests/regrtest_data/settings_project/settings.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regrtest_data/test_no_name_in_module.py b/tests/regrtest_data/test_no_name_in_module.py new file mode 100644 index 0000000000..326a25999f --- /dev/null +++ b/tests/regrtest_data/test_no_name_in_module.py @@ -0,0 +1 @@ +from pkg_mod_imports.base.errors import SomeError diff --git a/tests/unittest_reporters_json.py b/tests/reporters/unittest_json_reporter.py similarity index 69% rename from tests/unittest_reporters_json.py rename to tests/reporters/unittest_json_reporter.py index 2a0843e7f2..9104016ea4 100644 --- a/tests/unittest_reporters_json.py +++ b/tests/reporters/unittest_json_reporter.py @@ -10,10 +10,15 @@ from io import StringIO from typing import Any +import pytest + from pylint import checkers +from pylint.interfaces import UNDEFINED from pylint.lint import PyLinter +from pylint.message import Message from pylint.reporters import JSONReporter from pylint.reporters.ureports.nodes import EvaluationSection +from pylint.typing import MessageLocationTuple expected_score_message = "Expected score message" @@ -97,4 +102,36 @@ def get_linter_result(score: bool, message: dict[str, Any]) -> list[dict[str, An reporter.display_reports(EvaluationSection(expected_score_message)) reporter.display_messages(None) report_result = json.loads(output.getvalue()) - return report_result + return report_result # type: ignore[no-any-return] + + +@pytest.mark.parametrize( + "message", + [ + pytest.param( + Message( + msg_id="C0111", + symbol="missing-docstring", + location=MessageLocationTuple( + # abs-path and path must be equal because one of them is removed + # in the JsonReporter + abspath=__file__, + path=__file__, + module="unittest_json_reporter", + obj="obj", + line=1, + column=3, + end_line=3, + end_column=5, + ), + msg="This is the actual message", + confidence=UNDEFINED, + ), + id="everything-defined", + ) + ], +) +def test_serialize_deserialize(message: Message) -> None: + # TODO: 3.0: Add confidence handling, add path and abs path handling or a new JSONReporter + json_message = JSONReporter.serialize(message) + assert message == JSONReporter.deserialize(json_message) diff --git a/tests/unittest_reporting.py b/tests/reporters/unittest_reporting.py similarity index 77% rename from tests/unittest_reporting.py rename to tests/reporters/unittest_reporting.py index ebc4a225f4..40a18036a4 100644 --- a/tests/unittest_reporting.py +++ b/tests/reporters/unittest_reporting.py @@ -11,31 +11,35 @@ from contextlib import redirect_stdout from io import StringIO from json import dumps -from typing import TYPE_CHECKING +from pathlib import Path +from typing import TYPE_CHECKING, TextIO import pytest +from _pytest.recwarn import WarningsRecorder from pylint import checkers +from pylint.interfaces import HIGH from pylint.lint import PyLinter -from pylint.reporters import BaseReporter +from pylint.message.message import Message +from pylint.reporters import BaseReporter, MultiReporter from pylint.reporters.text import ParseableTextReporter, TextReporter -from pylint.typing import FileItem +from pylint.typing import FileItem, MessageLocationTuple if TYPE_CHECKING: from pylint.reporters.ureports.nodes import Section @pytest.fixture(scope="module") -def reporter(): +def reporter() -> type[TextReporter]: return TextReporter @pytest.fixture(scope="module") -def disable(): +def disable() -> list[str]: return ["I"] -def test_template_option(linter): +def test_template_option(linter: PyLinter) -> None: output = StringIO() linter.reporter.out = output linter.config.msg_template = "{msg_id}:{line:03d}" @@ -46,7 +50,7 @@ def test_template_option(linter): assert output.getvalue() == "************* Module 0123\nC0301:001\nC0301:002\n" -def test_template_option_default(linter) -> None: +def test_template_option_default(linter: PyLinter) -> None: """Test the default msg-template setting.""" output = StringIO() linter.reporter.out = output @@ -60,7 +64,7 @@ def test_template_option_default(linter) -> None: assert out_lines[2] == "my_module:2:0: C0301: Line too long (3/4) (line-too-long)" -def test_template_option_end_line(linter) -> None: +def test_template_option_end_line(linter: PyLinter) -> None: """Test the msg-template option with end_line and end_column.""" output = StringIO() linter.reporter.out = output @@ -79,23 +83,19 @@ def test_template_option_end_line(linter) -> None: assert out_lines[2] == "my_mod:2:0:2:4: C0301: Line too long (3/4) (line-too-long)" -def test_template_option_non_existing(linter) -> 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 """ output = StringIO() linter.reporter.out = output - linter.config.msg_template = ( - "{path}:{line}:{a_new_option}:({a_second_new_option:03d})" - ) + linter.config.msg_template = "{path}:{line}:{categ}:({a_second_new_option:03d})" linter.open() with pytest.warns(UserWarning) as records: linter.set_current_module("my_mod") assert len(records) == 2 - assert ( - "Don't recognize the argument 'a_new_option'" in records[0].message.args[0] - ) + assert "Don't recognize the argument 'categ'" in records[0].message.args[0] assert ( "Don't recognize the argument 'a_second_new_option'" in records[1].message.args[0] @@ -111,7 +111,24 @@ def test_template_option_non_existing(linter) -> None: assert out_lines[2] == "my_mod:2::()" -def test_deprecation_set_output(recwarn): +def test_template_option_with_header(linter: PyLinter) -> None: + output = StringIO() + linter.reporter.out = output + linter.config.msg_template = '{{ "Category": "{category}" }}' + linter.open() + linter.set_current_module("my_mod") + + linter.add_message("C0301", line=1, args=(1, 2)) + linter.add_message( + "line-too-long", line=2, end_lineno=2, end_col_offset=4, args=(3, 4) + ) + + out_lines = output.getvalue().split("\n") + assert out_lines[1] == '{ "Category": "convention" }' + assert out_lines[2] == '{ "Category": "convention" }' + + +def test_deprecation_set_output(recwarn: WarningsRecorder) -> None: """TODO remove in 3.0.""" reporter = BaseReporter() # noinspection PyDeprecation @@ -121,7 +138,7 @@ def test_deprecation_set_output(recwarn): assert reporter.out == sys.stdout -def test_parseable_output_deprecated(): +def test_parseable_output_deprecated() -> None: with warnings.catch_warnings(record=True) as cm: warnings.simplefilter("always") ParseableTextReporter() @@ -130,9 +147,10 @@ def test_parseable_output_deprecated(): assert isinstance(cm[0].message, DeprecationWarning) -def test_parseable_output_regression(): +def test_parseable_output_regression() -> None: output = StringIO() with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", category=DeprecationWarning) linter = PyLinter(reporter=ParseableTextReporter()) checkers.initialize(linter) @@ -153,18 +171,18 @@ class NopReporter(BaseReporter): name = "nop-reporter" extension = "" - def __init__(self, output=None): + def __init__(self, output: TextIO | None = None) -> None: super().__init__(output) print("A NopReporter was initialized.", file=self.out) - def writeln(self, string=""): + def writeln(self, string: str = "") -> None: pass def _display(self, layout: Section) -> None: pass -def test_multi_format_output(tmp_path): +def test_multi_format_output(tmp_path: Path) -> None: text = StringIO(newline=None) json = tmp_path / "somefile.json" @@ -189,12 +207,15 @@ def test_multi_format_output(tmp_path): linter.reporter.out = text linter.open() - linter.check_single_file_item(FileItem("somemodule", source_file, "somemodule")) + linter.check_single_file_item( + FileItem("somemodule", str(source_file), "somemodule") + ) linter.add_message("line-too-long", line=1, args=(1, 2)) linter.generate_reports() linter.reporter.writeln("direct output") # Ensure the output files are flushed and closed + assert isinstance(linter.reporter, MultiReporter) linter.reporter.close_output_files() del linter.reporter @@ -329,7 +350,51 @@ def test_multi_format_output(tmp_path): ) -def test_display_results_is_renamed(): +def test_multi_reporter_independant_messages() -> None: + """Messages should not be modified by multiple reporters""" + + check_message = "Not modified" + + class ReporterModify(BaseReporter): + def handle_message(self, msg: Message) -> None: + msg.msg = "Modified message" + + def writeln(self, string: str = "") -> None: + pass + + def _display(self, layout: Section) -> None: + pass + + class ReporterCheck(BaseReporter): + def handle_message(self, msg: Message) -> None: + assert ( + msg.msg == check_message + ), "Message object should not be changed by other reporters." + + def writeln(self, string: str = "") -> None: + pass + + def _display(self, layout: Section) -> None: + pass + + multi_reporter = MultiReporter([ReporterModify(), ReporterCheck()], lambda: None) + + message = Message( + symbol="missing-docstring", + msg_id="C0123", + location=MessageLocationTuple("abspath", "path", "module", "obj", 1, 2, 1, 3), + msg=check_message, + confidence=HIGH, + ) + + multi_reporter.handle_message(message) + + assert ( + message.msg == check_message + ), "Message object should not be changed by reporters." + + +def test_display_results_is_renamed() -> None: class CustomReporter(TextReporter): def _display(self, layout: Section) -> None: return None @@ -337,5 +402,5 @@ def _display(self, layout: Section) -> None: reporter = CustomReporter() with pytest.raises(AttributeError) as exc: # pylint: disable=no-member - reporter.display_results() + reporter.display_results() # type: ignore[attr-defined] assert "no attribute 'display_results'" in str(exc) diff --git a/tests/test_check_parallel.py b/tests/test_check_parallel.py index 259c236d42..96e67517ec 100644 --- a/tests/test_check_parallel.py +++ b/tests/test_check_parallel.py @@ -9,8 +9,10 @@ from __future__ import annotations import argparse -import multiprocessing import os +from concurrent.futures import ProcessPoolExecutor +from concurrent.futures.process import BrokenProcessPool +from pickle import PickleError import dill import pytest @@ -109,7 +111,7 @@ def close(self) -> None: for _ in self.data[1::2]: # Work on pairs of files, see class docstring. self.add_message("R9999", args=("From process_module, two files seen.",)) - def get_map_data(self): + def get_map_data(self) -> list[str]: return self.data def reduce_map_data(self, linter: PyLinter, data: list[list[str]]) -> None: @@ -160,10 +162,10 @@ class ThirdParallelTestChecker(ParallelTestChecker): class TestCheckParallelFramework: """Tests the check_parallel() function's framework.""" - def setup_class(self): + def setup_class(self) -> None: self._prev_global_linter = pylint.lint.parallel._worker_linter - def teardown_class(self): + def teardown_class(self) -> None: pylint.lint.parallel._worker_linter = self._prev_global_linter def test_worker_initialize(self) -> None: @@ -181,15 +183,15 @@ def test_worker_initialize_pickling(self) -> None: """ linter = PyLinter(reporter=Reporter()) linter.attribute = argparse.ArgumentParser() # type: ignore[attr-defined] - with multiprocessing.Pool( - 2, initializer=worker_initialize, initargs=[dill.dumps(linter)] - ) as pool: - pool.imap_unordered(print, [1, 2]) + with ProcessPoolExecutor( + max_workers=2, initializer=worker_initialize, initargs=(dill.dumps(linter),) + ) as executor: + executor.map(print, [1, 2]) def test_worker_check_single_file_uninitialised(self) -> None: pylint.lint.parallel._worker_linter = None with pytest.raises( # Objects that do not match the linter interface will fail - Exception, match="Worker linter not yet initialised" + RuntimeError, match="Worker linter not yet initialised" ): worker_check_single_file(_gen_file_data()) @@ -231,6 +233,28 @@ def test_worker_check_single_file_no_checkers(self) -> None: assert stats.statement == 18 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""" + linter = PyLinter(reporter=Reporter()) + # We load an extension that we know is not pickle-safe + linter.load_plugin_modules(["pylint.extensions.overlapping_exceptions"]) + try: + dill.dumps(linter) + raise AssertionError( + "Plugins loaded were pickle-safe! This test needs altering" + ) + except (KeyError, TypeError, PickleError, NotImplementedError): + pass + + # And expect this call to make it pickle-able + linter.load_plugin_configuration() + try: + dill.dumps(linter) + except KeyError as exc: + raise AssertionError( + "Cannot pickle linter when using non-pickleable plugin" + ) from exc + def test_worker_check_sequential_checker(self) -> None: """Same as test_worker_check_single_file_no_checkers with SequentialTestChecker.""" linter = PyLinter(reporter=Reporter()) @@ -411,7 +435,9 @@ def test_invoke_single_job(self) -> None: (10, 2, 3), ], ) - def test_compare_workers_to_single_proc(self, num_files, num_jobs, num_checkers): + def test_compare_workers_to_single_proc( + self, num_files: int, num_jobs: int, num_checkers: int + ) -> None: """Compares the 3 key parameters for check_parallel() produces the same results. The intent here is to ensure that the check_parallel() operates on each file, @@ -467,8 +493,10 @@ def test_compare_workers_to_single_proc(self, num_files, num_jobs, num_checkers) # establish the baseline assert ( linter.config.jobs == 1 - ), "jobs>1 are ignored when calling _check_files" - linter._check_files(linter.get_ast, file_infos) + ), "jobs>1 are ignored when calling _lint_files" + ast_mapping = linter._get_asts(iter(file_infos), None) + with linter._astroid_module_checker() as check_astroid_module: + linter._lint_files(ast_mapping, check_astroid_module) assert linter.msg_status == 0, "We should not fail the lint" stats_single_proc = linter.stats else: @@ -506,7 +534,7 @@ def test_compare_workers_to_single_proc(self, num_files, num_jobs, num_checkers) (10, 2, 3), ], ) - def test_map_reduce(self, num_files, num_jobs, num_checkers): + def test_map_reduce(self, num_files: int, num_jobs: int, num_checkers: int) -> None: """Compares the 3 key parameters for check_parallel() produces the same results. The intent here is to validate the reduce step: no stats should be lost. @@ -534,8 +562,10 @@ def test_map_reduce(self, num_files, num_jobs, num_checkers): # establish the baseline assert ( linter.config.jobs == 1 - ), "jobs>1 are ignored when calling _check_files" - linter._check_files(linter.get_ast, file_infos) + ), "jobs>1 are ignored when calling _lint_files" + ast_mapping = linter._get_asts(iter(file_infos), None) + with linter._astroid_module_checker() as check_astroid_module: + linter._lint_files(ast_mapping, check_astroid_module) stats_single_proc = linter.stats else: check_parallel( @@ -548,3 +578,27 @@ def test_map_reduce(self, num_files, num_jobs, num_checkers): assert str(stats_single_proc.by_msg) == str( stats_check_parallel.by_msg ), "Single-proc and check_parallel() should return the same thing" + + @pytest.mark.timeout(5) + def test_no_deadlock_due_to_initializer_error(self) -> None: + """Tests that an error in the initializer for the parallel jobs doesn't + lead to a deadlock. + """ + linter = PyLinter(reporter=Reporter()) + + linter.register_checker(SequentialTestChecker(linter)) + + # Create a dummy file, the actual contents of which will be ignored by the + # register test checkers, but it will trigger at least a single-job to be run. + single_file_container = _gen_file_datas(count=1) + + # The error in the initializer should trigger a BrokenProcessPool exception + with pytest.raises(BrokenProcessPool): + check_parallel( + linter, + jobs=1, + files=iter(single_file_container), + # This will trigger an exception in the initializer for the parallel jobs + # because arguments has to be an Iterable. + arguments=1, # type: ignore[arg-type] + ) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index d04925eeef..d30e69b85d 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -44,10 +44,9 @@ def reduce_map_data(self, linter: PyLinter, data: list[Any]) -> None: def test_reporter_implements() -> None: - """Test that __implements__ on BaseReporer has been deprecated correctly.""" + """Test that __implements__ on BaseReporter has been deprecated correctly.""" class MyReporter(BaseReporter): - __implements__ = IReporter def _display(self, layout: Section) -> None: @@ -61,7 +60,6 @@ def test_checker_implements() -> None: """Test that __implements__ on BaseChecker has been deprecated correctly.""" class MyChecker(BaseChecker): - __implements__ = IAstroidChecker with pytest.warns(DeprecationWarning): diff --git a/tests/test_epylint.py b/tests/test_epylint.py index e1b090395a..7e9116e99a 100644 --- a/tests/test_epylint.py +++ b/tests/test_epylint.py @@ -25,12 +25,12 @@ def run(self): def test_epylint_good_command(example_path: PosixPath) -> None: - out, err = lint.py_run( - # pylint: disable-next=consider-using-f-string - "%s -E --disable=E1111 --msg-template '{category} {module} {obj} {line} {column} {msg}'" - % example_path, - return_std=True, - ) + with pytest.warns(DeprecationWarning): + out, _ = lint.py_run( + f"{example_path} -E --disable=E1111 --msg-template " + "'{category} {module} {obj} {line} {column} {msg}'", + return_std=True, + ) msg = out.read() assert ( msg @@ -39,16 +39,16 @@ def test_epylint_good_command(example_path: PosixPath) -> None: error my_app IvrAudioApp.run 4 8 Instance of 'IvrAudioApp' has no 'hassan' member """ ) - assert err.read() == "" def test_epylint_strange_command(example_path: PosixPath) -> None: - out, err = lint.py_run( - # pylint: disable-next=consider-using-f-string - "%s -E --disable=E1111 --msg-template={category} {module} {obj} {line} {column} {msg}" - % example_path, - return_std=True, - ) + with pytest.warns(DeprecationWarning): + out, _ = lint.py_run( + # pylint: disable-next=consider-using-f-string + "%s -E --disable=E1111 --msg-template={category} {module} {obj} {line} {column} {msg}" + % example_path, + return_std=True, + ) assert ( out.read() == """\ @@ -66,4 +66,3 @@ def test_epylint_strange_command(example_path: PosixPath) -> None: error """ ) - assert err.read() == "" diff --git a/tests/test_func.py b/tests/test_func.py index 4769af4260..a032baf5df 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -14,20 +14,24 @@ from pylint.testutils import UPDATE_FILE, UPDATE_OPTION, _get_tests_info, linter from pylint.testutils.reporter_for_tests import GenericTestReporter +from pylint.testutils.utils import _test_cwd -INPUT_DIR = join(dirname(abspath(__file__)), "input") -MSG_DIR = join(dirname(abspath(__file__)), "messages") +TESTS_DIR = dirname(abspath(__file__)) +INPUT_DIR = join(TESTS_DIR, "input") +MSG_DIR = join(TESTS_DIR, "messages") FILTER_RGX = None INFO_TEST_RGX = re.compile(r"^func_i\d\d\d\d$") -def exception_str(self, ex) -> str: # pylint: disable=unused-argument +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 """ - return f"in {ex.file}\n:: {', '.join(ex.args)}" + return f"in {ex.file}\n:: {', '.join(ex.args)}" # type: ignore[attr-defined] # Defined in the caller class LintTestUsingModule: @@ -46,7 +50,11 @@ def _test_functionality(self) -> None: tocheck += [ self.package + f".{name.replace('.py', '')}" for name, _ in self.depends ] - self._test(tocheck) + # given that TESTS_DIR could be treated as a namespace package + # when under the current directory, cd to it so that "tests." is not + # prepended to module names in the output of cyclic-import + with _test_cwd(TESTS_DIR): + self._test(tocheck) def _check_result(self, got: str) -> None: error_msg = ( @@ -65,9 +73,11 @@ def _test(self, tocheck: list[str]) -> None: self.linter.check(tocheck) except Exception as ex: print(f"Exception: {ex} in {tocheck}:: {'‚ '.join(ex.args)}") - ex.file = tocheck # type: ignore[attr-defined] # This is legacy code we're trying to remove, not worth it to type correctly + # This is legacy code we're trying to remove, not worth it to type correctly + ex.file = tocheck # type: ignore[attr-defined] print(ex) - ex.__str__ = exception_str # type: ignore[assignment] # This is legacy code we're trying to remove, impossible to type correctly + # This is legacy code we're trying to remove, not worth it to type correctly + ex.__str__ = exception_str # type: ignore[assignment] raise assert isinstance(self.linter.reporter, GenericTestReporter) self._check_result(self.linter.reporter.finalize()) @@ -86,7 +96,7 @@ def _get_expected(self) -> str: class LintTestUpdate(LintTestUsingModule): - def _check_result(self, got): + def _check_result(self, got: str) -> None: if not self._has_output(): return try: @@ -94,18 +104,20 @@ def _check_result(self, got): except OSError: expected = "" if got != expected: - with open(self.output, "w", encoding="utf-8") as f: + with open(self.output or "", "w", encoding="utf-8") as f: f.write(got) -def gen_tests(filter_rgx): +def gen_tests( + filter_rgx: str | re.Pattern[str] | None, +) -> list[tuple[str, str, list[tuple[str, str]]]]: if filter_rgx: is_to_run = re.compile(filter_rgx).search else: is_to_run = ( - lambda x: 1 # pylint: disable=unnecessary-lambda-assignment + lambda x: 1 # type: ignore[assignment,misc] # pylint: disable=unnecessary-lambda-assignment ) # noqa: E731 We're going to throw all this anyway - tests = [] + tests: list[tuple[str, str, list[tuple[str, str]]]] = [] for module_file, messages_file in _get_tests_info(INPUT_DIR, MSG_DIR, "func_", ""): if not is_to_run(module_file) or module_file.endswith((".pyc", "$py.class")): continue @@ -129,7 +141,10 @@ def gen_tests(filter_rgx): ids=[o[0] for o in gen_tests(FILTER_RGX)], ) def test_functionality( - module_file, messages_file, dependencies, recwarn: pytest.WarningsRecorder + module_file: str, + messages_file: str, + dependencies: list[tuple[str, str]], + recwarn: pytest.WarningsRecorder, ) -> None: __test_functionality(module_file, messages_file, dependencies) if recwarn.list: diff --git a/tests/test_functional.py b/tests/test_functional.py index 74b541bcf7..77cdbc58f8 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -11,7 +11,6 @@ import pytest from _pytest.config import Config -from _pytest.recwarn import WarningsRecorder from pylint import testutils from pylint.testutils import UPDATE_FILE, UPDATE_OPTION @@ -33,34 +32,28 @@ ] TESTS_NAMES = [t.base for t in TESTS] TEST_WITH_EXPECTED_DEPRECATION = [ + "anomalous_backslash_escape", + "anomalous_unicode_escape", + "excess_escapes", "future_unicode_literals", - "anomalous_unicode_escape_py3", ] @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES) -def test_functional( - test_file: FunctionalTestFile, recwarn: WarningsRecorder, pytestconfig: Config -) -> None: +def test_functional(test_file: FunctionalTestFile, pytestconfig: Config) -> None: __tracebackhide__ = True # pylint: disable=unused-variable + lint_test: LintModuleOutputUpdate | testutils.LintModuleTest if UPDATE_FILE.exists(): - lint_test: ( - LintModuleOutputUpdate | testutils.LintModuleTest - ) = LintModuleOutputUpdate(test_file, pytestconfig) + lint_test = LintModuleOutputUpdate(test_file, pytestconfig) else: lint_test = testutils.LintModuleTest(test_file, pytestconfig) lint_test.setUp() - lint_test.runTest() - if recwarn.list: - if ( - test_file.base in TEST_WITH_EXPECTED_DEPRECATION - and sys.version_info.minor > 5 - ): - assert any( - "invalid escape sequence" in str(i.message) - for i in recwarn.list - if issubclass(i.category, DeprecationWarning) - ) + + if test_file.base in TEST_WITH_EXPECTED_DEPRECATION: + with pytest.warns(DeprecationWarning, match="invalid escape sequence"): + lint_test.runTest() + else: + lint_test.runTest() if __name__ == "__main__": diff --git a/tests/test_import_graph.py b/tests/test_import_graph.py index a05ebbd711..2ad51f8899 100644 --- a/tests/test_import_graph.py +++ b/tests/test_import_graph.py @@ -20,7 +20,7 @@ @pytest.fixture -def dest(request: SubRequest) -> Iterator[Iterator | Iterator[str]]: +def dest(request: SubRequest) -> Iterator[str]: dest = request.param yield dest try: @@ -74,7 +74,7 @@ def linter() -> PyLinter: @pytest.fixture -def remove_files() -> Iterator: +def remove_files() -> Iterator[None]: yield for fname in ("import.dot", "ext_import.dot", "int_import.dot"): try: diff --git a/tests/test_numversion.py b/tests/test_numversion.py index 1bfb451da7..2c34c1aa39 100644 --- a/tests/test_numversion.py +++ b/tests/test_numversion.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + import pytest from pylint.__pkginfo__ import get_numversion_from_version @@ -23,5 +25,5 @@ ["2.8.3.dev3+g28c093c2.d20210428", (2, 8, 3)], ], ) -def test_numversion(version, expected_numversion): +def test_numversion(version: str, expected_numversion: tuple[int, int, int]) -> None: assert get_numversion_from_version(version) == expected_numversion diff --git a/tests/test_pylint_runners.py b/tests/test_pylint_runners.py index 9764ddbfdd..6a55db9ebe 100644 --- a/tests/test_pylint_runners.py +++ b/tests/test_pylint_runners.py @@ -5,71 +5,124 @@ from __future__ import annotations +import contextlib import os import pathlib +import shlex import sys -from collections.abc import Callable +from collections.abc import Sequence +from io import BufferedReader +from typing import Any, NoReturn from unittest.mock import MagicMock, mock_open, patch import pytest -from py._path.local import LocalPath # type: ignore[import] from pylint import run_epylint, run_pylint, run_pyreverse, run_symilar -from pylint.lint import Run from pylint.testutils import GenericTestReporter as Reporter +from pylint.testutils._run import _Run as Run +from pylint.testutils.utils import _test_cwd +if sys.version_info >= (3, 8): + from typing import Protocol +else: + from typing_extensions import Protocol -@pytest.mark.parametrize( - "runner", [run_epylint, run_pylint, run_pyreverse, run_symilar] -) -def test_runner(runner: Callable, tmpdir: LocalPath) -> None: + +class _RunCallable(Protocol): # pylint: disable=too-few-public-methods + def __call__(self, argv: Sequence[str] | None = None) -> NoReturn | None: + ... + + +@pytest.mark.parametrize("runner", [run_pylint, run_pyreverse, run_symilar]) +def test_runner(runner: _RunCallable, tmp_path: pathlib.Path) -> None: filepath = os.path.abspath(__file__) testargs = ["", filepath] - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): with patch.object(sys, "argv", testargs): with pytest.raises(SystemExit) as err: runner() assert err.value.code == 0 -@pytest.mark.parametrize( - "runner", [run_epylint, run_pylint, run_pyreverse, run_symilar] -) -def test_runner_with_arguments(runner: Callable, tmpdir: LocalPath) -> None: +def test_epylint(tmp_path: pathlib.Path) -> None: + """TODO: 3.0 delete with epylint.""" + filepath = os.path.abspath(__file__) + with _test_cwd(tmp_path): + with patch.object(sys, "argv", ["", filepath]): + with pytest.raises(SystemExit) as err: + with pytest.warns(DeprecationWarning): + run_epylint() + assert err.value.code == 0 + + +@pytest.mark.parametrize("runner", [run_pylint, run_pyreverse, run_symilar]) +def test_runner_with_arguments(runner: _RunCallable, tmp_path: pathlib.Path) -> None: """Check the runners with arguments as parameter instead of sys.argv.""" filepath = os.path.abspath(__file__) testargs = [filepath] - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): with pytest.raises(SystemExit) as err: runner(testargs) assert err.value.code == 0 +def test_epylint_with_arguments(tmp_path: pathlib.Path) -> None: + """TODO: 3.0 delete with epylint.""" + filepath = os.path.abspath(__file__) + testargs = [filepath] + with _test_cwd(tmp_path): + with pytest.raises(SystemExit) as err: + with pytest.warns(DeprecationWarning): + run_epylint(testargs) + assert err.value.code == 0 + + +def test_pylint_argument_deduplication( + tmp_path: pathlib.Path, tests_directory: pathlib.Path +) -> None: + """Check that the Pylint runner does not over-report on duplicate + arguments. + + See https://github.com/PyCQA/pylint/issues/6242 and + https://github.com/PyCQA/pylint/issues/4053 + """ + filepath = str(tests_directory / "functional/t/too/too_many_branches.py") + testargs = shlex.split("--report n --score n --max-branches 13") + testargs.extend([filepath] * 4) + exit_stack = contextlib.ExitStack() + exit_stack.enter_context(_test_cwd(tmp_path)) + exit_stack.enter_context(patch.object(sys, "argv", testargs)) + err = exit_stack.enter_context(pytest.raises(SystemExit)) + with exit_stack: + run_pylint(testargs) + assert err.value.code == 0 + + def test_pylint_run_jobs_equal_zero_dont_crash_with_cpu_fraction( - tmpdir: LocalPath, + tmp_path: pathlib.Path, ) -> None: """Check that the pylint runner does not crash if `pylint.lint.run._query_cpu` determines only a fraction of a CPU core to be available. """ builtin_open = open - def _mock_open(*args, **kwargs): + def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader: if args[0] == "/sys/fs/cgroup/cpu/cpu.cfs_quota_us": - return mock_open(read_data=b"-1")(*args, **kwargs) + return mock_open(read_data=b"-1")(*args, **kwargs) # type: ignore[no-any-return] if args[0] == "/sys/fs/cgroup/cpu/cpu.shares": - return mock_open(read_data=b"2")(*args, **kwargs) - return builtin_open(*args, **kwargs) + return mock_open(read_data=b"2")(*args, **kwargs) # type: ignore[no-any-return] + return builtin_open(*args, **kwargs) # type: ignore[no-any-return] pathlib_path = pathlib.Path - def _mock_path(*args, **kwargs): + def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path: if args[0] == "/sys/fs/cgroup/cpu/cpu.shares": return MagicMock(is_file=lambda: True) return pathlib_path(*args, **kwargs) filepath = os.path.abspath(__file__) testargs = [filepath, "--jobs=0"] - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): with pytest.raises(SystemExit) as err: with patch("builtins.open", _mock_open): with patch("pylint.lint.run.Path", _mock_path): diff --git a/tests/test_regr.py b/tests/test_regr.py index 80492ae78e..eb8ad6c5d7 100644 --- a/tests/test_regr.py +++ b/tests/test_regr.py @@ -27,12 +27,12 @@ @pytest.fixture(scope="module") -def reporter(): +def reporter() -> type[testutils.GenericTestReporter]: return testutils.GenericTestReporter @pytest.fixture(scope="module") -def disable(): +def disable() -> list[str]: return ["I"] @@ -48,7 +48,7 @@ def finalize_linter(linter: PyLinter) -> Iterator[PyLinter]: linter.reporter.finalize() -def Equals(expected): +def Equals(expected: str) -> Callable[[str], bool]: return lambda got: got == expected @@ -67,7 +67,7 @@ def Equals(expected): ], ) def test_package( - finalize_linter: PyLinter, file_names: list[str], check: Callable + finalize_linter: PyLinter, file_names: list[str], check: Callable[[str], bool] ) -> None: finalize_linter.check(file_names) finalize_linter.reporter = cast( # Due to fixture @@ -101,7 +101,7 @@ def test_descriptor_crash(fname: str, finalize_linter: PyLinter) -> None: @pytest.fixture -def modify_path() -> Iterator: +def modify_path() -> Iterator[None]: cwd = os.getcwd() sys.path.insert(0, "") yield diff --git a/tests/test_self.py b/tests/test_self.py index 83aa55265c..7dbbcf565e 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -17,7 +17,7 @@ import tempfile import textwrap import warnings -from collections.abc import Generator, Iterator +from collections.abc import Iterator from copy import copy from io import BytesIO, StringIO from os.path import abspath, dirname, join @@ -27,17 +27,21 @@ from unittest.mock import patch import pytest -from py._path.local import LocalPath # type: ignore[import] from pylint import extensions, modify_sys_path from pylint.constants import MAIN_CHECKER_NAME, MSG_TYPES_STATUS from pylint.lint.pylinter import PyLinter from pylint.message import Message -from pylint.reporters import JSONReporter -from pylint.reporters.text import BaseReporter, ColorizedTextReporter, TextReporter +from pylint.reporters import BaseReporter, JSONReporter +from pylint.reporters.text import ColorizedTextReporter, TextReporter from pylint.testutils._run import _add_rcfile_default_pylintrc from pylint.testutils._run import _Run as Run -from pylint.testutils.utils import _patch_streams +from pylint.testutils.utils import ( + _patch_streams, + _test_cwd, + _test_environ_pythonpath, + _test_sys_path, +) from pylint.utils import utils if sys.version_info >= (3, 11): @@ -57,7 +61,7 @@ @contextlib.contextmanager -def _configure_lc_ctype(lc_ctype: str) -> Iterator: +def _configure_lc_ctype(lc_ctype: str) -> Iterator[None]: lc_ctype_env = "LC_CTYPE" original_lctype = os.environ.get(lc_ctype_env) os.environ[lc_ctype_env] = lc_ctype @@ -69,28 +73,10 @@ def _configure_lc_ctype(lc_ctype: str) -> Iterator: os.environ[lc_ctype_env] = original_lctype -@contextlib.contextmanager -def _test_sys_path() -> Generator[None, None, None]: - original_path = sys.path - try: - yield - finally: - sys.path = original_path - - -@contextlib.contextmanager -def _test_cwd() -> Generator[None, None, None]: - original_dir = os.getcwd() - try: - yield - finally: - os.chdir(original_dir) - - class MultiReporter(BaseReporter): def __init__(self, reporters: list[BaseReporter]) -> None: # pylint: disable=super-init-not-called - # We don't call it because there is an attribute "linter" that is set inside the base class + # We don't call it because there is an attribute "linter" that is set inside the base class, # and we have another setter here using yet undefined attribute. # I don't think fixing the init order in a test class used once is worth it. self._reporters = reporters @@ -111,8 +97,8 @@ def _display(self, layout: Section) -> None: def out(self) -> TextIO: # type: ignore[override] return self._reporters[0].out - @property # type: ignore[override] - def linter(self) -> PyLinter: # type: ignore[override] + @property + def linter(self) -> PyLinter: return self._linter @linter.setter @@ -153,7 +139,7 @@ def _run_pylint(args: list[str], out: TextIO, reporter: Any = None) -> int: with warnings.catch_warnings(): warnings.simplefilter("ignore") Run(args, reporter=reporter) - return cm.value.code + return int(cm.value.code) @staticmethod def _clean_paths(output: str) -> str: @@ -161,7 +147,9 @@ def _clean_paths(output: str) -> str: output = re.sub(CLEAN_PATH, "", output, flags=re.MULTILINE) return output.replace("\\", "/") - def _test_output(self, args: list[str], expected_output: str) -> None: + def _test_output( + self, args: list[str], expected_output: str, unexpected_output: str = "" + ) -> None: out = StringIO() args = _add_rcfile_default_pylintrc(args) self._run_pylint(args, out=out) @@ -169,8 +157,11 @@ def _test_output(self, args: list[str], expected_output: str) -> None: expected_output = self._clean_paths(expected_output) assert expected_output.strip() in actual_output.strip() + if unexpected_output: + assert unexpected_output.strip() not in actual_output.strip() + def _test_output_file( - self, args: list[str], filename: LocalPath, expected_output: str + self, args: list[str], filename: Path, expected_output: str ) -> None: """Run Pylint with the ``output`` option set (must be included in the ``args`` passed to this method!) and check the file content afterwards. @@ -305,6 +296,39 @@ def test_wrong_import_position_when_others_disabled(self) -> None: actual_output = actual_output[actual_output.find("\n") :] assert self._clean_paths(expected_output.strip()) == actual_output.strip() + def test_type_annotation_names(self) -> None: + """Test resetting the `_type_annotation_names` list to `[]` when leaving a module. + + An import inside `module_a`, which is used as a type annotation in `module_a`, should not prevent + emitting the `unused-import` message when the same import occurs in `module_b` & is unused. + See: https://github.com/PyCQA/pylint/issues/4150 + """ + module1 = join( + HERE, "regrtest_data", "imported_module_in_typehint", "module_a.py" + ) + + module2 = join( + HERE, "regrtest_data", "imported_module_in_typehint", "module_b.py" + ) + expected_output = textwrap.dedent( + f""" + ************* Module module_b + {module2}:1:0: W0611: Unused import uuid (unused-import) + """ + ) + args = [ + module1, + module2, + "--disable=all", + "--enable=unused-import", + "-rn", + "-sn", + ] + out = StringIO() + self._run_pylint(args, out=out) + actual_output = self._clean_paths(out.getvalue().strip()) + assert self._clean_paths(expected_output.strip()) in actual_output.strip() + def test_import_itself_not_accounted_for_relative_imports(self) -> None: expected = "Your code has been rated at 10.00/10" package = join(HERE, "regrtest_data", "dummy") @@ -485,7 +509,7 @@ def test_getdefaultencoding_crashes_with_lc_ctype_utf8(self) -> None: self._test_output([module, "-E"], expected_output=expected_output) @pytest.mark.skipif(sys.platform == "win32", reason="only occurs on *nix") - def test_parseable_file_path(self): + def test_parseable_file_path(self) -> None: file_name = "test_target.py" fake_path = HERE + os.getcwd() module = join(fake_path, file_name) @@ -511,7 +535,7 @@ def test_parseable_file_path(self): ("mymodule.py", "mymodule", "mymodule.py"), ], ) - def test_stdin(self, input_path, module, expected_path): + def test_stdin(self, input_path: str, module: str, expected_path: str) -> None: expected_output = f"""************* Module {module} {expected_path}:1:0: W0611: Unused import os (unused-import) @@ -530,8 +554,8 @@ def test_stdin_missing_modulename(self) -> None: self._runtest(["--from-stdin"], code=32) @pytest.mark.parametrize("write_bpy_to_disk", [False, True]) - def test_relative_imports(self, write_bpy_to_disk, tmpdir): - a = tmpdir.join("a") + def test_relative_imports(self, write_bpy_to_disk: bool, tmp_path: Path) -> None: + a = tmp_path / "a" b_code = textwrap.dedent( """ @@ -551,12 +575,12 @@ def foobar(arg): ) a.mkdir() - a.join("__init__.py").write("") + (a / "__init__.py").write_text("") if write_bpy_to_disk: - a.join("b.py").write(b_code) - a.join("c.py").write(c_code) + (a / "b.py").write_text(b_code) + (a / "c.py").write_text(c_code) - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): # why don't we start pylint in a sub-process? expected = ( "************* Module a.b\n" @@ -582,12 +606,9 @@ def foobar(arg): expected_output=expected, ) - def test_stdin_syntaxerror(self) -> None: - expected_output = ( - "************* Module a\n" - "a.py:1:4: E0001: invalid syntax (, line 1) (syntax-error)" - ) - + def test_stdin_syntax_error(self) -> None: + expected_output = """************* Module a +a.py:1:4: E0001: Parsing failed: 'invalid syntax (, line 1)' (syntax-error)""" with mock.patch( "pylint.lint.pylinter._read_stdin", return_value="for\n" ) as mock_stdin: @@ -707,14 +728,14 @@ def test_fail_under(self) -> None: (-9, "missing-function-docstring", "fail_under_minus10.py", 22), (-5, "missing-function-docstring", "fail_under_minus10.py", 22), # --fail-under should guide whether error code as missing-function-docstring is not hit - (-10, "broad-except", "fail_under_plus7_5.py", 0), - (6, "broad-except", "fail_under_plus7_5.py", 0), - (7.5, "broad-except", "fail_under_plus7_5.py", 0), - (7.6, "broad-except", "fail_under_plus7_5.py", 16), - (-11, "broad-except", "fail_under_minus10.py", 0), - (-10, "broad-except", "fail_under_minus10.py", 0), - (-9, "broad-except", "fail_under_minus10.py", 22), - (-5, "broad-except", "fail_under_minus10.py", 22), + (-10, "broad-exception-caught", "fail_under_plus7_5.py", 0), + (6, "broad-exception-caught", "fail_under_plus7_5.py", 0), + (7.5, "broad-exception-caught", "fail_under_plus7_5.py", 0), + (7.6, "broad-exception-caught", "fail_under_plus7_5.py", 16), + (-11, "broad-exception-caught", "fail_under_minus10.py", 0), + (-10, "broad-exception-caught", "fail_under_minus10.py", 0), + (-9, "broad-exception-caught", "fail_under_minus10.py", 22), + (-5, "broad-exception-caught", "fail_under_minus10.py", 22), # Enable by message id (-10, "C0116", "fail_under_plus7_5.py", 16), # Enable by category @@ -724,7 +745,7 @@ def test_fail_under(self) -> None: (-10, "C0115", "fail_under_plus7_5.py", 0), ], ) - def test_fail_on(self, fu_score, fo_msgs, fname, out): + def test_fail_on(self, fu_score: int, fo_msgs: str, fname: str, out: int) -> None: self._runtest( [ "--fail-under", @@ -752,7 +773,7 @@ def test_fail_on(self, fu_score, fo_msgs, fname, out): (["--fail-on=C0116", "--disable=C0116"], 16), ], ) - def test_fail_on_edge_case(self, opts, out): + def test_fail_on_edge_case(self, opts: list[str], out: int) -> None: self._runtest( opts + [join(HERE, "regrtest_data", "fail_under_plus7_5.py")], code=out, @@ -760,281 +781,187 @@ def test_fail_on_edge_case(self, opts, out): @staticmethod def test_modify_sys_path() -> None: - @contextlib.contextmanager - def test_environ_pythonpath( - new_pythonpath: str | None, - ) -> Generator[None, None, None]: - original_pythonpath = os.environ.get("PYTHONPATH") - if new_pythonpath: - os.environ["PYTHONPATH"] = new_pythonpath - elif new_pythonpath is None and original_pythonpath is not None: - # If new_pythonpath is None, make sure to delete PYTHONPATH if present - del os.environ["PYTHONPATH"] - try: - yield - finally: - if original_pythonpath: - os.environ["PYTHONPATH"] = original_pythonpath - elif new_pythonpath is not None: - # Only delete PYTHONPATH if new_pythonpath wasn't None - del os.environ["PYTHONPATH"] - + # pylint: disable = too-many-statements + cwd = "/tmp/pytest-of-root/pytest-0/test_do_not_import_files_from_0" + default_paths = [ + "/usr/local/lib/python39.zip", + "/usr/local/lib/python3.9", + "/usr/local/lib/python3.9/lib-dynload", + "/usr/local/lib/python3.9/site-packages", + ] with _test_sys_path(), patch("os.getcwd") as mock_getcwd: - cwd = "/tmp/pytest-of-root/pytest-0/test_do_not_import_files_from_0" mock_getcwd.return_value = cwd - default_paths = [ - "/usr/local/lib/python39.zip", - "/usr/local/lib/python3.9", - "/usr/local/lib/python3.9/lib-dynload", - "/usr/local/lib/python3.9/site-packages", - ] + paths = [cwd, *default_paths] + sys.path = copy(paths) + with _test_environ_pythonpath(): + modify_sys_path() + assert sys.path == paths[1:] - paths = [ - cwd, - *default_paths, - ] + paths = ["", *default_paths] sys.path = copy(paths) - with test_environ_pythonpath(None): + with _test_environ_pythonpath(): modify_sys_path() assert sys.path == paths[1:] - paths = [ - cwd, - cwd, - *default_paths, - ] + paths = [".", *default_paths] sys.path = copy(paths) - with test_environ_pythonpath("."): + with _test_environ_pythonpath(): modify_sys_path() assert sys.path == paths[1:] - paths = [ - cwd, - "/custom_pythonpath", - *default_paths, - ] + paths = ["/do_not_remove", *default_paths] + sys.path = copy(paths) + with _test_environ_pythonpath(): + modify_sys_path() + assert sys.path == paths + + paths = [cwd, cwd, *default_paths] sys.path = copy(paths) - with test_environ_pythonpath("/custom_pythonpath"): + with _test_environ_pythonpath("."): modify_sys_path() assert sys.path == paths[1:] - paths = [ - cwd, - "/custom_pythonpath", - cwd, - *default_paths, - ] + paths = [cwd, "/custom_pythonpath", *default_paths] sys.path = copy(paths) - with test_environ_pythonpath("/custom_pythonpath:"): + with _test_environ_pythonpath("/custom_pythonpath"): + modify_sys_path() + assert sys.path == paths[1:] + + paths = [cwd, "/custom_pythonpath", cwd, *default_paths] + sys.path = copy(paths) + with _test_environ_pythonpath("/custom_pythonpath:"): modify_sys_path() assert sys.path == [paths[1]] + paths[3:] - paths = [ - "", - cwd, - "/custom_pythonpath", - *default_paths, - ] + paths = ["", cwd, "/custom_pythonpath", *default_paths] sys.path = copy(paths) - with test_environ_pythonpath(":/custom_pythonpath"): + with _test_environ_pythonpath(":/custom_pythonpath"): modify_sys_path() assert sys.path == paths[2:] - paths = [ - cwd, - cwd, - "/custom_pythonpath", - *default_paths, - ] + paths = [cwd, cwd, "/custom_pythonpath", *default_paths] sys.path = copy(paths) - with test_environ_pythonpath(":/custom_pythonpath:"): + with _test_environ_pythonpath(":/custom_pythonpath:"): modify_sys_path() assert sys.path == paths[2:] - paths = [ - cwd, - cwd, - *default_paths, - ] + paths = [cwd, cwd, *default_paths] sys.path = copy(paths) - with test_environ_pythonpath(":."): + with _test_environ_pythonpath(":."): modify_sys_path() assert sys.path == paths[1:] sys.path = copy(paths) - with test_environ_pythonpath(f":{cwd}"): + with _test_environ_pythonpath(f":{cwd}"): modify_sys_path() assert sys.path == paths[1:] sys.path = copy(paths) - with test_environ_pythonpath(".:"): + with _test_environ_pythonpath(".:"): modify_sys_path() assert sys.path == paths[1:] sys.path = copy(paths) - with test_environ_pythonpath(f"{cwd}:"): + with _test_environ_pythonpath(f"{cwd}:"): modify_sys_path() assert sys.path == paths[1:] - paths = [ - "", - cwd, - *default_paths, - cwd, - ] + paths = ["", cwd, *default_paths, cwd] sys.path = copy(paths) - with test_environ_pythonpath(cwd): + with _test_environ_pythonpath(cwd): modify_sys_path() assert sys.path == paths[1:] @staticmethod - def test_do_not_import_files_from_local_directory(tmpdir: LocalPath) -> None: - p_astroid = tmpdir / "astroid.py" - p_astroid.write("'Docstring'\nimport completely_unknown\n") - p_hmac = tmpdir / "hmac.py" - p_hmac.write("'Docstring'\nimport completely_unknown\n") - - with tmpdir.as_cwd(): - subprocess.check_output( - [ - sys.executable, - "-m", - "pylint", - "astroid.py", - "--disable=import-error,unused-import", - ], - cwd=str(tmpdir), - ) - - # Linting this astroid file does not import it - with tmpdir.as_cwd(): - subprocess.check_output( - [ - sys.executable, - "-m", - "pylint", - "-j2", - "astroid.py", - "--disable=import-error,unused-import", - ], - cwd=str(tmpdir), - ) - - # Test with multiple jobs for hmac.py for which we have a - # CVE against: https://github.com/PyCQA/pylint/issues/959 - with tmpdir.as_cwd(): - subprocess.call( - [ - sys.executable, - "-m", - "pylint", - "-j2", - "hmac.py", - "--disable=import-error,unused-import", - ], - cwd=str(tmpdir), - ) + 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.""" + with _test_sys_path(): + # Enable --load-plugins=importing_plugin + sys.path.append(join(HERE, "regrtest_data", "importing_plugin")) + with _test_cwd(join(HERE, "regrtest_data", "settings_project")): + Run( + ["--load-plugins=importing_plugin", "models.py"], + exit=False, + ) - @staticmethod - def test_do_not_import_files_from_local_directory_with_pythonpath( - tmpdir: LocalPath, + @pytest.mark.parametrize( + "args", + [ + ["--disable=import-error,unused-import"], + # Test with multiple jobs for 'hmac.py' for which we have a + # CVE against: https://github.com/PyCQA/pylint/issues/959 + ["-j2", "--disable=import-error,unused-import"], + ], + ) + def test_do_not_import_files_from_local_directory( + self, tmp_path: Path, args: list[str] ) -> None: - p_astroid = tmpdir / "astroid.py" - p_astroid.write("'Docstring'\nimport completely_unknown\n") - p_hmac = tmpdir / "hmac.py" - p_hmac.write("'Docstring'\nimport completely_unknown\n") - - # Appending a colon to PYTHONPATH should not break path stripping - # https://github.com/PyCQA/pylint/issues/3636 - with tmpdir.as_cwd(): - orig_pythonpath = os.environ.get("PYTHONPATH") - os.environ["PYTHONPATH"] = f"{(orig_pythonpath or '').strip(':')}:" - subprocess.check_output( - [ - sys.executable, - "-m", - "pylint", - "astroid.py", - "--disable=import-error,unused-import", - ], - cwd=str(tmpdir), - ) - if orig_pythonpath: - os.environ["PYTHONPATH"] = orig_pythonpath - else: - del os.environ["PYTHONPATH"] + for path in ("astroid.py", "hmac.py"): + file_path = tmp_path / path + file_path.write_text("'Docstring'\nimport completely_unknown\n") + pylint_call = [sys.executable, "-m", "pylint"] + args + [path] + with _test_cwd(tmp_path): + subprocess.check_output(pylint_call, cwd=str(tmp_path)) + new_python_path = os.environ.get("PYTHONPATH", "").strip(":") + with _test_cwd(tmp_path), _test_environ_pythonpath(f"{new_python_path}:"): + # Appending a colon to PYTHONPATH should not break path stripping + # https://github.com/PyCQA/pylint/issues/3636 + subprocess.check_output(pylint_call, cwd=str(tmp_path)) @staticmethod def test_import_plugin_from_local_directory_if_pythonpath_cwd( - tmpdir: LocalPath, + tmp_path: Path, ) -> None: - p_plugin = tmpdir / "plugin.py" - p_plugin.write("# Some plugin content") - - with tmpdir.as_cwd(): - orig_pythonpath = os.environ.get("PYTHONPATH") - if sys.platform == "win32": - os.environ["PYTHONPATH"] = "." - else: - os.environ["PYTHONPATH"] = f"{(orig_pythonpath or '').strip(':')}:." + p_plugin = tmp_path / "plugin.py" + p_plugin.write_text("# Some plugin content") + if sys.platform == "win32": + python_path = "." + else: + python_path = f"{os.environ.get('PYTHONPATH', '').strip(':')}:." + with _test_cwd(tmp_path), _test_environ_pythonpath(python_path): + args = [sys.executable, "-m", "pylint", "--load-plugins", "plugin"] process = subprocess.run( - [ - sys.executable, - "-m", - "pylint", - "--load-plugins", - "plugin", - ], - cwd=str(tmpdir), - stderr=subprocess.PIPE, - check=False, + args, cwd=str(tmp_path), stderr=subprocess.PIPE, check=False ) assert ( "AttributeError: module 'plugin' has no attribute 'register'" in process.stderr.decode() ) - if orig_pythonpath: - os.environ["PYTHONPATH"] = orig_pythonpath - else: - del os.environ["PYTHONPATH"] def test_allow_import_of_files_found_in_modules_during_parallel_check( - self, tmpdir: LocalPath + self, tmp_path: Path ) -> None: - test_directory = tmpdir / "test_directory" + test_directory = tmp_path / "test_directory" test_directory.mkdir() spam_module = test_directory / "spam.py" - spam_module.write("'Empty'") + spam_module.write_text("'Empty'") init_module = test_directory / "__init__.py" - init_module.write("'Empty'") + init_module.write_text("'Empty'") # For multiple jobs we could not find the `spam.py` file. - with tmpdir.as_cwd(): - self._runtest( - [ - "-j2", - "--disable=missing-docstring, missing-final-newline", - "test_directory", - ], - code=0, - ) + with _test_cwd(tmp_path): + args = [ + "-j2", + "--disable=missing-docstring, missing-final-newline", + "test_directory", + ] + self._runtest(args, code=0) # A single job should be fine as well - with tmpdir.as_cwd(): - self._runtest( - [ - "-j1", - "--disable=missing-docstring, missing-final-newline", - "test_directory", - ], - code=0, - ) + with _test_cwd(tmp_path): + args = [ + "-j1", + "--disable=missing-docstring, missing-final-newline", + "test_directory", + ] + self._runtest(args, code=0) @staticmethod - def test_can_list_directories_without_dunder_init(tmpdir: LocalPath) -> None: - test_directory = tmpdir / "test_directory" + def test_can_list_directories_without_dunder_init(tmp_path: Path) -> None: + test_directory = tmp_path / "test_directory" test_directory.mkdir() spam_module = test_directory / "spam.py" - spam_module.write("'Empty'") + spam_module.write_text("'Empty'") subprocess.check_output( [ @@ -1044,7 +971,7 @@ def test_can_list_directories_without_dunder_init(tmpdir: LocalPath) -> None: "--disable=missing-docstring, missing-final-newline", "test_directory", ], - cwd=str(tmpdir), + cwd=str(tmp_path), stderr=subprocess.PIPE, ) @@ -1060,11 +987,11 @@ def test_regression_parallel_mode_without_filepath(self) -> None: path = join( HERE, "regrtest_data", "regression_missing_init_3564", "subdirectory/" ) - self._test_output([path, "-j2"], expected_output="No such file or directory") + self._test_output([path, "-j2"], expected_output="") - def test_output_file_valid_path(self, tmpdir: LocalPath) -> None: + def test_output_file_valid_path(self, tmp_path: Path) -> None: path = join(HERE, "regrtest_data", "unused_variable.py") - output_file = tmpdir / "output.txt" + output_file = tmp_path / "output.txt" expected = "Your code has been rated at 7.50/10" self._test_output_file( [path, f"--output={output_file}"], @@ -1091,13 +1018,13 @@ def test_output_file_invalid_path_exits_with_code_32(self) -> None: (["--fail-on=useless-suppression", "--enable=C"], 22), ], ) - def test_fail_on_exit_code(self, args, expected): + def test_fail_on_exit_code(self, args: list[str], expected: int) -> None: path = join(HERE, "regrtest_data", "fail_on.py") # We set fail-under to be something very low so that even with the warnings # and errors that are generated they don't affect the exit code. self._runtest([path, "--fail-under=-10", "--disable=C"] + args, code=expected) - def test_one_module_fatal_error(self): + def test_one_module_fatal_error(self) -> None: """Fatal errors in one of several modules linted still exits non-zero.""" valid_path = join(HERE, "conftest.py") invalid_path = join(HERE, "garbagePath.py") @@ -1117,7 +1044,7 @@ def test_one_module_fatal_error(self): (["--fail-on=useless-suppression", "--enable=C"], 1), ], ) - def test_fail_on_info_only_exit_code(self, args, expected): + def test_fail_on_info_only_exit_code(self, args: list[str], expected: int) -> None: path = join(HERE, "regrtest_data", "fail_on_info_only.py") self._runtest([path] + args, code=expected) @@ -1126,39 +1053,41 @@ def test_fail_on_info_only_exit_code(self, args, expected): [ ( "text", - "tests/regrtest_data/unused_variable.py:4:4: W0612: Unused variable 'variable' (unused-variable)", + "{path}:4:4: W0612: Unused variable 'variable' (unused-variable)", ), ( "parseable", - "tests/regrtest_data/unused_variable.py:4: [W0612(unused-variable), test] Unused variable 'variable'", + "{path}:4: [W0612(unused-variable), test] Unused variable 'variable'", ), ( "msvs", - "tests/regrtest_data/unused_variable.py(4): [W0612(unused-variable)test] Unused variable 'variable'", + "{path}(4): [W0612(unused-variable)test] Unused variable 'variable'", ), ( "colorized", - "tests/regrtest_data/unused_variable.py:4:4: W0612: \x1B[35mUnused variable 'variable'\x1B[0m (\x1B[35munused-variable\x1B[0m)", + ( + "{path}:4:4: W0612: \x1B[35mUnused variable 'variable'\x1B[0m (\x1B[35munused-variable\x1B[0m)" + ), ), ("json", '"message": "Unused variable \'variable\'",'), ], ) def test_output_file_can_be_combined_with_output_format_option( - self, tmpdir, output_format, expected_output - ): + self, tmp_path: Path, output_format: str, expected_output: str + ) -> None: path = join(HERE, "regrtest_data", "unused_variable.py") - output_file = tmpdir / "output.txt" + output_file = tmp_path / "output.txt" self._test_output_file( [path, f"--output={output_file}", f"--output-format={output_format}"], output_file, - expected_output, + expected_output.format(path="tests/regrtest_data/unused_variable.py"), ) def test_output_file_can_be_combined_with_custom_reporter( - self, tmpdir: LocalPath + self, tmp_path: Path ) -> None: path = join(HERE, "regrtest_data", "unused_variable.py") - output_file = tmpdir / "output.txt" + output_file = tmp_path / "output.txt" # It does not really have to be a truly custom reporter. # It is only important that it is being passed explicitly to ``Run``. myreporter = TextReporter() @@ -1169,9 +1098,9 @@ def test_output_file_can_be_combined_with_custom_reporter( ) assert output_file.exists() - def test_output_file_specified_in_rcfile(self, tmpdir: LocalPath) -> None: - output_file = tmpdir / "output.txt" - rcfile = tmpdir / "pylintrc" + def test_output_file_specified_in_rcfile(self, tmp_path: Path) -> None: + output_file = tmp_path / "output.txt" + rcfile = tmp_path / "pylintrc" rcfile_contents = textwrap.dedent( f""" [MAIN] @@ -1215,106 +1144,70 @@ def test_max_inferred_for_complicated_class_hierarchy() -> None: the standard max_inferred of 100. We used to crash when this happened. """ with pytest.raises(SystemExit) as ex: - Run( - [ - join( - HERE, - "regrtest_data", - "max_inferable_limit_for_classes", - "main.py", - ), - ] + path = join( + HERE, "regrtest_data", "max_inferable_limit_for_classes", "main.py" ) + Run([path]) # Error code should not include bit-value 1 for crash assert not ex.value.code % 2 - def test_regression_recursive(self): - """Tests if error is raised when linter is executed over directory not using --recursive=y""" - self._test_output( - [join(HERE, "regrtest_data", "directory", "subdirectory"), "--recursive=n"], - expected_output="No such file or directory", - ) - - def test_recursive(self): + def test_recursive(self) -> None: """Tests if running linter over directory using --recursive=y""" self._runtest( [join(HERE, "regrtest_data", "directory", "subdirectory"), "--recursive=y"], code=0, ) - def test_ignore_recursive(self): + @pytest.mark.parametrize("ignore_value", ["ignored_subdirectory", "failing.py"]) + def test_ignore_recursive(self, ignore_value: str) -> None: """Tests recursive run of linter ignoring directory using --ignore parameter. Ignored directory contains files yielding lint errors. If directory is not ignored test would fail due these errors. """ - self._runtest( - [ - join(HERE, "regrtest_data", "directory"), - "--recursive=y", - "--ignore=ignored_subdirectory", - ], - code=0, - ) - - self._runtest( - [ - join(HERE, "regrtest_data", "directory"), - "--recursive=y", - "--ignore=failing.py", - ], - code=0, - ) + directory = join(HERE, "regrtest_data", "directory") + self._runtest([directory, "--recursive=y", f"--ignore={ignore_value}"], code=0) - def test_ignore_pattern_recursive(self): + @pytest.mark.parametrize("ignore_pattern_value", ["ignored_.*", "failing.*"]) + def test_ignore_pattern_recursive(self, ignore_pattern_value: str) -> None: """Tests recursive run of linter ignoring directory using --ignore-parameter parameter. Ignored directory contains files yielding lint errors. If directory is not ignored test would fail due these errors. """ + directory = join(HERE, "regrtest_data", "directory") self._runtest( - [ - join(HERE, "regrtest_data", "directory"), - "--recursive=y", - "--ignore-patterns=ignored_.*", - ], + [directory, "--recursive=y", f"--ignore-patterns={ignore_pattern_value}"], code=0, ) - self._runtest( - [ - join(HERE, "regrtest_data", "directory"), - "--recursive=y", - "--ignore-patterns=failing.*", - ], - code=0, - ) + 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"): + self._runtest( + [ + "--from-stdin", + "mymodule.py", + "--disable=all", + "--enable=unused-import", + "--ignore-patterns=mymodule.py", + ], + code=0, + ) - def test_ignore_path_recursive(self): + @pytest.mark.parametrize("ignore_path_value", [".*ignored.*", ".*failing.*"]) + def test_ignore_path_recursive(self, ignore_path_value: str) -> None: """Tests recursive run of linter ignoring directory using --ignore-path parameter. Ignored directory contains files yielding lint errors. If directory is not ignored test would fail due these errors. """ + directory = join(HERE, "regrtest_data", "directory") self._runtest( - [ - join(HERE, "regrtest_data", "directory"), - "--recursive=y", - "--ignore-paths=.*ignored.*", - ], - code=0, - ) - - self._runtest( - [ - join(HERE, "regrtest_data", "directory"), - "--recursive=y", - "--ignore-paths=.*failing.*", - ], - code=0, + [directory, "--recursive=y", f"--ignore-paths={ignore_path_value}"], code=0 ) - def test_recursive_current_dir(self): + def test_recursive_current_dir(self) -> None: with _test_sys_path(): # pytest is including directory HERE/regrtest_data to sys.path which causes # astroid to believe that directory is a package. @@ -1351,21 +1244,63 @@ def test_ignore_path_recursive_current_dir(self) -> None: code=0, ) - def test_regression_recursive_current_dir(self): - with _test_sys_path(): - # pytest is including directory HERE/regrtest_data to sys.path which causes - # astroid to believe that directory is a package. - sys.path = [ - path - for path in sys.path - if not os.path.basename(path) == "regrtest_data" - ] - with _test_cwd(): - os.chdir(join(HERE, "regrtest_data", "directory")) - self._test_output( - ["."], - expected_output="No such file or directory", - ) + def test_syntax_error_invalid_encoding(self) -> None: + module = join(HERE, "regrtest_data", "invalid_encoding.py") + expected_output = "unknown encoding" + self._test_output([module, "-E"], expected_output=expected_output) + + @pytest.mark.parametrize( + "module_name,expected_output", + [ + ("good.py", ""), + ("bad_wrong_num.py", "(syntax-error)"), + ("bad_missing_num.py", "(bad-file-encoding)"), + ], + ) + def test_encoding(self, module_name: str, expected_output: str) -> None: + path = join(HERE, "regrtest_data", "encoding", module_name) + self._test_output( + [path], expected_output=expected_output, unexpected_output="(astroid-error)" + ) + + def test_line_too_long_useless_suppression(self) -> None: + """A test that demonstrates a known false positive for useless-suppression + + See https://github.com/PyCQA/pylint/issues/3368 + + If you manage to make this test fail and remove the useless-suppression + warning please contact open a Pylint PR! + """ + module = join(HERE, "regrtest_data", "line_too_long_no_code.py") + expected = textwrap.dedent( + f""" + {module}:1:0: I0011: Locally disabling line-too-long (C0301) (locally-disabled) + {module}:1:0: I0021: Useless suppression of 'line-too-long' (useless-suppression) + """ + ) + + self._test_output([module, "--enable=all"], expected_output=expected) + + def test_output_no_header(self) -> None: + module = join(HERE, "data", "clientmodule_test.py") + expected = "Unused variable 'local_variable'" + not_expected = textwrap.dedent( + """************* Module data.clientmodule_test""" + ) + + args = [module, "--output-format=no-header"] + self._test_output( + args, expected_output=expected, unexpected_output=not_expected + ) + + 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.""" + 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( + [module, "-E"], expected_output="", unexpected_output=unexpected + ) class TestCallbackOptions: @@ -1384,7 +1319,9 @@ class TestCallbackOptions: (["--long-help"], "Environment variables:"), ], ) - def test_output_of_callback_options(command: list[str], expected: str) -> None: + def test_output_of_callback_options( + command: list[str], expected: str, tmp_path: Path + ) -> None: """Test whether certain strings are in the output of a callback command.""" command = _add_rcfile_default_pylintrc(command) process = subprocess.run( @@ -1392,6 +1329,7 @@ def test_output_of_callback_options(command: list[str], expected: str) -> None: capture_output=True, encoding="utf-8", check=False, + cwd=str(tmp_path), ) assert expected in process.stdout @@ -1402,9 +1340,12 @@ def test_output_of_callback_options(command: list[str], expected: str) -> None: [["--help-msg", "W0101"], ":unreachable (W0101)", False], [["--help-msg", "WX101"], "No such message id", False], [["--help-msg"], "--help-msg: expected at least one argumen", True], + [["--help-msg", "C0102,C0103"], ":invalid-name (C0103):", False], ], ) - def test_help_msg(args: list[str], expected: str, error: bool) -> None: + def test_help_msg( + args: list[str], expected: str, error: bool, tmp_path: Path + ) -> None: """Test the --help-msg flag.""" args = _add_rcfile_default_pylintrc(args) process = subprocess.run( @@ -1412,6 +1353,7 @@ def test_help_msg(args: list[str], expected: str, error: bool) -> None: capture_output=True, encoding="utf-8", check=False, + cwd=str(tmp_path), ) if error: result = process.stderr @@ -1420,7 +1362,7 @@ def test_help_msg(args: list[str], expected: str, error: bool) -> None: assert expected in result @staticmethod - def test_generate_rcfile() -> None: + def test_generate_rcfile(tmp_path: Path) -> None: """Test the --generate-rcfile flag.""" args = _add_rcfile_default_pylintrc(["--generate-rcfile"]) process = subprocess.run( @@ -1428,6 +1370,7 @@ def test_generate_rcfile() -> None: capture_output=True, encoding="utf-8", check=False, + cwd=str(tmp_path), ) assert "[MAIN]" in process.stdout assert "[MASTER]" not in process.stdout @@ -1438,6 +1381,7 @@ def test_generate_rcfile() -> None: capture_output=True, encoding="utf-8", check=False, + cwd=str(tmp_path), ) assert process.stdout == process_two.stdout @@ -1476,7 +1420,7 @@ def test_generate_config_disable_symbolic_names() -> None: assert "suppressed-message" in messages @staticmethod - def test_generate_toml_config() -> None: + def test_generate_toml_config(tmp_path: Path) -> None: """Test the --generate-toml-config flag.""" args = _add_rcfile_default_pylintrc( [ @@ -1489,6 +1433,7 @@ def test_generate_toml_config() -> None: capture_output=True, encoding="utf-8", check=False, + cwd=str(tmp_path), ) assert "[tool.pylint.main]" in process.stdout assert "[tool.pylint.master]" not in process.stdout @@ -1501,6 +1446,7 @@ def test_generate_toml_config() -> None: capture_output=True, encoding="utf-8", check=False, + cwd=str(tmp_path), ) assert process.stdout == process_two.stdout @@ -1546,7 +1492,7 @@ def test_errors_only_functions_as_disable() -> None: it no longer enables any messages.""" run = Run( [str(UNNECESSARY_LAMBDA), "--disable=import-error", "--errors-only"], - do_exit=False, + exit=False, ) assert not run.linter.is_message_enabled("import-error") diff --git a/tests/test_similar.py b/tests/test_similar.py index d59602ff6f..5558b70e73 100644 --- a/tests/test_similar.py +++ b/tests/test_similar.py @@ -36,7 +36,7 @@ def _runtest(self, args: list[str], code: int) -> None: @staticmethod def _run_pylint(args: list[str], out: TextIO) -> int: """Runs pylint with a patched output.""" - args = args + [ + args += [ "--persistent=no", "--enable=astroid-error", # Enable functionality that will build another ast @@ -48,7 +48,7 @@ def _run_pylint(args: list[str], out: TextIO) -> int: with warnings.catch_warnings(): warnings.simplefilter("ignore") Run(args) - return cm.value.code + return int(cm.value.code) @staticmethod def _clean_paths(output: str) -> str: diff --git a/tests/testutils/_primer/fixtures/both_empty/expected.txt b/tests/testutils/_primer/fixtures/both_empty/expected.txt new file mode 100644 index 0000000000..c8b7173956 --- /dev/null +++ b/tests/testutils/_primer/fixtures/both_empty/expected.txt @@ -0,0 +1,3 @@ +🤖 According to the primer, this change has **no effect** on the checked open source code. 🤖🎉 + +*This comment was generated for commit v2.14.2* diff --git a/tests/testutils/_primer/fixtures/both_empty/main.json b/tests/testutils/_primer/fixtures/both_empty/main.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/testutils/_primer/fixtures/both_empty/main.json @@ -0,0 +1 @@ +{} diff --git a/tests/testutils/_primer/fixtures/both_empty/pr.json b/tests/testutils/_primer/fixtures/both_empty/pr.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/testutils/_primer/fixtures/both_empty/pr.json @@ -0,0 +1 @@ +{} diff --git a/tests/testutils/_primer/fixtures/message_changed/expected.txt b/tests/testutils/_primer/fixtures/message_changed/expected.txt new file mode 100644 index 0000000000..9640bed9e3 --- /dev/null +++ b/tests/testutils/_primer/fixtures/message_changed/expected.txt @@ -0,0 +1,26 @@ +🤖 **Effect of this PR on checked open source code:** 🤖 + + + +**Effect on [astroid](https://github.com/PyCQA/astroid):** +The following messages are now emitted: + +
+ +1) locally-disabled: +*Locally disabling redefined-builtin [we added some text in the message] (W0622)* +https://github.com/PyCQA/astroid/blob/123456789abcdef/astroid/__init__.py#L91 + +
+ +The following messages are no longer emitted: + +
+ +1) locally-disabled: +*Locally disabling redefined-builtin (W0622)* +https://github.com/PyCQA/astroid/blob/123456789abcdef/astroid/__init__.py#L91 + +
+ +*This comment was generated for commit v2.14.2* diff --git a/tests/testutils/_primer/fixtures/message_changed/expected_truncated.txt b/tests/testutils/_primer/fixtures/message_changed/expected_truncated.txt new file mode 100644 index 0000000000..3912ea15bc --- /dev/null +++ b/tests/testutils/_primer/fixtures/message_changed/expected_truncated.txt @@ -0,0 +1,20 @@ +🤖 **Effect of this PR on checked open source code:** 🤖 + + + +**Effect on [astroid](https://github.com/PyCQA/astroid):** +The following messages are now emitted: + +
+ +1) locally-disabled: +*Locally disabling redefined-builtin [we added some text in the message] (W0622)* +https://github.com/PyCQA/astroid/blob/123456789abcdef/astroid/__init__.py#L91 + +
+ +The following message... + +*This comment was truncated because GitHub allows only 525 characters in a comment.* + +*This comment was generated for commit v2.14.2* diff --git a/tests/testutils/_primer/fixtures/message_changed/main.json b/tests/testutils/_primer/fixtures/message_changed/main.json new file mode 100644 index 0000000000..96f0aaaef7 --- /dev/null +++ b/tests/testutils/_primer/fixtures/message_changed/main.json @@ -0,0 +1,33 @@ +{ + "astroid": { + "commit": "123456789abcdef", + "messages": [ + { + "type": "info", + "module": "astroid", + "obj": "", + "line": 91, + "column": 0, + "endLine": null, + "endColumn": null, + "path": "tests/.pylint_primer_tests/PyCQA/astroid/astroid/__init__.py", + "symbol": "locally-disabled", + "message": "Locally disabling redefined-builtin (W0622)", + "message-id": "I0011" + }, + { + "type": "warning", + "module": "astroid", + "obj": "", + "line": 91, + "column": 0, + "endLine": null, + "endColumn": null, + "path": "tests/.pylint_primer_tests/PyCQA/astroid/astroid/__init__.py", + "symbol": "unknown-option-value", + "message": "Unknown option value for 'disable', expected a valid pylint message and got 'Ellipsis'", + "message-id": "W0012" + } + ] + } +} diff --git a/tests/testutils/_primer/fixtures/message_changed/pr.json b/tests/testutils/_primer/fixtures/message_changed/pr.json new file mode 100644 index 0000000000..9e2eda37b8 --- /dev/null +++ b/tests/testutils/_primer/fixtures/message_changed/pr.json @@ -0,0 +1,33 @@ +{ + "astroid": { + "commit": "123456789abcdef", + "messages": [ + { + "type": "info", + "module": "astroid", + "obj": "", + "line": 91, + "column": 0, + "endLine": null, + "endColumn": null, + "path": "tests/.pylint_primer_tests/PyCQA/astroid/astroid/__init__.py", + "symbol": "locally-disabled", + "message": "Locally disabling redefined-builtin [we added some text in the message] (W0622)", + "message-id": "I0011" + }, + { + "type": "warning", + "module": "astroid", + "obj": "", + "line": 91, + "column": 0, + "endLine": null, + "endColumn": null, + "path": "tests/.pylint_primer_tests/PyCQA/astroid/astroid/__init__.py", + "symbol": "unknown-option-value", + "message": "Unknown option value for 'disable', expected a valid pylint message and got 'Ellipsis'", + "message-id": "W0012" + } + ] + } +} diff --git a/tests/testutils/_primer/fixtures/no_change/expected.txt b/tests/testutils/_primer/fixtures/no_change/expected.txt new file mode 100644 index 0000000000..c8b7173956 --- /dev/null +++ b/tests/testutils/_primer/fixtures/no_change/expected.txt @@ -0,0 +1,3 @@ +🤖 According to the primer, this change has **no effect** on the checked open source code. 🤖🎉 + +*This comment was generated for commit v2.14.2* diff --git a/tests/testutils/_primer/fixtures/no_change/main.json b/tests/testutils/_primer/fixtures/no_change/main.json new file mode 100644 index 0000000000..cd361e6188 --- /dev/null +++ b/tests/testutils/_primer/fixtures/no_change/main.json @@ -0,0 +1,33 @@ +{ + "astroid": { + "commit": "1234567890abcdef", + "messages": [ + { + "type": "info", + "module": "astroid", + "obj": "", + "line": 91, + "column": 0, + "endLine": null, + "endColumn": null, + "path": "tests/.pylint_primer_tests/PyCQA/astroid/astroid/__init__.py", + "symbol": "locally-disabled", + "message": "Locally disabling redefined-builtin (W0622)", + "message-id": "I0011" + }, + { + "type": "warning", + "module": "astroid", + "obj": "", + "line": 91, + "column": 0, + "endLine": null, + "endColumn": null, + "path": "tests/.pylint_primer_tests/PyCQA/astroid/astroid/__init__.py", + "symbol": "unknown-option-value", + "message": "Unknown option value for 'disable', expected a valid pylint message and got 'Ellipsis'", + "message-id": "W0012" + } + ] + } +} diff --git a/tests/testutils/_primer/fixtures/no_change/pr.json b/tests/testutils/_primer/fixtures/no_change/pr.json new file mode 100644 index 0000000000..96f0aaaef7 --- /dev/null +++ b/tests/testutils/_primer/fixtures/no_change/pr.json @@ -0,0 +1,33 @@ +{ + "astroid": { + "commit": "123456789abcdef", + "messages": [ + { + "type": "info", + "module": "astroid", + "obj": "", + "line": 91, + "column": 0, + "endLine": null, + "endColumn": null, + "path": "tests/.pylint_primer_tests/PyCQA/astroid/astroid/__init__.py", + "symbol": "locally-disabled", + "message": "Locally disabling redefined-builtin (W0622)", + "message-id": "I0011" + }, + { + "type": "warning", + "module": "astroid", + "obj": "", + "line": 91, + "column": 0, + "endLine": null, + "endColumn": null, + "path": "tests/.pylint_primer_tests/PyCQA/astroid/astroid/__init__.py", + "symbol": "unknown-option-value", + "message": "Unknown option value for 'disable', expected a valid pylint message and got 'Ellipsis'", + "message-id": "W0012" + } + ] + } +} diff --git a/tests/testutils/test_package_to_lint.py b/tests/testutils/_primer/test_package_to_lint.py similarity index 93% rename from tests/testutils/test_package_to_lint.py rename to tests/testutils/_primer/test_package_to_lint.py index c3d2a4ab4d..220e2c0b28 100644 --- a/tests/testutils/test_package_to_lint.py +++ b/tests/testutils/_primer/test_package_to_lint.py @@ -2,7 +2,7 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt -from pylint.testutils.primer import PRIMER_DIRECTORY_PATH, PackageToLint +from pylint.testutils._primer import PRIMER_DIRECTORY_PATH, PackageToLint def test_package_to_lint() -> None: @@ -42,8 +42,8 @@ def test_package_to_lint_default_value() -> None: branch="main", directories=["src/flask"], # Must work on Windows (src\\flask) ) - assert package_to_lint.pylintrc is None + assert package_to_lint.pylintrc == "" expected_path_to_lint = ( PRIMER_DIRECTORY_PATH / "pallets" / "flask" / "src" / "flask" ) - assert package_to_lint.pylint_args == [str(expected_path_to_lint)] + assert package_to_lint.pylint_args == [str(expected_path_to_lint), "--rcfile="] diff --git a/tests/testutils/_primer/test_primer.py b/tests/testutils/_primer/test_primer.py new file mode 100644 index 0000000000..d57e98d213 --- /dev/null +++ b/tests/testutils/_primer/test_primer.py @@ -0,0 +1,96 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Test the primer commands. """ +from __future__ import annotations + +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest +from _pytest.capture import CaptureFixture + +from pylint.constants import IS_PYPY +from pylint.testutils._primer.primer import Primer + +HERE = Path(__file__).parent +TEST_DIR_ROOT = HERE.parent.parent +PRIMER_DIRECTORY = TEST_DIR_ROOT / ".pylint_primer_tests/" +PACKAGES_TO_PRIME_PATH = TEST_DIR_ROOT / "primer/packages_to_prime.json" +FIXTURES_PATH = HERE / "fixtures" + +PRIMER_CURRENT_INTERPRETER = (3, 10) + +DEFAULT_ARGS = ["python tests/primer/__main__.py", "compare", "--commit=v2.14.2"] + + +@pytest.mark.parametrize("args", [[], ["wrong_command"]]) +def test_primer_launch_bad_args(args: list[str], capsys: CaptureFixture) -> None: + with pytest.raises(SystemExit): + with patch("sys.argv", ["python tests/primer/__main__.py"] + args): + Primer(PRIMER_DIRECTORY, PACKAGES_TO_PRIME_PATH).run() + out, err = capsys.readouterr() + assert not out + assert "usage: Pylint Primer" in err + + +@pytest.mark.skipif( + sys.version_info[:2] != PRIMER_CURRENT_INTERPRETER or IS_PYPY, + reason=( + "Primers are internal and will always be run for only one interpreter (currently" + f" {PRIMER_CURRENT_INTERPRETER})" + ), +) +class TestPrimer: + @pytest.mark.parametrize( + "directory", + [ + pytest.param(p, id=str(p.relative_to(FIXTURES_PATH))) + for p in FIXTURES_PATH.iterdir() + if p.is_dir() + ], + ) + def test_compare(self, directory: Path) -> None: + """Test for the standard case. + + Directory in 'fixtures/' with 'main.json', 'pr.json' and 'expected.txt'.""" + self.__assert_expected(directory) + + def test_truncated_compare(self) -> None: + """Test for the truncation of comments that are too long.""" + max_comment_length = 525 + directory = FIXTURES_PATH / "message_changed" + with patch( + "pylint.testutils._primer.primer_compare_command.MAX_GITHUB_COMMENT_LENGTH", + max_comment_length, + ): + content = self.__assert_expected( + directory, expected_file=directory / "expected_truncated.txt" + ) + assert len(content) < max_comment_length + + @staticmethod + def __assert_expected( + directory: Path, + main: Path | None = None, + pr: Path | None = None, + expected_file: Path | None = None, + ) -> str: + if main is None: + main = directory / "main.json" + if pr is None: + pr = directory / "pr.json" + if expected_file is None: + expected_file = directory / "expected.txt" + new_argv = DEFAULT_ARGS + [f"--base-file={main}", f"--new-file={pr}"] + with patch("sys.argv", new_argv): + Primer(PRIMER_DIRECTORY, PACKAGES_TO_PRIME_PATH).run() + with open(PRIMER_DIRECTORY / "comment.txt", encoding="utf8") as f: + content = f.read() + with open(expected_file, encoding="utf8") as f: + expected = f.read() + # rstrip so the expected.txt can end with a newline + assert content == expected.rstrip("\n") + return content diff --git a/tests/testutils/test_functional_testutils.py b/tests/testutils/test_functional_testutils.py index fb664959cf..68dad697d4 100644 --- a/tests/testutils/test_functional_testutils.py +++ b/tests/testutils/test_functional_testutils.py @@ -22,7 +22,7 @@ @pytest.fixture(name="pytest_config") def pytest_config_fixture() -> MagicMock: - def _mock_getoption(option): + def _mock_getoption(option: str) -> bool: if option == "minimal_messages_config": return True return False @@ -45,7 +45,7 @@ def test_get_functional_test_files_from_directory() -> None: get_functional_test_files_from_directory(DATA_DIRECTORY) -def test_minimal_messages_config_enabled(pytest_config) -> None: +def test_minimal_messages_config_enabled(pytest_config: MagicMock) -> None: """Test that all messages not targeted in the functional test are disabled when running with --minimal-messages-config. """ @@ -68,7 +68,7 @@ def test_minimal_messages_config_enabled(pytest_config) -> None: assert not mod_test._linter.is_message_enabled("unused-import") -def test_minimal_messages_config_excluded_file(pytest_config) -> None: +def test_minimal_messages_config_excluded_file(pytest_config: MagicMock) -> None: """Test that functional test files can be excluded from the run with --minimal-messages-config if they set the exclude_from_minimal_messages_config option in their rcfile. diff --git a/tests/testutils/test_lint_module_output_update.py b/tests/testutils/test_lint_module_output_update.py index 1c9e589af7..2e387a118c 100644 --- a/tests/testutils/test_lint_module_output_update.py +++ b/tests/testutils/test_lint_module_output_update.py @@ -63,7 +63,7 @@ def test_lint_module_output_update_effective( assert (expected_output_file).exists() assert ( expected_output_file.read_text(encoding="utf8") - == 'disallowed-name:1:0:None:None::"Disallowed name ""foo""":UNDEFINED\n' + == 'disallowed-name:1:0:None:None::"Disallowed name ""foo""":HIGH\n' ) diff --git a/tests/testutils/test_output_line.py b/tests/testutils/test_output_line.py index 0a194b9735..5b2bf1a1bb 100644 --- a/tests/testutils/test_output_line.py +++ b/tests/testutils/test_output_line.py @@ -6,19 +6,29 @@ from __future__ import annotations -from collections.abc import Callable +import sys import pytest from pylint.constants import PY38_PLUS from pylint.interfaces import HIGH, INFERENCE, Confidence from pylint.message import Message -from pylint.testutils.output_line import MalformedOutputLineException, OutputLine +from pylint.testutils.output_line import OutputLine from pylint.typing import MessageLocationTuple +if sys.version_info >= (3, 8): + from typing import Protocol +else: + from typing_extensions import Protocol + + +class _MessageCallable(Protocol): + def __call__(self, confidence: Confidence = HIGH) -> Message: + ... + @pytest.fixture() -def message() -> Callable: +def message() -> _MessageCallable: def inner(confidence: Confidence = HIGH) -> Message: return Message( symbol="missing-docstring", @@ -55,7 +65,7 @@ def test_output_line() -> None: assert output_line.confidence == "HIGH" -def test_output_line_from_message(message: Callable) -> None: +def test_output_line_from_message(message: _MessageCallable) -> None: """Test that the OutputLine NamedTuple is instantiated correctly with from_msg.""" expected_column = 2 if PY38_PLUS else 0 @@ -91,7 +101,7 @@ def test_output_line_from_message(message: Callable) -> None: @pytest.mark.parametrize("confidence", [HIGH, INFERENCE]) -def test_output_line_to_csv(confidence: Confidence, message: Callable) -> None: +def test_output_line_to_csv(confidence: Confidence, message: _MessageCallable) -> None: """Test that the OutputLine NamedTuple is instantiated correctly with from_msg and then converted to csv. """ @@ -127,20 +137,20 @@ def test_output_line_to_csv(confidence: Confidence, message: Callable) -> None: def test_output_line_from_csv_error() -> None: """Test that errors are correctly raised for incorrect OutputLine's.""" # Test a csv-string which does not have a number for line and column - with pytest.raises( - MalformedOutputLineException, + with pytest.warns( + UserWarning, match="msg-symbolic-name:42:27:MyClass.my_function:The message", ): OutputLine.from_csv("'missing-docstring', 'line', 'column', 'obj', 'msg'", True) # Test a tuple which does not have a number for line and column - with pytest.raises( - MalformedOutputLineException, match="symbol='missing-docstring' ?" + with pytest.warns( + UserWarning, match="we got 'missing-docstring:line:column:obj:msg'" ): csv = ("missing-docstring", "line", "column", "obj", "msg") OutputLine.from_csv(csv, True) # Test a csv-string that is too long - with pytest.raises( - MalformedOutputLineException, + with pytest.warns( + UserWarning, match="msg-symbolic-name:42:27:MyClass.my_function:The message", ): OutputLine.from_csv( diff --git a/tests/testutils/test_testutils_utils.py b/tests/testutils/test_testutils_utils.py new file mode 100644 index 0000000000..b521e25c4c --- /dev/null +++ b/tests/testutils/test_testutils_utils.py @@ -0,0 +1,79 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +import os +import sys +from pathlib import Path + +import pytest + +from pylint.testutils.utils import _test_cwd, _test_environ_pythonpath, _test_sys_path + + +def test__test_sys_path_no_arg() -> None: + sys_path = sys.path + with _test_sys_path(): + assert sys.path == sys_path + new_sys_path = ["new_sys_path"] + sys.path = new_sys_path + assert sys.path == new_sys_path + assert sys.path == sys_path + + +def test__test_sys_path() -> None: + sys_path = sys.path + new_sys_path = [".", "/home"] + with _test_sys_path(new_sys_path): + assert sys.path == new_sys_path + newer_sys_path = ["new_sys_path"] + sys.path = newer_sys_path + assert sys.path == newer_sys_path + assert sys.path == sys_path + + +def test__test_cwd_no_arg(tmp_path: Path) -> None: + cwd = os.getcwd() + with _test_cwd(): + assert os.getcwd() == cwd + os.chdir(tmp_path) + assert os.getcwd() == str(tmp_path) + assert os.getcwd() == cwd + + +def test__test_cwd(tmp_path: Path) -> None: + cwd = os.getcwd() + with _test_cwd(tmp_path): + assert os.getcwd() == str(tmp_path) + new_path = tmp_path / "another_dir" + new_path.mkdir() + os.chdir(new_path) + assert os.getcwd() == str(new_path) + assert os.getcwd() == cwd + + +@pytest.mark.parametrize("old_pythonpath", ["./oldpath/:", None]) +def test__test_environ_pythonpath_no_arg(old_pythonpath: str) -> None: + real_pythonpath = os.environ.get("PYTHONPATH") + with _test_environ_pythonpath(old_pythonpath): + with _test_environ_pythonpath(): + assert os.environ.get("PYTHONPATH") is None + new_pythonpath = "./whatever/:" + os.environ["PYTHONPATH"] = new_pythonpath + assert os.environ.get("PYTHONPATH") == new_pythonpath + assert os.environ.get("PYTHONPATH") == old_pythonpath + assert os.environ.get("PYTHONPATH") == real_pythonpath + + +@pytest.mark.parametrize("old_pythonpath", ["./oldpath/:", None]) +def test__test_environ_pythonpath(old_pythonpath: str) -> None: + real_pythonpath = os.environ.get("PYTHONPATH") + with _test_environ_pythonpath(old_pythonpath): + new_pythonpath = "./whatever/:" + with _test_environ_pythonpath(new_pythonpath): + assert os.environ.get("PYTHONPATH") == new_pythonpath + newer_pythonpath = "./something_else/:" + os.environ["PYTHONPATH"] = newer_pythonpath + assert os.environ.get("PYTHONPATH") == newer_pythonpath + assert os.environ.get("PYTHONPATH") == old_pythonpath + assert os.environ.get("PYTHONPATH") == real_pythonpath diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index e8a8ff79fd..e69de29bb2 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -1,3 +0,0 @@ -# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt diff --git a/tests/utils/unittest_ast_walker.py b/tests/utils/unittest_ast_walker.py index 43614c0edb..5a2dc66095 100644 --- a/tests/utils/unittest_ast_walker.py +++ b/tests/utils/unittest_ast_walker.py @@ -7,6 +7,7 @@ import warnings import astroid +from astroid import nodes from pylint.checkers.base_checker import BaseChecker from pylint.checkers.utils import only_required_for_messages @@ -27,19 +28,23 @@ def __init__(self) -> None: self.called: set[str] = set() @only_required_for_messages("first-message") - def visit_module(self, module): # pylint: disable=unused-argument + def visit_module( + self, module: nodes.Module # pylint: disable=unused-argument + ) -> None: self.called.add("module") @only_required_for_messages("second-message") - def visit_call(self, module): + def visit_call(self, module: nodes.Call) -> None: raise NotImplementedError @only_required_for_messages("second-message", "third-message") - def visit_assignname(self, module): # pylint: disable=unused-argument + def visit_assignname( + self, module: nodes.AssignName # pylint: disable=unused-argument + ) -> None: self.called.add("assignname") @only_required_for_messages("second-message") - def leave_assignname(self, module): + def leave_assignname(self, module: nodes.AssignName) -> None: raise NotImplementedError def test_only_required_for_messages(self) -> None: @@ -59,7 +64,9 @@ def __init__(self) -> None: self.called = False @only_required_for_messages("first-message") - def visit_assname(self, node): # pylint: disable=unused-argument + def visit_assname( + self, node: nodes.AssignName # pylint: disable=unused-argument + ) -> None: self.called = True linter = self.MockLinter({"first-message": True}) diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 0000000000..a65d4681d2 --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,67 @@ +[tool.towncrier] +version = "2.16.3" +directory = "doc/whatsnew/fragments" +filename = "doc/whatsnew/2/2.16/index.rst" +template = "doc/whatsnew/fragments/_template.rst" +issue_format = "`#{issue} `_" +wrap = true + +# Definition of fragment types. +# We want the changelog to show in the same order as the fragment types +# are defined here. Therefore we have to use the array-style fragment definition. +# The table-style definition, although more concise, would be sorted alphabetically. +# https://github.com/twisted/towncrier/issues/437 +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "user_action" +name = "Changes requiring user actions" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "New Features" +showcontent = true + +[[tool.towncrier.type]] +directory = "new_check" +name = "New Checks" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed_check" +name = "Removed Checks" +showcontent = true + +[[tool.towncrier.type]] +directory = "extension" +name = "Extensions" +showcontent = true + +[[tool.towncrier.type]] +directory = "false_positive" +name = "False Positives Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "false_negative" +name = "False Negatives Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Other Bug Fixes" +showcontent = true + +[[tool.towncrier.type]] +directory = "other" +name = "Other Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal Changes" +showcontent = true diff --git a/tox.ini b/tox.ini index 0f3192e27a..0c78b87982 100644 --- a/tox.ini +++ b/tox.ini @@ -1,28 +1,27 @@ [tox] -minversion = 2.4 -envlist = formatting, py37, py38, py39, py310, pypy, benchmark +minversion = 3.0 +envlist = formatting, py37, py38, py39, py310, py311, pypy, benchmark skip_missing_interpreters = true requires = pip >=21.3.1 +isolated_build = true [testenv:pylint] deps = - -r {toxinidir}/requirements_test_min.txt - pre-commit~=2.13 + -r {toxinidir}/requirements_test.txt commands = pre-commit run pylint --all-files [testenv:formatting] basepython = python3 deps = - -r {toxinidir}/requirements_test_min.txt - pre-commit~=2.13 + -r {toxinidir}/requirements_test.txt commands = pre-commit run --all-files [testenv:mypy] basepython = python3 deps = - pre-commit~=2.13 + pre-commit~=2.20 commands = pre-commit run mypy --all-files @@ -59,6 +58,12 @@ deps = commands = sphinx-build -W -b html -d _build/doctrees . _build/html +[testenv:test_doc] +deps = + -r {toxinidir}/requirements_test.txt +commands = + pytest {toxinidir}/doc/test_messages_documentation.py + [testenv:benchmark] deps = -r {toxinidir}/requirements_test.txt