diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 92ae83f9fdb..664f6f1d2f3 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -81,11 +81,11 @@ body: label: Pylint version description: >- Please copy and paste the result of `pylint --version` or specify the range of - version affected. + versions affected. placeholder: | - pylint 2.9.6 - astroid 2.6.5 - Python 3.8.10 (default, Jun 2 2021, 10:49:15) [GCC 9.4.0] + pylint 3.3.0 + astroid 3.3.0 + Python 3.12.0 (v3.12.0:0fb18b02c8, Oct 2 2023, 09:45:56) render: shell validations: required: true diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 40a3660c813..cb16f9fdfb4 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -8,7 +8,7 @@ on: env: CACHE_VERSION: 1 KEY_PREFIX: base-venv - DEFAULT_PYTHON: "3.12" + DEFAULT_PYTHON: "3.13" permissions: contents: read @@ -21,14 +21,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 with: # `towncrier check` runs `git diff --name-only origin/main...`, which # needs a non-shallow clone. fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -41,7 +41,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv key: >- diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 09b3a3cb285..a2a5c86a61b 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -11,9 +11,9 @@ on: - "maintenance/**" env: - CACHE_VERSION: 1 + CACHE_VERSION: 2 KEY_PREFIX: base-venv - DEFAULT_PYTHON: "3.11" + DEFAULT_PYTHON: "3.13" PRE_COMMIT_CACHE: ~/.cache/pre-commit concurrency: @@ -33,10 +33,10 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -49,7 +49,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv key: >- @@ -71,7 +71,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -89,16 +89,16 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv fail-on-cache-miss: true @@ -107,7 +107,7 @@ jobs: needs.prepare-base.outputs.python-key }} - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -130,16 +130,16 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv fail-on-cache-miss: true @@ -158,16 +158,16 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv fail-on-cache-miss: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2e168207e93..6a450cd31a2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/primer-test.yaml b/.github/workflows/primer-test.yaml index d8f7a84b27e..ba9d79ce0fd 100644 --- a/.github/workflows/primer-test.yaml +++ b/.github/workflows/primer-test.yaml @@ -30,15 +30,15 @@ jobs: timeout-minutes: 5 strategy: matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] outputs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -51,7 +51,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv key: >- @@ -72,19 +72,19 @@ jobs: needs: prepare-tests-linux strategy: matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv fail-on-cache-miss: true diff --git a/.github/workflows/primer_comment.yaml b/.github/workflows/primer_comment.yaml index 9d8f02687c5..e904be0b005 100644 --- a/.github/workflows/primer_comment.yaml +++ b/.github/workflows/primer_comment.yaml @@ -16,7 +16,7 @@ env: # This needs to be the SAME as in the Main and PR job CACHE_VERSION: 4 KEY_PREFIX: venv-primer - DEFAULT_PYTHON: "3.12" + DEFAULT_PYTHON: "3.13" permissions: contents: read @@ -30,10 +30,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -41,7 +41,7 @@ jobs: # Restore cached Python environment - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv key: diff --git a/.github/workflows/primer_run_main.yaml b/.github/workflows/primer_run_main.yaml index c498be95514..1b7b1c3d8fa 100644 --- a/.github/workflows/primer_run_main.yaml +++ b/.github/workflows/primer_run_main.yaml @@ -29,23 +29,23 @@ jobs: timeout-minutes: 45 strategy: matrix: - python-version: ["3.8", "3.12"] + python-version: ["3.9", "3.13"] batches: [4] batchIdx: [0, 1, 2, 3] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true # Create a re-usable virtual environment - - name: Create Python virtual environment cache + - name: Restore Python virtual environment cache id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache/restore@v4.1.2 with: path: venv key: @@ -60,6 +60,17 @@ jobs: . venv/bin/activate python -m pip install -U pip setuptools wheel pip install -U -r requirements_test.txt + # Save cached Python environment (explicit because cancel-in-progress: true) + - name: Save Python virtual environment to cache + if: steps.cache-venv.outputs.cache-hit != 'true' + uses: actions/cache/save@v4.1.2 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml', + 'requirements_test.txt', 'requirements_test_min.txt', + 'requirements_test_pre_commit.txt') }} # Cache primer packages - name: Get commit string @@ -71,7 +82,7 @@ jobs: echo "commitstring=$output" >> $GITHUB_OUTPUT - name: Restore projects cache id: cache-projects - uses: actions/cache@v4.0.2 + uses: actions/cache/restore@v4.1.2 with: path: tests/.pylint_primer_tests/ key: >- @@ -82,8 +93,16 @@ jobs: run: | . venv/bin/activate python tests/primer/__main__.py prepare --clone + - name: Save projects cache + if: steps.cache-projects.outputs.cache-hit != 'true' + uses: actions/cache/save@v4.1.2 + with: + path: tests/.pylint_primer_tests/ + key: >- + ${{ runner.os }}-${{ matrix.python-version }}-${{ + steps.commitstring.outputs.commitstring }}-primer - name: Upload commit string - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.4.3 if: matrix.batchIdx == 0 with: name: primer_commitstring_${{ matrix.python-version }} @@ -104,7 +123,7 @@ jobs: then echo "::warning ::$WARNINGS" fi - name: Upload output - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.4.3 with: name: primer_output_main_${{ matrix.python-version }}_batch${{ matrix.batchIdx }} diff --git a/.github/workflows/primer_run_pr.yaml b/.github/workflows/primer_run_pr.yaml index 5ff65bafe34..44c40b5d598 100644 --- a/.github/workflows/primer_run_pr.yaml +++ b/.github/workflows/primer_run_pr.yaml @@ -38,17 +38,17 @@ jobs: timeout-minutes: 45 strategy: matrix: - python-version: ["3.8", "3.12"] + python-version: ["3.9", "3.13"] batches: [4] batchIdx: [0, 1, 2, 3] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -56,7 +56,7 @@ jobs: # Restore cached Python environment - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache/restore@v4.1.2 with: path: venv key: @@ -72,6 +72,17 @@ jobs: . venv/bin/activate python -m pip install -U pip setuptools wheel pip install -U -r requirements_test.txt + # Save cached Python environment (explicit because cancel-in-progress: true) + - name: Save Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + uses: actions/cache/save@v4.1.2 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml', + 'requirements_test.txt', 'requirements_test_min.txt', + 'requirements_test_pre_commit.txt') }} # Cache primer packages - name: Download last 'main' run info @@ -140,7 +151,7 @@ jobs: echo "commitstring=$output" >> $GITHUB_OUTPUT - name: Restore projects cache id: cache-projects - uses: actions/cache@v4.0.2 + uses: actions/cache/restore@v4.1.2 with: path: tests/.pylint_primer_tests/ key: >- @@ -151,6 +162,14 @@ jobs: run: | . venv/bin/activate python tests/primer/__main__.py prepare --clone + - name: Save projects cache + if: steps.cache-projects.outputs.cache-hit != 'true' + uses: actions/cache/save@v4.1.2 + with: + path: tests/.pylint_primer_tests/ + key: >- + ${{ runner.os }}-${{ matrix.python-version }}-${{ + steps.commitstring.outputs.commitstring }}-primer - name: Check cache run: | . venv/bin/activate @@ -178,7 +197,7 @@ jobs: then echo "::warning ::$WARNINGS" fi - name: Upload output of PR - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.4.3 with: name: primer_output_pr_${{ matrix.python-version }}_batch${{ matrix.batchIdx }} @@ -186,7 +205,7 @@ jobs: tests/.pylint_primer_tests/output_${{ matrix.python-version }}_pr_batch${{ matrix.batchIdx }}.txt - name: Upload output of 'main' - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.4.3 with: name: primer_output_main_${{ matrix.python-version }}_batch${{ matrix.batchIdx }} @@ -198,8 +217,8 @@ jobs: echo ${{ github.event.pull_request.number }} | tee pr_number.txt - name: Upload PR number if: - startsWith(steps.python.outputs.python-version, '3.8') && matrix.batchIdx == 0 - uses: actions/upload-artifact@v4.3.3 + startsWith(steps.python.outputs.python-version, '3.9') && matrix.batchIdx == 0 + uses: actions/upload-artifact@v4.4.3 with: name: pr_number path: pr_number.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c54bf2d034..aba113717f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - published env: - DEFAULT_PYTHON: "3.12" + DEFAULT_PYTHON: "3.13" permissions: contents: read @@ -20,10 +20,10 @@ jobs: url: https://pypi.org/project/pylint/ steps: - name: Check out code from Github - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 987ec5ef751..162173efa56 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -31,15 +31,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] outputs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -52,7 +52,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv key: >- @@ -76,9 +76,10 @@ jobs: pip list | grep 'astroid\|pylint' python -m pytest -vv --minimal-messages-config tests/test_functional.py - name: Upload coverage artifact - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.4.3 with: name: coverage-${{ matrix.python-version }} + include-hidden-files: true path: .coverage coverage: @@ -88,16 +89,16 @@ jobs: needs: tests-linux steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python 3.12 id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: - python-version: "3.12" + python-version: "3.13" check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv fail-on-cache-miss: true @@ -105,13 +106,13 @@ jobs: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.tests-linux.outputs.python-key }} - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4.1.8 - name: Combine coverage results run: | . venv/bin/activate coverage combine coverage*/.coverage coverage xml - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -125,19 +126,19 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.12"] + python-version: ["3.13"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv fail-on-cache-miss: true @@ -160,11 +161,12 @@ jobs: run: >- echo "datetime="$(date "+%Y%m%d_%H%M") >> $GITHUB_OUTPUT - name: Upload benchmark artifact - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.4.3 with: name: benchmark-${{ runner.os }}-${{ matrix.python-version }}_${{ steps.artifact-name-suffix.outputs.datetime }} + include-hidden-files: true path: .benchmarks/ tests-windows: @@ -175,17 +177,17 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 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@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -197,7 +199,7 @@ jobs: }}" >> $env:GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv key: >- @@ -225,13 +227,13 @@ jobs: fail-fast: false matrix: # We only run on the oldest supported version on Mac - python-version: [3.8] + python-version: [3.9] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -243,7 +245,7 @@ jobs: }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv key: >- @@ -269,13 +271,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "pypy-3.9"] + python-version: ["pypy-3.9", "pypy-3.10"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -287,7 +289,7 @@ jobs: }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: venv key: >- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a95efedecdb..ea00e89ed8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,10 +3,13 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace exclude: tests(/\w*)*/functional/t/trailing_whitespaces.py|tests/pyreverse/data/.*.html|doc/data/messages/t/trailing-whitespace/bad.py + # - id: file-contents-sorter # commented out because it does not preserve comments order + # args: ["--ignore-case", "--unique"] + # files: "custom_dict.txt" - id: end-of-file-fixer exclude: | (?x)^( @@ -17,15 +20,14 @@ repos: doc/data/messages/m/missing-final-newline/bad/crlf.py )$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.4.7" + rev: "v0.7.4" hooks: - id: ruff args: ["--fix"] - exclude: &fixtures tests(/\w*)*/functional/|tests/input|doc/data/messages|tests(/\w*)*data/ + exclude: doc/data/messages - id: ruff name: ruff-doc files: doc/data/messages - args: ["--config", "doc/data/ruff.toml"] - repo: https://github.com/Pierre-Sassoulas/copyright_notice_precommit rev: 0.1.2 hooks: @@ -39,11 +41,11 @@ repos: - id: isort exclude: doc/data/messages/ - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.10.0 hooks: - id: black args: [--safe, --quiet] - exclude: *fixtures + exclude: &fixtures tests(/\w*)*/functional/|tests/input|doc/data/messages|tests(/\w*)*data/ - id: black name: black-doc args: [--safe, --quiet] @@ -107,7 +109,7 @@ repos: entry: make -C doc/ html pass_filenames: false language: system - stages: [push] + stages: [pre-push] - id: check-newsfragments name: Check newsfragments entry: python3 -m script.check_newsfragments @@ -116,14 +118,14 @@ repos: files: ^(doc/whatsnew/fragments) exclude: doc/whatsnew/fragments/_.*.rst - repo: https://github.com/rstcheck/rstcheck - rev: "v6.2.0" + rev: "v6.2.4" hooks: - id: rstcheck args: ["--report-level=warning"] files: ^(doc/(.*/)*.*\.rst) - additional_dependencies: [Sphinx==5.0.1] + additional_dependencies: [Sphinx==7.4.3] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.13.0 hooks: - id: mypy name: mypy @@ -142,7 +144,7 @@ repos: ] exclude: tests(/\w*)*/functional/|tests/input|tests(/.*)+/conftest.py|doc/data/messages|tests(/\w*)*data/ - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.3.0 + rev: v3.3.3 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] @@ -169,8 +171,15 @@ repos: setup.cfg )$ - repo: https://github.com/PyCQA/bandit - rev: 1.7.8 + rev: 1.7.10 hooks: - id: bandit args: ["-r", "-lll"] exclude: *fixtures + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: ["--toml=pyproject.toml"] + additional_dependencies: + - tomli diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 24959e454d4..4e238428edf 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -158,6 +158,7 @@ contributors: * Added new useless-return checker, * Added new try-except-raise checker - theirix +- correctmost <134317971+correctmost@users.noreply.github.com> - Téo Bouvard - Stavros Ntentos <133706+stdedos@users.noreply.github.com> - Nicolas Boulenguez @@ -210,6 +211,7 @@ contributors: - wtracy - jessebrennan - chohner +- aatle <168398276+aatle@users.noreply.github.com> - Tiago Honorato <61059243+tiagohonorato@users.noreply.github.com> - Steven M. Vascellaro - Robin Tweedie <70587124+robin-wayve@users.noreply.github.com> @@ -264,6 +266,7 @@ contributors: - Samuel FORESTIER - Rémi Cardona - Ryan Ozawa +- Roger Sheu <78449574+rogersheu@users.noreply.github.com> - Raphael Gaschignard - Ram Rachum (cool-RR) - Radostin Stoyanov @@ -287,9 +290,11 @@ contributors: - Justin Li - John Kirkham - Jens H. Nielsen +- Jake Lishman - Ioana Tagirta : fix bad thread instantiation check - Ikraduya Edian : Added new checks 'consider-using-generator' and 'use-a-generator'. - Hugues Bruant +- Hashem Nasarat - Harut - Grygorii Iermolenko - Grizzly Nyo @@ -317,9 +322,11 @@ contributors: - Ben Green - Batuhan Taskaya - Alexander Kapshuna +- Akhil Kamat - Adam Parkin - 谭九鼎 <109224573@qq.com> - Łukasz Sznuk +- zasca - y2kbugger - vinnyrose - ttenhoeve-aa @@ -382,10 +389,12 @@ contributors: - Trevor Bekolay * Added --list-msgs-enabled command - Tomer Chachamu : simplifiable-if-expression +- Tomasz Michalski - Tomasz Magulski - Tom - Tim Hatch - Tim Gates +- Tianyu Chen <124018391+UTsweetyfish@users.noreply.github.com> - Théo Battrel - Thomas Benhamou - Theodore Ni <3806110+tjni@users.noreply.github.com> @@ -412,6 +421,7 @@ contributors: - Ryan McGuire - Ry4an Brase - Ruro +- Roshan Shetty - Roman Ivanov - Robert Schweizer - Reverb Chu @@ -438,6 +448,7 @@ contributors: - Oisín Moran - Obscuron - Noam Yorav-Raphael +- Noah-Agnel <138210920+Noah-Agnel@users.noreply.github.com> - Nir Soffer - Niko Wenselowski - Nikita Sobolev @@ -515,7 +526,6 @@ contributors: - James Broadhead - Jakub Kulík - Jakob Normark -- Jake Lishman - Jacques Kvam - Jace Browning : updated default report format with clickable paths - JT Olds @@ -523,7 +533,6 @@ contributors: - Hayden Richards <62866982+SupImDos@users.noreply.github.com> * Fixed "no-self-use" for async methods * Fixed "docparams" extension for async functions and methods -- Hashem Nasarat - Harshil <37377066+harshil21@users.noreply.github.com> - Harry - Grégoire <96051754+gregoire-mullvad@users.noreply.github.com> @@ -537,6 +546,7 @@ contributors: - Eric Froemling - Emmanuel Chaudron - Elizabeth Bott <52465744+elizabethbott@users.noreply.github.com> +- Ekin Dursun - Eisuke Kawashima - Edward K. Ream - Edgemaster @@ -547,6 +557,7 @@ contributors: - Dmytro Kyrychuk - Dionisio E Alonso - DetachHead <57028336+DetachHead@users.noreply.github.com> +- Dennis Keck <26092524+fellhorn@users.noreply.github.com> - Denis Laxalde - David Lawson - David Cain @@ -582,12 +593,14 @@ contributors: - Benjamin Graham - Benedikt Morbach - Ben Greiner +- Barak Shoshany - Banjamin Freeman - Avram Lubkin - Athos Ribeiro : Fixed dict-keys-not-iterating false positive for inverse containment checks - Arun Persaud - Arthur Lutz - Antonio Ossa +- Antonio Gámiz Delgado <73933988+antoniogamizbadger@users.noreply.github.com> - Anthony VEREZ - Anthony Tan - Anthony Foglia (Google): Added simple string slots check. @@ -617,6 +630,7 @@ contributors: - Adrian Chirieac - Aditya Gupta (adityagupta1089) * Added ignore_signatures to duplicate checker +- Adam Tuft <73994535+adamtuft@users.noreply.github.com> - Adam Dangoor - 243f6a88 85a308d3 <33170174+243f6a8885a308d313198a2e037@users.noreply.github.com> diff --git a/README.rst b/README.rst index 2910eba6809..76134f428ca 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ :target: https://codecov.io/gh/pylint-dev/pylint .. image:: https://img.shields.io/pypi/v/pylint.svg - :alt: Pypi Package version + :alt: PyPI Package version :target: https://pypi.python.org/pypi/pylint .. image:: https://readthedocs.org/projects/pylint/badge/?version=latest @@ -45,7 +45,7 @@ What is Pylint? --------------- Pylint is a `static code analyser`_ for Python 2 or 3. The latest version supports Python -3.8.0 and above. +3.9.0 and above. .. _`static code analyser`: https://en.wikipedia.org/wiki/Static_code_analysis @@ -81,7 +81,7 @@ It can also be integrated in most editors or IDEs. More information can be found What differentiates Pylint? --------------------------- -Pylint is not trusting your typing and is inferring the actual value of nodes (for a +Pylint is not trusting your typing and is inferring the actual values of nodes (for a start because there was no typing when pylint started off) using its internal code representation (astroid). If your code is ``import logging as argparse``, Pylint can check and know that ``argparse.error(...)`` is in fact a logging call and not an @@ -123,7 +123,7 @@ ecosystem of existing plugins for popular frameworks and third-party libraries. .. _`plugins`: https://pylint.readthedocs.io/en/latest/development_guide/how_tos/plugins.html#plugins .. _`pylint-pydantic`: https://pypi.org/project/pylint-pydantic .. _`pylint-django`: https://github.com/pylint-dev/pylint-django -.. _`pylint-sonarjson`: https://github.com/omegacen/pylint-sonarjson +.. _`pylint-sonarjson`: https://github.com/cnescatlab/pylint-sonarjson-catlab Advised linters alongside pylint -------------------------------- @@ -135,7 +135,7 @@ mypy_, pyright_ / pylance or pyre_ (typing checks), bandit_ (security oriented c isort_ (auto-formatting), autoflake_ (automated removal of unused imports or variables), pyupgrade_ (automated upgrade to newer python syntax) and pydocstringformatter_ (automated pep257). -.. _ruff: https://github.com/charliermarsh/ruff +.. _ruff: https://github.com/astral-sh/ruff .. _flake8: https://github.com/PyCQA/flake8 .. _bandit: https://github.com/PyCQA/bandit .. _mypy: https://github.com/python/mypy @@ -155,8 +155,8 @@ Pylint ships with two additional tools: - pyreverse_ (standalone tool that generates package and class diagrams.) - symilar_ (duplicate code finder that is also integrated in pylint) -.. _pyreverse: https://pylint.readthedocs.io/en/latest/pyreverse.html -.. _symilar: https://pylint.readthedocs.io/en/latest/symilar.html +.. _pyreverse: https://pylint.readthedocs.io/en/latest/additional_tools/pyreverse/index.html +.. _symilar: https://pylint.readthedocs.io/en/latest/additional_tools/symilar/index.html .. This is used inside the doc to recover the end of the introduction diff --git a/.pyenchant_pylint_custom_dict.txt b/custom_dict.txt similarity index 98% rename from .pyenchant_pylint_custom_dict.txt rename to custom_dict.txt index 78d861aea35..576ef703e0e 100644 --- a/.pyenchant_pylint_custom_dict.txt +++ b/custom_dict.txt @@ -13,10 +13,11 @@ argumentparser argumentsparser argv ascii +asend assignattr assignname -ast AST +ast astroid async asynccontextmanager @@ -67,8 +68,8 @@ contextlib contextmanager contravariance contravariant -cpython CPython +cpython csv CVE cwd @@ -126,8 +127,8 @@ formfeed fromlineno fullname func -functiøn functiondef +functiøn functools genexpr getattr @@ -160,10 +161,11 @@ isfile isinstance isort iter -itered iterable iterables +itered iteritems +iTerm jn jpg json @@ -206,10 +208,10 @@ monkeypatch mro # Used so much that we need the abbreviation msg +msg-template msgid msgids msgs -msg-template mult multiline multiset @@ -249,8 +251,9 @@ paren parens passthru pathlib -positionals +patternerror png +positionals pragma pragma's pragmas @@ -263,9 +266,9 @@ pyenchant pyfile pyi pylint +pylint's pylintdict pylintrc -pylint's pyproject pypy pyreverse @@ -274,6 +277,7 @@ qname rawcheckers rc rcfile +re-usable readlines recognise recurse @@ -344,9 +348,9 @@ tomlkit toplevel towncrier tp +truthey truthness truthy -truthey tryexcept txt typecheck @@ -361,8 +365,8 @@ unary unflattens unhandled unicode -Uninferable uninferable +Uninferable unittest unraisablehook untriggered diff --git a/doc/additional_tools/pyreverse/configuration.rst b/doc/additional_tools/pyreverse/configuration.rst new file mode 100644 index 00000000000..845386e5335 --- /dev/null +++ b/doc/additional_tools/pyreverse/configuration.rst @@ -0,0 +1,194 @@ +.. This file is auto-generated. Make any changes to the associated +.. docs extension in 'doc/exts/pyreverse_configuration.py'. + + +Usage +##### + + +``pyreverse`` is run from the command line using the following syntax:: + + pyreverse [options] + +where ```` is one or more Python packages or modules to analyze. + +The available options are organized into the following categories: + +* :ref:`filtering-and-scope` - Control which classes and relationships appear in your diagrams +* :ref:`display-options` - Customize the visual appearance including colors and labels +* :ref:`output-control` - Select output formats and set the destination directory +* :ref:`project-configuration` - Define project settings like source roots and ignored files + + +.. _filtering-and-scope: + +Filtering and Scope +=================== + + +--all-ancestors +--------------- +*Show all ancestors of all classes in .* + +**Default:** ``None`` + + +--all-associated +---------------- +*Show all classes associated with the target classes, including indirect associations.* + +**Default:** ``None`` + + +--class +------- +*Create a class diagram with all classes related to ; this uses by default the options -ASmy* + +**Default:** ``None`` + + +--filter-mode +------------- +*Filter attributes and functions according to . Correct modes are: +'PUB_ONLY' filter all non public attributes [DEFAULT], equivalent to PRIVATE+SPECIAL +'ALL' no filter +'SPECIAL' filter Python special functions except constructor +'OTHER' filter protected and private attributes* + +**Default:** ``PUB_ONLY`` + + +--show-ancestors +---------------- +*Show generations of ancestor classes not in .* + +**Default:** ``None`` + + +--show-associated +----------------- +*Show levels of associated classes not in .* + +**Default:** ``None`` + + +--show-builtin +-------------- +*Include builtin objects in representation of classes.* + +**Default:** ``False`` + + +--show-stdlib +------------- +*Include standard library objects in representation of classes.* + +**Default:** ``False`` + + + + +.. _display-options: + +Display Options +=============== + + +--color-palette +--------------- +*Comma separated list of colors to use for the package depth coloring.* + +**Default:** ``('#77AADD', '#99DDFF', '#44BB99', '#BBCC33', '#AAAA00', '#EEDD88', '#EE8866', '#FFAABB', '#DDDDDD')`` + + +--colorized +----------- +*Use colored output. Classes/modules of the same package get the same color.* + +**Default:** ``False`` + + +--max-color-depth +----------------- +*Use separate colors up to package depth of . Higher depths will reuse colors.* + +**Default:** ``2`` + + +--module-names +-------------- +*Include module name in the representation of classes.* + +**Default:** ``None`` + + +--no-standalone +--------------- +*Only show nodes with connections.* + +**Default:** ``False`` + + +--only-classnames +----------------- +*Don't show attributes and methods in the class boxes; this disables -f values.* + +**Default:** ``False`` + + + + +.. _output-control: + +Output Control +============== + + +--output +-------- +*Create a *. output file if format is available. Available formats are: .dot, .puml, .plantuml, .mmd, .html. Any other format will be tried to be created by using the 'dot' command line tool, which requires a graphviz installation. In this case, these additional formats are available (see `Graphviz output formats `_).* + +**Default:** ``dot`` + + +--output-directory +------------------ +*Set the output directory path.* + +**Default:** ``""`` + + + + +.. _project-configuration: + +Project Configuration +===================== + + +--ignore +-------- +*Files or directories to be skipped. They should be base names, not paths.* + +**Default:** ``('CVS',)`` + + +--project +--------- +*Set the project name. This will later be appended to the output file names.* + +**Default:** ``""`` + + +--source-roots +-------------- +*Add paths to the list of the source roots. Supports globbing patterns. The source root is an absolute path or a path relative to the current working directory used to determine a package namespace for modules located under the source root.* + +**Default:** ``()`` + + +--verbose +--------- +*Makes pyreverse more verbose/talkative. Mostly useful for debugging.* + +**Default:** ``False`` diff --git a/doc/additional_tools/pyreverse/index.rst b/doc/additional_tools/pyreverse/index.rst new file mode 100644 index 00000000000..4dc8a9b4b19 --- /dev/null +++ b/doc/additional_tools/pyreverse/index.rst @@ -0,0 +1,40 @@ +.. _pyreverse: + +========= +Pyreverse +========= + +``pyreverse`` is a powerful tool that creates UML diagrams from your Python code. It helps you visualize: + +- Package dependencies and structure +- Class hierarchies and relationships +- Method and attribute organization + +Output Formats +============== + +``pyreverse`` supports multiple output formats: + +* Native formats: + * ``.dot``/``.gv`` (Graphviz) + * ``.puml``/``.plantuml`` (PlantUML) + * ``.mmd``/``.html`` (MermaidJS) + +* Additional formats (requires Graphviz installation): + * All `Graphviz output formats `_ (PNG, SVG, PDF, etc.) + * ``pyreverse`` first generates a temporary ``.gv`` file, which is then fed to Graphviz to generate the final image + +Getting Started +=============== + +Check out the :doc:`configuration` guide to learn about available options, or see :doc:`output_examples` +for sample diagrams and common use cases. + +.. toctree:: + :maxdepth: 2 + :caption: Pyreverse + :titlesonly: + :hidden: + + configuration + output_examples diff --git a/doc/pyreverse.rst b/doc/additional_tools/pyreverse/output_examples.rst similarity index 55% rename from doc/pyreverse.rst rename to doc/additional_tools/pyreverse/output_examples.rst index 7595683d753..32fdfc8f07a 100644 --- a/doc/pyreverse.rst +++ b/doc/additional_tools/pyreverse/output_examples.rst @@ -1,52 +1,25 @@ -.. _pyreverse: - -Pyreverse ---------- - -``pyreverse`` analyzes your source code and generates package and class diagrams. - -It supports output to ``.dot``/``.gv``, ``.puml``/``.plantuml`` (PlantUML) and ``.mmd``/``.html`` (MermaidJS) file formats. -If Graphviz (or the ``dot`` command) is installed, all `output formats supported by Graphviz `_ -can be used as well. In this case, ``pyreverse`` first generates a temporary ``.gv`` file, which is then -fed to Graphviz to generate the final image. - -Running Pyreverse -''''''''''''''''' - -To run ``pyreverse``, use:: - - pyreverse [options] - - can also be a single Python module. -To see a full list of the available options, run:: - - pyreverse -h - - Example Output -'''''''''''''' +############## Example diagrams generated with the ``.puml`` output format are shown below. -Class Diagram -............. - -.. image:: media/pyreverse_example_classes.png - :width: 625 - :height: 589 - :alt: Class diagram generated by pyreverse - :align: center - - Package Diagram ............... -.. image:: media/pyreverse_example_packages.png +.. image:: ../../media/pyreverse_example_packages.png :width: 344 :height: 177 :alt: Package diagram generated by pyreverse :align: center +Class Diagram +............. + +.. image:: ../../media/pyreverse_example_classes.png + :width: 625 + :height: 589 + :alt: Class diagram generated by pyreverse + :align: center Creating Class Diagrams for Specific Classes '''''''''''''''''''''''''''''''''''''''''''' @@ -60,7 +33,7 @@ For example, running:: will generate the full class and package diagrams for ``pylint``, but will additionally generate a file ``pylint.checkers.classes.ClassChecker.dot``: -.. image:: media/ClassChecker_diagram.png +.. image:: ../../media/ClassChecker_diagram.png :width: 757 :height: 1452 :alt: Package diagram generated by pyreverse diff --git a/doc/symilar.rst b/doc/additional_tools/symilar/index.rst similarity index 83% rename from doc/symilar.rst rename to doc/additional_tools/symilar/index.rst index fd7124e1db9..b79a8357a35 100644 --- a/doc/symilar.rst +++ b/doc/additional_tools/symilar/index.rst @@ -3,8 +3,8 @@ Symilar ------- -The console script ``symilar`` finds copy pasted blocks in a set of files. It provides a command line interface to the ``Similar`` class, which includes the logic for -Pylint's ``duplicate-code`` message. +The console script ``symilar`` finds copy pasted block of text in a set of files. It provides a command line interface to check only the ``duplicate-code`` message. + It can be invoked with:: symilar [-d|--duplicates min_duplicated_lines] [-i|--ignore-comments] [--ignore-docstrings] [--ignore-imports] [--ignore-signatures] file1... diff --git a/doc/conf.py b/doc/conf.py index 50d891eb931..5abb8df942f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -44,6 +44,7 @@ "pylint_extensions", "pylint_messages", "pylint_options", + "pyreverse_configuration", "sphinx.ext.autosectionlabel", "sphinx.ext.intersphinx", "sphinx_reredirects", @@ -94,6 +95,8 @@ "user_guide/output": "usage/output.html", "user_guide/pre-commit-integration": "installation/pre-commit-integration.html", "user_guide/run": "usage/run.html", + "pyreverse": "additional_tools/pyreverse/index.html", + "symilar": "additional_tools/symilar/index.html", } diff --git a/doc/data/messages/a/anomalous-backslash-in-string/bad.py b/doc/data/messages/a/anomalous-backslash-in-string/bad.py index 08d8d1d6f43..32da7ddcc0f 100644 --- a/doc/data/messages/a/anomalous-backslash-in-string/bad.py +++ b/doc/data/messages/a/anomalous-backslash-in-string/bad.py @@ -1 +1 @@ -string = "\z" # [anomalous-backslash-in-string] +string = "\z" # [syntax-error] diff --git a/doc/data/messages/a/anomalous-backslash-in-string/details.rst b/doc/data/messages/a/anomalous-backslash-in-string/details.rst index e716bc2d9cb..7f73b513e90 100644 --- a/doc/data/messages/a/anomalous-backslash-in-string/details.rst +++ b/doc/data/messages/a/anomalous-backslash-in-string/details.rst @@ -1,2 +1,6 @@ ``\z`` is same as ``\\z`` because there's no escape sequence for ``z``. But it is not clear for the reader of the code. + +The only reason this is demonstrated to raise ``syntax-error`` is because +pylint's CI now runs on Python 3.12, where this truly raises a ``SyntaxError``. +We hope to address this discrepancy in the documentation in the future. 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 40275f0551c..21d25eadf09 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%b" % b"0394") # [anomalous-unicode-escape-in-string] +print(b"\u%b" % b"0394") # [syntax-error] diff --git a/doc/data/messages/b/bare-except/related.rst b/doc/data/messages/b/bare-except/related.rst index 978b57937a3..afbc33fe3e2 100644 --- a/doc/data/messages/b/bare-except/related.rst +++ b/doc/data/messages/b/bare-except/related.rst @@ -1 +1,3 @@ - `Programming recommendation in PEP8 `_ +- `PEP 760 – No More Bare Excepts (Rejected) `_ +- `Discussion about PEP 760 `_ diff --git a/doc/data/messages/c/c-extension-no-member/details.rst b/doc/data/messages/c/c-extension-no-member/details.rst index c1e9b976acc..3f19677ff4a 100644 --- a/doc/data/messages/c/c-extension-no-member/details.rst +++ b/doc/data/messages/c/c-extension-no-member/details.rst @@ -1 +1,4 @@ -You can help us make the doc better `by contributing `_ ! +``c-extension-no-member`` is an informational variant of ``no-member`` to encourage +allowing introspection of C extensions as described in the +`page `_ +for ``no-member``. diff --git a/doc/data/messages/c/consider-alternative-union-syntax/bad.py b/doc/data/messages/c/consider-alternative-union-syntax/bad.py index ea31c5f8aaf..2f836286095 100644 --- a/doc/data/messages/c/consider-alternative-union-syntax/bad.py +++ b/doc/data/messages/c/consider-alternative-union-syntax/bad.py @@ -1,3 +1,8 @@ -from typing import Union +from typing import Optional, Union -cats: Union[int, str] # [consider-alternative-union-syntax] + +def forecast( + temp: Union[int, float], # [consider-alternative-union-syntax] + unit: Optional[str], # [consider-alternative-union-syntax] +) -> None: + print(f'Temperature: {temp}{unit or ""}') diff --git a/doc/data/messages/c/consider-alternative-union-syntax/details.rst b/doc/data/messages/c/consider-alternative-union-syntax/details.rst new file mode 100644 index 00000000000..c5e122020e1 --- /dev/null +++ b/doc/data/messages/c/consider-alternative-union-syntax/details.rst @@ -0,0 +1,6 @@ +Using the shorthand syntax for union types is |recommended over the typing module|__. This is consistent with the broader recommendation to prefer built-in types over imports (for example, using ``list`` instead of the now-deprecated ``typing.List``). + +``typing.Optional`` can also cause confusion in annotated function arguments, since an argument annotated as ``Optional`` is still a *required* argument when a default value is not set. Explicitly annotating such arguments with ``type | None`` makes the intention clear. + +.. |recommended over the typing module| replace:: recommended over the ``typing`` module +__ https://docs.python.org/3/library/typing.html#typing.Union diff --git a/doc/data/messages/c/consider-alternative-union-syntax/good.py b/doc/data/messages/c/consider-alternative-union-syntax/good.py index 07eb1d6aa9e..29f87973f01 100644 --- a/doc/data/messages/c/consider-alternative-union-syntax/good.py +++ b/doc/data/messages/c/consider-alternative-union-syntax/good.py @@ -1 +1,2 @@ -cats: int | str +def forecast(temp: int | float, unit: str | None) -> None: + print(f'Temperature: {temp}{unit or ""}') diff --git a/doc/data/messages/d/declare-non-slot/bad.py b/doc/data/messages/d/declare-non-slot/bad.py new file mode 100644 index 00000000000..5e39d479534 --- /dev/null +++ b/doc/data/messages/d/declare-non-slot/bad.py @@ -0,0 +1,5 @@ +class Student: + __slots__ = ("name",) + + name: str + surname: str # [declare-non-slot] diff --git a/doc/data/messages/d/declare-non-slot/good.py b/doc/data/messages/d/declare-non-slot/good.py new file mode 100644 index 00000000000..1ca1de19c1d --- /dev/null +++ b/doc/data/messages/d/declare-non-slot/good.py @@ -0,0 +1,5 @@ +class Student: + __slots__ = ("name", "surname") + + name: str + surname: str diff --git a/doc/data/messages/i/invalid-name/details.rst b/doc/data/messages/i/invalid-name/details.rst index 14cfe6e592f..7cbf7563801 100644 --- a/doc/data/messages/i/invalid-name/details.rst +++ b/doc/data/messages/i/invalid-name/details.rst @@ -23,7 +23,7 @@ name is found in, and not the type of object assigned. +--------------------+---------------------------------------------------------------------------------------------------+ | ``class-attribute``| Attributes defined in class bodies. | +--------------------+---------------------------------------------------------------------------------------------------+ -| ``class-const`` | Enum constants and class variables annotated with ``ClassVar`` | +| ``class-const`` | Enum constants and class variables annotated with ``Final`` | +--------------------+---------------------------------------------------------------------------------------------------+ | ``inlinevar`` | Loop variables in list comprehensions and generator expressions. | +--------------------+---------------------------------------------------------------------------------------------------+ diff --git a/doc/data/messages/m/missing-format-argument-key/related.rst b/doc/data/messages/m/missing-format-argument-key/related.rst index 31321fb2354..b9f320bbd76 100644 --- a/doc/data/messages/m/missing-format-argument-key/related.rst +++ b/doc/data/messages/m/missing-format-argument-key/related.rst @@ -1,2 +1,2 @@ - `PEP 3101 `_ -- `Custom String Formmating `_ +- `Custom String Formatting `_ diff --git a/doc/data/messages/p/possibly-used-before-assignment/details.rst b/doc/data/messages/p/possibly-used-before-assignment/details.rst index 4737d266854..49d90e53008 100644 --- a/doc/data/messages/p/possibly-used-before-assignment/details.rst +++ b/doc/data/messages/p/possibly-used-before-assignment/details.rst @@ -1,15 +1,54 @@ -If you rely on a pattern like: +You can use ``assert_never`` to mark exhaustive choices: + +.. sourcecode:: python + + from typing import assert_never + + def handle_date_suffix(suffix): + if suffix == "d": + ... + elif suffix == "m": + ... + elif suffix == "y": + ... + else: + assert_never(suffix) + + if suffix in "dmy": + handle_date_suffix(suffix) + +Or, instead of `assert_never()`, you can call a function with a return +annotation of `Never` or `NoReturn`. Unlike in the general case, where +by design pylint ignores type annotations and does its own static analysis, +here, pylint treats these special annotations like a disable comment. + +Pylint currently allows repeating the same test like this, even though this +lets some error cases through, as pylint does not assess the intervening code: .. sourcecode:: python if guarded(): var = 1 + # what if code here affects the result of guarded()? + if guarded(): - print(var) # emits possibly-used-before-assignment + print(var) + +But this exception is limited to the repeating the exact same test. +This warns: + +.. sourcecode:: python + + if guarded(): + var = 1 + + if guarded() or other_condition: + print(var) # [possibly-used-before-assignment] -you may be concerned that ``possibly-used-before-assignment`` is not totally useful -in this instance. However, consider that pylint, as a static analysis tool, does -not know if ``guarded()`` is deterministic or talks to -a database. (Likewise, for ``guarded`` instead of ``guarded()``, any other -part of your program may have changed its value in the meantime.) +If you find this surprising, consider that pylint, as a static analysis +tool, does not know if ``guarded()`` is deterministic or talks to +a database. For variables (e.g. ``guarded`` versus ``guarded()``), +this is less of an issue, so in this case, +``possibly-used-before-assignment`` acts more like a future-proofing style +preference than an error, per se. diff --git a/doc/data/messages/t/too-few-format-args/related.rst b/doc/data/messages/t/too-few-format-args/related.rst index a35c0d1294e..75780499a8c 100644 --- a/doc/data/messages/t/too-few-format-args/related.rst +++ b/doc/data/messages/t/too-few-format-args/related.rst @@ -1 +1 @@ -- `String Formmating `_ +- `String Formatting `_ diff --git a/doc/data/messages/t/too-many-format-args/related.rst b/doc/data/messages/t/too-many-format-args/related.rst index a35c0d1294e..75780499a8c 100644 --- a/doc/data/messages/t/too-many-format-args/related.rst +++ b/doc/data/messages/t/too-many-format-args/related.rst @@ -1 +1 @@ -- `String Formmating `_ +- `String Formatting `_ diff --git a/doc/data/messages/t/too-many-positional-arguments/bad.py b/doc/data/messages/t/too-many-positional-arguments/bad.py new file mode 100644 index 00000000000..3cce780547c --- /dev/null +++ b/doc/data/messages/t/too-many-positional-arguments/bad.py @@ -0,0 +1,7 @@ +# +1: [too-many-positional-arguments] +def calculate_drag_force(velocity, area, density, drag_coefficient): + """Each argument is positional-or-keyword unless a `/` or `*` is present.""" + return 0.5 * drag_coefficient * density * area * velocity**2 + + +drag_force = calculate_drag_force(30, 2.5, 1.225, 0.47) diff --git a/doc/data/messages/t/too-many-positional-arguments/details.rst b/doc/data/messages/t/too-many-positional-arguments/details.rst new file mode 100644 index 00000000000..da5e3e0b969 --- /dev/null +++ b/doc/data/messages/t/too-many-positional-arguments/details.rst @@ -0,0 +1,11 @@ +Good function signatures don’t have many positional parameters. For almost all +interfaces, comprehensibility suffers beyond a handful of arguments. + +Positional arguments work well for cases where the the use cases are +self-evident, such as unittest's ``assertEqual(first, second, "assert msg")`` +or ``zip(fruits, vegetables)``. + +There are a few exceptions where four or more positional parameters make sense, +for example ``rgba(1.0, 0.5, 0.3, 1.0)``, because it uses a very well-known and +well-established convention, and using keywords all the time would be a waste +of time. diff --git a/doc/data/messages/t/too-many-positional-arguments/good.py b/doc/data/messages/t/too-many-positional-arguments/good.py new file mode 100644 index 00000000000..0e232e15a8f --- /dev/null +++ b/doc/data/messages/t/too-many-positional-arguments/good.py @@ -0,0 +1,14 @@ +def calculate_drag_force(*, velocity, area, density, drag_coefficient): + """This function is 'Keyword only' for all args due to the '*'.""" + return 0.5 * drag_coefficient * density * area * velocity**2 + + +# This is now impossible to do and will raise a TypeError: +# drag_force = calculate_drag_force(30, 2.5, 1.225, 0.47) +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# TypeError: calculate_drag_force() takes 0 positional arguments but 4 were given + +# And this is the only way to call 'calculate_drag_force' +drag_force = calculate_drag_force( + velocity=30, area=2.5, density=1.225, drag_coefficient=0.47 +) diff --git a/doc/data/messages/t/too-many-positional-arguments/pylintrc b/doc/data/messages/t/too-many-positional-arguments/pylintrc new file mode 100644 index 00000000000..dceb2066b7e --- /dev/null +++ b/doc/data/messages/t/too-many-positional-arguments/pylintrc @@ -0,0 +1,2 @@ +[DESIGN] +max-positional-arguments=3 diff --git a/doc/data/messages/t/too-many-positional-arguments/related.rst b/doc/data/messages/t/too-many-positional-arguments/related.rst new file mode 100644 index 00000000000..31aef065b3d --- /dev/null +++ b/doc/data/messages/t/too-many-positional-arguments/related.rst @@ -0,0 +1 @@ +- `Special parameters in python `_ diff --git a/doc/data/messages/t/too-many-positional/details.rst b/doc/data/messages/t/too-many-positional/details.rst deleted file mode 100644 index b9acd905f03..00000000000 --- a/doc/data/messages/t/too-many-positional/details.rst +++ /dev/null @@ -1 +0,0 @@ -Reserved message name, not yet implemented. diff --git a/doc/data/messages/t/too-many-positional/related.rst b/doc/data/messages/t/too-many-positional/related.rst deleted file mode 100644 index 4db259748f6..00000000000 --- a/doc/data/messages/t/too-many-positional/related.rst +++ /dev/null @@ -1,2 +0,0 @@ -- `Ruff discussion `_ -- `Pylint issue `_ diff --git a/doc/data/messages/t/typevar-name-incorrect-variance/details.rst b/doc/data/messages/t/typevar-name-incorrect-variance/details.rst new file mode 100644 index 00000000000..ae7e3db6483 --- /dev/null +++ b/doc/data/messages/t/typevar-name-incorrect-variance/details.rst @@ -0,0 +1 @@ +When naming type vars, only use a ``_co`` suffix when indicating covariance or ``_contra`` when indicating contravariance. diff --git a/doc/data/messages/u/unnecessary-default-type-args/bad.py b/doc/data/messages/u/unnecessary-default-type-args/bad.py new file mode 100644 index 00000000000..e3d97799a53 --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/bad.py @@ -0,0 +1,4 @@ +from collections.abc import AsyncGenerator, Generator + +a1: AsyncGenerator[int, None] # [unnecessary-default-type-args] +b1: Generator[int, None, None] # [unnecessary-default-type-args] diff --git a/doc/data/messages/u/unnecessary-default-type-args/details.rst b/doc/data/messages/u/unnecessary-default-type-args/details.rst new file mode 100644 index 00000000000..754f532caa8 --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/details.rst @@ -0,0 +1,6 @@ +At the moment, this check only works for ``Generator`` and ``AsyncGenerator``. + +Starting with Python 3.13, the ``SendType`` and ``ReturnType`` default to ``None``. +As such it's no longer necessary to specify them. The ``collections.abc`` variants +don't validate the number of type arguments. Therefore the defaults for these +can be used in earlier versions as well. diff --git a/doc/data/messages/u/unnecessary-default-type-args/good.py b/doc/data/messages/u/unnecessary-default-type-args/good.py new file mode 100644 index 00000000000..e77c0ee4292 --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/good.py @@ -0,0 +1,4 @@ +from collections.abc import AsyncGenerator, Generator + +a1: AsyncGenerator[int] +b1: Generator[int] diff --git a/doc/data/messages/u/unnecessary-default-type-args/pylintrc b/doc/data/messages/u/unnecessary-default-type-args/pylintrc new file mode 100644 index 00000000000..825e13ec0bf --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/pylintrc @@ -0,0 +1,2 @@ +[main] +load-plugins=pylint.extensions.typing diff --git a/doc/data/messages/u/unnecessary-default-type-args/related.rst b/doc/data/messages/u/unnecessary-default-type-args/related.rst new file mode 100644 index 00000000000..1f988ae98bb --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/related.rst @@ -0,0 +1,2 @@ +- `Python documentation for AsyncGenerator `_ +- `Python documentation for Generator `_ diff --git a/doc/data/messages/u/unrecognize-option/details.rst b/doc/data/messages/u/unrecognize-option/details.rst deleted file mode 100644 index efa9a206cd4..00000000000 --- a/doc/data/messages/u/unrecognize-option/details.rst +++ /dev/null @@ -1,3 +0,0 @@ -``Pylint`` warns about options it doesn't recognize both in configuration files -and on the command-line. For example, this message would be raised when invoking -pylint with ``pylint --unknown-option=yes test.py``. diff --git a/doc/data/messages/u/unrecognized-option/details.rst b/doc/data/messages/u/unrecognized-option/details.rst index 66440857789..5cd4d220f1b 100644 --- a/doc/data/messages/u/unrecognized-option/details.rst +++ b/doc/data/messages/u/unrecognized-option/details.rst @@ -2,7 +2,9 @@ One of your options is not recognized. There's nothing to change in your code, but your pylint configuration or the way you launch pylint needs to be modified. -For example you might be launching pylint with the following ``toml`` configuration:: +For example, this message would be raised when invoking pylint with +``pylint --unknown-option=yes test.py``. Or you might be launching +pylint with the following ``toml`` configuration:: [tool.pylint] jars = "10" diff --git a/doc/data/messages/u/using-assignment-expression-in-unsupported-version/bad.py b/doc/data/messages/u/using-assignment-expression-in-unsupported-version/bad.py new file mode 100644 index 00000000000..ca4e4a64ae5 --- /dev/null +++ b/doc/data/messages/u/using-assignment-expression-in-unsupported-version/bad.py @@ -0,0 +1,5 @@ +import random + +# +1: [using-assignment-expression-in-unsupported-version] +if zero_or_one := random.randint(0, 1): + assert zero_or_one == 1 diff --git a/doc/data/messages/u/using-assignment-expression-in-unsupported-version/details.rst b/doc/data/messages/u/using-assignment-expression-in-unsupported-version/details.rst new file mode 100644 index 00000000000..b8ea375aaca --- /dev/null +++ b/doc/data/messages/u/using-assignment-expression-in-unsupported-version/details.rst @@ -0,0 +1 @@ +The assignment expression (walrus) operator (`:=`) was introduced in Python 3.8; to use it, please use a more recent version of Python. diff --git a/doc/data/messages/u/using-assignment-expression-in-unsupported-version/good.py b/doc/data/messages/u/using-assignment-expression-in-unsupported-version/good.py new file mode 100644 index 00000000000..a31a74a6642 --- /dev/null +++ b/doc/data/messages/u/using-assignment-expression-in-unsupported-version/good.py @@ -0,0 +1,5 @@ +import random + +zero_or_one = random.randint(0, 1) +if zero_or_one: + assert zero_or_one == 1 diff --git a/doc/data/messages/u/using-assignment-expression-in-unsupported-version/pylintrc b/doc/data/messages/u/using-assignment-expression-in-unsupported-version/pylintrc new file mode 100644 index 00000000000..77eb3be6456 --- /dev/null +++ b/doc/data/messages/u/using-assignment-expression-in-unsupported-version/pylintrc @@ -0,0 +1,2 @@ +[main] +py-version=3.7 diff --git a/doc/data/messages/u/using-exception-groups-in-unsupported-version/bad.py b/doc/data/messages/u/using-exception-groups-in-unsupported-version/bad.py new file mode 100644 index 00000000000..c225e6e6a87 --- /dev/null +++ b/doc/data/messages/u/using-exception-groups-in-unsupported-version/bad.py @@ -0,0 +1,12 @@ +def f(): + excs = [OSError("error 1"), SystemError("error 2")] + # +1: [using-exception-groups-in-unsupported-version] + raise ExceptionGroup("there were problems", excs) + + +try: # [using-exception-groups-in-unsupported-version] + f() +except* OSError as e: + print("There were OSErrors") +except* SystemError as e: + print("There were SystemErrors") diff --git a/doc/data/messages/u/using-exception-groups-in-unsupported-version/details.rst b/doc/data/messages/u/using-exception-groups-in-unsupported-version/details.rst new file mode 100644 index 00000000000..8d05a037a17 --- /dev/null +++ b/doc/data/messages/u/using-exception-groups-in-unsupported-version/details.rst @@ -0,0 +1 @@ +Exception groups were introduced in Python 3.11; to use it, please use a more recent version of Python. diff --git a/doc/data/messages/u/using-exception-groups-in-unsupported-version/good.py b/doc/data/messages/u/using-exception-groups-in-unsupported-version/good.py new file mode 100644 index 00000000000..e7ac7a5b7aa --- /dev/null +++ b/doc/data/messages/u/using-exception-groups-in-unsupported-version/good.py @@ -0,0 +1,10 @@ +def f(): + raise OSError("error 1") + + +try: + f() +except OSError as e: + print("There were OSErrors") +except SystemError as e: + print("There were SystemErrors") diff --git a/doc/data/messages/u/using-exception-groups-in-unsupported-version/pylintrc b/doc/data/messages/u/using-exception-groups-in-unsupported-version/pylintrc new file mode 100644 index 00000000000..d36622d880e --- /dev/null +++ b/doc/data/messages/u/using-exception-groups-in-unsupported-version/pylintrc @@ -0,0 +1,2 @@ +[main] +py-version=3.10 diff --git a/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/bad.py b/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/bad.py new file mode 100644 index 00000000000..b47bddd33e3 --- /dev/null +++ b/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/bad.py @@ -0,0 +1 @@ +type Vector = list[float] # [using-generic-type-syntax-in-unsupported-version] diff --git a/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/details.rst b/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/details.rst new file mode 100644 index 00000000000..731616e8530 --- /dev/null +++ b/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/details.rst @@ -0,0 +1 @@ +Generic type syntax was introduced in Python 3.12; to use it, please use a more recent version of Python. diff --git a/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/good.py b/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/good.py new file mode 100644 index 00000000000..3f80b01c52d --- /dev/null +++ b/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/good.py @@ -0,0 +1,3 @@ +from typing import TypeAlias + +Vector: TypeAlias = list[float] diff --git a/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/pylintrc b/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/pylintrc new file mode 100644 index 00000000000..8e00cbfea27 --- /dev/null +++ b/doc/data/messages/u/using-generic-type-syntax-in-unsupported-version/pylintrc @@ -0,0 +1,2 @@ +[main] +py-version=3.11 diff --git a/doc/data/messages/u/using-positional-only-args-in-unsupported-version/bad.py b/doc/data/messages/u/using-positional-only-args-in-unsupported-version/bad.py new file mode 100644 index 00000000000..3923db16822 --- /dev/null +++ b/doc/data/messages/u/using-positional-only-args-in-unsupported-version/bad.py @@ -0,0 +1,2 @@ +def add(x, y, /): # [using-positional-only-args-in-unsupported-version] + return x + y diff --git a/doc/data/messages/u/using-positional-only-args-in-unsupported-version/details.rst b/doc/data/messages/u/using-positional-only-args-in-unsupported-version/details.rst new file mode 100644 index 00000000000..b00ed798894 --- /dev/null +++ b/doc/data/messages/u/using-positional-only-args-in-unsupported-version/details.rst @@ -0,0 +1 @@ +Positional-only arguments were introduced in Python 3.8; to use them, please use a more recent version of Python. diff --git a/doc/data/messages/u/using-positional-only-args-in-unsupported-version/good.py b/doc/data/messages/u/using-positional-only-args-in-unsupported-version/good.py new file mode 100644 index 00000000000..bfc542d6aa9 --- /dev/null +++ b/doc/data/messages/u/using-positional-only-args-in-unsupported-version/good.py @@ -0,0 +1,3 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring +def add(x, y): + return x + y diff --git a/doc/data/messages/u/using-positional-only-args-in-unsupported-version/pylintrc b/doc/data/messages/u/using-positional-only-args-in-unsupported-version/pylintrc new file mode 100644 index 00000000000..77eb3be6456 --- /dev/null +++ b/doc/data/messages/u/using-positional-only-args-in-unsupported-version/pylintrc @@ -0,0 +1,2 @@ +[main] +py-version=3.7 diff --git a/doc/data/ruff.toml b/doc/data/ruff.toml index 773f7a313d0..2ad67f67535 100644 --- a/doc/data/ruff.toml +++ b/doc/data/ruff.toml @@ -1,13 +1,20 @@ -ignore = [] # Reading ease is drastically reduced on read the doc after 103 chars # (Because of horizontal scrolling) line-length = 103 + +extend-exclude = [ + "messages/d/duplicate-argument-name/bad.py", + "messages/s/syntax-error/bad.py", +] + +[lint] +ignore = [] select = ["E501", "I"] -[per-file-ignores] -"doc/data/messages/r/reimported/bad.py" = ["I"] -"doc/data/messages/w/wrong-import-order/bad.py" = ["I"] -"doc/data/messages/u/ungrouped-imports/bad.py" = ["I"] -"doc/data/messages/m/misplaced-future/bad.py" = ["I"] -"doc/data/messages/m/multiple-imports/bad.py" = ["I"] +[lint.per-file-ignores] +"messages/m/misplaced-future/bad.py" = ["I"] +"messages/m/multiple-imports/bad.py" = ["I"] +"messages/r/reimported/bad.py" = ["I"] +"messages/u/ungrouped-imports/bad.py" = ["I"] +"messages/w/wrong-import-order/bad.py" = ["I"] diff --git a/doc/development_guide/contributor_guide/contribute.rst b/doc/development_guide/contributor_guide/contribute.rst index dee22aa6912..5746768fad7 100644 --- a/doc/development_guide/contributor_guide/contribute.rst +++ b/doc/development_guide/contributor_guide/contribute.rst @@ -130,7 +130,7 @@ documentation. To test smaller changes you can consider ``build-html``, which sk $ cd doc $ make install-dependencies - $ make build-html + $ make html We're reusing generated files for speed, use ``make clean`` when you want to start from scratch. diff --git a/doc/development_guide/contributor_guide/index.rst b/doc/development_guide/contributor_guide/index.rst index 3576c68ff5a..0226f48239a 100644 --- a/doc/development_guide/contributor_guide/index.rst +++ b/doc/development_guide/contributor_guide/index.rst @@ -12,5 +12,6 @@ The contributor guide will help you if you want to contribute to pylint itself. contribute tests/index profiling + oss_fuzz release governance diff --git a/doc/development_guide/contributor_guide/oss_fuzz.rst b/doc/development_guide/contributor_guide/oss_fuzz.rst new file mode 100644 index 00000000000..6a561b8d8af --- /dev/null +++ b/doc/development_guide/contributor_guide/oss_fuzz.rst @@ -0,0 +1,165 @@ +====================== + OSS-Fuzz integration +====================== + +Platform overview +----------------- + +`OSS-Fuzz `_ is Google's free fuzzing platform for open source +software. It runs astroid's fuzz targets to help detect reliability issues that could affect astroid +and Pylint. + +Google provides public `build logs `_ +and `fuzzing stats `_, but most +of the details about bug reports and fuzzed testcases require approved access. + +Gaining access +^^^^^^^^^^^^^^ + +The configuration files for the OSS-Fuzz integration can be found in the +`OSS-Fuzz repository `_. +The ``project.yaml`` file controls who has access to bug reports and testcases. Ping the +maintainers if you'd like to be added to the list (note: a Google account is required for +access). + +Fuzzing progress +---------------- + +Once you have access to OSS-Fuzz, you can log in to https://oss-fuzz.com/ with your Google account +to see a dashboard of astroid's fuzzing progress. + +Testcases +^^^^^^^^^ + +The dashboard contains a link to a `testcases page `_ +that lists all testcases that currently trigger a bug in astroid. + +Every testcase has a dedicated page with links to view and download a minimized testcase for +reproducing the failure. Each testcase page also contains a stacktrace for the failure and stats +about how often the failure is encountered while fuzzing. + +Reproducing a failure +""""""""""""""""""""" + +You can download a minimized testcase and run it locally to debug a failure on your machine. +For example, to reproduce a failure with the ``fuzz_parse`` fuzz target, you can run the following +commands: + +.. code:: bash + + # Note: Atheris doesn't support Python 3.12+ yet: + # https://github.com/google/atheris/issues/82 + mkdir fuzzing-repro + cd fuzzing-repro + + pyenv install --skip-existing 3.11 + pyenv shell 3.11 + + python -m venv .venv-fuzzing-repro + source .venv-fuzzing-repro/bin/activate + + git clone https://github.com/pylint-dev/astroid.git + cd astroid + + pip install atheris + pip install --editable . + + # Save the minimized testcase as `minimized.py` in the astroid directory + + cat << EOF > ./run_fuzz_parse.py + + import astroid + import atheris + + with open('minimized.py', 'rb') as f: + fdp = atheris.FuzzedDataProvider(f.read()) + + code = fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(0, 4096)) + astroid.builder.parse(code) + EOF + + python ./run_fuzz_parse.py + + +If the failure does not reproduce locally, you can try reproducing the issue in an OSS-Fuzz +container: + +.. code:: bash + + git clone https://github.com/google/oss-fuzz.git + cd oss-fuzz + + python infra/helper.py build_image astroid + python infra/helper.py build_fuzzers astroid + python infra/helper.py reproduce astroid fuzz_parse minimized.py + +Some failures may only be reproducible in an OSS-Fuzz container because of differences in Python +versions between the OSS-Fuzz platform and your local environment. + +Code coverage +^^^^^^^^^^^^^ + +The dashboard also links to code coverage data for individual fuzz targets and combined code +coverage data for all targets (click on the "TOTAL COVERAGE" link for the combined data). + +The combined coverage data is helpful for identifying coverage gaps, insufficient corpus data, and +potential candidates for future fuzz targets. + +Bug reports +^^^^^^^^^^^ + +Bug reports for new failures are automatically filed in the OSS-Fuzz bug tracker with an +`astroid label `_. +Make sure you are logged in to view all existing issues. + +Build maintenance +----------------- + +Google runs compiled fuzz targets on Google Compute Engine VMs. This architecture requires each +project to provide a ``Dockerfile`` and ``build.sh`` script to download code, configure +dependencies, compile fuzz targets, and package any corpus files. + +astroid's build files and fuzz-target code can be found in the +`OSS-Fuzz repo `_. + +If dependencies change or if new fuzz targets are added, then you may need to modify the build files +and build a new Docker image for OSS-Fuzz. + +Building an image +^^^^^^^^^^^^^^^^^ + +Run the following commands to build astroid's OSS-Fuzz image and fuzz targets: + +.. code:: bash + + git clone https://github.com/google/oss-fuzz.git + cd oss-fuzz + + python infra/helper.py build_image astroid + python infra/helper.py build_fuzzers astroid + +Any changes you make to the build files must be submitted as pull requests to the OSS-Fuzz repo. + +Debugging build failures +"""""""""""""""""""""""" + +You can debug build failures during the ``build_fuzzers`` stage by creating a container and manually +running the ``compile`` command: + +.. code:: bash + + # Create a container for building fuzz targets + python infra/helper.py shell astroid + + # Run this command inside the container to build the fuzz targets + compile + +The ``build.sh`` script will be located at ``/src/build.sh`` inside the container. + +Quick links +----------- + +- `OSS-Fuzz dashboard `_ +- `OSS-Fuzz configuration files, build scripts, and fuzz targets for astroid `_ +- `All open OSS-Fuzz bugs for astroid `_ +- `Google's OSS-Fuzz documentation `_ diff --git a/doc/development_guide/contributor_guide/profiling.rst b/doc/development_guide/contributor_guide/profiling.rst index bb49038fff1..9472247eb9e 100644 --- a/doc/development_guide/contributor_guide/profiling.rst +++ b/doc/development_guide/contributor_guide/profiling.rst @@ -90,7 +90,7 @@ and thus is displayed as being 0. Often you are more interested in the cumulative time (per call). This refers to the time spent within the function and any of the functions it called or the functions they called (etc.). In our example, the ``visit_importfrom`` -method and all of its child-functions took a little over 8 seconds to exectute, with an execution time of +method and all of its child-functions took a little over 8 seconds to execute, with an execution time of 0.013 ms per call. You can also search the ``profiler_stats`` for an individual function you want to check. For example diff --git a/doc/development_guide/contributor_guide/tests/launching_test.rst b/doc/development_guide/contributor_guide/tests/launching_test.rst index 78f42e144ae..48c3a0649b0 100644 --- a/doc/development_guide/contributor_guide/tests/launching_test.rst +++ b/doc/development_guide/contributor_guide/tests/launching_test.rst @@ -30,7 +30,7 @@ tox You can also *optionally* install tox_ and run our tests using the tox_ package, as in:: python -m tox - python -m tox -epy38 # for Python 3.8 suite only + python -m tox -epy312 # for Python 3.12 suite only python -m tox -epylint # for running Pylint over Pylint's codebase python -m tox -eformatting # for running formatting checks over Pylint's codebase diff --git a/doc/development_guide/contributor_guide/tests/writing_test.rst b/doc/development_guide/contributor_guide/tests/writing_test.rst index 9ce9ca1f0d6..481bd27cef3 100644 --- a/doc/development_guide/contributor_guide/tests/writing_test.rst +++ b/doc/development_guide/contributor_guide/tests/writing_test.rst @@ -21,7 +21,7 @@ Unittest tests Most other tests reside in the '/pylint/test' directory. These unittests can be used to test almost all functionality within Pylint. A good step before writing any new unittests is to look -at some tests that test a similar funcitionality. This can often help write new tests. +at some tests that test a similar functionality. This can often help write new tests. If your new test requires any additional files you can put those in the ``/pylint/test/regrtest_data`` directory. This is the directory we use to store any data needed for diff --git a/doc/exts/pylint_messages.py b/doc/exts/pylint_messages.py index 7fc694bcc21..6da05064433 100644 --- a/doc/exts/pylint_messages.py +++ b/doc/exts/pylint_messages.py @@ -12,7 +12,7 @@ from inspect import getmodule from itertools import chain, groupby from pathlib import Path -from typing import DefaultDict, Dict, List, NamedTuple, Tuple +from typing import NamedTuple from sphinx.application import Sphinx @@ -34,6 +34,27 @@ MSG_TYPES_DOC = {k: v if v != "info" else "information" for k, v in MSG_TYPES.items()} +MESSAGES_WITHOUT_EXAMPLES = { + "astroid-error", # internal + "bad-configuration-section", # configuration + "bad-plugin-value", # internal + "c-extension-no-member", # not easy to implement in the current doc framework + "config-parse-error", # configuration + "fatal", # internal + "import-self", # not easy to implement in the current doc framework + "invalid-character-nul", # not easy to implement in the current doc framework + "invalid-characters-in-docstring", # internal in py-enchant + "invalid-unicode-codec", # placeholder (not implemented yet) + "method-check-failed", # internal + "parse-error", # internal + "raw-checker-failed", # internal + "unrecognized-option", # configuration +} +MESSAGES_WITHOUT_BAD_EXAMPLES = { + "invalid-character-carriage-return", # can't be raised in normal pylint use + "return-arg-in-generator", # can't be raised in modern python +} + class MessageData(NamedTuple): checker: str @@ -52,8 +73,8 @@ class ExampleType(str, Enum): BAD = "bad" -MessagesDict = Dict[str, List[MessageData]] -OldMessagesDict = Dict[str, DefaultDict[Tuple[str, str], List[Tuple[str, str]]]] +MessagesDict = dict[str, list[MessageData]] +OldMessagesDict = dict[str, defaultdict[tuple[str, str], list[tuple[str, str]]]] """DefaultDict is indexed by tuples of (old name symbol, old name id) and values are tuples of (new name symbol, new name category). """ @@ -99,6 +120,11 @@ def _get_pylintrc_code(data_path: Path) -> str: def _get_demo_code_for(data_path: Path, example_type: ExampleType) -> str: """Get code examples while handling multi-file code templates.""" + if data_path.name in MESSAGES_WITHOUT_EXAMPLES or ( + data_path.name in MESSAGES_WITHOUT_BAD_EXAMPLES + and example_type is ExampleType.BAD + ): + return "" single_file_path = data_path / f"{example_type.value}.py" multiple_code_path = data_path / f"{example_type.value}" @@ -130,8 +156,9 @@ def _get_demo_code_for(data_path: Path, example_type: ExampleType) -> str: """ ) return _get_titled_rst(title=title, text="\n".join(files)) - - return "" + raise AssertionError( + f"Please add a {example_type.value} code example for {data_path}" + ) def _check_placeholders( @@ -187,9 +214,7 @@ def _get_ini_as_rst(code_path: Path) -> str: """ -def _get_all_messages( - linter: PyLinter, -) -> tuple[MessagesDict, OldMessagesDict]: +def _get_all_messages(linter: PyLinter) -> tuple[MessagesDict, OldMessagesDict]: """Get all messages registered to a linter and return a dictionary indexed by message type. @@ -241,8 +266,8 @@ def _get_all_messages( if message.old_names: for old_name in message.old_names: category = MSG_TYPES_DOC[old_name[0][0]] - # We check if the message is already in old_messages so - # we don't duplicate shared messages. + # 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]) ]: @@ -328,7 +353,7 @@ def _generate_single_message_body(message: MessageData) -> str: """ - body += "\n" + message.example_code + "\n" + body += f"\n{message.example_code}\n" if message.checker_module_name.startswith("pylint.extensions."): body += f""" diff --git a/doc/exts/pylint_options.py b/doc/exts/pylint_options.py index d06a9b87d06..7ec9baa26e3 100644 --- a/doc/exts/pylint_options.py +++ b/doc/exts/pylint_options.py @@ -10,7 +10,7 @@ from collections import defaultdict from inspect import getmodule from pathlib import Path -from typing import Dict, List, NamedTuple +from typing import NamedTuple import tomlkit from sphinx.application import Sphinx @@ -30,7 +30,7 @@ class OptionsData(NamedTuple): extension: bool -OptionsDataDict = Dict[str, List[OptionsData]] +OptionsDataDict = dict[str, list[OptionsData]] PYLINT_BASE_PATH = Path(__file__).resolve().parent.parent.parent """Base path to the project folder.""" diff --git a/doc/exts/pyreverse_configuration.py b/doc/exts/pyreverse_configuration.py new file mode 100644 index 00000000000..f4dde543574 --- /dev/null +++ b/doc/exts/pyreverse_configuration.py @@ -0,0 +1,97 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Script used to generate the pyreverse configuration page.""" + +from __future__ import annotations + +from pathlib import Path +from typing import NamedTuple + +from sphinx.application import Sphinx + +from pylint.pyreverse.main import OPTIONS_GROUPS, Run +from pylint.typing import OptionDict +from pylint.utils import get_rst_title + + +class OptionsData(NamedTuple): + name: str + optdict: OptionDict + + +PYREVERSE_PATH = ( + Path(__file__).resolve().parent.parent / "additional_tools" / "pyreverse" +) +"""Path to the pyreverse documentation folder.""" + + +def _write_config_page(run: Run) -> None: + """Create or overwrite the configuration page.""" + sections: list[str] = [ + f"""\ +.. This file is auto-generated. Make any changes to the associated +.. docs extension in 'doc/exts/pyreverse_configuration.py'. + + +{get_rst_title("Usage", "#")} + +``pyreverse`` is run from the command line using the following syntax:: + + pyreverse [options] + +where ```` is one or more Python packages or modules to analyze. + +The available options are organized into the following categories: + +* :ref:`filtering-and-scope` - Control which classes and relationships appear in your diagrams +* :ref:`display-options` - Customize the visual appearance including colors and labels +* :ref:`output-control` - Select output formats and set the destination directory +* :ref:`project-configuration` - Define project settings like source roots and ignored files +""" + ] + options: list[OptionsData] = [OptionsData(name, info) for name, info in run.options] + option_groups: dict[str, list[str]] = {g: [] for g in OPTIONS_GROUPS.values()} + + for option in sorted(options, key=lambda x: x.name): + option_string = get_rst_title(f"--{option.name}", "-") + option_string += f"*{option.optdict.get('help')}*\n\n" + + if option.optdict.get("default") == "": + option_string += '**Default:** ``""``\n\n\n' + else: + option_string += f"**Default:** ``{option.optdict.get('default')}``\n\n\n" + + option_groups[str(option.optdict.get("group"))].append(option_string) + + for group_title in OPTIONS_GROUPS.values(): + ref_title = group_title.lower().replace(" ", "-") + sections.append( + f"""\ +.. _{ref_title}: + +{get_rst_title(group_title, "=")} + +{"".join(option_groups[group_title])}""" + ) + + # Join all sections and remove the final two newlines + final_page = "\n\n".join(sections)[:-2] + + with open(PYREVERSE_PATH / "configuration.rst", "w", encoding="utf-8") as stream: + stream.write(final_page) + + +# pylint: disable-next=unused-argument +def build_options_page(app: Sphinx | None) -> None: + # Write configuration page + _write_config_page(Run([])) + + +def setup(app: Sphinx) -> dict[str, bool]: + """Connects the extension to the Sphinx process.""" + # Register callback at the builder-inited Sphinx event + # See https://www.sphinx-doc.org/en/master/extdev/appapi.html + app.connect("builder-inited", build_options_page) + return {"parallel_read_safe": True} diff --git a/doc/faq.rst b/doc/faq.rst index 3706c25914e..a828e3dc8e8 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -67,12 +67,16 @@ to not be included as default messages. You can see the plugin you need to explicitly :ref:`load in the technical reference `. -I want to use pylint on each keystroke in my IDE, how can I do that ? +I want to run pylint on each keystroke in my IDE. How do I do that? --------------------------------------------------------------------- -Don't do it: pylint's full suite of checks is not fast enough for that and never -will be. pylint is best suited for linting on save for small projects, or for a continuous -integration job or a git ``pre-push`` hook for big projects. The larger your repository +Pylint full suite of checks will never be fast enough to run on every keystroke. +However, some IDEs can run pylint when the IDE opens or saves files. +See, for example, the `Microsoft plugin for VS Code`_. + + +That said, pylint is best suited for linting on save for small projects, for continuous +integration jobs, or a git ``pre-push`` hook for big projects. The larger your repository is, the slower pylint will be. If you want to make pylint faster for this type of use case, you can use the ``--errors-only`` @@ -80,6 +84,8 @@ option, which will remove all the refactor, convention, and warning checks. You checks with inherently high complexity that need to analyse the full code base like ``duplicate-code`` or ``cyclic-import`` (this list is not exhaustive). +.. _`Microsoft plugin for VS Code`: https://github.com/microsoft/vscode-pylint#readme + Why do I have non-deterministic results when I try to parallelize pylint ? -------------------------------------------------------------------------- @@ -102,7 +108,7 @@ pydocstyle_: missing-module-docstring, missing-class-docstring, missing-function pep8-naming_: invalid-name, bad-classmethod-argument, bad-mcs-classmethod-argument, no-self-argument -isort_ and flake8-import-order_: wrong-import-order +isort_ and flake8-import-order_: ungrouped-imports, wrong-import-order .. _`pycodestyle`: https://github.com/PyCQA/pycodestyle .. _`pyflakes`: https://github.com/PyCQA/pyflakes diff --git a/doc/index.rst b/doc/index.rst index 1e2c8f044af..d1e862987be 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -30,11 +30,12 @@ .. toctree:: :caption: Additional tools + :maxdepth: 3 :titlesonly: :hidden: - pyreverse - symilar + additional_tools/pyreverse/index + additional_tools/symilar/index .. toctree:: :caption: Changelog diff --git a/doc/make.bat b/doc/make.bat index e789688bb41..b327f5acc39 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -11,172 +11,38 @@ if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) +REM Check if no argument is provided, or if "help" is requested if "%1" == "" goto help +if "%1" == "help" goto help -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end +REM Command options +if "%1" == "install-dependencies" ( + echo Installing dependencies... + cd .. && pip install -r doc/requirements.txt + goto end ) if "%1" == "html" ( + echo Building HTML... %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 %SPHINXBUILD% -b linkcheck -q %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\a.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\a.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. + echo Link check complete; look for any errors in the above output or in %BUILDDIR%/linkcheck/output.txt. goto end ) -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %ALLSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck -q %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) +REM Help section +:help +echo. Please use `make ^` where ^ is one of: +echo. +echo. install-dependencies to install required documentation dependencies +echo. html to make standalone HTML files +echo. +goto end :end +echo Script completed. diff --git a/doc/requirements.txt b/doc/requirements.txt index f4373c3deb1..7ebf8712c8c 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ -Sphinx==7.3.7 +Sphinx==8.1.3 sphinx-reredirects<1 -towncrier~=23.11 -furo==2024.5.6 +towncrier~=24.8 +furo==2024.8.6 -e . diff --git a/doc/test_messages_documentation.py b/doc/test_messages_documentation.py index 96d98ed6c53..6898384f790 100644 --- a/doc/test_messages_documentation.py +++ b/doc/test_messages_documentation.py @@ -6,21 +6,9 @@ from __future__ import annotations -import sys - -if sys.version_info[:2] > (3, 9): - from collections import Counter -else: - from collections import Counter as _Counter - - class Counter(_Counter): - def total(self): - return len(tuple(self.elements())) - - +from collections import Counter from pathlib import Path -from typing import Counter as CounterType -from typing import TextIO, Tuple +from typing import TextIO import pytest @@ -31,7 +19,7 @@ def total(self): from pylint.testutils.constants import _EXPECTED_RE from pylint.testutils.reporter_for_tests import FunctionalTestReporter -MessageCounter = CounterType[Tuple[int, str]] +MessageCounter = Counter[tuple[int, str]] def get_functional_test_files_from_directory(input_dir: Path) -> list[tuple[str, Path]]: diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index 95462f92189..bb8c57eccb9 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -685,9 +685,15 @@ Typing checker Messages :consider-using-alias (R6002): *'%s' will be deprecated with PY39, consider using '%s' instead%s* Only emitted if 'runtime-typing=no' and a deprecated typing alias is used in a type annotation context in Python 3.7 or 3.8. -: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'. +: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 + shorthand union syntax. For example, ``Union[int, float]`` instead of ``int | + float``. Using the shorthand for unions aligns with Python typing + recommendations, removes the need for imports, and avoids confusion in + function signatures. +:unnecessary-default-type-args (R6007): *Type `%s` has unnecessary default type args. Change it to `%s`.* + Emitted when types have default type args which can be omitted. Mainly used + for `typing.Generator` and `typing.AsyncGenerator`. :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. diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index 786495754f0..670b7d67ec1 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -282,6 +282,9 @@ Classes checker Messages 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! +:declare-non-slot (E0245): *No such name %r in __slots__* + Raised when a type annotation on a class is absent from the list of names in + __slots__, and __slots__ does not contain a __dict__ entry. :unexpected-special-method-signature (E0302): *The special method %r expects %s param(s), %d %s given* Emitted when a special method was defined with an invalid number of parameters. If it has too few or too many, it might not work at all. @@ -325,7 +328,7 @@ Classes checker Messages interface or in an overridden method. :protected-access (W0212): *Access to a protected member %s of a client class* Used when a protected member (i.e. class member with a name beginning with an - underscore) is access outside the class or a descendant of the class where + underscore) is accessed outside the class or a descendant of the class where it's defined. :attribute-defined-outside-init (W0201): *Attribute %r defined outside __init__* Used when an instance attribute is defined outside the __init__ method. @@ -434,10 +437,8 @@ Design checker Messages simpler (and so easier to use) class. :too-many-locals (R0914): *Too many local variables (%s/%s)* Used when a function or method has too many local variables. -:too-many-positional (R0917): *Too many positional arguments in a function call.* - Will be implemented in https://github.com/pylint- - dev/pylint/issues/9099,msgid/symbol pair reserved for compatibility with - ruff, see https://github.com/astral-sh/ruff/issues/8946. +:too-many-positional-arguments (R0917): *Too many positional arguments (%s/%s)* + Used when a function has too many positional arguments. :too-many-public-methods (R0904): *Too many public methods (%s/%s)* Used when class has too many public methods, try to reduce this to get a simpler (and so easier to use) class. @@ -473,9 +474,8 @@ Exceptions checker Messages :raising-bad-type (E0702): *Raising %s while only classes or instances are allowed* Used when something which is neither a class nor an instance is raised (i.e. a `TypeError` will be raised). -:raising-non-exception (E0710): *Raising a new style class which doesn't inherit from BaseException* - Used when a new style class which doesn't inherit from BaseException is - raised. +:raising-non-exception (E0710): *Raising a class which doesn't inherit from BaseException* + Used when a class which doesn't inherit from BaseException is raised. :misplaced-bare-raise (E0704): *The raise statement is not inside an except clause* Used when a bare raise is not used inside an except clause. This generates an error, since there are no active exceptions to be reraised. An exception to @@ -684,8 +684,7 @@ 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. + Remove the keywords for the affected arguments in the function call. :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. @@ -892,13 +891,15 @@ Refactoring checker Messages containing a continue statement. As such, it will warn when it encounters an else following a chain of ifs, all of them containing a continue statement. :no-else-raise (R1720): *Unnecessary "%s" after "raise", %s* - Used in order to highlight an unnecessary block of code following an if - containing a raise statement. As such, it will warn when it encounters an - else following a chain of ifs, all of them containing a raise statement. + Used in order to highlight an unnecessary block of code following an if, or a + try/except containing a raise statement. As such, it will warn when it + encounters an else following a chain of ifs, all of them containing a raise + statement. :no-else-return (R1705): *Unnecessary "%s" after "return", %s* - Used in order to highlight an unnecessary block of code following an if - containing a return statement. As such, it will warn when it encounters an - else following a chain of ifs, all of them containing a return statement. + Used in order to highlight an unnecessary block of code following an if, or a + try/except containing a return statement. As such, it will warn when it + encounters an else following a chain of ifs, all of them containing a return + statement. :unnecessary-dict-index-lookup (R1733): *Unnecessary dictionary index lookup, use '%s' instead* Emitted when iterating over the dictionary items (key-item pairs) and accessing the value by index lookup. The value can be accessed directly @@ -919,16 +920,16 @@ 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-to-string (C1804): *"%s" can be simplified to "%s", if it is striclty a string, as an empty string is falsey* - Empty string are considered false in a boolean context. Following this check - blindly in weakly typed code base can create hard to debug issues. If the - value can be something else that is falsey but not a string (for example - ``None``, an empty sequence, or ``0``) the code will not be equivalent. :use-implicit-booleaness-not-comparison (C1803): *"%s" can be simplified to "%s", if it is strictly a sequence, as an empty %s is falsey* Empty sequences are considered false in a boolean context. Following this check blindly in weakly typed code base can create hard to debug issues. If the value can be something else that is falsey but not a sequence (for example ``None``, an empty string, or ``0``) the code will not be equivalent. +:use-implicit-booleaness-not-comparison-to-string (C1804): *"%s" can be simplified to "%s", if it is strictly a string, as an empty string is falsey* + Empty string are considered false in a boolean context. Following this check + blindly in weakly typed code base can create hard to debug issues. If the + value can be something else that is falsey but not a string (for example + ``None``, an empty sequence, or ``0``) the code will not be equivalent. :use-implicit-booleaness-not-comparison-to-zero (C1805): *"%s" can be simplified to "%s", if it is strictly an int, as 0 is falsey* 0 is considered false in a boolean context. Following this check blindly in weakly typed code base can create hard to debug issues. If the value can be @@ -1349,9 +1350,21 @@ Verbatim name of the checker is ``unsupported_version``. Unsupported Version checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:using-assignment-expression-in-unsupported-version (W2605): *Assignment expression is not supported by all versions included in the py-version setting* + Used when the py-version set by the user is lower than 3.8 and pylint + encounters an assignment expression (walrus) operator. +:using-exception-groups-in-unsupported-version (W2603): *Exception groups are not supported by all versions included in the py-version setting* + Used when the py-version set by the user is lower than 3.11 and pylint + encounters ``except*`` or `ExceptionGroup``. :using-f-string-in-unsupported-version (W2601): *F-strings are not supported by all versions included in the py-version setting* Used when the py-version set by the user is lower than 3.6 and pylint encounters an f-string. +:using-generic-type-syntax-in-unsupported-version (W2604): *Generic type syntax (PEP 695) is not supported by all versions included in the py-version setting* + Used when the py-version set by the user is lower than 3.12 and pylint + encounters generic type syntax. +:using-positional-only-args-in-unsupported-version (W2606): *Positional-only arguments are not supported by all versions included in the py-version setting* + Used when the py-version set by the user is lower than 3.8 and pylint + encounters positional-only arguments. :using-final-decorator-in-unsupported-version (W2602): *typing.final is not supported by all versions included in the py-version setting* Used when the py-version set by the user is lower than 3.8 and pylint encounters a ``typing.final`` decorator. diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index eb7d72f2a36..58f3a04b7c4 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -765,6 +765,13 @@ Standard Checkers **Default:** ``7`` +--max-positional-arguments +"""""""""""""""""""""""""" +*Maximum number of positional arguments for function / method.* + +**Default:** ``5`` + + --max-public-methods """""""""""""""""""" *Maximum number of public methods for a class (see R0904).* @@ -822,6 +829,8 @@ Standard Checkers max-parents = 7 + max-positional-arguments = 5 + max-public-methods = 20 max-returns = 6 @@ -1151,6 +1160,13 @@ Standard Checkers ``Miscellaneous`` **Checker** ----------------------------- +--check-fixme-in-docstring +"""""""""""""""""""""""""" +*Whether or not to search for fixme's in docstrings.* + +**Default:** ``False`` + + --notes """"""" *List of note tags to take in consideration, separated by a comma.* @@ -1176,6 +1192,8 @@ Standard Checkers .. code-block:: toml [tool.pylint.miscellaneous] + check-fixme-in-docstring = false + notes = ["FIXME", "XXX", "TODO"] notes-rgx = "" diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index 99cf238480e..fc487fc25c0 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -69,6 +69,7 @@ All messages in the error category: error/catching-non-exception error/class-variable-slots-conflict error/continue-in-finally + error/declare-non-slot error/dict-iter-missing-items error/duplicate-argument-name error/duplicate-bases @@ -347,9 +348,13 @@ All messages in the warning category: warning/useless-parent-delegation warning/useless-type-doc warning/useless-with-lock + warning/using-assignment-expression-in-unsupported-version warning/using-constant-test + warning/using-exception-groups-in-unsupported-version warning/using-f-string-in-unsupported-version warning/using-final-decorator-in-unsupported-version + warning/using-generic-type-syntax-in-unsupported-version + warning/using-positional-only-args-in-unsupported-version warning/while-used warning/wildcard-import warning/wrong-exception-operation @@ -534,12 +539,13 @@ All messages in the refactor category: refactor/too-many-instance-attributes refactor/too-many-locals refactor/too-many-nested-blocks - refactor/too-many-positional + refactor/too-many-positional-arguments refactor/too-many-public-methods refactor/too-many-return-statements refactor/too-many-statements refactor/trailing-comma-tuple refactor/unnecessary-comprehension + refactor/unnecessary-default-type-args refactor/unnecessary-dict-index-lookup refactor/unnecessary-list-index-lookup refactor/use-a-generator diff --git a/doc/user_guide/usage/run.rst b/doc/user_guide/usage/run.rst index e7462a8f5d1..8c4d520cea5 100644 --- a/doc/user_guide/usage/run.rst +++ b/doc/user_guide/usage/run.rst @@ -18,8 +18,7 @@ On versions below 2.15, specifying a directory that is not an explicit package 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) -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:: +Thus, on versions before 2.15 using the ``--recursive=y`` option allows for linting a namespace package:: pylint --recursive=y mydir mymodule mypackage diff --git a/doc/whatsnew/0/0.x.rst b/doc/whatsnew/0/0.x.rst index 68abe047673..793f07a941c 100644 --- a/doc/whatsnew/0/0.x.rst +++ b/doc/whatsnew/0/0.x.rst @@ -1001,7 +1001,7 @@ Release date: 2004-10-19 * avoid importing analyzed modules ! * new Refactor and Convention message categories. Some Warnings have been - remaped into those new categories + remapped into those new categories * added "similar", a tool to find copied and pasted lines of code, both using a specific command line tool and integrated as a diff --git a/doc/whatsnew/1/1.5.rst b/doc/whatsnew/1/1.5.rst index f8ce0360606..51c7cf2ce95 100644 --- a/doc/whatsnew/1/1.5.rst +++ b/doc/whatsnew/1/1.5.rst @@ -435,7 +435,7 @@ Release date: 2015-11-29 * astroid.utils.LocalsVisitor was moved to pylint.pyreverse.LocalsVisitor. * pylint.checkers.utils.excepts_import_error was removed. - Use pylint.chekcers.utils.error_of_type instead. + Use pylint.checkers.utils.error_of_type instead. * Don't emit undefined-all-variables for nodes which can't be inferred (YES nodes). diff --git a/doc/whatsnew/2/2.0/summary.rst b/doc/whatsnew/2/2.0/summary.rst index c74f4dc5b80..52d89450733 100644 --- a/doc/whatsnew/2/2.0/summary.rst +++ b/doc/whatsnew/2/2.0/summary.rst @@ -221,7 +221,7 @@ Other Changes ``pylint`` should be a bit faster as well. We added a new flag, ``max_inferable_values`` on ``astroid.MANAGER`` for - limitting the maximum amount of values that ``astroid`` can infer when inferring + limiting the maximum amount of values that ``astroid`` can infer when inferring values. This change should improve the performance when dealing with large frameworks such as ``django``. You can also control this behaviour with ``pylint --limit-inference-results`` diff --git a/doc/whatsnew/2/2.13/summary.rst b/doc/whatsnew/2/2.13/summary.rst index ddfb98f8400..e2ad8698e66 100644 --- a/doc/whatsnew/2/2.13/summary.rst +++ b/doc/whatsnew/2/2.13/summary.rst @@ -81,7 +81,7 @@ New checkers creates memory leaks by never letting the instance get garbage collected. Closes #5670 - Clsoes #6180 + Closes #6180 Removed checkers ================ diff --git a/doc/whatsnew/2/2.4/full.rst b/doc/whatsnew/2/2.4/full.rst index 079e37707bb..ab871c8b5c3 100644 --- a/doc/whatsnew/2/2.4/full.rst +++ b/doc/whatsnew/2/2.4/full.rst @@ -137,7 +137,7 @@ Release date: 2019-09-24 Closes #2925 * ``useless-suppression`` check now ignores ``cyclic-import`` suppressions, - which could lead to false postiives due to incomplete context at the time + which could lead to false positives due to incomplete context at the time of the check. Closes #3064 diff --git a/doc/whatsnew/2/2.6/full.rst b/doc/whatsnew/2/2.6/full.rst index 3896677dc09..dba21434979 100644 --- a/doc/whatsnew/2/2.6/full.rst +++ b/doc/whatsnew/2/2.6/full.rst @@ -54,7 +54,7 @@ Release date: 2020-08-20 * Add ``raise-missing-from`` check for exceptions that should have a cause. -* Support both isort 4 and isort 5. If you have pinned isort 4 in your projet requirements, nothing changes. If you use isort 5, though, note that the ``known-standard-library`` option is not interpreted the same in isort 4 and isort 5 (see the migration guide in isort documentation for further details). For compatibility's sake for most pylint users, the ``known-standard-library`` option in pylint now maps to ``extra-standard-library`` in isort 5. If you really want what ``known-standard-library`` now means in isort 5, you must disable the ``wrong-import-order`` check in pylint and run isort manually with a proper isort configuration file. +* Support both isort 4 and isort 5. If you have pinned isort 4 in your project requirements, nothing changes. If you use isort 5, though, note that the ``known-standard-library`` option is not interpreted the same in isort 4 and isort 5 (see the migration guide in isort documentation for further details). For compatibility's sake for most pylint users, the ``known-standard-library`` option in pylint now maps to ``extra-standard-library`` in isort 5. If you really want what ``known-standard-library`` now means in isort 5, you must disable the ``wrong-import-order`` check in pylint and run isort manually with a proper isort configuration file. Closes #3722 diff --git a/doc/whatsnew/2/2.6/summary.rst b/doc/whatsnew/2/2.6/summary.rst index e4aee082f6e..9061bd00c05 100644 --- a/doc/whatsnew/2/2.6/summary.rst +++ b/doc/whatsnew/2/2.6/summary.rst @@ -25,4 +25,4 @@ Other Changes * Fix superfluous-parens false-positive for the walrus operator -* Add support for both isort 4 and isort 5. If you have pinned isort 4 in your projet requirements, nothing changes. If you use isort 5, though, note that the ``known-standard-library`` option is not interpreted the same in isort 4 and isort 5 (see `the migration guide in isort documentation` (no longer available) for further details). For compatibility's sake for most pylint users, the ``known-standard-library`` option in pylint now maps to ``extra-standard-library`` in isort 5. If you really want what ``known-standard-library`` now means in isort 5, you must disable the ``wrong-import-order`` check in pylint and run isort manually with a proper isort configuration file. +* Add support for both isort 4 and isort 5. If you have pinned isort 4 in your project requirements, nothing changes. If you use isort 5, though, note that the ``known-standard-library`` option is not interpreted the same in isort 4 and isort 5 (see `the migration guide in isort documentation` (no longer available) for further details). For compatibility's sake for most pylint users, the ``known-standard-library`` option in pylint now maps to ``extra-standard-library`` in isort 5. If you really want what ``known-standard-library`` now means in isort 5, you must disable the ``wrong-import-order`` check in pylint and run isort manually with a proper isort configuration file. diff --git a/doc/whatsnew/3/3.0/index.rst b/doc/whatsnew/3/3.0/index.rst index aad65c984c5..cc975ca40d7 100644 --- a/doc/whatsnew/3/3.0/index.rst +++ b/doc/whatsnew/3/3.0/index.rst @@ -388,7 +388,7 @@ New Checks Closes #8260 (`#8260 `_) -- Add a new checker ``kwarg-superseded-by-positional-arg`` to warn when a function is called with a keyword argument which shares a name with a positional-only parameter and the function contains a keyword variadic parameter dictionary. It may be surprising behaviour when the keyword argument is added to the keyword variadic parameter dictionary. +- Add a new message ``kwarg-superseded-by-positional-arg`` to warn when a function is called with a keyword argument which shares a name with a positional-only parameter and the function contains a keyword variadic parameter dictionary. It may be surprising behaviour when the keyword argument is added to the keyword variadic parameter dictionary. Closes #8558 (`#8558 `_) diff --git a/doc/whatsnew/3/3.1/index.rst b/doc/whatsnew/3/3.1/index.rst index 1abe832dcbd..900a5ef0b62 100644 --- a/doc/whatsnew/3/3.1/index.rst +++ b/doc/whatsnew/3/3.1/index.rst @@ -118,7 +118,7 @@ Other Bug Fixes The message will report imports as follows: For "import X", it will report "(standard/third party/first party/local) import X" For "import X.Y" and "from X import Y", it will report "(standard/third party/first party/local) import X.Y" - The import category is specified to provide explanation as to why pylint has issued the message and guidence to the developer on how to fix the problem. + The import category is specified to provide explanation as to why pylint has issued the message and guidance to the developer on how to fix the problem. Closes #8808 (`#8808 `_) diff --git a/doc/whatsnew/3/3.2/index.rst b/doc/whatsnew/3/3.2/index.rst index c71ae72197c..f2e2cff46a3 100644 --- a/doc/whatsnew/3/3.2/index.rst +++ b/doc/whatsnew/3/3.2/index.rst @@ -14,6 +14,155 @@ Summary -- Release highlights .. towncrier release notes start +What's new in Pylint 3.2.7? +--------------------------- +Release date: 2024-08-31 + + +False Positives Fixed +--------------------- + +- Fixed a false positive `unreachable` for `NoReturn` coroutine functions. + + Closes #9840. (`#9840 `_) + + + +Other Bug Fixes +--------------- + +- Fix crash in refactoring checker when calling a lambda bound as a method. + + Closes #9865 (`#9865 `_) + +- Fix a crash in ``undefined-loop-variable`` when providing the ``iterable`` argument to ``enumerate()``. + + Closes #9875 (`#9875 `_) + +- Fix to address indeterminacy of error message in case a module name is same as another in a separate namespace. + + Refs #9883 (`#9883 `_) + + + +What's new in Pylint 3.2.6? +--------------------------- +Release date: 2024-07-21 + + +False Positives Fixed +--------------------- + +- Quiet false positives for `unexpected-keyword-arg` when pylint cannot + determine which of two or more dynamically defined classes is being instantiated. + + Closes #9672 (`#9672 `_) + +- Fix a false positive for ``missing-param-doc`` where a method which is decorated with ``typing.overload`` was expected to have a docstring specifying its parameters. + + Closes #9739 (`#9739 `_) + +- Fix a regression that raised ``invalid-name`` on class attributes merely + overriding invalid names from an ancestor. + + Closes #9765 (`#9765 `_) + +- Treat `assert_never()` the same way when imported from `typing_extensions`. + + Closes #9780 (`#9780 `_) + +- Fix a false positive for `consider-using-min-max-builtin` when the assignment target is an attribute. + + Refs #9800 (`#9800 `_) + + + +Other Bug Fixes +--------------- + +- Fix an `AssertionError` arising from properties that return partial functions. + + Closes #9214 (`#9214 `_) + +- Fix a crash when a subclass extends ``__slots__``. + + Closes #9814 (`#9814 `_) + + + +What's new in Pylint 3.2.5? +--------------------------- +Release date: 2024-06-28 + + +Other Bug Fixes +--------------- + +- Fixed a false positive ``unreachable-code`` when using ``typing.Any`` as return type in python + 3.8, the ``typing.NoReturn`` are not taken into account anymore for python 3.8 however. + + Closes #9751 (`#9751 `_) + + + +What's new in Pylint 3.2.4? +--------------------------- +Release date: 2024-06-26 + + +False Positives Fixed +--------------------- + +- Prevent emitting ``possibly-used-before-assignment`` when relying on names + only potentially not defined in conditional blocks guarded by functions + annotated with ``typing.Never`` or ``typing.NoReturn``. + + Closes #9674 (`#9674 `_) + + + +Other Bug Fixes +--------------- + +- Fixed a crash when the lineno of a variable used as an annotation wasn't available for ``undefined-variable``. + + Closes #8866 (`#8866 `_) + +- Fixed a crash when the ``start`` value in an ``enumerate`` was non-constant and impossible to infer + (like in``enumerate(apples, start=int(random_apple_index)``) for ``unnecessary-list-index-lookup``. + + Closes #9078 (`#9078 `_) + +- Fixed a crash in ``symilar`` when the ``-d`` or ``-i`` short option were not properly recognized. + It's still impossible to do ``-d=1`` (you must do ``-d 1``). + + Closes #9343 (`#9343 `_) + + + +What's new in Pylint 3.2.3? +--------------------------- +Release date: 2024-06-06 + + +False Positives Fixed +--------------------- + +- Classes with only an Ellipsis (``...``) in their body do not trigger 'multiple-statements' + anymore if they are inlined (in accordance with black's 2024 style). + + Closes #9398 (`#9398 `_) + +- Fix a false positive for ``redefined-outer-name`` when there is a name defined in an exception-handling block which shares the same name as a local variable that has been defined in a function body. + + Closes #9671 (`#9671 `_) + +- Fix a false positive for ``use-yield-from`` when using the return value from the ``yield`` atom. + + Closes #9696 (`#9696 `_) + + + What's new in Pylint 3.2.2? --------------------------- Release date: 2024-05-20 diff --git a/doc/whatsnew/3/3.3/index.rst b/doc/whatsnew/3/3.3/index.rst index 2cae04e110e..9b47c852ab7 100644 --- a/doc/whatsnew/3/3.3/index.rst +++ b/doc/whatsnew/3/3.3/index.rst @@ -7,10 +7,162 @@ :maxdepth: 2 :Release:3.3 -:Date: TBA +:Date: 2024-09-20 Summary -- Release highlights ============================= - .. towncrier release notes start + +What's new in Pylint 3.3.1? +--------------------------- +Release date: 2024-09-24 + + +False Positives Fixed +--------------------- + +- Fix regression causing some f-strings to not be inferred as strings. + + Closes #9947 (`#9947 `_) + + + +What's new in Pylint 3.3.0? +--------------------------- +Release date: 2024-09-20 + + +Changes requiring user actions +------------------------------ + +- We migrated ``symilar`` to argparse, from getopt, so the error and help output changed + (for the better). We exit with 2 instead of sometime 1, sometime 2. The error output + is not captured by the runner anymore. It's not possible to use a value for the + boolean options anymore (``--ignore-comments 1`` should become ``--ignore-comments``). + + Refs #9731 (`#9731 `_) + + + +New Features +------------ + +- Add new `declare-non-slot` error which reports when a class has a `__slots__` member and a type hint on the class is not present in `__slots__`. + + Refs #9499 (`#9499 `_) + + + +New Checks +---------- + +- Added `too-many-positional-arguments` to allow distinguishing the configuration for too many + total arguments (with keyword-only params specified after `*`) from the configuration + for too many positional-or-keyword or positional-only arguments. + + As part of evaluating whether this check makes sense for your project, ensure you + adjust the value of `--max-positional-arguments`. + + Closes #9099 (`#9099 `_) + +- Add `using-exception-groups-in-unsupported-version` and + `using-generic-type-syntax-in-unsupported-version` for uses of Python 3.11+ or + 3.12+ features on lower supported versions provided with `--py-version`. + + Closes #9791 (`#9791 `_) + +- Add `using-assignment-expression-in-unsupported-version` for uses of `:=` (walrus operator) + on Python versions below 3.8 provided with `--py-version`. + + Closes #9820 (`#9820 `_) + +- Add `using-positional-only-args-in-unsupported-version` for uses of positional-only args on + Python versions below 3.8 provided with `--py-version`. + + Closes #9823 (`#9823 `_) + +- Add ``unnecessary-default-type-args`` to the ``typing`` extension to detect the use + of unnecessary default type args for ``typing.Generator`` and ``typing.AsyncGenerator``. + + Refs #9938 (`#9938 `_) + + + +False Negatives Fixed +--------------------- + +- Fix computation of never-returning function: `Never` is handled in addition to `NoReturn`, and priority is given to the explicit `--never-returning-functions` option. + + Closes #7565. (`#7565 `_) + +- Fix a false negative for `await-outside-async` when await is inside Lambda. + + Refs #9653 (`#9653 `_) + +- Fix a false negative for ``duplicate-argument-name`` by including ``positional-only``, ``*args`` and ``**kwargs`` arguments in the check. + + Closes #9669 (`#9669 `_) + +- Fix false negative for `multiple-statements` when multiple statements are present on `else` and `finally` lines of `try`. + + Refs #9759 (`#9759 `_) + +- Fix false negatives when `isinstance` does not have exactly two arguments. + pylint now emits a `too-many-function-args` or `no-value-for-parameter` + appropriately for `isinstance` calls. + + Closes #9847 (`#9847 `_) + + + +Other Bug Fixes +--------------- + +- `--enable` with `--disable=all` now produces an error, when an unknown msg code is used. Internal `pylint` messages are no longer affected by `--disable=all`. + + Closes #9403 (`#9403 `_) + +- Impossible to compile regexes for paths in the configuration or argument given to pylint won't crash anymore but + raise an argparse error and display the error message from ``re.compile`` instead. + + Closes #9680 (`#9680 `_) + +- Fix a bug where a ``tox.ini`` file with pylint configuration was ignored and it exists in the current directory. + + ``.cfg`` and ``.ini`` files containing a ``Pylint`` configuration may now use a section named ``[pylint]``. This enhancement impacts the scenario where these file types are used as defaults when they are present and have not been explicitly referred to, using the ``--rcfile`` option. + + Closes #9727 (`#9727 `_) + +- Improve file discovery for directories that are not python packages. + + Closes #9764 (`#9764 `_) + + + +Other Changes +------------- + +- Remove support for launching pylint with Python 3.8. + Code that supports Python 3.8 can still be linted with the ``--py-version=3.8`` setting. + + Refs #9774 (`#9774 `_) + +- Add support for Python 3.13. + + Refs #9852 (`#9852 `_) + + + +Internal Changes +---------------- + +- All variables, classes, functions and file names containing the word 'similar', when it was, + in fact, referring to 'symilar' (the standalone program for the duplicate-code check) were renamed + to 'symilar'. + + Closes #9734 (`#9734 `_) + +- Remove old-style classes (Python 2) code and remove check for new-style class since everything is new-style in Python 3. Updated doc for exception checker to remove reference to new style class. + + Refs #9925 (`#9925 `_) diff --git a/doc/whatsnew/4/4.0/index.rst b/doc/whatsnew/4/4.0/index.rst new file mode 100644 index 00000000000..865dc112e5c --- /dev/null +++ b/doc/whatsnew/4/4.0/index.rst @@ -0,0 +1,16 @@ + +*************************** + What's New in Pylint 4.0 +*************************** + +.. toctree:: + :maxdepth: 2 + +:Release:4.0 +:Date: TBA + +Summary -- Release highlights +============================= + + +.. towncrier release notes start diff --git a/doc/whatsnew/4/index.rst b/doc/whatsnew/4/index.rst new file mode 100644 index 00000000000..65727109909 --- /dev/null +++ b/doc/whatsnew/4/index.rst @@ -0,0 +1,9 @@ +4.x +=== + +This is the full list of change in pylint 4.x minors, by categories. + +.. toctree:: + :maxdepth: 2 + + 4.0/index diff --git a/doc/whatsnew/fragments/10026.bugfix b/doc/whatsnew/fragments/10026.bugfix new file mode 100644 index 00000000000..3a80c193096 --- /dev/null +++ b/doc/whatsnew/fragments/10026.bugfix @@ -0,0 +1,3 @@ +Fixes the issue with --source-root option not working when the source files are in a subdirectory of the source root (e.g. when using a /src layout). + +Closes #10026 diff --git a/doc/whatsnew/fragments/10028.false_negative b/doc/whatsnew/fragments/10028.false_negative new file mode 100644 index 00000000000..eaf356ff6c5 --- /dev/null +++ b/doc/whatsnew/fragments/10028.false_negative @@ -0,0 +1,3 @@ +Fix false negative for `used-before-assignment` when a function is defined inside a `TYPE_CHECKING` guard block and used later. + +Closes #10028 diff --git a/doc/whatsnew/fragments/10061.false_positive b/doc/whatsnew/fragments/10061.false_positive new file mode 100644 index 00000000000..9c4bfea0a0c --- /dev/null +++ b/doc/whatsnew/fragments/10061.false_positive @@ -0,0 +1,5 @@ +Fix a false positive for `used-before-assignment` when a variable defined under +an `if` and via a named expression (walrus operator) is used later when guarded +under the same `if` test. + +Closes #10061 diff --git a/doc/whatsnew/fragments/10071.breaking b/doc/whatsnew/fragments/10071.breaking new file mode 100644 index 00000000000..7b75760043a --- /dev/null +++ b/doc/whatsnew/fragments/10071.breaking @@ -0,0 +1,4 @@ +The ``async.py`` checker module has been renamed to ``async_checker.py`` since ``async`` is a Python keyword +and cannot be imported directly. This allows for better testing and extensibility of the async checker functionality. + +Refs #10071 diff --git a/doc/whatsnew/fragments/8893.false_negative b/doc/whatsnew/fragments/8893.false_negative new file mode 100644 index 00000000000..5760b384f18 --- /dev/null +++ b/doc/whatsnew/fragments/8893.false_negative @@ -0,0 +1,3 @@ +Fix false negative for `used-before-assignment` when a `TYPE_CHECKING` import is used as a type annotation prior to erroneous usage. + +Refs #8893 diff --git a/doc/whatsnew/fragments/9139.internal b/doc/whatsnew/fragments/9139.internal deleted file mode 100644 index 98fbbabc2c2..00000000000 --- a/doc/whatsnew/fragments/9139.internal +++ /dev/null @@ -1,5 +0,0 @@ -Update astroid version to 3.2.1. This solves some reports of ``RecursionError`` -and also makes the *prefer .pyi stubs* feature in astroid 3.2.0 *opt-in* -with the aforementioned ``--prefer-stubs=y`` option. - -Refs #9139 diff --git a/doc/whatsnew/fragments/9145.bugfix b/doc/whatsnew/fragments/9145.bugfix deleted file mode 100644 index 9cb32d9c67d..00000000000 --- a/doc/whatsnew/fragments/9145.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Restore "errors / warnings by module" section to report output (with `-ry`). - -Closes #9145 diff --git a/doc/whatsnew/fragments/9255.breaking b/doc/whatsnew/fragments/9255.breaking new file mode 100644 index 00000000000..076b96e05c8 --- /dev/null +++ b/doc/whatsnew/fragments/9255.breaking @@ -0,0 +1,3 @@ +Commented out code blocks such as ``# bar() # TODO: remove dead code`` will no longer emit ``fixme``. + +Refs #9255 diff --git a/doc/whatsnew/fragments/9255.feature b/doc/whatsnew/fragments/9255.feature new file mode 100644 index 00000000000..bc6b1033dad --- /dev/null +++ b/doc/whatsnew/fragments/9255.feature @@ -0,0 +1,4 @@ +The `fixme` check can now search through docstrings as well as comments, by using +``check-fixme-in-docstring = true`` in the ``[tool.pylint.miscellaneous]`` section. + +Closes #9255 diff --git a/doc/whatsnew/fragments/9273.false_negative b/doc/whatsnew/fragments/9273.false_negative deleted file mode 100644 index 4a982ee7e5f..00000000000 --- a/doc/whatsnew/fragments/9273.false_negative +++ /dev/null @@ -1,3 +0,0 @@ -Fix a false negative for ``--ignore-patterns`` when the directory to be linted is specified using a dot(``.``) and all files are ignored instead of only the files whose name begin with a dot. - -Closes #9273 diff --git a/doc/whatsnew/fragments/9406.false_positive b/doc/whatsnew/fragments/9406.false_positive deleted file mode 100644 index 0c85ef699c7..00000000000 --- a/doc/whatsnew/fragments/9406.false_positive +++ /dev/null @@ -1,3 +0,0 @@ -Fix multiple false positives for generic class syntax added in Python 3.12 (PEP 695). - -Closes #9406 diff --git a/doc/whatsnew/fragments/9608.bugfix b/doc/whatsnew/fragments/9608.bugfix deleted file mode 100644 index badcf32d197..00000000000 --- a/doc/whatsnew/fragments/9608.bugfix +++ /dev/null @@ -1,4 +0,0 @@ -``trailing-comma-tuple`` should now be correctly emitted when it was disabled globally -but enabled via local message control, after removal of an over-optimisation. - -Refs #9608. diff --git a/doc/whatsnew/fragments/9625.false_positive b/doc/whatsnew/fragments/9625.false_positive deleted file mode 100644 index 90d4a7a076f..00000000000 --- a/doc/whatsnew/fragments/9625.false_positive +++ /dev/null @@ -1,4 +0,0 @@ -Exclude context manager without cleanup from -``contextmanager-generator-missing-cleanup`` checks. - -Closes #9625 diff --git a/doc/whatsnew/fragments/9626.bugfix b/doc/whatsnew/fragments/9626.bugfix deleted file mode 100644 index 44b7539eac4..00000000000 --- a/doc/whatsnew/fragments/9626.bugfix +++ /dev/null @@ -1,9 +0,0 @@ -Add `--prefer-stubs=yes` option to opt-in to the astroid 3.2 feature -that prefers `.pyi` stubs over same-named `.py` files. This has the -potential to reduce `no-member` errors but at the cost of more errors -such as `not-an-iterable` from function bodies appearing as `...`. - -Defaults to `no`. - -Closes #9626 -Closes #9623 diff --git a/doc/whatsnew/fragments/9627.false_positive b/doc/whatsnew/fragments/9627.false_positive deleted file mode 100644 index 2a9edb26d88..00000000000 --- a/doc/whatsnew/fragments/9627.false_positive +++ /dev/null @@ -1,4 +0,0 @@ -Exclude if/else branches containing terminating functions (e.g. `sys.exit()`) -from `possibly-used-before-assignment` checks. - -Closes #9627 diff --git a/doc/whatsnew/fragments/9638.false_positive b/doc/whatsnew/fragments/9638.false_positive deleted file mode 100644 index 8076ad555ce..00000000000 --- a/doc/whatsnew/fragments/9638.false_positive +++ /dev/null @@ -1,5 +0,0 @@ -Don't emit ``typevar-name-incorrect-variance`` warnings for PEP 695 style TypeVars. -The variance is inferred automatically by the type checker. -Adding ``_co`` or ``_contra`` suffix can help to reason about TypeVar. - -Refs #9638 diff --git a/doc/whatsnew/fragments/9643.false_positive b/doc/whatsnew/fragments/9643.false_positive deleted file mode 100644 index 471807d3b21..00000000000 --- a/doc/whatsnew/fragments/9643.false_positive +++ /dev/null @@ -1,4 +0,0 @@ -Fix a false positive for `possibly-used-before-assignment` when using -`typing.assert_never()` (3.11+) to indicate exhaustiveness. - -Closes #9643 diff --git a/doc/whatsnew/fragments/9653.false_negative b/doc/whatsnew/fragments/9653.false_negative deleted file mode 100644 index c576e071ead..00000000000 --- a/doc/whatsnew/fragments/9653.false_negative +++ /dev/null @@ -1,3 +0,0 @@ -Fix a false negative for `await-outside-async` when await is inside Lambda. - -Refs #9653 diff --git a/doc/whatsnew/fragments/9669.false_negative b/doc/whatsnew/fragments/9669.false_negative deleted file mode 100644 index 483de2a77d9..00000000000 --- a/doc/whatsnew/fragments/9669.false_negative +++ /dev/null @@ -1,3 +0,0 @@ -Fix a false negative for ``duplicate-argument-name`` by including ``positional-only``, ``*args`` and ``**kwargs`` arguments in the check. - -Closes #9669 diff --git a/doc/whatsnew/fragments/9671.false_positive b/doc/whatsnew/fragments/9671.false_positive deleted file mode 100644 index 23dafff10f1..00000000000 --- a/doc/whatsnew/fragments/9671.false_positive +++ /dev/null @@ -1,3 +0,0 @@ -Fix a false positive for ``redefined-outer-name`` when there is a name defined in an exception-handling block which shares the same name as a local variable that has been defined in a function body. - -Closes #9671 diff --git a/doc/whatsnew/fragments/9689.breaking b/doc/whatsnew/fragments/9689.breaking new file mode 100644 index 00000000000..5b175304a55 --- /dev/null +++ b/doc/whatsnew/fragments/9689.breaking @@ -0,0 +1,7 @@ +`pyreverse` `Run` was changed to no longer call `sys.exit()` in its `__init__`. +You should now call `Run(args).run()` which will return the exit code instead. +Having a class that always raised a `SystemExit` exception was considered a bug. + +Normal usage of pyreverse through the CLI will not be affected by this change. + +Refs #9689 diff --git a/doc/whatsnew/index.rst b/doc/whatsnew/index.rst index 4e9c22fea95..e2b9c9c93bb 100644 --- a/doc/whatsnew/index.rst +++ b/doc/whatsnew/index.rst @@ -5,6 +5,7 @@ :titlesonly: :hidden: + 4/index 3/index 2/index 1/index diff --git a/examples/pylintrc b/examples/pylintrc index 0a917e9cef0..a26254c1993 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -87,8 +87,8 @@ load-plugins= # Pickle collected data for later comparisons. persistent=yes -# Resolve imports to .pyi stubs if available. May reduce no-member messages -# and increase not-an-iterable messages. +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. prefer-stubs=no # Minimum Python version to use for version dependent checks. Will default to @@ -307,6 +307,9 @@ max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 +# Maximum number of positional arguments for function / method. +max-positional-arguments=5 + # Maximum number of public methods for a class (see R0904). max-public-methods=20 diff --git a/examples/pyproject.toml b/examples/pyproject.toml index 68e8c667376..a647cbb5779 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -77,9 +77,9 @@ limit-inference-results = 100 # Pickle collected data for later comparisons. persistent = true -# Resolve imports to .pyi stubs if available. May reduce no-member messages -# and increase not-an-iterable messages. -prefer-stubs = false +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. +# prefer-stubs = # Minimum Python version to use for version dependent checks. Will default to the # version used to run pylint. @@ -269,6 +269,9 @@ max-locals = 15 # Maximum number of parents for a class (see R0901). max-parents = 7 +# Maximum number of positional arguments for function / method. +max-positional-arguments = 5 + # Maximum number of public methods for a class (see R0904). max-public-methods = 20 diff --git a/pylint/__init__.py b/pylint/__init__.py index 75cf2b6ee4c..d3ddf71f627 100644 --- a/pylint/__init__.py +++ b/pylint/__init__.py @@ -53,7 +53,7 @@ def run_pyreverse(argv: Sequence[str] | None = None) -> NoReturn: """ from pylint.pyreverse.main import Run as PyreverseRun - PyreverseRun(argv or sys.argv[1:]) + sys.exit(PyreverseRun(argv or sys.argv[1:]).run()) def run_symilar(argv: Sequence[str] | None = None) -> NoReturn: @@ -61,9 +61,9 @@ def run_symilar(argv: Sequence[str] | None = None) -> NoReturn: argv can be a sequence of strings normally supplied as arguments on the command line """ - from pylint.checkers.similar import Run as SimilarRun + from pylint.checkers.symilar import Run as SymilarRun - SimilarRun(argv or sys.argv[1:]) + SymilarRun(argv or sys.argv[1:]) def modify_sys_path() -> None: diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 0953e70e7b5..9be35c2816b 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "3.3.0-dev0" +__version__ = "4.0.0-dev0" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/pylint/checkers/async.py b/pylint/checkers/async_checker.py similarity index 100% rename from pylint/checkers/async.py rename to pylint/checkers/async_checker.py diff --git a/pylint/checkers/base/function_checker.py b/pylint/checkers/base/function_checker.py index f7d92a46499..2826b40b4d7 100644 --- a/pylint/checkers/base/function_checker.py +++ b/pylint/checkers/base/function_checker.py @@ -45,7 +45,7 @@ def _check_contextmanager_generator_missing_cleanup( :param node: FunctionDef node to check :type node: nodes.FunctionDef """ - # if function does not use a Yield statement, it cant be a generator + # if function does not use a Yield statement, it can't be a generator with_nodes = list(node.nodes_of_class(nodes.With)) if not with_nodes: return diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 68f5767206b..1d8589a5767 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -14,7 +14,7 @@ from collections.abc import Iterable from enum import Enum, auto from re import Pattern -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING import astroid from astroid import nodes @@ -34,7 +34,7 @@ if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter -_BadNamesTuple = Tuple[nodes.NodeNG, str, str, interfaces.Confidence] +_BadNamesTuple = tuple[nodes.NodeNG, str, str, interfaces.Confidence] # Default patterns for name types that do not have styles DEFAULT_PATTERNS = { @@ -491,7 +491,9 @@ def visit_assignname( # pylint: disable=too-many-branches self._check_name("variable", node.name, node) # Check names defined in class scopes - elif isinstance(frame, nodes.ClassDef): + elif isinstance(frame, nodes.ClassDef) and not any( + frame.local_attr_ancestors(node.name) + ): if utils.is_enum_member(node) or utils.is_assign_name_annotated_with( node, "Final" ): diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index ffe47ab1569..b493a1ba739 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -470,13 +470,8 @@ def _is_attribute_property(name: str, klass: nodes.ClassDef) -> bool: inferred ): return True - if inferred.pytype() != property_name: - continue - - cls = node_frame_class(inferred) - if cls == klass.declared_metaclass(): - continue - return True + if inferred.pytype() == property_name: + return True return False @@ -523,7 +518,7 @@ def _has_same_layout_slots( "Access to a protected member %s of a client class", # E0214 "protected-access", "Used when a protected member (i.e. class member with a name " - "beginning with an underscore) is access outside the class or a " + "beginning with an underscore) is accessed outside the class or a " "descendant of the class where it's defined.", ), "W0213": ( @@ -702,6 +697,12 @@ def _has_same_layout_slots( "Used when a class tries to extend an inherited Enum class. " "Doing so will raise a TypeError at runtime.", ), + "E0245": ( + "No such name %r in __slots__", + "declare-non-slot", + "Raised when a type annotation on a class is absent from the list of names in __slots__, " + "and __slots__ does not contain a __dict__ entry.", + ), "R0202": ( "Consider using a decorator instead of calling classmethod", "no-classmethod-decorator", @@ -870,6 +871,7 @@ def _dummy_rgx(self) -> Pattern[str]: "invalid-enum-extension", "subclassed-final-class", "implicit-flag-alias", + "declare-non-slot", ) def visit_classdef(self, node: nodes.ClassDef) -> None: """Init visit variable _accessed.""" @@ -878,6 +880,50 @@ def visit_classdef(self, node: nodes.ClassDef) -> None: self._check_proper_bases(node) self._check_typing_final(node) self._check_consistent_mro(node) + self._check_declare_non_slot(node) + + def _check_declare_non_slot(self, node: nodes.ClassDef) -> None: + if not self._has_valid_slots(node): + return + + slot_names = self._get_classdef_slots_names(node) + + # Stop if empty __slots__ in the class body, this likely indicates that + # this class takes part in multiple inheritance with other slotted classes. + if not slot_names: + return + + # Stop if we find __dict__, since this means attributes can be set + # dynamically + if "__dict__" in slot_names: + return + + for base in node.bases: + ancestor = safe_infer(base) + if not isinstance(ancestor, nodes.ClassDef): + continue + # if any base doesn't have __slots__, attributes can be set dynamically, so stop + if not self._has_valid_slots(ancestor): + return + for slot_name in self._get_classdef_slots_names(ancestor): + if slot_name == "__dict__": + return + slot_names.append(slot_name) + + # Every class in bases has __slots__, our __slots__ is non-empty and there is no __dict__ + + for child in node.body: + if isinstance(child, nodes.AnnAssign): + if child.value is not None: + continue + if isinstance(child.target, nodes.AssignName): + if child.target.name not in slot_names: + self.add_message( + "declare-non-slot", + args=child.target.name, + node=child.target, + confidence=INFERENCE, + ) def _check_consistent_mro(self, node: nodes.ClassDef) -> None: """Detect that a class has a consistent mro or duplicate bases.""" @@ -1482,11 +1528,37 @@ def _check_functools_or_not(self, decorator: nodes.Attribute) -> bool: return "functools" in dict(import_node.names) + def _has_valid_slots(self, node: nodes.ClassDef) -> bool: + if "__slots__" not in node.locals: + return False + + try: + inferred_slots = tuple(node.ilookup("__slots__")) + except astroid.InferenceError: + return False + for slots in inferred_slots: + # check if __slots__ is a valid type + if isinstance(slots, util.UninferableBase): + return False + if not is_iterable(slots) and not is_comprehension(slots): + return False + if isinstance(slots, nodes.Const): + return False + if not hasattr(slots, "itered"): + # we can't obtain the values, maybe a .deque? + return False + + return True + def _check_slots(self, node: nodes.ClassDef) -> None: if "__slots__" not in node.locals: return - for slots in node.ilookup("__slots__"): + try: + inferred_slots = tuple(node.ilookup("__slots__")) + except astroid.InferenceError: + return + for slots in inferred_slots: # check if __slots__ is a valid type if isinstance(slots, util.UninferableBase): continue @@ -1515,13 +1587,23 @@ def _check_slots(self, node: nodes.ClassDef) -> None: continue self._check_redefined_slots(node, slots, values) - def _check_redefined_slots( - self, - node: nodes.ClassDef, - slots_node: nodes.NodeNG, - slots_list: list[nodes.NodeNG], - ) -> None: - """Check if `node` redefines a slot which is defined in an ancestor class.""" + def _get_classdef_slots_names(self, node: nodes.ClassDef) -> list[str]: + + slots_names: list[str] = [] + try: + inferred_slots = tuple(node.ilookup("__slots__")) + except astroid.InferenceError: # pragma: no cover + return slots_names + for slots in inferred_slots: + if isinstance(slots, nodes.Dict): + values = [item[0] for item in slots.items] + else: + values = slots.itered() + slots_names.extend(self._get_slots_names(values)) + + return slots_names + + def _get_slots_names(self, slots_list: list[nodes.NodeNG]) -> list[str]: slots_names: list[str] = [] for slot in slots_list: if isinstance(slot, nodes.Const): @@ -1531,6 +1613,16 @@ def _check_redefined_slots( inferred_slot_value = getattr(inferred_slot, "value", None) if isinstance(inferred_slot_value, str): slots_names.append(inferred_slot_value) + return slots_names + + def _check_redefined_slots( + self, + node: nodes.ClassDef, + slots_node: nodes.NodeNG, + slots_list: list[nodes.NodeNG], + ) -> None: + """Check if `node` redefines a slot which is defined in an ancestor class.""" + slots_names: list[str] = self._get_slots_names(slots_list) # Slots of all parent classes ancestors_slots_names = { @@ -1681,7 +1773,7 @@ def _check_in_slots(self, node: nodes.AssignAttr) -> None: klass = inferred._proxied if not has_known_bases(klass): return - if "__slots__" not in klass.locals or not klass.newstyle: + if "__slots__" not in klass.locals: return # If `__setattr__` is defined on the class, then we can't reason about # what will happen when assigning to an attribute. diff --git a/pylint/checkers/deprecated.py b/pylint/checkers/deprecated.py index 028dc13f384..1d6bac0a8ff 100644 --- a/pylint/checkers/deprecated.py +++ b/pylint/checkers/deprecated.py @@ -133,10 +133,12 @@ def visit_decorators(self, node: nodes.Decorators) -> None: if not children: return if isinstance(children[0], nodes.Call): - inf = safe_infer(children[0].func) + inferred = safe_infer(children[0].func) else: - inf = safe_infer(children[0]) - qname = inf.qname() if inf else None + inferred = safe_infer(children[0]) + if not inferred: + return + qname = inferred.qname() if qname in self.deprecated_decorators(): self.add_message("deprecated-decorator", node=node, args=qname) diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index 78378e92c91..5c1adbc888f 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -16,6 +16,7 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import is_enum, only_required_for_messages +from pylint.interfaces import HIGH from pylint.typing import MessageDefinitionTuple if TYPE_CHECKING: @@ -81,11 +82,9 @@ "Used when an if statement contains too many boolean expressions.", ), "R0917": ( - "Too many positional arguments in a function call.", - "too-many-positional", - "Will be implemented in https://github.com/pylint-dev/pylint/issues/9099," - "msgid/symbol pair reserved for compatibility with ruff, " - "see https://github.com/astral-sh/ruff/issues/8946.", + "Too many positional arguments (%s/%s)", + "too-many-positional-arguments", + "Used when a function has too many positional arguments.", ), } ) @@ -311,6 +310,15 @@ class MisdesignChecker(BaseChecker): "help": "Maximum number of arguments for function / method.", }, ), + ( + "max-positional-arguments", + { + "default": 5, + "type": "int", + "metavar": "", + "help": "Maximum number of positional arguments for function / method.", + }, + ), ( "max-locals", { @@ -525,6 +533,7 @@ def leave_classdef(self, node: nodes.ClassDef) -> None: "too-many-branches", "too-many-arguments", "too-many-locals", + "too-many-positional-arguments", "too-many-statements", "keyword-arg-before-vararg", ) @@ -536,13 +545,20 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: self._returns.append(0) # check number of arguments args = node.args.args + node.args.posonlyargs + node.args.kwonlyargs + pos_args = node.args.args + node.args.posonlyargs ignored_argument_names = self.linter.config.ignored_argument_names if args is not None: ignored_args_num = 0 if ignored_argument_names: - ignored_args_num = sum( - 1 for arg in args if ignored_argument_names.match(arg.name) + ignored_pos_args_num = sum( + 1 for arg in pos_args if ignored_argument_names.match(arg.name) + ) + ignored_kwonly_args_num = sum( + 1 + for arg in node.args.kwonlyargs + if ignored_argument_names.match(arg.name) ) + ignored_args_num = ignored_pos_args_num + ignored_kwonly_args_num argnum = len(args) - ignored_args_num if argnum > self.linter.config.max_args: @@ -551,6 +567,16 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: node=node, args=(len(args), self.linter.config.max_args), ) + pos_args_count = ( + len(args) - len(node.args.kwonlyargs) - ignored_pos_args_num + ) + if pos_args_count > self.linter.config.max_positional_arguments: + self.add_message( + "too-many-positional-arguments", + node=node, + args=(pos_args_count, self.linter.config.max_positional_arguments), + confidence=HIGH, + ) else: ignored_args_num = 0 # check number of local variables diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index 688dc829a0e..3834f260be2 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -35,7 +35,7 @@ def predicate(obj: Any) -> bool: def _annotated_unpack_infer( stmt: nodes.NodeNG, context: InferenceContext | None = None -) -> Generator[tuple[nodes.NodeNG, SuccessfulInferenceResult], None, None]: +) -> Generator[tuple[nodes.NodeNG, SuccessfulInferenceResult]]: """Recursively generate nodes inferred by the given statement. If the inferred value is a list or a tuple, recurse on the elements. @@ -92,10 +92,9 @@ def _is_raising(body: list[nodes.NodeNG]) -> bool: {"old_names": [("E0703", "bad-exception-context")]}, ), "E0710": ( - "Raising a new style class which doesn't inherit from BaseException", + "Raising a class which doesn't inherit from BaseException", "raising-non-exception", - "Used when a new style class which doesn't inherit from " - "BaseException is raised.", + "Used when a class which doesn't inherit from BaseException is raised.", ), "E0711": ( "NotImplemented raised - should raise NotImplementedError", @@ -262,12 +261,11 @@ 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, - confidence=INFERENCE, - ) + self._checker.add_message( + "raising-non-exception", + node=self._node, + confidence=INFERENCE, + ) def visit_tuple(self, _: nodes.Tuple) -> None: self._checker.add_message( diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index f4de6165984..12fa9301621 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -496,6 +496,10 @@ def visit_default(self, node: nodes.NodeNG) -> None: prev_sibl = node.previous_sibling() if prev_sibl is not None: prev_line = prev_sibl.fromlineno + elif isinstance( + node.parent, nodes.Try + ) and self._is_first_node_in_else_finally_body(node, node.parent): + prev_line = self._infer_else_finally_line_number(node, node.parent) elif isinstance(node.parent, nodes.Module): prev_line = 0 else: @@ -520,11 +524,32 @@ def visit_default(self, node: nodes.NodeNG) -> None: except KeyError: lines.append("") + def _is_first_node_in_else_finally_body( + self, node: nodes.NodeNG, parent: nodes.Try + ) -> bool: + if parent.orelse and node == parent.orelse[0]: + return True + if parent.finalbody and node == parent.finalbody[0]: + return True + return False + + def _infer_else_finally_line_number( + self, node: nodes.NodeNG, parent: nodes.Try + ) -> int: + last_line_of_prev_block = 0 + if node in parent.finalbody and parent.orelse: + last_line_of_prev_block = parent.orelse[-1].tolineno + elif parent.handlers and parent.handlers[-1].body: + last_line_of_prev_block = parent.handlers[-1].body[-1].tolineno + elif parent.body: + last_line_of_prev_block = parent.body[-1].tolineno + + return last_line_of_prev_block + 1 if last_line_of_prev_block else 0 + 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. if isinstance(node, nodes.With): + # Do not warn about multiple nested context managers in with statements. return if ( isinstance(node.parent, nodes.If) @@ -539,16 +564,16 @@ def _check_multi_statement_line(self, node: nodes.NodeNG, line: int) -> None: ): return - # Functions stubs with ``Ellipsis`` as body are exempted. + # Functions stubs and class with ``Ellipsis`` as body are exempted. if ( - isinstance(node.parent, nodes.FunctionDef) - and isinstance(node, nodes.Expr) + isinstance(node, nodes.Expr) + and isinstance(node.parent, (nodes.FunctionDef, nodes.ClassDef)) and isinstance(node.value, nodes.Const) and node.value.value is Ellipsis ): return - self.add_message("multiple-statements", node=node) + self.add_message("multiple-statements", node=node, confidence=HIGH) self._visited_lines[line] = 2 def check_trailing_whitespace_ending(self, line: str, i: int) -> None: diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index afef0277ee7..2fa212cd7a6 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -13,7 +13,7 @@ from collections import defaultdict from collections.abc import ItemsView, Sequence from functools import cached_property -from typing import TYPE_CHECKING, Any, Dict, List, Union +from typing import TYPE_CHECKING, Any, Union import astroid from astroid import nodes @@ -43,7 +43,7 @@ # 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]]] +_ImportTree = dict[str, Union[list[dict[str, Any]], list[str]]] DEPRECATED_MODULES = { (0, 0, 0): {"tkinter.tix", "fpectl"}, @@ -79,6 +79,7 @@ "uu", "xdrlib", }, + (3, 13, 0): {"getopt"}, } diff --git a/pylint/checkers/method_args.py b/pylint/checkers/method_args.py index 59083fa25ac..565309d2824 100644 --- a/pylint/checkers/method_args.py +++ b/pylint/checkers/method_args.py @@ -41,7 +41,6 @@ class MethodArgsChecker(BaseChecker): "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 = ( diff --git a/pylint/checkers/misc.py b/pylint/checkers/misc.py index 78c21d0c5e8..ea2d9e324f7 100644 --- a/pylint/checkers/misc.py +++ b/pylint/checkers/misc.py @@ -51,7 +51,7 @@ def process_module(self, node: nodes.Module) -> None: class EncodingChecker(BaseTokenChecker, BaseRawFileChecker): - """BaseChecker for encoding issues. + """BaseChecker for encoding issues and fixme notes. Checks for: * warning notes in the code like FIXME, XXX @@ -90,6 +90,15 @@ class EncodingChecker(BaseTokenChecker, BaseRawFileChecker): "default": "", }, ), + ( + "check-fixme-in-docstring", + { + "type": "yn", + "metavar": "", + "default": False, + "help": "Whether or not to search for fixme's in docstrings.", + }, + ), ) def open(self) -> None: @@ -97,11 +106,21 @@ def open(self) -> None: notes = "|".join(re.escape(note) for note in self.linter.config.notes) if self.linter.config.notes_rgx: - regex_string = rf"#\s*({notes}|{self.linter.config.notes_rgx})(?=(:|\s|\Z))" - else: - regex_string = rf"#\s*({notes})(?=(:|\s|\Z))" + notes += f"|{self.linter.config.notes_rgx}" - self._fixme_pattern = re.compile(regex_string, re.I) + comment_regex = rf"#\s*(?P({notes})(?=(:|\s|\Z)).*?$)" + self._comment_fixme_pattern = re.compile(comment_regex, re.I) + + # single line docstring like '''this''' or """this""" + docstring_regex = rf"((\"\"\")|(\'\'\'))\s*(?P({notes})(?=(:|\s|\Z)).*?)((\"\"\")|(\'\'\'))" + self._docstring_fixme_pattern = re.compile(docstring_regex, re.I) + + # multiline docstrings which will be split into newlines + # so we do not need to look for quotes/double-quotes + multiline_docstring_regex = rf"^\s*(?P({notes})(?=(:|\s|\Z)).*$)" + self._multiline_docstring_fixme_pattern = re.compile( + multiline_docstring_regex, re.I + ) def _check_encoding( self, lineno: int, line: bytes, file_encoding: str @@ -133,16 +152,39 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: if not self.linter.config.notes: return for token_info in tokens: - if token_info.type != tokenize.COMMENT: - continue - comment_text = token_info.string[1:].lstrip() # trim '#' and white-spaces - if self._fixme_pattern.search("#" + comment_text.lower()): - self.add_message( - "fixme", - col_offset=token_info.start[1] + 1, - args=comment_text, - line=token_info.start[0], - ) + if token_info.type == tokenize.COMMENT: + if match := self._comment_fixme_pattern.match(token_info.string): + self.add_message( + "fixme", + col_offset=token_info.start[1] + 1, + args=match.group("msg"), + line=token_info.start[0], + ) + elif self.linter.config.check_fixme_in_docstring: + if self._is_multiline_docstring(token_info): + docstring_lines = token_info.string.split("\n") + for line_no, line in enumerate(docstring_lines): + if match := self._multiline_docstring_fixme_pattern.match(line): + self.add_message( + "fixme", + col_offset=token_info.start[1] + 1, + args=match.group("msg"), + line=token_info.start[0] + line_no, + ) + elif match := self._docstring_fixme_pattern.match(token_info.string): + self.add_message( + "fixme", + col_offset=token_info.start[1] + 1, + args=match.group("msg"), + line=token_info.start[0], + ) + + def _is_multiline_docstring(self, token_info: tokenize.TokenInfo) -> bool: + return ( + token_info.type == tokenize.STRING + and (token_info.line.lstrip().startswith(('"""', "'''"))) + and "\n" in token_info.line.rstrip() + ) def register(linter: PyLinter) -> None: diff --git a/pylint/checkers/nested_min_max.py b/pylint/checkers/nested_min_max.py index c8231fe7d2b..2a3e05459ed 100644 --- a/pylint/checkers/nested_min_max.py +++ b/pylint/checkers/nested_min_max.py @@ -14,7 +14,6 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import only_required_for_messages, safe_infer -from pylint.constants import PY39_PLUS from pylint.interfaces import INFERENCE if TYPE_CHECKING: @@ -139,8 +138,8 @@ def _is_splattable_expression(self, arg: nodes.NodeNG) -> bool: return self._is_splattable_expression( arg.left ) and self._is_splattable_expression(arg.right) - # Support dict merge (operator __or__ in Python 3.9) - if isinstance(arg, nodes.BinOp) and arg.op == "|" and PY39_PLUS: + # Support dict merge (operator __or__) + if isinstance(arg, nodes.BinOp) and arg.op == "|": return self._is_splattable_expression( arg.left ) and self._is_splattable_expression(arg.right) diff --git a/pylint/checkers/newstyle.py b/pylint/checkers/newstyle.py index 0c2c559fe84..920c8cc41e9 100644 --- a/pylint/checkers/newstyle.py +++ b/pylint/checkers/newstyle.py @@ -12,11 +12,7 @@ from astroid import nodes from pylint.checkers import BaseChecker -from pylint.checkers.utils import ( - has_known_bases, - node_frame_class, - only_required_for_messages, -) +from pylint.checkers.utils import node_frame_class, only_required_for_messages from pylint.typing import MessageDefinitionTuple if TYPE_CHECKING: @@ -72,55 +68,51 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: ): continue - # super should not be used on an old style class - if klass.newstyle or not has_known_bases(klass): - # super first arg should not be the class - if not call.args: - continue - - # calling super(type(self), self) can lead to recursion loop - # in derived classes - arg0 = call.args[0] - if ( - isinstance(arg0, nodes.Call) - and isinstance(arg0.func, nodes.Name) - and arg0.func.name == "type" - ): - self.add_message("bad-super-call", node=call, args=("type",)) - continue - - # calling super(self.__class__, self) can lead to recursion loop - # in derived classes - if ( - len(call.args) >= 2 - and isinstance(call.args[1], nodes.Name) - and call.args[1].name == "self" - and isinstance(arg0, nodes.Attribute) - and arg0.attrname == "__class__" - ): - self.add_message( - "bad-super-call", node=call, args=("self.__class__",) - ) - continue - - try: - supcls = call.args and next(call.args[0].infer(), None) - except astroid.InferenceError: - continue - - # If the supcls is in the ancestors of klass super can be used to skip - # a step in the mro() and get a method from a higher parent - if klass is not supcls and all(i != supcls for i in klass.ancestors()): - name = None - # if supcls is not Uninferable, then supcls was inferred - # and use its name. Otherwise, try to look - # for call.args[0].name - if supcls: - name = supcls.name - elif call.args and hasattr(call.args[0], "name"): - name = call.args[0].name - if name: - self.add_message("bad-super-call", node=call, args=(name,)) + # super first arg should not be the class + if not call.args: + continue + + # calling super(type(self), self) can lead to recursion loop + # in derived classes + arg0 = call.args[0] + if ( + isinstance(arg0, nodes.Call) + and isinstance(arg0.func, nodes.Name) + and arg0.func.name == "type" + ): + self.add_message("bad-super-call", node=call, args=("type",)) + continue + + # calling super(self.__class__, self) can lead to recursion loop + # in derived classes + if ( + len(call.args) >= 2 + and isinstance(call.args[1], nodes.Name) + and call.args[1].name == "self" + and isinstance(arg0, nodes.Attribute) + and arg0.attrname == "__class__" + ): + self.add_message("bad-super-call", node=call, args=("self.__class__",)) + continue + + try: + supcls = call.args and next(call.args[0].infer(), None) + except astroid.InferenceError: + continue + + # If the supcls is in the ancestors of klass super can be used to skip + # a step in the mro() and get a method from a higher parent + if klass is not supcls and all(i != supcls for i in klass.ancestors()): + name = None + # if supcls is not Uninferable, then supcls was inferred + # and use its name. Otherwise, try to look + # for call.args[0].name + if supcls: + name = supcls.name + elif call.args and hasattr(call.args[0], "name"): + name = call.args[0].name + if name: + self.add_message("bad-super-call", node=call, args=(name,)) visit_asyncfunctiondef = visit_functiondef diff --git a/pylint/checkers/refactoring/implicit_booleaness_checker.py b/pylint/checkers/refactoring/implicit_booleaness_checker.py index b5109fc5797..4d3d47cf9ff 100644 --- a/pylint/checkers/refactoring/implicit_booleaness_checker.py +++ b/pylint/checkers/refactoring/implicit_booleaness_checker.py @@ -81,7 +81,7 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): "equivalent.", ), "C1804": ( - '"%s" can be simplified to "%s", if it is striclty a string, as an empty string is falsey', + '"%s" can be simplified to "%s", if it is strictly a string, as an empty string is falsey', "use-implicit-booleaness-not-comparison-to-string", "Empty string are considered false in a boolean context. Following this" " check blindly in weakly typed code base can create hard to debug issues." diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 94e722b177d..517ed667e02 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -44,6 +44,7 @@ ( "_io.open", # regular 'open()' call "pathlib.Path.open", + "pathlib._local.Path.open", # Python 3.13 "codecs.open", "urllib.request.urlopen", "tempfile.NamedTemporaryFile", @@ -284,7 +285,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): 'Unnecessary "%s" after "return", %s', "no-else-return", "Used in order to highlight an unnecessary block of " - "code following an if containing a return statement. " + "code following an if, or a try/except containing a return statement. " "As such, it will warn when it encounters an else " "following a chain of ifs, all of them containing a " "return statement.", @@ -386,7 +387,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): 'Unnecessary "%s" after "raise", %s', "no-else-raise", "Used in order to highlight an unnecessary block of " - "code following an if containing a raise statement. " + "code following an if, or a try/except containing a raise statement. " "As such, it will warn when it encounters an else " "following a chain of ifs, all of them containing a " "raise statement.", @@ -919,11 +920,9 @@ def get_node_name(node: nodes.NodeNG) -> str: """Obtain simplest representation of a node as a string.""" if isinstance(node, nodes.Name): return node.name # type: ignore[no-any-return] - if isinstance(node, nodes.Attribute): - return node.attrname # type: ignore[no-any-return] if isinstance(node, nodes.Const): return str(node.value) - # this is a catch-all for nodes that are not of type Name or Attribute + # this is a catch-all for nodes that are not of type Name or Const # extremely helpful for Call or BinOp return node.as_string() # type: ignore[no-any-return] @@ -944,13 +943,11 @@ def get_node_name(node: nodes.NodeNG) -> str: # is of type name or attribute. Attribute referring to NamedTuple.x perse. # So we have to check that target is of these types - if hasattr(target, "name"): - target_assignation = target.name - elif hasattr(target, "attrname"): - target_assignation = target.attrname - else: + if not (hasattr(target, "name") or hasattr(target, "attrname")): return + target_assignation = get_node_name(target) + if len(node.test.ops) > 1: return operator, right_statement = node.test.ops[0] @@ -1169,21 +1166,24 @@ def visit_yield(self, node: nodes.Yield) -> None: if not isinstance(node.value, nodes.Name): return - parent = node.parent.parent + loop_node = node.parent.parent if ( - not isinstance(parent, nodes.For) - or isinstance(parent, nodes.AsyncFor) - or len(parent.body) != 1 + not isinstance(loop_node, nodes.For) + or isinstance(loop_node, nodes.AsyncFor) + or len(loop_node.body) != 1 + # Avoid a false positive if the return value from `yield` is used, + # (such as via Assign, AugAssign, etc). + or not isinstance(node.parent, nodes.Expr) ): return - if parent.target.name != node.value.name: + if loop_node.target.name != node.value.name: return if isinstance(node.frame(), nodes.AsyncFunctionDef): return - self.add_message("use-yield-from", node=parent, confidence=HIGH) + self.add_message("use-yield-from", node=loop_node, confidence=HIGH) @staticmethod def _has_exit_in_scope(scope: nodes.LocalsDictNodeNG) -> bool: @@ -1692,7 +1692,7 @@ def _check_consider_using_with(self, node: nodes.Call) -> None: return inferred = utils.safe_infer(node.func) if not inferred or not isinstance( - inferred, (nodes.FunctionDef, nodes.ClassDef, bases.UnboundMethod) + inferred, (nodes.FunctionDef, nodes.ClassDef, bases.BoundMethod) ): return could_be_used_in_with = ( @@ -2027,12 +2027,13 @@ def _is_node_return_ended(self, node: nodes.NodeNG) -> bool: if isinstance(node, nodes.Return): return True if isinstance(node, nodes.Call): - try: - funcdef_node = node.func.inferred()[0] - if self._is_function_def_never_returning(funcdef_node): - return True - except astroid.InferenceError: - pass + return any( + ( + isinstance(maybe_func, (nodes.FunctionDef, bases.BoundMethod)) + and self._is_function_def_never_returning(maybe_func) + ) + for maybe_func in utils.infer_all(node.func) + ) if isinstance(node, nodes.While): # A while-loop is considered return-ended if it has a # truthy test and no break statements @@ -2084,17 +2085,23 @@ def _is_function_def_never_returning( Returns: bool: True if the function never returns, False otherwise. """ - if isinstance(node, (nodes.FunctionDef, astroid.BoundMethod)) and node.returns: - return ( - isinstance(node.returns, nodes.Attribute) - and node.returns.attrname == "NoReturn" - or isinstance(node.returns, nodes.Name) - and node.returns.name == "NoReturn" - ) try: - return node.qname() in self._never_returning_functions + if node.qname() in self._never_returning_functions: + return True except (TypeError, AttributeError): - return False + pass + + try: + returns: nodes.NodeNG | None = node.returns + except AttributeError: + return False # the BoundMethod proxy may be a lambda without a returns + + return ( + isinstance(returns, nodes.Attribute) + and returns.attrname in {"NoReturn", "Never"} + or isinstance(returns, nodes.Name) + and returns.name in {"NoReturn", "Never"} + ) def _check_return_at_the_end(self, node: nodes.FunctionDef) -> None: """Check for presence of a *single* return statement at the end of a @@ -2451,7 +2458,9 @@ def _get_start_value(self, node: nodes.NodeNG) -> tuple[int | None, Confidence]: and isinstance(node.operand, (nodes.Attribute, nodes.Name)) ): inferred = utils.safe_infer(node) - start_val = inferred.value if inferred else None + # inferred can be an astroid.base.Instance as in 'enumerate(x, int(y))' or + # not correctly inferred (None) + start_val = inferred.value if isinstance(inferred, nodes.Const) else None return start_val, INFERENCE if isinstance(node, nodes.UnaryOp): return node.operand.value, HIGH diff --git a/pylint/checkers/spelling.py b/pylint/checkers/spelling.py index 27d1c7ce0b6..6cc258ebf23 100644 --- a/pylint/checkers/spelling.py +++ b/pylint/checkers/spelling.py @@ -60,8 +60,7 @@ def get_tokenizer( def _get_enchant_dicts() -> list[tuple[Any, enchant.ProviderDesc]]: - # Broker().list_dicts() is not typed in enchant, but it does return tuples - return enchant.Broker().list_dicts() if PYENCHANT_AVAILABLE else [] # type: ignore[no-any-return] + return enchant.Broker().list_dicts() if PYENCHANT_AVAILABLE else [] def _get_enchant_dict_choices( diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 10c1d54bfcb..9225cd4d269 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -8,7 +8,7 @@ import sys from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, Dict, Set, Tuple +from typing import TYPE_CHECKING, Any import astroid from astroid import nodes, util @@ -22,7 +22,7 @@ if TYPE_CHECKING: from pylint.lint import PyLinter -DeprecationDict = Dict[Tuple[int, int, int], Set[str]] +DeprecationDict = dict[tuple[int, int, int], set[str]] OPEN_FILES_MODE = ("open", "file") OPEN_FILES_FUNCS = (*OPEN_FILES_MODE, "read_text", "write_text") @@ -33,7 +33,8 @@ ENV_GETTERS = ("os.getenv",) SUBPROCESS_POPEN = "subprocess.Popen" SUBPROCESS_RUN = "subprocess.run" -OPEN_MODULE = {"_io", "pathlib"} +OPEN_MODULE = {"_io", "pathlib", "pathlib._local"} +PATHLIB_MODULE = {"pathlib", "pathlib._local"} DEBUG_BREAKPOINTS = ("builtins.breakpoint", "sys.breakpointhook", "pdb.set_trace") LRU_CACHE = { "functools.lru_cache", # Inferred for @lru_cache @@ -53,6 +54,9 @@ "bool": ((None, "x"),), "float": ((None, "x"),), }, + (3, 5, 0): { + "importlib._bootstrap_external.cache_from_source": ((1, "debug_override"),), + }, (3, 8, 0): { "asyncio.tasks.sleep": ((None, "loop"),), "asyncio.tasks.gather": ((None, "loop"),), @@ -89,6 +93,9 @@ "email.utils.localtime": ((1, "isdst"),), "shutil.rmtree": ((2, "onerror"),), }, + (3, 13, 0): { + "dis.get_instructions": ((2, "show_caches"),), + }, } DEPRECATED_DECORATORS: DeprecationDict = { @@ -99,6 +106,7 @@ "abc.abstractproperty", }, (3, 4, 0): {"importlib.util.module_for_loader"}, + (3, 13, 0): {"typing.no_type_check_decorator"}, } @@ -268,6 +276,10 @@ "unittest.TestProgram.usageExit", }, (3, 12, 0): { + "asyncio.get_child_watcher", + "asyncio.set_child_watcher", + "asyncio.AbstractEventLoopPolicy.get_child_watcher", + "asyncio.AbstractEventLoopPolicy.set_child_watcher", "builtins.bool.__invert__", "datetime.datetime.utcfromtimestamp", "datetime.datetime.utcnow", @@ -277,6 +289,19 @@ "pty.slave_open", "xml.etree.ElementTree.Element.__bool__", }, + (3, 13, 0): { + "ctypes.SetPointerType", + "pathlib.PurePath.is_reserved", + "platform.java_ver", + "pydoc.is_package", + "sys._enablelegacywindowsfsencoding", + "wave.Wave_read.getmark", + "wave.Wave_read.getmarkers", + "wave.Wave_read.setmark", + "wave.Wave_write.getmark", + "wave.Wave_write.getmarkers", + "wave.Wave_write.setmark", + }, }, } @@ -333,6 +358,9 @@ "typing": { "Text", }, + "urllib.parse": { + "Quoter", + }, "webbrowser": { "MacOSX", }, @@ -365,6 +393,15 @@ "Sized", }, }, + (3, 13, 0): { + "glob": { + "glob.glob0", + "glob.glob1", + }, + "http.server": { + "CGIHTTPRequestHandler", + }, + }, } @@ -375,10 +412,18 @@ (3, 12, 0): { "calendar.January", "calendar.February", + "sqlite3.version", + "sqlite3.version_info", "sys.last_traceback", "sys.last_type", "sys.last_value", }, + (3, 13, 0): { + "dis.HAVE_ARGUMENT", + "tarfile.TarFile.tarfile", + "traceback.TracebackException.exc_type", + "typing.AnyStr", + }, } @@ -784,7 +829,7 @@ def _check_open_call( mode_arg = utils.get_argument_from_call( node, position=1, keyword="mode" ) - elif open_module == "pathlib": + elif open_module in PATHLIB_MODULE: mode_arg = utils.get_argument_from_call( node, position=0, keyword="mode" ) @@ -814,7 +859,7 @@ def _check_open_call( ): confidence = HIGH try: - if open_module == "pathlib": + if open_module in PATHLIB_MODULE: if node.func.attrname == "read_text": encoding_arg = utils.get_argument_from_call( node, position=0, keyword="encoding" diff --git a/pylint/checkers/similar.py b/pylint/checkers/symilar.py similarity index 91% rename from pylint/checkers/similar.py rename to pylint/checkers/symilar.py index cfe649b1809..1e82633e65a 100644 --- a/pylint/checkers/similar.py +++ b/pylint/checkers/symilar.py @@ -39,20 +39,9 @@ import warnings from collections import defaultdict from collections.abc import Callable, Generator, Iterable, Sequence -from getopt import getopt from io import BufferedIOBase, BufferedReader, BytesIO from itertools import chain -from typing import ( - TYPE_CHECKING, - Dict, - List, - NamedTuple, - NewType, - NoReturn, - TextIO, - Tuple, - Union, -) +from typing import TYPE_CHECKING, NamedTuple, NewType, NoReturn, TextIO, Union import astroid from astroid import nodes @@ -84,10 +73,10 @@ class LineSpecifs(NamedTuple): # Links LinesChunk object to the starting indices (in lineset's stripped lines) # of the different chunk of lines that are used to compute the hash -HashToIndex_T = Dict["LinesChunk", List[Index]] +HashToIndex_T = dict["LinesChunk", list[Index]] # Links index in the lineset's stripped lines to the real lines in the file -IndexToLines_T = Dict[Index, "SuccessiveLinesLimits"] +IndexToLines_T = dict[Index, "SuccessiveLinesLimits"] # The types the streams read by pylint can take. Originating from astroid.nodes.Module.stream() and open() STREAM_TYPES = Union[TextIO, BufferedReader, BytesIO] @@ -113,7 +102,7 @@ def __init__( # Links the indices to the starting line in both lineset's stripped lines to # the start and end lines in both files -CplIndexToCplLines_T = Dict["LineSetStartCouple", CplSuccessiveLinesLimits] +CplIndexToCplLines_T = dict["LineSetStartCouple", CplSuccessiveLinesLimits] class LinesChunk: @@ -212,7 +201,7 @@ def increment(self, value: Index) -> LineSetStartCouple: ) -LinesChunkLimits_T = Tuple["LineSet", LineNumber, LineNumber] +LinesChunkLimits_T = tuple["LineSet", LineNumber, LineNumber] def hash_lineset( @@ -343,7 +332,7 @@ class Commonality(NamedTuple): snd_file_end: LineNumber -class Similar: +class Symilar: """Finds copy-pasted lines of code in a project.""" def __init__( @@ -479,7 +468,7 @@ def _get_similarity_report( # pylint: disable = too-many-locals def _find_common( self, lineset1: LineSet, lineset2: LineSet - ) -> Generator[Commonality, None, None]: + ) -> Generator[Commonality]: """Find similarities in the two given linesets. This the core of the algorithm. The idea is to compute the hashes of a @@ -552,7 +541,7 @@ def _find_common( if eff_cmn_nb > self.namespace.min_similarity_lines: yield com - def _iter_sims(self) -> Generator[Commonality, None, None]: + def _iter_sims(self) -> Generator[Commonality]: """Iterate on similarities among all files, by making a Cartesian product. """ @@ -751,19 +740,21 @@ def report_similarities( # wrapper to get a pylint checker from the similar class -class SimilarChecker(BaseRawFileChecker, Similar): +class SimilaritiesChecker(BaseRawFileChecker, Symilar): """Checks for similarities and duplicated code. This computation may be memory / CPU intensive, so you should disable it if you experience some problems. """ - # configuration section name name = "similarities" - # messages msgs = MSGS - # configuration options - # for available dict keys/values see the optik parser 'add_option' method + MIN_SIMILARITY_HELP = "Minimum lines number of a similarity." + IGNORE_COMMENTS_HELP = "Comments are removed from the similarity computation" + IGNORE_DOCSTRINGS_HELP = "Docstrings are removed from the similarity computation" + IGNORE_IMPORTS_HELP = "Imports are removed from the similarity computation" + IGNORE_SIGNATURES_HELP = "Signatures are removed from the similarity computation" + # for available dict keys/values see the option parser 'add_option' method options: Options = ( ( "min-similarity-lines", @@ -771,7 +762,7 @@ class SimilarChecker(BaseRawFileChecker, Similar): "default": DEFAULT_MIN_SIMILARITY_LINE, "type": "int", "metavar": "", - "help": "Minimum lines number of a similarity.", + "help": MIN_SIMILARITY_HELP, }, ), ( @@ -780,7 +771,7 @@ class SimilarChecker(BaseRawFileChecker, Similar): "default": True, "type": "yn", "metavar": "", - "help": "Comments are removed from the similarity computation", + "help": IGNORE_COMMENTS_HELP, }, ), ( @@ -789,7 +780,7 @@ class SimilarChecker(BaseRawFileChecker, Similar): "default": True, "type": "yn", "metavar": "", - "help": "Docstrings are removed from the similarity computation", + "help": IGNORE_DOCSTRINGS_HELP, }, ), ( @@ -798,7 +789,7 @@ class SimilarChecker(BaseRawFileChecker, Similar): "default": True, "type": "yn", "metavar": "", - "help": "Imports are removed from the similarity computation", + "help": IGNORE_IMPORTS_HELP, }, ), ( @@ -807,16 +798,15 @@ class SimilarChecker(BaseRawFileChecker, Similar): "default": True, "type": "yn", "metavar": "", - "help": "Signatures are removed from the similarity computation", + "help": IGNORE_SIGNATURES_HELP, }, ), ) - # reports reports = (("RP0801", "Duplication", report_similarities),) def __init__(self, linter: PyLinter) -> None: BaseRawFileChecker.__init__(self, linter) - Similar.__init__( + Symilar.__init__( self, min_lines=self.linter.config.min_similarity_lines, ignore_comments=self.linter.config.ignore_comments, @@ -873,7 +863,7 @@ def close(self) -> None: def get_map_data(self) -> list[LineSet]: """Passthru override.""" - return Similar.get_map_data(self) + return Symilar.get_map_data(self) def reduce_map_data(self, linter: PyLinter, data: list[list[LineSet]]) -> None: """Reduces and recombines data into a format that we can report on. @@ -882,67 +872,61 @@ def reduce_map_data(self, linter: PyLinter, data: list[list[LineSet]]) -> None: Calls self.close() to actually calculate and report duplicate code. """ - Similar.combine_mapreduce_data(self, linesets_collection=data) + Symilar.combine_mapreduce_data(self, linesets_collection=data) self.close() def register(linter: PyLinter) -> None: - linter.register_checker(SimilarChecker(linter)) - - -def usage(status: int = 0) -> NoReturn: - """Display command line usage information.""" - print("finds copy pasted blocks in a set of files") - print() - print( - "Usage: symilar [-d|--duplicates min_duplicated_lines] \ -[-i|--ignore-comments] [--ignore-docstrings] [--ignore-imports] [--ignore-signatures] file1..." - ) - sys.exit(status) + linter.register_checker(SimilaritiesChecker(linter)) def Run(argv: Sequence[str] | None = None) -> NoReturn: """Standalone command line access point.""" - if argv is None: - argv = sys.argv[1:] - - s_opts = "hdi" - l_opts = [ - "help", - "duplicates=", - "ignore-comments", - "ignore-imports", - "ignore-docstrings", - "ignore-signatures", - ] - min_lines = DEFAULT_MIN_SIMILARITY_LINE - ignore_comments = False - ignore_docstrings = False - ignore_imports = False - ignore_signatures = False - opts, args = getopt(list(argv), s_opts, l_opts) - for opt, val in opts: - if opt in {"-d", "--duplicates"}: - min_lines = int(val) - elif opt in {"-h", "--help"}: - usage() - elif opt in {"-i", "--ignore-comments"}: - ignore_comments = True - elif opt in {"--ignore-docstrings"}: - ignore_docstrings = True - elif opt in {"--ignore-imports"}: - ignore_imports = True - elif opt in {"--ignore-signatures"}: - ignore_signatures = True - if not args: - usage(1) - sim = Similar( - min_lines, ignore_comments, ignore_docstrings, ignore_imports, ignore_signatures + parser = argparse.ArgumentParser( + prog="symilar", description="Finds copy pasted blocks in a set of files." + ) + parser.add_argument("files", nargs="+") + parser.add_argument( + "-d", + "--duplicates", + type=int, + default=DEFAULT_MIN_SIMILARITY_LINE, + help=SimilaritiesChecker.MIN_SIMILARITY_HELP, + ) + parser.add_argument( + "-i", + "--ignore-comments", + action="store_true", + help=SimilaritiesChecker.IGNORE_COMMENTS_HELP, + ) + parser.add_argument( + "--ignore-docstrings", + action="store_true", + help=SimilaritiesChecker.IGNORE_DOCSTRINGS_HELP, + ) + parser.add_argument( + "--ignore-imports", + action="store_true", + help=SimilaritiesChecker.IGNORE_IMPORTS_HELP, + ) + parser.add_argument( + "--ignore-signatures", + action="store_true", + help=SimilaritiesChecker.IGNORE_SIGNATURES_HELP, + ) + parsed_args = parser.parse_args(args=argv) + similar_runner = Symilar( + min_lines=parsed_args.duplicates, + ignore_comments=parsed_args.ignore_comments, + ignore_docstrings=parsed_args.ignore_docstrings, + ignore_imports=parsed_args.ignore_imports, + ignore_signatures=parsed_args.ignore_signatures, ) - for filename in args: + for filename in parsed_args.files: with open(filename, encoding="utf-8") as stream: - sim.append_stream(filename, stream) - sim.run() + similar_runner.append_stream(filename, stream) + similar_runner.run() + # the sys exit must be kept because of the unit tests that rely on it sys.exit(0) diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 67852d38fff..bc7ddfc2a40 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1419,9 +1419,27 @@ def _check_argument_order( 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: nodes.Call) -> None: - if len(node.args) != 2: - # isinstance called with wrong number of args + def _check_isinstance_args(self, node: nodes.Call, callable_name: str) -> None: + if len(node.args) > 2: + # for when isinstance called with too many args + self.add_message( + "too-many-function-args", + node=node, + args=(callable_name,), + confidence=HIGH, + ) + elif len(node.args) < 2: + # NOTE: Hard-coding the parameters for `isinstance` is fragile, + # but as noted elsewhere, built-in functions do not provide + # argument info, making this necessary for now. + parameters = ("'_obj'", "'__class_or_tuple'") + for parameter in parameters[len(node.args) :]: + self.add_message( + "no-value-for-parameter", + node=node, + args=(parameter, callable_name), + confidence=HIGH, + ) return second_arg = node.args[1] @@ -1437,7 +1455,7 @@ 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. """ - called = safe_infer(node.func) + called = safe_infer(node.func, compare_constructors=True) self._check_not_callable(node, called) @@ -1451,7 +1469,7 @@ def visit_call(self, node: nodes.Call) -> None: if called.args.args is None: if called.name == "isinstance": # Verify whether second argument of isinstance is a valid type - self._check_isinstance_args(node) + self._check_isinstance_args(node, callable_name) # Built-in functions have no argument information. return @@ -1554,7 +1572,9 @@ def visit_call(self, node: nodes.Call) -> None: elif not overload_function: # Too many positional arguments. self.add_message( - "too-many-function-args", node=node, args=(callable_name,) + "too-many-function-args", + node=node, + args=(callable_name,), ) break diff --git a/pylint/checkers/unsupported_version.py b/pylint/checkers/unsupported_version.py index 64f2630d8b1..53b5f63fbab 100644 --- a/pylint/checkers/unsupported_version.py +++ b/pylint/checkers/unsupported_version.py @@ -18,6 +18,7 @@ safe_infer, uninferable_final_decorators, ) +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter @@ -42,6 +43,30 @@ class UnsupportedVersionChecker(BaseChecker): "Used when the py-version set by the user is lower than 3.8 and pylint encounters " "a ``typing.final`` decorator.", ), + "W2603": ( + "Exception groups are not supported by all versions included in the py-version setting", + "using-exception-groups-in-unsupported-version", + "Used when the py-version set by the user is lower than 3.11 and pylint encounters " + "``except*`` or `ExceptionGroup``.", + ), + "W2604": ( + "Generic type syntax (PEP 695) is not supported by all versions included in the py-version setting", + "using-generic-type-syntax-in-unsupported-version", + "Used when the py-version set by the user is lower than 3.12 and pylint encounters " + "generic type syntax.", + ), + "W2605": ( + "Assignment expression is not supported by all versions included in the py-version setting", + "using-assignment-expression-in-unsupported-version", + "Used when the py-version set by the user is lower than 3.8 and pylint encounters " + "an assignment expression (walrus) operator.", + ), + "W2606": ( + "Positional-only arguments are not supported by all versions included in the py-version setting", + "using-positional-only-args-in-unsupported-version", + "Used when the py-version set by the user is lower than 3.8 and pylint encounters " + "positional-only arguments.", + ), } def open(self) -> None: @@ -49,12 +74,34 @@ def open(self) -> None: py_version = self.linter.config.py_version self._py36_plus = py_version >= (3, 6) self._py38_plus = py_version >= (3, 8) + self._py311_plus = py_version >= (3, 11) + self._py312_plus = py_version >= (3, 12) @only_required_for_messages("using-f-string-in-unsupported-version") def visit_joinedstr(self, node: nodes.JoinedStr) -> None: """Check f-strings.""" if not self._py36_plus: - self.add_message("using-f-string-in-unsupported-version", node=node) + self.add_message( + "using-f-string-in-unsupported-version", node=node, confidence=HIGH + ) + + @only_required_for_messages("using-assignment-expression-in-unsupported-version") + def visit_namedexpr(self, node: nodes.JoinedStr) -> None: + if not self._py38_plus: + self.add_message( + "using-assignment-expression-in-unsupported-version", + node=node, + confidence=HIGH, + ) + + @only_required_for_messages("using-positional-only-args-in-unsupported-version") + def visit_arguments(self, node: nodes.Arguments) -> None: + if not self._py38_plus and node.posonlyargs: + self.add_message( + "using-positional-only-args-in-unsupported-version", + node=node, + confidence=HIGH, + ) @only_required_for_messages("using-final-decorator-in-unsupported-version") def visit_decorators(self, node: nodes.Decorators) -> None: @@ -76,7 +123,72 @@ def _check_typing_final(self, node: nodes.Decorators) -> None: for decorator in decorators or uninferable_final_decorators(node): self.add_message( - "using-final-decorator-in-unsupported-version", node=decorator + "using-final-decorator-in-unsupported-version", + node=decorator, + confidence=HIGH, + ) + + @only_required_for_messages("using-exception-groups-in-unsupported-version") + def visit_trystar(self, node: nodes.TryStar) -> None: + if not self._py311_plus: + self.add_message( + "using-exception-groups-in-unsupported-version", + node=node, + confidence=HIGH, + ) + + @only_required_for_messages("using-exception-groups-in-unsupported-version") + def visit_excepthandler(self, node: nodes.ExceptHandler) -> None: + if ( + not self._py311_plus + and isinstance(node.type, nodes.Name) + and node.type.name == "ExceptionGroup" + ): + self.add_message( + "using-exception-groups-in-unsupported-version", + node=node, + confidence=HIGH, + ) + + @only_required_for_messages("using-exception-groups-in-unsupported-version") + def visit_raise(self, node: nodes.Raise) -> None: + if ( + not self._py311_plus + and isinstance(node.exc, nodes.Call) + and isinstance(node.exc.func, nodes.Name) + and node.exc.func.name == "ExceptionGroup" + ): + self.add_message( + "using-exception-groups-in-unsupported-version", + node=node, + confidence=HIGH, + ) + + @only_required_for_messages("using-generic-type-syntax-in-unsupported-version") + def visit_typealias(self, node: nodes.TypeAlias) -> None: + if not self._py312_plus: + self.add_message( + "using-generic-type-syntax-in-unsupported-version", + node=node, + confidence=HIGH, + ) + + @only_required_for_messages("using-generic-type-syntax-in-unsupported-version") + def visit_typevar(self, node: nodes.TypeVar) -> None: + if not self._py312_plus: + self.add_message( + "using-generic-type-syntax-in-unsupported-version", + node=node, + confidence=HIGH, + ) + + @only_required_for_messages("using-generic-type-syntax-in-unsupported-version") + def visit_typevartuple(self, node: nodes.TypeVarTuple) -> None: + if not self._py312_plus: + self.add_message( + "using-generic-type-syntax-in-unsupported-version", + node=node, + confidence=HIGH, ) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index b1cadda1d6f..bfc4bc61da6 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -13,10 +13,10 @@ import numbers import re import string -from collections.abc import Iterable, Iterator +from collections.abc import Callable, Iterable, Iterator from functools import lru_cache, partial from re import Match -from typing import TYPE_CHECKING, Any, Callable, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar import astroid.objects from astroid import TooManyLevelsError, nodes, util @@ -25,6 +25,8 @@ from astroid.nodes._base_nodes import ImportNode, Statement from astroid.typing import InferenceResult, SuccessfulInferenceResult +from pylint.constants import TYPING_NEVER, TYPING_NORETURN + if TYPE_CHECKING: from functools import _lru_cache_wrapper @@ -1344,6 +1346,7 @@ def safe_infer( context: InferenceContext | None = None, *, compare_constants: bool = False, + compare_constructors: bool = False, ) -> InferenceResult | None: """Return the inferred value for the given node. @@ -1352,6 +1355,9 @@ def safe_infer( If compare_constants is True and if multiple constants are inferred, unequal inferred values are also considered ambiguous and return None. + + If compare_constructors is True and if multiple classes are inferred, + constructors with different signatures are held ambiguous and return None. """ inferred_types: set[str | None] = set() try: @@ -1384,6 +1390,13 @@ def safe_infer( and function_arguments_are_ambiguous(inferred, value) ): return None + if ( + compare_constructors + and isinstance(inferred, nodes.ClassDef) + and isinstance(value, nodes.ClassDef) + and class_constructors_are_ambiguous(inferred, value) + ): + return None except astroid.InferenceError: return None # There is some kind of ambiguity except StopIteration: @@ -1432,6 +1445,21 @@ def function_arguments_are_ambiguous( return False +def class_constructors_are_ambiguous( + class1: nodes.ClassDef, class2: nodes.ClassDef +) -> bool: + try: + constructor1 = class1.local_attr("__init__")[0] + constructor2 = class2.local_attr("__init__")[0] + except astroid.NotFoundError: + return False + if not isinstance(constructor1, nodes.FunctionDef): + return False + if not isinstance(constructor2, nodes.FunctionDef): + return False + return function_arguments_are_ambiguous(constructor1, constructor2) + + def has_known_bases( klass: nodes.ClassDef, context: InferenceContext | None = None ) -> bool: @@ -1815,10 +1843,7 @@ def is_sys_guard(node: nodes.If) -> bool: """Return True if IF stmt is a sys.version_info guard. >>> import sys - >>> if sys.version_info > (3, 8): - >>> from typing import Literal - >>> else: - >>> from typing_extensions import Literal + >>> from typing import Literal """ if isinstance(node.test, nodes.Compare): value = node.test.left @@ -2150,7 +2175,9 @@ def is_singleton_const(node: nodes.NodeNG) -> bool: def is_terminating_func(node: nodes.Call) -> bool: - """Detect call to exit(), quit(), os._exit(), or sys.exit().""" + """Detect call to exit(), quit(), os._exit(), sys.exit(), or + functions annotated with `typing.NoReturn` or `typing.Never`. + """ if ( not isinstance(node.func, nodes.Attribute) and not (isinstance(node.func, nodes.Name)) @@ -2165,6 +2192,31 @@ def is_terminating_func(node: nodes.Call) -> bool: and inferred.qname() in TERMINATING_FUNCS_QNAMES ): return True + # Unwrap to get the actual function node object + if isinstance(inferred, astroid.BoundMethod) and isinstance( + inferred._proxied, astroid.UnboundMethod + ): + inferred = inferred._proxied._proxied + if ( # pylint: disable=too-many-boolean-expressions + isinstance(inferred, nodes.FunctionDef) + and ( + not isinstance(inferred, nodes.AsyncFunctionDef) + or isinstance(node.parent, nodes.Await) + ) + and isinstance(inferred.returns, nodes.Name) + and (inferred_func := safe_infer(inferred.returns)) + and hasattr(inferred_func, "qname") + and inferred_func.qname() + in ( + *TYPING_NEVER, + *TYPING_NORETURN, + # In Python 3.7 - 3.8, NoReturn is alias of '_SpecialForm' + # "typing._SpecialForm", + # But 'typing.Any' also inherits _SpecialForm + # See #9751 + ) + ): + return True except (StopIteration, astroid.InferenceError): pass diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 8447320b89a..7a63798d916 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -6,23 +6,19 @@ from __future__ import annotations -import collections import copy import itertools import math import os import re from collections import defaultdict -from collections.abc import Generator, Iterable, Iterator from enum import Enum from functools import cached_property -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import TYPE_CHECKING import astroid import astroid.exceptions from astroid import bases, extract_node, nodes, util -from astroid.nodes import _base_nodes -from astroid.typing import InferenceResult from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import ( @@ -32,12 +28,20 @@ is_sys_guard, overridden_method, ) -from pylint.constants import PY39_PLUS, PY311_PLUS, TYPING_NEVER, TYPING_NORETURN +from pylint.constants import TYPING_NEVER, TYPING_NORETURN from pylint.interfaces import CONTROL_FLOW, HIGH, INFERENCE, INFERENCE_FAILURE -from pylint.typing import MessageDefinitionTuple if TYPE_CHECKING: + from collections.abc import Generator, Iterable, Iterator + + from astroid.nodes import _base_nodes + from astroid.typing import InferenceResult + from pylint.lint import PyLinter + from pylint.typing import MessageDefinitionTuple + + Consumption = dict[str, list[nodes.NodeNG]] + SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") FUTURE = "__future__" @@ -49,63 +53,6 @@ METACLASS_NAME_TRANSFORMS = {"_py_abc": "abc"} BUILTIN_RANGE = "builtins.range" TYPING_MODULE = "typing" -TYPING_NAMES = frozenset( - { - "Any", - "Callable", - "ClassVar", - "Generic", - "Optional", - "Tuple", - "Type", - "TypeVar", - "Union", - "AbstractSet", - "ByteString", - "Container", - "ContextManager", - "Hashable", - "ItemsView", - "Iterable", - "Iterator", - "KeysView", - "Mapping", - "MappingView", - "MutableMapping", - "MutableSequence", - "MutableSet", - "Sequence", - "Sized", - "ValuesView", - "Awaitable", - "AsyncIterator", - "AsyncIterable", - "Coroutine", - "Collection", - "AsyncGenerator", - "AsyncContextManager", - "Reversible", - "SupportsAbs", - "SupportsBytes", - "SupportsComplex", - "SupportsFloat", - "SupportsInt", - "SupportsRound", - "Counter", - "Deque", - "Dict", - "DefaultDict", - "List", - "Set", - "FrozenSet", - "NamedTuple", - "Generator", - "AnyStr", - "Text", - "Pattern", - "BinaryIO", - } -) DICT_TYPES = ( astroid.objects.DictValues, @@ -174,7 +121,9 @@ def _get_unpacking_extra_info(node: nodes.Assign, inferred: InferenceResult) -> def _detect_global_scope( - node: nodes.Name, frame: nodes.LocalsDictNodeNG, defframe: nodes.LocalsDictNodeNG + node: nodes.Name, + frame: nodes.LocalsDictNodeNG, + defframe: nodes.LocalsDictNodeNG, ) -> bool: """Detect that the given frames share a global scope. @@ -249,16 +198,14 @@ class C: ... return frame.lineno < defframe.lineno # type: ignore[no-any-return] -def _infer_name_module( - node: nodes.Import, name: str -) -> Generator[InferenceResult, None, None]: +def _infer_name_module(node: nodes.Import, name: str) -> Generator[InferenceResult]: context = astroid.context.InferenceContext() context.lookupname = name return node.infer(context, asname=False) # type: ignore[no-any-return] def _fix_dot_imports( - not_consumed: dict[str, list[nodes.NodeNG]] + not_consumed: Consumption, ) -> list[tuple[str, _base_nodes.ImportNode]]: """Try to fix imports with multiple dots, by returning a dictionary with the import names expanded. @@ -326,7 +273,8 @@ def _find_frame_imports(name: str, frame: nodes.LocalsDictNodeNG) -> bool: def _import_name_is_global( - stmt: nodes.Global | _base_nodes.ImportNode, global_names: set[str] + 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. @@ -355,6 +303,31 @@ def _assigned_locally(name_node: nodes.Name) -> bool: ) +def _is_before(node: nodes.NodeNG, reference_node: nodes.NodeNG) -> bool: + """Checks if node appears before reference_node.""" + if node.lineno < reference_node.lineno: + return True + if ( + node.lineno == reference_node.lineno + and node.col_offset < reference_node.col_offset + ): + return True + return False + + +def _is_nonlocal_name(node: nodes.Name, frame: nodes.LocalsDictNodeNG) -> bool: + """Checks if name node has a nonlocal declaration in the given frame.""" + if not isinstance(frame, nodes.FunctionDef): + return False + + return any( + isinstance(stmt, nodes.Nonlocal) + and node.name in stmt.names + and _is_before(stmt, node) + for stmt in frame.body + ) + + def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) -> bool: skip_nodes = ( nodes.FunctionDef, @@ -526,32 +499,39 @@ def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) -> } -class ScopeConsumer(NamedTuple): - """Store nodes and their consumption states.""" +class NamesConsumer: + """A simple class to handle consumed, to consume and scope type info of node locals.""" - to_consume: dict[str, list[nodes.NodeNG]] - consumed: dict[str, list[nodes.NodeNG]] - consumed_uncertain: defaultdict[str, list[nodes.NodeNG]] + node: nodes.NodeNG scope_type: str + to_consume: Consumption + consumed: Consumption + consumed_uncertain: Consumption + """Retrieves nodes filtered out by get_next_to_consume() that may not + have executed. -class NamesConsumer: - """A simple class to handle consumed, to consume and scope type info of node locals.""" + These include nodes such as statements in except blocks, or statements + in try blocks (when evaluating their corresponding except and finally + blocks). Checkers that want to treat the statements as executed + (e.g. for unused-variable) may need to add them back. + """ - def __init__(self, node: nodes.NodeNG, scope_type: str) -> None: - self._atomic = ScopeConsumer( - copy.copy(node.locals), {}, collections.defaultdict(list), scope_type - ) + def __init__(self, node: nodes.NodeNG, scope_type: str): self.node = node + self.scope_type = scope_type + + self.to_consume = copy.copy(node.locals) + self.consumed = {} + self.consumed_uncertain = defaultdict(list) + self.names_under_always_false_test: set[str] = set() self.names_defined_under_one_branch_only: set[str] = set() def __repr__(self) -> str: - _to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()] - _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 = [f"{k}->{v}" for k, v in self.to_consume.items()] + _consumed = [f"{k}->{v}" for k, v in self.consumed.items()] + _consumed_uncertain = [f"{k}->{v}" for k, v in self.consumed_uncertain.items()] to_consumes = ", ".join(_to_consumes) consumed = ", ".join(_consumed) consumed_uncertain = ", ".join(_consumed_uncertain) @@ -559,36 +539,9 @@ def __repr__(self) -> str: to_consume : {to_consumes} consumed : {consumed} consumed_uncertain: {consumed_uncertain} -scope_type : {self._atomic.scope_type} +scope_type : {self.scope_type} """ - def __iter__(self) -> Iterator[Any]: - return iter(self._atomic) - - @property - def to_consume(self) -> dict[str, list[nodes.NodeNG]]: - return self._atomic.to_consume - - @property - def consumed(self) -> dict[str, list[nodes.NodeNG]]: - return self._atomic.consumed - - @property - def consumed_uncertain(self) -> defaultdict[str, list[nodes.NodeNG]]: - """Retrieves nodes filtered out by get_next_to_consume() that may not - have executed. - - These include nodes such as statements in except blocks, or statements - in try blocks (when evaluating their corresponding except and finally - blocks). Checkers that want to treat the statements as executed - (e.g. for unused-variable) may need to add them back. - """ - return self._atomic.consumed_uncertain - - @property - def scope_type(self) -> str: - return self._atomic.scope_type - def mark_as_consumed(self, name: str, consumed_nodes: list[nodes.NodeNG]) -> None: """Mark the given nodes as consumed for the name. @@ -634,10 +587,7 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: found_nodes = None # Before filtering, check that this node's name is not a nonlocal - if any( - isinstance(child, nodes.Nonlocal) and node.name in child.names - for child in node.frame().get_children() - ): + if _is_nonlocal_name(node, node.frame()): return found_nodes # And no comprehension is under the node's frame @@ -697,7 +647,9 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: return found_nodes def _inferred_to_define_name_raise_or_return( - self, name: str, node: nodes.NodeNG + self, + name: str, + node: nodes.NodeNG, ) -> bool: """Return True if there is a path under this `if_node` that is inferred to define `name`, raise, or return. @@ -777,7 +729,9 @@ def _branch_handles_name(self, name: str, body: Iterable[nodes.NodeNG]) -> bool: ) def _uncertain_nodes_if_tests( - self, found_nodes: list[nodes.NodeNG], node: nodes.NodeNG + self, + found_nodes: list[nodes.NodeNG], + node: nodes.NodeNG, ) -> list[nodes.NodeNG]: """Identify nodes of uncertain execution because they are defined under if tests. @@ -791,6 +745,8 @@ def _uncertain_nodes_if_tests( name = other_node.name elif isinstance(other_node, (nodes.Import, nodes.ImportFrom)): name = node.name + elif isinstance(other_node, (nodes.FunctionDef, nodes.ClassDef)): + name = other_node.name else: continue @@ -831,10 +787,14 @@ def _node_guarded_by_same_test(node: nodes.NodeNG, other_if: nodes.If) -> bool: or if their inferred values consist only of constants and those constants are identical, and the if test guarding `node` is not a Name. """ - other_if_test_as_string = other_if.test.as_string() - other_if_test_all_inferred = utils.infer_all(other_if.test) + if isinstance(other_if.test, nodes.NamedExpr): + other_if_test = other_if.test.target + else: + other_if_test = other_if.test + other_if_test_as_string = other_if_test.as_string() + other_if_test_all_inferred = utils.infer_all(other_if_test) for ancestor in node.node_ancestors(): - if not isinstance(ancestor, nodes.If): + if not isinstance(ancestor, (nodes.If, nodes.IfExp)): continue if ancestor.test.as_string() == other_if_test_as_string: return True @@ -944,8 +904,7 @@ def _defines_name_raises_or_returns(name: str, node: nodes.NodeNG) -> bool: if utils.is_terminating_func(node.value): return True if ( - PY311_PLUS - and isinstance(node.value.func, nodes.Name) + isinstance(node.value.func, nodes.Name) and node.value.func.name == "assert_never" ): return True @@ -991,7 +950,8 @@ def _defines_name_raises_or_returns(name: str, node: nodes.NodeNG) -> bool: @staticmethod def _defines_name_raises_or_returns_recursive( - name: str, node: nodes.NodeNG + name: str, + node: nodes.NodeNG, ) -> bool: """Return True if some child of `node` defines the name `name`, raises, or returns. @@ -1015,7 +975,8 @@ def _defines_name_raises_or_returns_recursive( @staticmethod def _check_loop_finishes_via_except( - node: nodes.NodeNG, other_node_try_except: nodes.Try + node: nodes.NodeNG, + other_node_try_except: nodes.Try, ) -> bool: """Check for a specific control flow scenario. @@ -1058,7 +1019,8 @@ def _check_loop_finishes_via_except( return False def _try_in_loop_body( - other_node_try_except: nodes.Try, loop: nodes.For | nodes.While + other_node_try_except: nodes.Try, + loop: nodes.For | nodes.While, ) -> bool: """Return True if `other_node_try_except` is a descendant of `loop`.""" return any( @@ -1088,7 +1050,8 @@ def _try_in_loop_body( @staticmethod def _recursive_search_for_continue_before_break( - stmt: _base_nodes.Statement, break_stmt: nodes.Break + stmt: _base_nodes.Statement, + break_stmt: nodes.Break, ) -> bool: """Return True if any Continue node can be found in descendants of `stmt` before encountering `break_stmt`, ignoring any nested loops. @@ -1108,7 +1071,8 @@ def _recursive_search_for_continue_before_break( @staticmethod def _uncertain_nodes_in_try_blocks_when_evaluating_except_blocks( - found_nodes: list[nodes.NodeNG], node_statement: _base_nodes.Statement + found_nodes: list[nodes.NodeNG], + node_statement: _base_nodes.Statement, ) -> list[nodes.NodeNG]: """Return any nodes in ``found_nodes`` that should be treated as uncertain. @@ -1327,7 +1291,7 @@ def __init__(self, linter: PyLinter) -> None: tuple[nodes.ExceptHandler, nodes.AssignName] ] = [] """This is a queue, last in first out.""" - self._evaluated_type_checking_scopes: dict[ + self._reported_type_checking_usage_scopes: dict[ str, list[nodes.LocalsDictNodeNG] ] = {} self._postponed_evaluation_enabled = False @@ -1708,7 +1672,9 @@ def leave_excepthandler(self, node: nodes.ExceptHandler) -> None: self._except_handler_names_queue.pop() def _undefined_and_used_before_checker( - self, node: nodes.Name, stmt: nodes.NodeNG + self, + node: nodes.Name, + stmt: nodes.NodeNG, ) -> None: frame = stmt.scope() start_index = len(self._to_consume) - 1 @@ -1756,7 +1722,10 @@ def _undefined_and_used_before_checker( self.add_message("undefined-variable", args=node.name, node=node) def _should_node_be_skipped( - self, node: nodes.Name, consumer: NamesConsumer, is_start_index: bool + self, + node: nodes.Name, + consumer: NamesConsumer, + is_start_index: bool, ) -> bool: """Tests a consumer and node for various conditions in which the node shouldn't be checked for the undefined-variable and used-before-assignment checks. @@ -1822,12 +1791,12 @@ def _check_consumer( if found_nodes is None: return (VariableVisitConsumerAction.CONTINUE, None) if not found_nodes: - self._report_unfound_name_definition(node, current_consumer) + is_reported = self._report_unfound_name_definition(node, current_consumer) # Mark for consumption any nodes added to consumed_uncertain by # get_next_to_consume() because they might not have executed. nodes_to_consume = current_consumer.consumed_uncertain[node.name] - nodes_to_consume = self._filter_type_checking_import_from_consumption( - node, nodes_to_consume + nodes_to_consume = self._filter_type_checking_definitions_from_consumption( + node, nodes_to_consume, is_reported ) return ( VariableVisitConsumerAction.RETURN, @@ -1998,25 +1967,33 @@ def _check_consumer( return (VariableVisitConsumerAction.RETURN, found_nodes) def _report_unfound_name_definition( - self, node: nodes.NodeNG, current_consumer: NamesConsumer - ) -> None: - """Reports used-before-assignment when all name definition nodes - get filtered out by NamesConsumer. + self, + node: nodes.Name, + current_consumer: NamesConsumer, + ) -> bool: + """Reports used-before-assignment error when all name definition nodes + are filtered out by NamesConsumer. + + Returns True if an error is reported; otherwise, returns False. """ if ( self._postponed_evaluation_enabled and utils.is_node_in_type_annotation_context(node) ): - return + return False if self._is_builtin(node.name): - return + return False if self._is_variable_annotation_in_function(node): - return + return False + if self._has_nonlocal_in_enclosing_frame( + node, current_consumer.consumed_uncertain.get(node.name, []) + ): + return False if ( - node.name in self._evaluated_type_checking_scopes - and node.scope() in self._evaluated_type_checking_scopes[node.name] + node.name in self._reported_type_checking_usage_scopes + and node.scope() in self._reported_type_checking_usage_scopes[node.name] ): - return + return False confidence = HIGH if node.name in current_consumer.names_under_always_false_test: @@ -2036,29 +2013,33 @@ def _report_unfound_name_definition( confidence=confidence, ) - def _filter_type_checking_import_from_consumption( - self, node: nodes.NodeNG, nodes_to_consume: list[nodes.NodeNG] + return True + + def _filter_type_checking_definitions_from_consumption( + self, + node: nodes.NodeNG, + nodes_to_consume: list[nodes.NodeNG], + is_reported: bool, ) -> list[nodes.NodeNG]: - """Do not consume type-checking import node as used-before-assignment - may invoke in different scopes. + """Filters out type-checking definition nodes (e.g. imports, class definitions) + from consumption, as used-before-assignment may invoke in a different context. + + If used-before-assignment is reported for the usage of a type-checking definition, + track the scope of that usage for future evaluation. """ - type_checking_import = next( - ( - n - for n in nodes_to_consume - if isinstance(n, (nodes.Import, nodes.ImportFrom)) - and in_type_checking_block(n) - ), - None, - ) - # If used-before-assignment reported for usage of type checking import - # keep track of its scope - if type_checking_import and not self._is_variable_annotation_in_function(node): - self._evaluated_type_checking_scopes.setdefault(node.name, []).append( + type_checking_definitions = { + n + for n in nodes_to_consume + if isinstance(n, (nodes.Import, nodes.ImportFrom, nodes.ClassDef)) + and in_type_checking_block(n) + } + + if type_checking_definitions and is_reported: + self._reported_type_checking_usage_scopes.setdefault(node.name, []).append( node.scope() ) - nodes_to_consume = [n for n in nodes_to_consume if n != type_checking_import] - return nodes_to_consume + + return [n for n in nodes_to_consume if n not in type_checking_definitions] @utils.only_required_for_messages("no-name-in-module") def visit_import(self, node: nodes.Import) -> None: @@ -2172,7 +2153,8 @@ def _allow_global_unused_variables(self) -> bool: @staticmethod def _defined_in_function_definition( - node: nodes.NodeNG, frame: nodes.NodeNG + node: nodes.NodeNG, + frame: nodes.NodeNG, ) -> bool: in_annotation_or_default_or_decorator = False if isinstance(frame, nodes.FunctionDef) and node.statement() is frame: @@ -2195,7 +2177,8 @@ def _defined_in_function_definition( @staticmethod def _in_lambda_or_comprehension_body( - node: nodes.NodeNG, frame: nodes.NodeNG + node: nodes.NodeNG, + frame: nodes.NodeNG, ) -> bool: """Return True if node within a lambda/comprehension body (or similar) and thus should not have access to class attributes in frame. @@ -2262,10 +2245,7 @@ def _is_variable_violation( ) # check if we have a nonlocal elif node.name in defframe.locals: - maybe_before_assign = not any( - isinstance(child, nodes.Nonlocal) and node.name in child.names - for child in defframe.get_children() - ) + maybe_before_assign = not _is_nonlocal_name(node, defframe) if ( base_scope_type == "lambda" @@ -2295,13 +2275,14 @@ def _is_variable_violation( # using a name defined earlier in the class containing the function. if node is frame.returns and defframe.parent_of(frame.returns): annotation_return = True - if ( - frame.returns.name in defframe.locals - and defframe.locals[node.name][0].lineno < frame.lineno - ): - # Detect class assignments with a name defined earlier in the - # class. In this case, no warning should be raised. - maybe_before_assign = False + if frame.returns.name in defframe.locals: + definition = defframe.locals[node.name][0] + if definition.lineno is None or definition.lineno < frame.lineno: + # Detect class assignments with a name defined earlier in the + # class. In this case, no warning should be raised. + maybe_before_assign = False + else: + maybe_before_assign = True else: maybe_before_assign = True if isinstance(node.parent, nodes.Arguments): @@ -2337,36 +2318,11 @@ def _is_variable_violation( # x = b if (b := True) else False maybe_before_assign = False elif ( - isinstance( # pylint: disable=too-many-boolean-expressions - defnode, nodes.NamedExpr - ) + isinstance(defnode, nodes.NamedExpr) and frame is defframe and defframe.parent_of(stmt) and stmt is defstmt - and ( - ( - defnode.lineno == node.lineno - and defnode.col_offset < node.col_offset - ) - or (defnode.lineno < node.lineno) - or ( - # Issue in the `ast` module until py39 - # Nodes in a multiline string have the same lineno - # Could be false-positive without check - not PY39_PLUS - and defnode.lineno == node.lineno - and isinstance( - defstmt, - ( - nodes.Assign, - nodes.AnnAssign, - nodes.AugAssign, - nodes.Return, - ), - ) - and isinstance(defstmt.value, nodes.JoinedStr) - ) - ) + and _is_before(defnode, node) ): # Relation of a name to the same name in a named expression # Could be used before assignment if self-referencing: @@ -2421,9 +2377,29 @@ def _maybe_used_and_assigned_at_once(defstmt: _base_nodes.Statement) -> bool: def _is_builtin(self, name: str) -> bool: return name in self.linter.config.additional_builtins or utils.is_builtin(name) + def _has_nonlocal_in_enclosing_frame( + self, node: nodes.Name, uncertain_definitions: list[nodes.NodeNG] + ) -> bool: + """Check if there is a nonlocal declaration in the nearest frame that encloses + both usage and definitions. + """ + defining_frames = {definition.frame() for definition in uncertain_definitions} + frame = node.frame() + is_enclosing_frame = False + while frame and not is_enclosing_frame: + is_enclosing_frame = all( + (frame is defining_frame) or frame.parent_of(defining_frame) + for defining_frame in defining_frames + ) + if is_enclosing_frame and _is_nonlocal_name(node, frame): + return True + frame = frame.parent.frame() if frame.parent else None + return False + @staticmethod def _is_only_type_assignment( - node: nodes.Name, defstmt: _base_nodes.Statement + node: nodes.Name, + defstmt: _base_nodes.Statement, ) -> bool: """Check if variable only gets assigned a type and never a value.""" if not isinstance(defstmt, nodes.AnnAssign) or defstmt.value: @@ -2481,7 +2457,9 @@ def _is_only_type_assignment( @staticmethod def _is_first_level_self_reference( - node: nodes.Name, defstmt: nodes.ClassDef, found_nodes: list[nodes.NodeNG] + node: nodes.Name, + defstmt: nodes.ClassDef, + found_nodes: list[nodes.NodeNG], ) -> tuple[VariableVisitConsumerAction, list[nodes.NodeNG] | None]: """Check if a first level method's annotation or default values refers to its own class, and return a consumer action. @@ -2502,7 +2480,8 @@ def _is_first_level_self_reference( @staticmethod def _is_never_evaluated( - defnode: nodes.NamedExpr, defnode_parent: nodes.IfExp + defnode: nodes.NamedExpr, + defnode_parent: nodes.IfExp, ) -> bool: """Check if a NamedExpr is inside a side of if ... else that never gets evaluated. @@ -2629,7 +2608,8 @@ def _loopvar_name(self, node: astroid.Name) -> None: else_stmt, (nodes.Return, nodes.Raise, nodes.Break, nodes.Continue) ): return - # TODO: 4.0: Consider using RefactoringChecker._is_function_def_never_returning + # TODO: 4.0: Consider using utils.is_terminating_func + # after merging it with RefactoringChecker._is_function_def_never_returning if isinstance(else_stmt, nodes.Expr) and isinstance( else_stmt.value, nodes.Call ): @@ -2683,7 +2663,7 @@ def _loopvar_name(self, node: astroid.Name) -> None: likely_call = assign.iter if isinstance(assign.iter, nodes.IfExp): likely_call = assign.iter.body - if isinstance(likely_call, nodes.Call): + if isinstance(likely_call, nodes.Call) and likely_call.args: inferred = next(likely_call.args[0].infer()) except astroid.InferenceError: self.add_message("undefined-loop-variable", args=node.name, node=node) @@ -2816,7 +2796,9 @@ def _check_is_unused( self.add_message(message_name, args=name, node=stmt) def _is_name_ignored( - self, stmt: nodes.NodeNG, name: str + self, + stmt: nodes.NodeNG, + name: str, ) -> re.Pattern[str] | re.Match[str] | None: authorized_rgx = self.linter.config.dummy_variables_rgx if ( @@ -2981,7 +2963,8 @@ def _store_type_annotation_node(self, type_annotation: nodes.NodeNG) -> None: ) def _store_type_annotation_names( - self, node: nodes.For | nodes.Assign | nodes.With + self, + node: nodes.For | nodes.Assign | nodes.With, ) -> None: type_annotation = node.type_annotation if not type_annotation: @@ -3019,7 +3002,10 @@ def _check_self_cls_assign(self, node: nodes.Assign) -> None: self.add_message("self-cls-assignment", node=node, args=(self_cls_name,)) def _check_unpacking( - self, inferred: InferenceResult, node: nodes.Assign, targets: list[nodes.NodeNG] + self, + inferred: InferenceResult, + node: nodes.Assign, + targets: list[nodes.NodeNG], ) -> None: """Check for unbalanced tuple unpacking and unpacking non sequences. @@ -3148,7 +3134,9 @@ def _check_module_attrs( return None def _check_all( - self, node: nodes.Module, not_consumed: dict[str, list[nodes.NodeNG]] + self, + node: nodes.Module, + not_consumed: Consumption, ) -> None: try: assigned = next(node.igetattr("__all__")) @@ -3203,7 +3191,7 @@ def _check_all( # when the file will be checked pass - def _check_globals(self, not_consumed: dict[str, nodes.NodeNG]) -> None: + def _check_globals(self, not_consumed: Consumption) -> None: if self._allow_global_unused_variables: return for name, node_lst in not_consumed.items(): @@ -3213,12 +3201,13 @@ def _check_globals(self, not_consumed: dict[str, nodes.NodeNG]) -> None: self.add_message("unused-variable", args=(name,), node=node) # pylint: disable = too-many-branches - def _check_imports(self, not_consumed: dict[str, list[nodes.NodeNG]]) -> None: + def _check_imports(self, not_consumed: Consumption) -> None: local_names = _fix_dot_imports(not_consumed) checked = set() unused_wildcard_imports: defaultdict[ - tuple[str, nodes.ImportFrom], list[str] - ] = collections.defaultdict(list) + tuple[str, nodes.ImportFrom], + list[str], + ] = defaultdict(list) for name, stmt in local_names: for imports in stmt.names: real_name = imported_name = imports[0] @@ -3304,7 +3293,7 @@ def _check_imports(self, not_consumed: dict[str, list[nodes.NodeNG]]) -> None: def _check_metaclasses(self, node: nodes.Module | nodes.FunctionDef) -> None: """Update consumption analysis for metaclasses.""" - consumed: list[tuple[dict[str, list[nodes.NodeNG]], str]] = [] + consumed: list[tuple[Consumption, str]] = [] for child_node in node.get_children(): if isinstance(child_node, nodes.ClassDef): @@ -3316,13 +3305,15 @@ def _check_metaclasses(self, node: nodes.Module | nodes.FunctionDef) -> None: scope_locals.pop(name, None) def _check_classdef_metaclasses( - self, klass: nodes.ClassDef, parent_node: nodes.Module | nodes.FunctionDef - ) -> list[tuple[dict[str, list[nodes.NodeNG]], str]]: + self, + klass: nodes.ClassDef, + parent_node: nodes.Module | nodes.FunctionDef, + ) -> list[tuple[Consumption, str]]: if not klass._metaclass: # Skip if this class doesn't use explicitly a metaclass, but inherits it from ancestors return [] - consumed: list[tuple[dict[str, list[nodes.NodeNG]], str]] = [] + consumed: list[tuple[Consumption, str]] = [] metaclass = klass.metaclass() name = "" if isinstance(klass._metaclass, nodes.Name): @@ -3343,7 +3334,8 @@ def _check_classdef_metaclasses( name = METACLASS_NAME_TRANSFORMS.get(name, name) if name: # check enclosing scopes starting from most local - for scope_locals, _, _, _ in self._to_consume[::-1]: + for to_consume in self._to_consume[::-1]: + scope_locals = to_consume.to_consume found_nodes = scope_locals.get(name, []) for found_node in found_nodes: if found_node.lineno <= klass.lineno: @@ -3375,7 +3367,9 @@ def visit_subscript(self, node: nodes.Subscript) -> None: self._check_potential_index_error(node, inferred_slice) def _check_potential_index_error( - self, node: nodes.Subscript, inferred_slice: nodes.NodeNG | None + self, + node: nodes.Subscript, + inferred_slice: nodes.NodeNG | None, ) -> None: """Check for the potential-index-error message.""" # Currently we only check simple slices of a single integer diff --git a/pylint/config/argument.py b/pylint/config/argument.py index 2d2a46a3fda..a515a942b47 100644 --- a/pylint/config/argument.py +++ b/pylint/config/argument.py @@ -13,9 +13,10 @@ import os import pathlib import re -from collections.abc import Callable +from collections.abc import Callable, Sequence from glob import glob -from typing import Any, Literal, Pattern, Sequence, Tuple, Union +from re import Pattern +from typing import Any, Literal, Union from pylint import interfaces from pylint import utils as pylint_utils @@ -30,7 +31,7 @@ Pattern[str], Sequence[str], Sequence[Pattern[str]], - Tuple[int, ...], + tuple[int, ...], ] """List of possible argument types.""" @@ -103,7 +104,7 @@ def _py_version_transformer(value: str) -> tuple[int, ...]: def _regex_transformer(value: str) -> Pattern[str]: - """Return `re.compile(value)`.""" + """Prevents 're.error' from propagating and crash pylint.""" try: return re.compile(value) except re.error as e: @@ -124,7 +125,7 @@ def _regexp_paths_csv_transfomer(value: str) -> Sequence[Pattern[str]]: patterns: list[Pattern[str]] = [] for pattern in _csv_transformer(value): patterns.append( - re.compile( + _regex_transformer( str(pathlib.PureWindowsPath(pattern)).replace("\\", "\\\\") + "|" + pathlib.PureWindowsPath(pattern).as_posix() diff --git a/pylint/config/arguments_manager.py b/pylint/config/arguments_manager.py index aca8f5f8781..e8300475629 100644 --- a/pylint/config/arguments_manager.py +++ b/pylint/config/arguments_manager.py @@ -133,7 +133,7 @@ def _add_parser_option( *argument.flags, action=argument.action, default=argument.default, - type=argument.type, # type: ignore[arg-type] # incorrect typing in typeshed + type=argument.type, help=argument.help, metavar=argument.metavar, choices=argument.choices, @@ -144,7 +144,7 @@ def _add_parser_option( **argument.kwargs, action=argument.action, default=argument.default, - type=argument.type, # type: ignore[arg-type] # incorrect typing in typeshed + type=argument.type, help=argument.help, metavar=argument.metavar, choices=argument.choices, @@ -157,7 +157,7 @@ def _add_parser_option( f"--{old_name}", action="store", default=argument.default, - type=argument.type, # type: ignore[arg-type] # incorrect typing in typeshed + type=argument.type, help=argparse.SUPPRESS, metavar=argument.metavar, choices=argument.choices, @@ -168,7 +168,7 @@ def _add_parser_option( **argument.kwargs, action=argument.action, default=argument.default, - type=argument.type, # type: ignore[arg-type] # incorrect typing in typeshed + type=argument.type, help=argument.help, metavar=argument.metavar, choices=argument.choices, @@ -193,7 +193,7 @@ def _add_parser_option( *argument.flags, action=argument.action, default=argument.default, - type=argument.type, # type: ignore[arg-type] # incorrect typing in typeshed + type=argument.type, help=argument.help, metavar=argument.metavar, choices=argument.choices, diff --git a/pylint/config/config_file_parser.py b/pylint/config/config_file_parser.py index efc085e5901..4ceed28d6ec 100644 --- a/pylint/config/config_file_parser.py +++ b/pylint/config/config_file_parser.py @@ -10,7 +10,7 @@ import os import sys from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Tuple +from typing import TYPE_CHECKING from pylint.config.utils import _parse_rich_type_value @@ -22,7 +22,7 @@ if TYPE_CHECKING: from pylint.lint import PyLinter -PylintConfigFileData = Tuple[Dict[str, str], List[str]] +PylintConfigFileData = tuple[dict[str, str], list[str]] class _RawConfParser: diff --git a/pylint/config/find_default_config_files.py b/pylint/config/find_default_config_files.py index 346393cf9a4..7e53e772078 100644 --- a/pylint/config/find_default_config_files.py +++ b/pylint/config/find_default_config_files.py @@ -22,7 +22,7 @@ Path(".pylintrc.toml"), ) PYPROJECT_NAME = Path("pyproject.toml") -CONFIG_NAMES = (*RC_NAMES, PYPROJECT_NAME, Path("setup.cfg")) +CONFIG_NAMES = (*RC_NAMES, PYPROJECT_NAME, Path("setup.cfg"), Path("tox.ini")) def _find_pyproject() -> Path: @@ -55,13 +55,16 @@ def _toml_has_config(path: Path | str) -> bool: return "pylint" in content.get("tool", []) -def _cfg_has_config(path: Path | str) -> bool: +def _cfg_or_ini_has_config(path: Path | str) -> bool: parser = configparser.ConfigParser() try: parser.read(path, encoding="utf-8") except configparser.Error: return False - return any(section.startswith("pylint.") for section in parser.sections()) + return any( + section == "pylint" or section.startswith("pylint.") + for section in parser.sections() + ) def _yield_default_files() -> Iterator[Path]: @@ -71,7 +74,10 @@ def _yield_default_files() -> Iterator[Path]: 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): + if config_name.suffix in { + ".cfg", + ".ini", + } and not _cfg_or_ini_has_config(config_name): continue yield config_name.resolve() diff --git a/pylint/constants.py b/pylint/constants.py index f147e5189af..0ba20162a1b 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -14,8 +14,6 @@ from pylint.__pkginfo__ import __version__ from pylint.typing import MessageTypesFullName -PY38_PLUS = sys.version_info[:2] >= (3, 8) -PY39_PLUS = sys.version_info[:2] >= (3, 9) PY310_PLUS = sys.version_info[:2] >= (3, 10) PY311_PLUS = sys.version_info[:2] >= (3, 11) PY312_PLUS = sys.version_info[:2] >= (3, 12) diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index 622601c75fd..00d539500c3 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -5,7 +5,7 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING, Tuple, Type, cast +from typing import TYPE_CHECKING, cast from astroid import nodes @@ -152,7 +152,7 @@ def _check_dict_consider_namedtuple_dataclass(self, node: nodes.Dict) -> None: if len(node.items) > 1 and all( isinstance(dict_value, nodes.Dict) for _, dict_value in node.items ): - KeyTupleT = Tuple[Type[nodes.NodeNG], str] + KeyTupleT = tuple[type[nodes.NodeNG], str] # Makes sure all keys are 'Const' string nodes keys_checked: set[KeyTupleT] = set() diff --git a/pylint/extensions/docparams.py b/pylint/extensions/docparams.py index 8e1987f7c9f..b19560b7fb6 100644 --- a/pylint/extensions/docparams.py +++ b/pylint/extensions/docparams.py @@ -196,6 +196,9 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: :param node: Node for a function or method definition in the AST :type node: :class:`astroid.scoped_nodes.Function` """ + if checker_utils.is_overload_stub(node): + return + node_doc = utils.docstringify( node.doc_node, self.linter.config.default_docstring_type ) @@ -318,6 +321,9 @@ def visit_raise(self, node: nodes.Raise) -> None: for found_exc in found_excs_class_names: if found_exc == expected.name: break + if found_exc == "error" and expected.name == "PatternError": + # Python 3.13: re.error aliases re.PatternError + break if any(found_exc == ancestor.name for ancestor in expected.ancestors()): break else: @@ -650,7 +656,7 @@ def _add_raise_message( """Adds a message on :param:`node` for the missing exception type. :param missing_exceptions: A list of missing exception types. - :param node: The node show the message on. + :param node: The node to show the message on. """ if node.is_abstract(): try: diff --git a/pylint/extensions/typing.py b/pylint/extensions/typing.py index 2956465cf67..8319910e190 100644 --- a/pylint/extensions/typing.py +++ b/pylint/extensions/typing.py @@ -85,6 +85,7 @@ class DeprecatedTypingAliasMsg(NamedTuple): parent_subscript: bool = False +# pylint: disable-next=too-many-instance-attributes class TypingChecker(BaseChecker): """Find issue specifically related to type annotations.""" @@ -103,10 +104,14 @@ class TypingChecker(BaseChecker): "Python 3.7 or 3.8.", ), "R6003": ( - "Consider using alternative Union syntax instead of '%s'%s", + "Consider using alternative union syntax instead of '%s'%s", "consider-alternative-union-syntax", - "Emitted when 'typing.Union' or 'typing.Optional' is used " - "instead of the alternative Union syntax 'int | None'.", + "Emitted when ``typing.Union`` or ``typing.Optional`` is used " + "instead of the shorthand union syntax. For example, " + "``Union[int, float]`` instead of ``int | float``. Using " + "the shorthand for unions aligns with Python typing " + "recommendations, removes the need for imports, and avoids " + "confusion in function signatures.", ), "E6004": ( "'NoReturn' inside compound types is broken in 3.7.0 / 3.7.1", @@ -130,6 +135,12 @@ class TypingChecker(BaseChecker): "Duplicated type arguments will be skipped by `mypy` tool, therefore should be " "removed to avoid confusion.", ), + "R6007": ( + "Type `%s` has unnecessary default type args. Change it to `%s`.", + "unnecessary-default-type-args", + "Emitted when types have default type args which can be omitted. " + "Mainly used for `typing.Generator` and `typing.AsyncGenerator`.", + ), } options = ( ( @@ -174,6 +185,7 @@ def open(self) -> None: self._py37_plus = py_version >= (3, 7) self._py39_plus = py_version >= (3, 9) self._py310_plus = py_version >= (3, 10) + self._py313_plus = py_version >= (3, 13) self._should_check_typing_alias = self._py39_plus or ( self._py37_plus and self.linter.config.runtime_typing is False @@ -248,6 +260,33 @@ def visit_annassign(self, node: nodes.AnnAssign) -> None: self._check_union_types(types, node) + @only_required_for_messages("unnecessary-default-type-args") + def visit_subscript(self, node: nodes.Subscript) -> None: + inferred = safe_infer(node.value) + if ( # pylint: disable=too-many-boolean-expressions + isinstance(inferred, nodes.ClassDef) + and ( + inferred.qname() in {"typing.Generator", "typing.AsyncGenerator"} + and self._py313_plus + or inferred.qname() + in {"_collections_abc.Generator", "_collections_abc.AsyncGenerator"} + ) + and isinstance(node.slice, nodes.Tuple) + and all( + (isinstance(el, nodes.Const) and el.value is None) + for el in node.slice.elts[1:] + ) + ): + suggested_str = ( + f"{node.value.as_string()}[{node.slice.elts[0].as_string()}]" + ) + self.add_message( + "unnecessary-default-type-args", + args=(node.as_string(), suggested_str), + node=node, + confidence=HIGH, + ) + @staticmethod def _is_deprecated_union_annotation( annotation: nodes.NodeNG, union_name: str diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index 04e70188433..a7d31dea6b0 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -33,7 +33,7 @@ def discover_package_path(modulepath: str, source_roots: Sequence[str]) -> str: # Look for a source root that contains the module directory for source_root in source_roots: source_root = os.path.realpath(os.path.expanduser(source_root)) - if os.path.commonpath([source_root, dirname]) == source_root: + if os.path.commonpath([source_root, dirname]) in [dirname, source_root]: return source_root # Fall back to legacy discovery by looking for __init__.py upwards as @@ -122,7 +122,7 @@ def expand_modules( ) except ImportError: # Might not be acceptable, don't crash. - is_namespace = False + is_namespace = not os.path.exists(filepath) is_directory = os.path.isdir(something) else: is_namespace = modutils.is_namespace(spec) diff --git a/pylint/lint/message_state_handler.py b/pylint/lint/message_state_handler.py index 2ddd7d4db3c..5c4498af96f 100644 --- a/pylint/lint/message_state_handler.py +++ b/pylint/lint/message_state_handler.py @@ -18,7 +18,7 @@ ) from pylint.interfaces import HIGH from pylint.message import MessageDefinition -from pylint.typing import ManagedMessage +from pylint.typing import ManagedMessage, MessageDefinitionTuple from pylint.utils.pragma_parser import ( OPTION_PO, InvalidPragmaError, @@ -37,6 +37,11 @@ class _MessageStateHandler: def __init__(self, linter: PyLinter) -> None: self.linter = linter + self.default_enabled_messages: dict[str, MessageDefinitionTuple] = { + k: v + for k, v in self.linter.msgs.items() + if len(v) == 3 or v[3].get("default_enabled", True) + } self._msgs_state: dict[str, bool] = {} self._options_methods = { "enable": self.enable, @@ -84,6 +89,14 @@ def _get_messages_to_set( message_definitions.extend( self._get_messages_to_set(_msgid, enable, ignore_unknown) ) + if not enable: + # "all" should not disable pylint's own warnings + message_definitions = list( + filter( + lambda m: m.msgid not in self.default_enabled_messages, + message_definitions, + ) + ) return message_definitions # msgid is a category? diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 387187975a5..6efe3d9c76a 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -741,10 +741,12 @@ def check(self, files_or_modules: Sequence[str]) -> None: ) extra_packages_paths = list( - { - discover_package_path(file_or_module, self.config.source_roots) - for file_or_module in files_or_modules - } + dict.fromkeys( + [ + discover_package_path(file_or_module, self.config.source_roots) + for file_or_module in files_or_modules + ] + ).keys() ) # TODO: Move the parallel invocation into step 3 of the checking process @@ -993,7 +995,7 @@ def _get_namespace_for_file( self, filepath: Path, namespaces: DirectoryNamespaceDict ) -> argparse.Namespace | None: for directory in namespaces: - if _is_relative_to(filepath, directory): + if Path.is_relative_to(filepath, directory): namespace = self._get_namespace_for_file( filepath, namespaces[directory][1] ) diff --git a/pylint/lint/run.py b/pylint/lint/run.py index 1a8d594a045..2bbbb337b9e 100644 --- a/pylint/lint/run.py +++ b/pylint/lint/run.py @@ -175,9 +175,13 @@ def __init__( sys.exit(code) return - # Display help if there are no files to lint or no checks enabled - if not args or len(linter.config.disable) == len( - linter.msgs_store._messages_definitions + # Display help if there are no files to lint or only internal checks enabled (`--disable=all`) + disable_all_msg_set = set( + msg.symbol for msg in linter.msgs_store.messages + ) - set(msg[1] for msg in linter.default_enabled_messages.values()) + if not args or ( + len(linter.config.enable) == 0 + and set(linter.config.disable) == disable_all_msg_set ): print("No files to lint: exiting.") sys.exit(32) diff --git a/pylint/lint/utils.py b/pylint/lint/utils.py index 6a89618ea51..14e9433b9de 100644 --- a/pylint/lint/utils.py +++ b/pylint/lint/utils.py @@ -152,3 +152,4 @@ def _is_relative_to(self: Path, *other: Path) -> bool: def _is_env_set_and_non_empty(env_var: str) -> bool: """Checks if env_var is set and non-empty.""" return bool(os.environ.get(env_var)) + diff --git a/pylint/message/_deleted_message_ids.py b/pylint/message/_deleted_message_ids.py index 60289e80539..149a800b7c8 100644 --- a/pylint/message/_deleted_message_ids.py +++ b/pylint/message/_deleted_message_ids.py @@ -4,7 +4,7 @@ from __future__ import annotations -from functools import lru_cache +from functools import cache from typing import NamedTuple @@ -131,7 +131,7 @@ class DeletedMessage(NamedTuple): } -@lru_cache(maxsize=None) +@cache def is_deleted_symbol(symbol: str) -> str | None: """Return the explanation for removal if the message was removed.""" for explanation, deleted_messages in DELETED_MESSAGES_IDS.items(): @@ -143,7 +143,7 @@ def is_deleted_symbol(symbol: str) -> str | None: return None -@lru_cache(maxsize=None) +@cache def is_deleted_msgid(msgid: str) -> str | None: """Return the explanation for removal if the message was removed.""" for explanation, deleted_messages in DELETED_MESSAGES_IDS.items(): @@ -155,7 +155,7 @@ def is_deleted_msgid(msgid: str) -> str | None: return None -@lru_cache(maxsize=None) +@cache def is_moved_symbol(symbol: str) -> str | None: """Return the explanation for moving if the message was moved to extensions.""" for explanation, moved_messages in MOVED_TO_EXTENSIONS.items(): @@ -167,7 +167,7 @@ def is_moved_symbol(symbol: str) -> str | None: return None -@lru_cache(maxsize=None) +@cache def is_moved_msgid(msgid: str) -> str | None: """Return the explanation for moving if the message was moved to extensions.""" for explanation, moved_messages in MOVED_TO_EXTENSIONS.items(): diff --git a/pylint/message/message_definition_store.py b/pylint/message/message_definition_store.py index cf271d7ffc2..d56308541a0 100644 --- a/pylint/message/message_definition_store.py +++ b/pylint/message/message_definition_store.py @@ -5,9 +5,9 @@ from __future__ import annotations import collections -import functools import sys from collections.abc import Sequence, ValuesView +from functools import cache from typing import TYPE_CHECKING from pylint.exceptions import UnknownMessageError @@ -58,9 +58,7 @@ def register_message(self, message: MessageDefinition) -> None: # and the arguments are relatively small we do not run the # risk of creating a large memory leak. # See discussion in: https://github.com/pylint-dev/pylint/pull/5673 - @functools.lru_cache( # pylint: disable=method-cache-max-size-none # noqa: B019 - maxsize=None - ) + @cache # pylint: disable=method-cache-max-size-none # noqa: B019 def get_message_definitions(self, msgid_or_symbol: str) -> list[MessageDefinition]: """Returns the Message definition for either a numeric or symbolic id. diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 88aea482ed1..59a2f595604 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -84,7 +84,7 @@ def add_class(self, node: nodes.ClassDef) -> None: def get_ancestors( self, node: nodes.ClassDef, level: int - ) -> Generator[nodes.ClassDef, None, None]: + ) -> Generator[nodes.ClassDef]: """Return ancestor nodes of a class node.""" if level == 0: return @@ -95,7 +95,7 @@ def get_ancestors( def get_associated( self, klass_node: nodes.ClassDef, level: int - ) -> Generator[nodes.ClassDef, None, None]: + ) -> Generator[nodes.ClassDef]: """Return associated nodes of a class node.""" if level == 0: return diff --git a/pylint/pyreverse/inspector.py b/pylint/pyreverse/inspector.py index 23ccfa6f301..40fb8d60792 100644 --- a/pylint/pyreverse/inspector.py +++ b/pylint/pyreverse/inspector.py @@ -13,7 +13,8 @@ import os import traceback from abc import ABC, abstractmethod -from typing import Callable, Optional +from collections.abc import Callable, Sequence +from typing import Optional import astroid from astroid import nodes @@ -345,7 +346,7 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: def project_from_files( - files: list[str], + files: Sequence[str], func_wrapper: _WrapperFuncT = _astroid_wrapper, project_name: str = "no name", black_list: tuple[str, ...] = constants.DEFAULT_IGNORE_LIST, diff --git a/pylint/pyreverse/main.py b/pylint/pyreverse/main.py index 3ba0b6c77d1..972a46741fe 100644 --- a/pylint/pyreverse/main.py +++ b/pylint/pyreverse/main.py @@ -8,7 +8,6 @@ import sys from collections.abc import Sequence -from typing import NoReturn from pylint import constants from pylint.config.arguments_manager import _ArgumentsManager @@ -46,7 +45,17 @@ "#DDDDDD", # pale grey ) + +OPTIONS_GROUPS = { + "FILTERING": "Filtering and Scope", + "DISPLAY": "Display Options", + "OUTPUT": "Output Control", + "PROJECT": "Project Configuration", +} + + OPTIONS: Options = ( + # Filtering and Scope options ( "filter-mode", { @@ -56,15 +65,12 @@ "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 - 'ALL' no filter - 'SPECIAL' filter Python special functions - except constructor - 'OTHER' filter protected and private - attributes""", + "group": OPTIONS_GROUPS["FILTERING"], + "help": """Filter attributes and functions according to . Correct modes are: +'PUB_ONLY' filter all non public attributes [DEFAULT], equivalent to PRIVATE+SPECIAL +'ALL' no filter +'SPECIAL' filter Python special functions except constructor +'OTHER' filter protected and private attributes""", }, ), ( @@ -76,7 +82,8 @@ "type": "csv", "dest": "classes", "default": None, - "help": "create a class diagram with all classes related to ;\ + "group": OPTIONS_GROUPS["FILTERING"], + "help": "Create a class diagram with all classes related to ;\ this uses by default the options -ASmy", }, ), @@ -88,7 +95,8 @@ "metavar": "", "type": "int", "default": None, - "help": "show generations of ancestor classes not in ", + "group": OPTIONS_GROUPS["FILTERING"], + "help": "Show generations of ancestor classes not in .", }, ), ( @@ -97,7 +105,8 @@ "short": "A", "default": None, "action": "store_true", - "help": "show all ancestors off all classes in ", + "group": OPTIONS_GROUPS["FILTERING"], + "help": "Show all ancestors of all classes in .", }, ), ( @@ -108,7 +117,8 @@ "metavar": "", "type": "int", "default": None, - "help": "show levels of associated classes not in ", + "group": OPTIONS_GROUPS["FILTERING"], + "help": "Show levels of associated classes not in .", }, ), ( @@ -117,7 +127,8 @@ "short": "S", "default": None, "action": "store_true", - "help": "show recursively all associated off all associated classes", + "group": OPTIONS_GROUPS["FILTERING"], + "help": "Show all classes associated with the target classes, including indirect associations.", }, ), ( @@ -126,7 +137,8 @@ "short": "b", "action": "store_true", "default": False, - "help": "include builtin objects in representation of classes", + "group": OPTIONS_GROUPS["FILTERING"], + "help": "Include builtin objects in representation of classes.", }, ), ( @@ -135,9 +147,11 @@ "short": "L", "action": "store_true", "default": False, - "help": "include standard library objects in representation of classes", + "group": OPTIONS_GROUPS["FILTERING"], + "help": "Include standard library objects in representation of classes.", }, ), + # Display Options ( "module-names", { @@ -145,7 +159,8 @@ "default": None, "type": "yn", "metavar": "", - "help": "include module name in representation of classes", + "group": OPTIONS_GROUPS["DISPLAY"], + "help": "Include module name in the representation of classes.", }, ), ( @@ -154,7 +169,8 @@ "short": "k", "action": "store_true", "default": False, - "help": "don't show attributes and methods in the class boxes; this disables -f values", + "group": OPTIONS_GROUPS["DISPLAY"], + "help": "Don't show attributes and methods in the class boxes; this disables -f values.", }, ), ( @@ -162,24 +178,8 @@ { "action": "store_true", "default": False, - "help": "only show nodes with connections", - }, - ), - ( - "output", - { - "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." - ), + "group": OPTIONS_GROUPS["DISPLAY"], + "help": "Only show nodes with connections.", }, ), ( @@ -188,6 +188,7 @@ "dest": "colorized", "action": "store_true", "default": False, + "group": OPTIONS_GROUPS["DISPLAY"], "help": "Use colored output. Classes/modules of the same package get the same color.", }, ), @@ -199,7 +200,8 @@ "default": 2, "metavar": "", "type": "int", - "help": "Use separate colors up to package depth of ", + "group": OPTIONS_GROUPS["DISPLAY"], + "help": "Use separate colors up to package depth of . Higher depths will reuse colors.", }, ), ( @@ -210,9 +212,43 @@ "default": DEFAULT_COLOR_PALETTE, "metavar": "", "type": "csv", - "help": "Comma separated list of colors to use", + "group": OPTIONS_GROUPS["DISPLAY"], + "help": "Comma separated list of colors to use for the package depth coloring.", }, ), + # Output Control options + ( + "output", + { + "short": "o", + "dest": "output_format", + "action": "store", + "default": "dot", + "metavar": "", + "type": "string", + "group": OPTIONS_GROUPS["OUTPUT"], + "help": ( + "Create a *. output file if format is available. Available " + f"formats are: {', '.join('.' + fmt for fmt in DIRECTLY_SUPPORTED_FORMATS)}. Any other " + "format will be tried to be created by using the 'dot' command line " + "tool, which requires a graphviz installation. In this case, these additional " + "formats are available (see `Graphviz output formats `_)." + ), + }, + ), + ( + "output-directory", + { + "default": "", + "type": "path", + "short": "d", + "action": "store", + "metavar": "", + "group": OPTIONS_GROUPS["OUTPUT"], + "help": "Set the output directory path.", + }, + ), + # Project Configuration options ( "ignore", { @@ -220,6 +256,7 @@ "metavar": "", "dest": "ignore_list", "default": constants.DEFAULT_IGNORE_LIST, + "group": OPTIONS_GROUPS["PROJECT"], "help": "Files or directories to be skipped. They should be base names, not paths.", }, ), @@ -230,18 +267,8 @@ "type": "string", "short": "p", "metavar": "", - "help": "set the project name.", - }, - ), - ( - "output-directory", - { - "default": "", - "type": "path", - "short": "d", - "action": "store", - "metavar": "", - "help": "set the output directory path.", + "group": OPTIONS_GROUPS["PROJECT"], + "help": "Set the project name. This will later be appended to the output file names.", }, ), ( @@ -250,6 +277,7 @@ "type": "glob_paths_csv", "metavar": "[,...]", "default": (), + "group": OPTIONS_GROUPS["PROJECT"], "help": "Add paths to the list of the source roots. Supports globbing patterns. The " "source root is an absolute path or a path relative to the current working directory " "used to determine a package namespace for modules located under the source root.", @@ -260,19 +288,19 @@ { "action": "store_true", "default": False, + "group": OPTIONS_GROUPS["PROJECT"], "help": "Makes pyreverse more verbose/talkative. Mostly useful for debugging.", }, ), ) +# Base class providing common behaviour for pyreverse commands class Run(_ArgumentsManager, _ArgumentsProvider): - """Base class providing common behaviour for pyreverse commands.""" - options = OPTIONS name = "pyreverse" - def __init__(self, args: Sequence[str]) -> NoReturn: + def __init__(self, args: Sequence[str]) -> None: # Immediately exit if user asks for version if "--version" in args: print("pyreverse is included in pylint:") @@ -284,7 +312,7 @@ def __init__(self, args: Sequence[str]) -> NoReturn: # Parse options insert_default_options() - args = self._parse_command_line_configuration(args) + self.args = self._parse_command_line_configuration(args) if self.config.output_format not in DIRECTLY_SUPPORTED_FORMATS: check_graphviz_availability() @@ -294,19 +322,17 @@ def __init__(self, args: Sequence[str]) -> NoReturn: ) check_if_graphviz_supports_format(self.config.output_format) - sys.exit(self.run(args)) - - def run(self, args: list[str]) -> int: + def run(self) -> int: """Checking arguments and run project.""" - if not args: + if not self.args: print(self.help()) return 1 extra_packages_paths = list( - {discover_package_path(arg, self.config.source_roots) for arg in args} + {discover_package_path(arg, self.config.source_roots) for arg in self.args} ) with augmented_sys_path(extra_packages_paths): project = project_from_files( - args, + self.args, project_name=self.config.project, black_list=self.config.ignore_list, verbose=self.config.verbose, @@ -319,4 +345,5 @@ def run(self, args: list[str]) -> int: if __name__ == "__main__": - Run(sys.argv[1:]) + arguments = sys.argv[1:] + Run(arguments).run() diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py index bdd28dc7c36..5ad92d32314 100644 --- a/pylint/pyreverse/utils.py +++ b/pylint/pyreverse/utils.py @@ -11,7 +11,8 @@ import shutil import subprocess import sys -from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Union +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Optional, Union import astroid from astroid import nodes @@ -22,9 +23,9 @@ _CallbackT = Callable[ [nodes.NodeNG], - Union[Tuple[ClassDiagram], Tuple[PackageDiagram, ClassDiagram], None], + Union[tuple[ClassDiagram], tuple[PackageDiagram, ClassDiagram], None], ] - _CallbackTupleT = Tuple[Optional[_CallbackT], Optional[_CallbackT]] + _CallbackTupleT = tuple[Optional[_CallbackT], Optional[_CallbackT]] RCFILE = ".pyreverserc" diff --git a/pylint/reporters/reports_handler_mix_in.py b/pylint/reporters/reports_handler_mix_in.py index 95d45ba9195..071879ca1ed 100644 --- a/pylint/reporters/reports_handler_mix_in.py +++ b/pylint/reporters/reports_handler_mix_in.py @@ -6,7 +6,7 @@ import collections from collections.abc import MutableSequence -from typing import TYPE_CHECKING, DefaultDict, List, Tuple +from typing import TYPE_CHECKING from pylint.exceptions import EmptyReportError from pylint.reporters.ureports.nodes import Section @@ -17,7 +17,9 @@ from pylint.checkers import BaseChecker from pylint.lint.pylinter import PyLinter -ReportsDict = DefaultDict["BaseChecker", List[Tuple[str, str, ReportsCallable]]] +ReportsDict = collections.defaultdict[ + "BaseChecker", list[tuple[str, str, ReportsCallable]] +] class ReportsHandlerMixIn: diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index 0e3577199ab..894207ad7d3 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -15,7 +15,7 @@ import sys import warnings from dataclasses import asdict, fields -from typing import TYPE_CHECKING, Dict, NamedTuple, TextIO +from typing import TYPE_CHECKING, NamedTuple, TextIO from pylint.message import Message from pylint.reporters import BaseReporter @@ -65,7 +65,7 @@ def _colorize_ansi(self, msg: str) -> str: return msg -ColorMappingDict = Dict[str, MessageStyle] +ColorMappingDict = dict[str, MessageStyle] TITLE_UNDERLINES = ["", "=", "-", "."] diff --git a/pylint/reporters/ureports/nodes.py b/pylint/reporters/ureports/nodes.py index 59443996db6..c4148651213 100644 --- a/pylint/reporters/ureports/nodes.py +++ b/pylint/reporters/ureports/nodes.py @@ -9,8 +9,8 @@ from __future__ import annotations -from collections.abc import Iterable, Iterator -from typing import Any, Callable, TypeVar +from collections.abc import Callable, Iterable, Iterator +from typing import Any, TypeVar from pylint.reporters.ureports.base_writer import BaseWriter diff --git a/pylint/testutils/_primer/primer_command.py b/pylint/testutils/_primer/primer_command.py index 817c1a0d31f..01c2bed368d 100644 --- a/pylint/testutils/_primer/primer_command.py +++ b/pylint/testutils/_primer/primer_command.py @@ -7,7 +7,7 @@ import abc import argparse from pathlib import Path -from typing import Dict, TypedDict +from typing import TypedDict from pylint.reporters.json_reporter import OldJsonExport from pylint.testutils._primer import PackageToLint @@ -18,7 +18,7 @@ class PackageData(TypedDict): messages: list[OldJsonExport] -PackageMessages = Dict[str, PackageData] +PackageMessages = dict[str, PackageData] class PrimerCommand: diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index 3ffbbc44ad0..951f38c0b99 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -10,7 +10,6 @@ from astroid import nodes -from pylint.constants import IS_PYPY, PY39_PLUS from pylint.testutils.global_test_linter import linter from pylint.testutils.output_line import MessageTest from pylint.testutils.unittest_linter import UnittestLinter @@ -41,7 +40,7 @@ def assertNoMessages(self) -> Iterator[None]: @contextlib.contextmanager def assertAddsMessages( self, *messages: MessageTest, ignore_position: bool = False - ) -> Generator[None, None, None]: + ) -> Generator[None]: """Assert that exactly the given method adds the given messages. The list of messages must exactly match *all* the messages added by the @@ -76,9 +75,8 @@ def assertAddsMessages( assert expected_msg.line == gotten_msg.line, msg assert expected_msg.col_offset == gotten_msg.col_offset, msg - if not IS_PYPY or PY39_PLUS: - assert expected_msg.end_line == gotten_msg.end_line, msg - assert expected_msg.end_col_offset == gotten_msg.end_col_offset, msg + assert expected_msg.end_line == gotten_msg.end_line, msg + assert expected_msg.end_col_offset == gotten_msg.end_col_offset, msg def walk(self, node: nodes.NodeNG) -> None: """Recursive walk on the given node.""" diff --git a/pylint/testutils/configuration_test.py b/pylint/testutils/configuration_test.py index 9933c9de810..ce2239e5c22 100644 --- a/pylint/testutils/configuration_test.py +++ b/pylint/testutils/configuration_test.py @@ -11,14 +11,14 @@ import logging import unittest from pathlib import Path -from typing import Any, Dict +from typing import Any from pylint.lint import Run # We use Any in this typing because the configuration contains real objects and constants # that could be a lot of things. ConfigurationValue = Any -PylintConfiguration = Dict[str, ConfigurationValue] +PylintConfiguration = dict[str, ConfigurationValue] def get_expected_or_default( diff --git a/pylint/testutils/lint_module_test.py b/pylint/testutils/lint_module_test.py index 48ee5a0b2fb..37839c89084 100644 --- a/pylint/testutils/lint_module_test.py +++ b/pylint/testutils/lint_module_test.py @@ -11,15 +11,13 @@ from collections import Counter from io import StringIO from pathlib import Path -from typing import Counter as CounterType -from typing import TextIO, Tuple +from typing import TextIO import pytest from _pytest.config import Config from pylint import checkers from pylint.config.config_initialization import _config_initialization -from pylint.constants import IS_PYPY from pylint.lint import PyLinter from pylint.message.message import Message from pylint.testutils.constants import _EXPECTED_RE, _OPERATORS, UPDATE_OPTION @@ -33,7 +31,7 @@ from pylint.testutils.output_line import OutputLine from pylint.testutils.reporter_for_tests import FunctionalTestReporter -MessageCounter = CounterType[Tuple[int, str]] +MessageCounter = Counter[tuple[int, str]] PYLINTRC = Path(__file__).parent / "testing_pylintrc" @@ -108,9 +106,6 @@ def __init__( self._check_end_position = ( sys.version_info >= self._linter.config.min_pyver_end_position ) - # TODO: PY3.9: PyPy supports end_lineno from 3.9 and above - if self._check_end_position and IS_PYPY: - self._check_end_position = sys.version_info >= (3, 9) # pragma: no cover self._config = config diff --git a/pylint/testutils/output_line.py b/pylint/testutils/output_line.py index c979a049c35..fbe5b3bbce7 100644 --- a/pylint/testutils/output_line.py +++ b/pylint/testutils/output_line.py @@ -44,8 +44,8 @@ class OutputLine(NamedTuple): def from_msg(cls, msg: Message, check_endline: bool = True) -> OutputLine: """Create an OutputLine from a Pylint Message.""" column = cls._get_column(msg.column) - end_line = cls._get_py38_none_value(msg.end_line, check_endline) - end_column = cls._get_py38_none_value(msg.end_column, check_endline) + end_line = cls._get_end_line_and_end_col(msg.end_line, check_endline) + end_column = cls._get_end_line_and_end_col(msg.end_column, check_endline) return cls( msg.symbol, msg.line, @@ -63,7 +63,7 @@ def _get_column(column: str | int) -> int: return int(column) @staticmethod - def _get_py38_none_value(value: _T, check_endline: bool) -> _T | None: + def _get_end_line_and_end_col(value: _T, check_endline: bool) -> _T | None: """Used to make end_line and end_column None as indicated by our version compared to `min_pyver_end_position`. """ @@ -84,10 +84,10 @@ def from_csv( line = int(row[1]) column = cls._get_column(row[2]) end_line = cls._value_to_optional_int( - cls._get_py38_none_value(row[3], check_endline) + cls._get_end_line_and_end_col(row[3], check_endline) ) end_column = cls._value_to_optional_int( - cls._get_py38_none_value(row[4], check_endline) + cls._get_end_line_and_end_col(row[4], check_endline) ) # symbol, line, column, end_line, end_column, node, msg, confidences assert len(row) == 8 diff --git a/pylint/testutils/pyreverse.py b/pylint/testutils/pyreverse.py index c621f9e7a9b..115eda416a2 100644 --- a/pylint/testutils/pyreverse.py +++ b/pylint/testutils/pyreverse.py @@ -25,6 +25,7 @@ class PyreverseConfig( def __init__( self, + *, mode: str = "PUB_ONLY", classes: list[str] | None = None, show_ancestors: int | None = None, diff --git a/pylint/testutils/utils.py b/pylint/testutils/utils.py index 1ff999b28da..3036d1fd62b 100644 --- a/pylint/testutils/utils.py +++ b/pylint/testutils/utils.py @@ -27,7 +27,7 @@ def _patch_streams(out: TextIO) -> Iterator[None]: @contextlib.contextmanager def _test_sys_path( replacement_sys_path: list[str] | None = None, -) -> Generator[None, None, None]: +) -> Generator[None]: original_path = sys.path try: if replacement_sys_path is not None: @@ -40,7 +40,7 @@ def _test_sys_path( @contextlib.contextmanager def _test_cwd( current_working_directory: str | Path | None = None, -) -> Generator[None, None, None]: +) -> Generator[None]: original_dir = os.getcwd() try: if current_working_directory is not None: @@ -53,7 +53,7 @@ def _test_cwd( @contextlib.contextmanager def _test_environ_pythonpath( new_pythonpath: str | None = None, -) -> Generator[None, None, None]: +) -> Generator[None]: original_pythonpath = os.environ.get("PYTHONPATH") if new_pythonpath: os.environ["PYTHONPATH"] = new_pythonpath diff --git a/pylint/typing.py b/pylint/typing.py index f9dde2e405f..963222871fd 100644 --- a/pylint/typing.py +++ b/pylint/typing.py @@ -7,20 +7,17 @@ from __future__ import annotations import argparse +from collections.abc import Iterable from pathlib import Path +from re import Pattern from typing import ( TYPE_CHECKING, Any, Callable, - Dict, - Iterable, Literal, NamedTuple, Optional, - Pattern, Protocol, - Tuple, - Type, TypedDict, Union, ) @@ -93,7 +90,7 @@ class ManagedMessage(NamedTuple): """All possible message categories.""" -OptionDict = Dict[ +OptionDict = dict[ str, Union[ None, @@ -102,12 +99,12 @@ class ManagedMessage(NamedTuple): int, Pattern[str], Iterable[Union[str, int, Pattern[str]]], - Type["_CallbackAction"], + type["_CallbackAction"], Callable[[Any], Any], Callable[[Any, Any, Any, Any], Any], ], ] -Options = Tuple[Tuple[str, OptionDict], ...] +Options = tuple[tuple[str, OptionDict], ...] ReportsCallable = Callable[["Section", "LinterStats", Optional["LinterStats"]], None] @@ -126,10 +123,10 @@ class ExtraMessageOptions(TypedDict, total=False): MessageDefinitionTuple = Union[ - Tuple[str, str, str], - Tuple[str, str, str, ExtraMessageOptions], + tuple[str, str, str], + tuple[str, str, str, ExtraMessageOptions], ] -DirectoryNamespaceDict = Dict[Path, Tuple[argparse.Namespace, "DirectoryNamespaceDict"]] +DirectoryNamespaceDict = dict[Path, tuple[argparse.Namespace, "DirectoryNamespaceDict"]] class GetProjectCallable(Protocol): diff --git a/pylint/utils/ast_walker.py b/pylint/utils/ast_walker.py index 367a39b817f..6cbc7751e73 100644 --- a/pylint/utils/ast_walker.py +++ b/pylint/utils/ast_walker.py @@ -7,8 +7,8 @@ import sys import traceback from collections import defaultdict -from collections.abc import Sequence -from typing import TYPE_CHECKING, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING from astroid import nodes @@ -75,12 +75,8 @@ def walk(self, astroid: nodes.NodeNG) -> None: """ cid = astroid.__class__.__name__.lower() - # Detect if the node is a new name for a deprecated alias. - # In this case, favour the methods for the deprecated - # alias if any, in order to maintain backwards - # compatibility. - visit_events: Sequence[AstCallback] = self.visit_events.get(cid, ()) - leave_events: Sequence[AstCallback] = self.leave_events.get(cid, ()) + visit_events = self.visit_events[cid] + leave_events = self.leave_events[cid] # pylint: disable = too-many-try-statements try: diff --git a/pylint/utils/file_state.py b/pylint/utils/file_state.py index bc2763eaa45..45217bb7eab 100644 --- a/pylint/utils/file_state.py +++ b/pylint/utils/file_state.py @@ -7,7 +7,7 @@ import collections from collections import defaultdict from collections.abc import Iterator -from typing import TYPE_CHECKING, Dict, Literal +from typing import TYPE_CHECKING, Literal from astroid import nodes @@ -21,7 +21,7 @@ from pylint.message import MessageDefinition, MessageDefinitionStore -MessageStateDict = Dict[str, Dict[int, bool]] +MessageStateDict = dict[str, dict[int, bool]] class FileState: diff --git a/pylint/utils/pragma_parser.py b/pylint/utils/pragma_parser.py index 12513e2843f..5e066653e45 100644 --- a/pylint/utils/pragma_parser.py +++ b/pylint/utils/pragma_parser.py @@ -86,7 +86,7 @@ class InvalidPragmaError(PragmaParserError): """Thrown in case the pragma is invalid.""" -def parse_pragma(pylint_pragma: str) -> Generator[PragmaRepresenter, None, None]: +def parse_pragma(pylint_pragma: str) -> Generator[PragmaRepresenter]: action: str | None = None messages: list[str] = [] assignment_required = False diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 73e9e6a5f3d..9316bcb7aa9 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -25,17 +25,8 @@ from collections import deque from collections.abc import Iterable, Sequence from io import BufferedReader, BytesIO -from typing import ( - TYPE_CHECKING, - Any, - List, - Literal, - Pattern, - TextIO, - Tuple, - TypeVar, - Union, -) +from re import Pattern +from typing import TYPE_CHECKING, Any, Literal, TextIO, TypeVar, Union from astroid import Module, modutils, nodes @@ -76,10 +67,10 @@ "T_GlobalOptionReturnTypes", bool, int, - List[str], + list[str], Pattern[str], - List[Pattern[str]], - Tuple[int, ...], + list[Pattern[str]], + tuple[int, ...], ) diff --git a/pylintrc b/pylintrc index fc3bc4cc9c3..bd6e8a2e22b 100644 --- a/pylintrc +++ b/pylintrc @@ -54,7 +54,7 @@ unsafe-load-any-extension=no extension-pkg-allow-list= # Minimum supported python version -py-version = 3.8.0 +py-version = 3.9.0 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or @@ -400,7 +400,7 @@ spelling-ignore-words= spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:,pragma:,# noinspection # A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file=.pyenchant_pylint_custom_dict.txt +spelling-private-dict-file=custom_dict.txt # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. @@ -433,6 +433,9 @@ max-attributes=11 # Maximum number of statements in a try-block max-try-statements = 7 +# Maximum number of positional arguments (see R0917). +max-positional-arguments = 12 + [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. diff --git a/pyproject.toml b/pyproject.toml index b5e430c7acd..5a912474104 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=66.1"] +requires = ["setuptools>=71.0.4"] build-backend = "setuptools.build_meta" [project] @@ -20,11 +20,11 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Debuggers", @@ -32,7 +32,7 @@ classifiers = [ "Topic :: Software Development :: Testing", "Typing :: Typed" ] -requires-python = ">=3.8.0" +requires-python = ">=3.9.0" dependencies = [ "dill>=0.2;python_version<'3.11'", "dill>=0.3.6;python_version>='3.11'", @@ -41,7 +41,7 @@ dependencies = [ # Also upgrade requirements_test_min.txt. # Pinned to dev of second minor update to allow editable installs and fix primer issues, # see https://github.com/pylint-dev/astroid/issues/1341 - "astroid>=3.2.2,<=3.3.0-dev0", + "astroid>=3.3.5,<=4.0.0-dev0", "isort>=4.2.5,<6,!=5.13.0", "mccabe>=0.6,<0.8", "tomli>=1.1.0;python_version<'3.11'", @@ -145,6 +145,13 @@ module = [ # (for docstrings, strings and comments in particular). line-length = 115 +extend-exclude = [ + "tests/**/data/", + "tests/**/functional/", + "tests/input/", + "tests/regrtest_data/", +] + [tool.ruff.lint] select = [ "B", # bugbear @@ -193,3 +200,32 @@ ignore = [ [tool.ruff.lint.pydocstyle] convention = "pep257" + +[tool.codespell] +ignore-words = ["custom_dict.txt"] + +# Disabled the spelling files for obvious reason, but also, +# the test file with typing extension imported as 'te' and: +# tests/functional/i/implicit/implicit_str_concat_latin1.py: +# - bad encoding +# pylint/pyreverse/diagrams.py and tests/pyreverse/test_diagrams.py: +# - An API from pyreverse use 'classe', and would need to be deprecated +# pylint/checkers/imports.py: +# - 'THIRDPARTY' is a value from isort that would need to be handled even +# if isort fix the typo in newer versions +# tests/functional/m/member/member_checks.py: +# - typos are voluntary to create credible 'no-member' + +skip = """ +tests/checkers/unittest_spelling.py,\ +CODE_OF_CONDUCT.md,\ +CONTRIBUTORS.txt,\ +pylint/checkers/imports.py,\ +pylint/pyreverse/diagrams.py,\ +tests/pyreverse/test_diagrams.py,\ +tests/functional/i/implicit/implicit_str_concat_latin1.py,\ +tests/functional/m/member/member_checks.py,\ +tests/functional/t/type/typevar_naming_style_rgx.py,\ +tests/functional/t/type/typevar_naming_style_default.py,\ +tests/functional/m/member/member_checks_async.py,\ +""" diff --git a/requirements_test.txt b/requirements_test.txt index a4a0eba0407..0d5d80b1c99 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,9 +1,8 @@ -r requirements_test_min.txt -coverage~=7.5 +coverage~=7.6 tbump~=6.11.0 contributors-txt>=1.0.0 -pytest-cov~=5.0 -pytest-profiling~=1.7 +pytest-cov~=6.0 pytest-xdist~=3.6 six # Type packages for mypy diff --git a/requirements_test_min.txt b/requirements_test_min.txt index e93ef7eb13b..11505dbeee3 100644 --- a/requirements_test_min.txt +++ b/requirements_test_min.txt @@ -1,12 +1,12 @@ .[testutils,spelling] # astroid dependency is also defined in pyproject.toml -astroid==3.2.2 # Pinned to a specific version for tests +astroid==3.3.5 # Pinned to a specific version for tests typing-extensions~=4.12 py~=1.11.0 -pytest~=8.2 -pytest-benchmark~=4.0 +pytest~=8.3 +pytest-benchmark~=5.1 pytest-timeout~=2.3 -towncrier~=23.11 +towncrier~=24.8 requests # Voluntary for test purpose, not actually used in prod, see #8904 -setuptools==41.6.0 +setuptools;python_version>='3.12' diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index d30e2dc055d..651aa4c4e6a 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -567,6 +567,10 @@ "mails": ["jaehoonhwang@users.noreply.github.com"], "name": "Jaehoon Hwang" }, + "jake.lishman@ibm.com": { + "mails": ["jake.lishman@ibm.com", "jake@binhbar.com"], + "name": "Jake Lishman" + }, "james.morgensen@gmail.com": { "comment": ": ignored-modules option applies to import errors.", "mails": ["james.morgensen@gmail.com"], diff --git a/tbump.toml b/tbump.toml index 6443128e33d..46f91bc29c4 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/pylint" [version] -current = "3.3.0-dev0" +current = "4.0.0-dev0" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/checkers/unittest_imports.py b/tests/checkers/unittest_imports.py index 2a5547d388e..92477ac2ea7 100644 --- a/tests/checkers/unittest_imports.py +++ b/tests/checkers/unittest_imports.py @@ -11,6 +11,7 @@ from pylint.checkers import imports from pylint.interfaces import UNDEFINED +from pylint.lint import augmented_sys_path, discover_package_path from pylint.testutils import CheckerTestCase, MessageTest from pylint.testutils._run import _Run as Run @@ -92,28 +93,33 @@ def test_relative_beyond_top_level_four(capsys: CaptureFixture[str]) -> None: assert errors == "" def test_wildcard_import_init(self) -> None: - module = astroid.MANAGER.ast_from_module_name("init_wildcard", REGR_DATA) - import_from = module.body[0] + context_file = os.path.join(REGR_DATA, "dummy_wildcard.py") - with self.assertNoMessages(): - self.checker.visit_importfrom(import_from) + with augmented_sys_path([discover_package_path(context_file, [])]): + module = astroid.MANAGER.ast_from_module_name("init_wildcard", context_file) + import_from = module.body[0] + + with self.assertNoMessages(): + self.checker.visit_importfrom(import_from) def test_wildcard_import_non_init(self) -> None: - module = astroid.MANAGER.ast_from_module_name("wildcard", REGR_DATA) - import_from = module.body[0] + context_file = os.path.join(REGR_DATA, "dummy_wildcard.py") - msg = MessageTest( - msg_id="wildcard-import", - node=import_from, - args="empty", - confidence=UNDEFINED, - line=1, - col_offset=0, - end_line=1, - end_col_offset=19, - ) - with self.assertAddsMessages(msg): - self.checker.visit_importfrom(import_from) + with augmented_sys_path([discover_package_path(context_file, [])]): + module = astroid.MANAGER.ast_from_module_name("wildcard", context_file) + import_from = module.body[0] + msg = MessageTest( + msg_id="wildcard-import", + node=import_from, + args="empty", + confidence=UNDEFINED, + line=1, + col_offset=0, + end_line=1, + end_col_offset=19, + ) + with self.assertAddsMessages(msg): + self.checker.visit_importfrom(import_from) @staticmethod def test_preferred_module(capsys: CaptureFixture[str]) -> None: diff --git a/tests/checkers/unittest_misc.py b/tests/checkers/unittest_misc.py index 2f292b81f10..dae07efae17 100644 --- a/tests/checkers/unittest_misc.py +++ b/tests/checkers/unittest_misc.py @@ -40,7 +40,7 @@ def test_xxx_without_space(self) -> None: def test_xxx_middle(self) -> None: code = """a = 1 - # midle XXX + # middle XXX """ with self.assertNoMessages(): self.checker.process_tokens(_tokenize_str(code)) diff --git a/tests/checkers/unittest_non_ascii_name.py b/tests/checkers/unittest_non_ascii_name.py index 0741a9fb081..2ef6c55e3ae 100644 --- a/tests/checkers/unittest_non_ascii_name.py +++ b/tests/checkers/unittest_non_ascii_name.py @@ -4,7 +4,6 @@ from __future__ import annotations -import sys from collections.abc import Iterable import astroid @@ -20,9 +19,6 @@ class TestNonAsciiChecker(pylint.testutils.CheckerTestCase): CHECKER_CLASS = pylint.checkers.non_ascii_names.NonAsciiNameChecker checker: pylint.checkers.non_ascii_names.NonAsciiNameChecker - @pytest.mark.skipif( - sys.version_info < (3, 8), reason="requires python3.8 or higher" - ) def test_kwargs_and_position_only(self) -> None: """Even the new position only and keyword only should be found.""" node = astroid.extract_node( diff --git a/tests/checkers/unittest_stdlib.py b/tests/checkers/unittest_stdlib.py index df0bcfd5183..9b45fd33b38 100644 --- a/tests/checkers/unittest_stdlib.py +++ b/tests/checkers/unittest_stdlib.py @@ -6,7 +6,7 @@ import contextlib from collections.abc import Callable, Iterator -from typing import Any, Type +from typing import Any import astroid from astroid import nodes @@ -16,7 +16,7 @@ from pylint.checkers import stdlib from pylint.testutils import CheckerTestCase -_NodeNGT = Type[nodes.NodeNG] +_NodeNGT = type[nodes.NodeNG] @contextlib.contextmanager diff --git a/tests/checkers/unittest_similar.py b/tests/checkers/unittest_symilar.py similarity index 82% rename from tests/checkers/unittest_similar.py rename to tests/checkers/unittest_symilar.py index 8c00faee54a..0e551e4a164 100644 --- a/tests/checkers/unittest_similar.py +++ b/tests/checkers/unittest_symilar.py @@ -7,9 +7,9 @@ from pathlib import Path import pytest +from _pytest.capture import CaptureFixture -from pylint.checkers import similar -from pylint.constants import IS_PYPY, PY39_PLUS +from pylint.checkers import symilar from pylint.lint import PyLinter from pylint.testutils import GenericTestReporter as Reporter @@ -31,7 +31,7 @@ def test_ignore_comments() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--ignore-comments", SIMILAR1, SIMILAR2]) + symilar.Run(["--ignore-comments", SIMILAR1, SIMILAR2]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -60,7 +60,7 @@ def test_ignore_comments() -> None: def test_ignore_docstrings() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--ignore-docstrings", SIMILAR1, SIMILAR2]) + symilar.Run(["--ignore-docstrings", SIMILAR1, SIMILAR2]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -95,7 +95,7 @@ def test_ignore_docstrings() -> None: def test_ignore_imports() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--ignore-imports", SIMILAR1, SIMILAR2]) + symilar.Run(["--ignore-imports", SIMILAR1, SIMILAR2]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -108,7 +108,7 @@ def test_ignore_imports() -> None: def test_multiline_imports() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run([MULTILINE, MULTILINE]) + symilar.Run([MULTILINE, MULTILINE]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -131,14 +131,10 @@ def test_multiline_imports() -> None: ) -@pytest.mark.skipif( - IS_PYPY and not PY39_PLUS, - reason="Requires accurate 'end_lineno' value", -) def test_ignore_multiline_imports() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--ignore-imports", MULTILINE, MULTILINE]) + symilar.Run(["--ignore-imports", MULTILINE, MULTILINE]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -151,7 +147,7 @@ def test_ignore_multiline_imports() -> None: def test_ignore_signatures_fail() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run([SIMILAR5, SIMILAR6]) + symilar.Run([SIMILAR5, SIMILAR6]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -189,7 +185,7 @@ def example(): def test_ignore_signatures_pass() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--ignore-signatures", SIMILAR5, SIMILAR6]) + symilar.Run(["--ignore-signatures", SIMILAR5, SIMILAR6]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -202,7 +198,7 @@ def test_ignore_signatures_pass() -> None: def test_ignore_signatures_class_methods_fail() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run([SIMILAR_CLS_B, SIMILAR_CLS_A]) + symilar.Run([SIMILAR_CLS_B, SIMILAR_CLS_A]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -248,7 +244,7 @@ def _internal_func( def test_ignore_signatures_class_methods_pass() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--ignore-signatures", SIMILAR_CLS_B, SIMILAR_CLS_A]) + symilar.Run(["--ignore-signatures", SIMILAR_CLS_B, SIMILAR_CLS_A]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -261,7 +257,7 @@ def test_ignore_signatures_class_methods_pass() -> None: def test_ignore_signatures_empty_functions_fail() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run([EMPTY_FUNCTION_1, EMPTY_FUNCTION_2]) + symilar.Run([EMPTY_FUNCTION_1, EMPTY_FUNCTION_2]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -285,7 +281,7 @@ def test_ignore_signatures_empty_functions_fail() -> None: def test_ignore_signatures_empty_functions_pass() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--ignore-signatures", EMPTY_FUNCTION_1, EMPTY_FUNCTION_2]) + symilar.Run(["--ignore-signatures", EMPTY_FUNCTION_1, EMPTY_FUNCTION_2]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -298,7 +294,7 @@ def test_ignore_signatures_empty_functions_pass() -> None: def test_no_hide_code_with_imports() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--ignore-imports"] + 2 * [HIDE_CODE_WITH_IMPORTS]) + symilar.Run(["--ignore-imports"] + 2 * [HIDE_CODE_WITH_IMPORTS]) assert ex.value.code == 0 assert "TOTAL lines=32 duplicates=0 percent=0.00" in output.getvalue() @@ -306,7 +302,7 @@ def test_no_hide_code_with_imports() -> None: def test_ignore_nothing() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run([SIMILAR1, SIMILAR2]) + symilar.Run([SIMILAR1, SIMILAR2]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -329,7 +325,7 @@ def test_ignore_nothing() -> None: def test_lines_without_meaningful_content_do_not_trigger_similarity() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run([SIMILAR3, SIMILAR4]) + symilar.Run([SIMILAR3, SIMILAR4]) assert ex.value.code == 0 assert ( output.getvalue().strip() @@ -362,29 +358,32 @@ def test_help() -> None: output = StringIO() with redirect_stdout(output): try: - similar.Run(["--help"]) + symilar.Run(["--help"]) except SystemExit as ex: assert ex.code == 0 else: pytest.fail("not system exit") -def test_no_args() -> None: +def test_no_args(capsys: CaptureFixture) -> None: output = StringIO() with redirect_stdout(output): try: - similar.Run([]) + symilar.Run([]) except SystemExit as ex: - assert ex.code == 1 + assert ex.code == 2 + out, err = capsys.readouterr() + assert not out + assert "the following arguments are required: files" in err else: pytest.fail("not system exit") def test_get_map_data() -> None: - """Tests that a SimilarChecker can return and reduce mapped data.""" + """Tests that a SymilarChecker can return and reduce mapped data.""" linter = PyLinter(reporter=Reporter()) # Add a parallel checker to ensure it can map and reduce - linter.register_checker(similar.SimilarChecker(linter)) + linter.register_checker(symilar.SimilaritiesChecker(linter)) source_streams = ( str(INPUT / "similar_lines_a.py"), str(INPUT / "similar_lines_b.py"), @@ -473,7 +472,7 @@ def test_get_map_data() -> None: # Manually perform a 'map' type function for source_fname in source_streams: - sim = similar.SimilarChecker(PyLinter()) + sim = symilar.SimilaritiesChecker(PyLinter()) sim.linter.set_option("ignore-imports", False) sim.linter.set_option("ignore-signatures", False) with open(source_fname, encoding="utf-8") as stream: @@ -494,6 +493,35 @@ def test_get_map_data() -> None: def test_set_duplicate_lines_to_zero() -> None: output = StringIO() with redirect_stdout(output), pytest.raises(SystemExit) as ex: - similar.Run(["--duplicates=0", SIMILAR1, SIMILAR2]) + symilar.Run(["--duplicates=0", SIMILAR1, SIMILAR2]) assert ex.value.code == 0 assert output.getvalue() == "" + + +def test_equal_short_form_option() -> None: + """Regression test for https://github.com/pylint-dev/pylint/issues/9343""" + output = StringIO() + with redirect_stdout(output), pytest.raises(SystemExit) as ex: + symilar.Run(["-d=2", SIMILAR1, SIMILAR2]) + assert ex.value.code == 0 + assert "similar lines in" in output.getvalue() + + +def test_space_short_form_option() -> None: + """Regression test for https://github.com/pylint-dev/pylint/issues/9343""" + output = StringIO() + with redirect_stdout(output), pytest.raises(SystemExit) as ex: + symilar.Run(["-d 2", SIMILAR1, SIMILAR2]) + assert ex.value.code == 0 + assert "similar lines in" in output.getvalue() + + +def test_bad_short_form_option(capsys: CaptureFixture) -> None: + """Regression test for https://github.com/pylint-dev/pylint/issues/9343""" + output = StringIO() + with redirect_stdout(output), pytest.raises(SystemExit) as ex: + symilar.Run(["-j=0", SIMILAR1, SIMILAR2]) + out, err = capsys.readouterr() + assert ex.value.code == 2 + assert not out + assert "unrecognized arguments: -j=0" in err diff --git a/tests/config/functional/toml/issue_9680/bad_regex_in_ignore_paths.32.out b/tests/config/functional/toml/issue_9680/bad_regex_in_ignore_paths.32.out new file mode 100644 index 00000000000..b28c8921038 --- /dev/null +++ b/tests/config/functional/toml/issue_9680/bad_regex_in_ignore_paths.32.out @@ -0,0 +1,2 @@ +usage: pylint [options] +pylint: error: argument --ignore-paths: Error in provided regular expression: project\\tooling_context\\**|project/tooling_context/** beginning at index 27: multiple repeat diff --git a/tests/config/functional/toml/issue_9680/bad_regex_in_ignore_paths.toml b/tests/config/functional/toml/issue_9680/bad_regex_in_ignore_paths.toml new file mode 100644 index 00000000000..528a6da5dd2 --- /dev/null +++ b/tests/config/functional/toml/issue_9680/bad_regex_in_ignore_paths.toml @@ -0,0 +1,3 @@ +# Check that we report regex error in configuration file properly +[tool.pylint."main"] +ignore-paths = ['project/tooling_context/**'] diff --git a/tests/config/functional/toml/unknown_msgid/enable_unknown_msgid.toml b/tests/config/functional/toml/unknown_msgid/enable_unknown_msgid.toml index 65fe5609047..c25ca46a505 100644 --- a/tests/config/functional/toml/unknown_msgid/enable_unknown_msgid.toml +++ b/tests/config/functional/toml/unknown_msgid/enable_unknown_msgid.toml @@ -1,4 +1,4 @@ -# Check the behavior for unkonwn symbol/msgid +# Check the behavior for unknown symbol/msgid # (Originally) reported in https://github.com/pylint-dev/pylint/pull/6293 [tool.pylint."messages control"] diff --git a/tests/config/test_find_default_config_files.py b/tests/config/test_find_default_config_files.py index ae879a10d03..2c50a556823 100644 --- a/tests/config/test_find_default_config_files.py +++ b/tests/config/test_find_default_config_files.py @@ -18,7 +18,10 @@ from pytest import CaptureFixture from pylint import config, testutils -from pylint.config.find_default_config_files import _cfg_has_config, _toml_has_config +from pylint.config.find_default_config_files import ( + _cfg_or_ini_has_config, + _toml_has_config, +) from pylint.lint.run import Run @@ -307,12 +310,13 @@ def test_toml_has_config(content: str, expected: bool, 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: - f.write(content) - assert _cfg_has_config(fake_cfg) == expected +def test_has_config(content: str, expected: bool, tmp_path: Path) -> None: + """Test that a .cfg file or .ini file has a pylint config.""" + for file_name in ("fake.cfg", "tox.ini"): + fake_conf = tmp_path / file_name + with open(fake_conf, "w", encoding="utf8") as f: + f.write(content) + assert _cfg_or_ini_has_config(fake_conf) == expected def test_non_existent_home() -> None: diff --git a/tests/config/test_functional_config_loading.py b/tests/config/test_functional_config_loading.py index fe5a39d45c1..26341cf3289 100644 --- a/tests/config/test_functional_config_loading.py +++ b/tests/config/test_functional_config_loading.py @@ -82,12 +82,24 @@ def test_functional_config_loading( expected_loaded_configuration = get_expected_configuration( configuration_path, default_configuration ) + runner = None # The runner can fail to init if conf is bad enough. with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message="The use of 'MASTER'.*", category=UserWarning ) - runner = run_using_a_configuration_file(configuration_path, file_to_lint_path) - assert runner.linter.msg_status == expected_code + try: + runner = run_using_a_configuration_file( + configuration_path, file_to_lint_path + ) + assert runner.linter.msg_status == expected_code + except SystemExit as e: + # Case where the conf exit with an argparse error + assert e.code == expected_code + out, err = capsys.readouterr() + assert out == "" + assert err.rstrip() == expected_output.rstrip() + return + out, err = capsys.readouterr() # 'rstrip()' applied, so we can have a final newline in the expected test file assert expected_output.rstrip() == out.rstrip(), msg diff --git a/tests/data/ascript b/tests/data/a_script similarity index 100% rename from tests/data/ascript rename to tests/data/a_script diff --git a/tests/functional/a/access/access_attr_before_def_false_positive.rc b/tests/functional/a/access/access_attr_before_def_false_positive.rc new file mode 100644 index 00000000000..7a083bad76b --- /dev/null +++ b/tests/functional/a/access/access_attr_before_def_false_positive.rc @@ -0,0 +1,2 @@ +[testoptions] +max_pyver=3.13 diff --git a/tests/functional/a/arguments.py b/tests/functional/a/arguments.py index a312c45e6a0..5807916aee5 100644 --- a/tests/functional/a/arguments.py +++ b/tests/functional/a/arguments.py @@ -331,3 +331,5 @@ def func(string): func(42) a = func(42) + +isinstance(1) # [no-value-for-parameter] diff --git a/tests/functional/a/arguments.txt b/tests/functional/a/arguments.txt index 70a99e1a234..7f20d23d115 100644 --- a/tests/functional/a/arguments.txt +++ b/tests/functional/a/arguments.txt @@ -39,3 +39,4 @@ no-value-for-parameter:217:0:217:30::No value for argument 'second' in function unexpected-keyword-arg:218:0:218:43::Unexpected keyword argument 'fourth' in function call:UNDEFINED redundant-keyword-arg:308:0:308:79::Argument 'banana' passed by position and keyword in function call:UNDEFINED no-value-for-parameter:318:0:318:16::No value for argument 'param1' in function call:UNDEFINED +no-value-for-parameter:335:0:335:13::No value for argument '__class_or_tuple' in function call:HIGH diff --git a/tests/functional/a/arguments_differ.py b/tests/functional/a/arguments_differ.py index 1c56ecc0834..effe96c9a5d 100644 --- a/tests/functional/a/arguments_differ.py +++ b/tests/functional/a/arguments_differ.py @@ -280,7 +280,7 @@ class ChildT3(ParentT3): def func(self, user_input: FooT1) -> None: pass -# Keyword and positional overriddes +# Keyword and positional overrides class AbstractFoo: def kwonly_1(self, first, *, second, third): diff --git a/tests/functional/a/async_functions.py b/tests/functional/a/async_functions.py index 75fa684dffe..5852dd24380 100644 --- a/tests/functional/a/async_functions.py +++ b/tests/functional/a/async_functions.py @@ -22,7 +22,8 @@ async def some_method(self): super(OtherClass, self).test() # [bad-super-call] -# +1: [too-many-arguments,too-many-return-statements, too-many-branches] +# +1: [line-too-long] +# +1: [too-many-arguments, too-many-positional-arguments, too-many-return-statements, too-many-branches] async def complex_function(this, function, has, more, arguments, than, one, _, should, have): if 1: diff --git a/tests/functional/a/async_functions.txt b/tests/functional/a/async_functions.txt index bfb9e520211..33052f8773e 100644 --- a/tests/functional/a/async_functions.txt +++ b/tests/functional/a/async_functions.txt @@ -1,10 +1,12 @@ redefined-builtin:5:0:5:14:next:Redefining built-in 'next':UNDEFINED unused-argument:8:30:8:34:some_function:Unused argument 'arg2':HIGH bad-super-call:22:8:22:31:Class.some_method:Bad first argument 'OtherClass' given to super():UNDEFINED -too-many-arguments:26:0:26:26:complex_function:Too many arguments (10/5):UNDEFINED -too-many-branches:26:0:26:26:complex_function:Too many branches (13/12):UNDEFINED -too-many-return-statements:26:0:26:26:complex_function:Too many return statements (10/6):UNDEFINED -dangerous-default-value:59:0:59:14:func:Dangerous default value [] as argument:UNDEFINED -duplicate-argument-name:59:18:59:19:func:Duplicate argument name 'a' in function definition:HIGH -disallowed-name:64:0:64:13:foo:"Disallowed name ""foo""":HIGH -empty-docstring:64:0:64:13:foo:Empty function docstring:HIGH +line-too-long:26:0:None:None::Line too long (104/100):UNDEFINED +too-many-arguments:27:0:27:26:complex_function:Too many arguments (10/5):UNDEFINED +too-many-branches:27:0:27:26:complex_function:Too many branches (13/12):UNDEFINED +too-many-positional-arguments:27:0:27:26:complex_function:Too many positional arguments (9/5):HIGH +too-many-return-statements:27:0:27:26:complex_function:Too many return statements (10/6):UNDEFINED +dangerous-default-value:60:0:60:14:func:Dangerous default value [] as argument:UNDEFINED +duplicate-argument-name:60:18:60:19:func:Duplicate argument name 'a' in function definition:HIGH +disallowed-name:65:0:65:13:foo:"Disallowed name ""foo""":HIGH +empty-docstring:65:0:65:13:foo:Empty function docstring:HIGH diff --git a/tests/functional/b/broad_exception/broad_exception_caught.txt b/tests/functional/b/broad_exception/broad_exception_caught.txt index 817e6201727..386423b63fc 100644 --- a/tests/functional/b/broad_exception/broad_exception_caught.txt +++ b/tests/functional/b/broad_exception/broad_exception_caught.txt @@ -1,3 +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 +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/broad_exception_caught_trystar.txt b/tests/functional/b/broad_exception/broad_exception_caught_trystar.txt index da8725e57a5..635a5c3fdad 100644 --- a/tests/functional/b/broad_exception/broad_exception_caught_trystar.txt +++ b/tests/functional/b/broad_exception/broad_exception_caught_trystar.txt @@ -1,3 +1,3 @@ -broad-exception-caught:14:8:14:17::Catching too general exception Exception:INFERENCE -broad-exception-caught:20:8:20:21::Catching too general exception BaseException:INFERENCE -broad-exception-caught:32:7:32:27::Catching too general exception CustomBroadException:INFERENCE +broad-exception-caught:14:8:14:17::Catching too general exception Exception:INFERENCE +broad-exception-caught:20:8:20:21::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/broad_exception_raised.txt b/tests/functional/b/broad_exception/broad_exception_raised.txt index 705bf45cd87..1e27b23f989 100644 --- a/tests/functional/b/broad_exception/broad_exception_raised.txt +++ b/tests/functional/b/broad_exception/broad_exception_raised.txt @@ -1,8 +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 +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/broad_exception/broad_exception_raised_trystar.txt b/tests/functional/b/broad_exception/broad_exception_raised_trystar.txt index c88b26b4be0..d009e255c89 100644 --- a/tests/functional/b/broad_exception/broad_exception_raised_trystar.txt +++ b/tests/functional/b/broad_exception/broad_exception_raised_trystar.txt @@ -1,8 +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_star:"Raising too general exception: Exception":INFERENCE -broad-exception-caught:21:12:21:21:raise_and_catch_star:Catching too general exception Exception:INFERENCE -broad-exception-raised:38:8:38:35:raise_catch_raise_star:"Raising too general exception: Exception":INFERENCE -broad-exception-raised:46:8:46:40:raise_catch_raise_using_alias_star:"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 +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_star:"Raising too general exception: Exception":INFERENCE +broad-exception-caught:21:12:21:21:raise_and_catch_star:Catching too general exception Exception:INFERENCE +broad-exception-raised:38:8:38:35:raise_catch_raise_star:"Raising too general exception: Exception":INFERENCE +broad-exception-raised:46:8:46:40:raise_catch_raise_using_alias_star:"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/c/consider/consider_merging_isinstance.py b/tests/functional/c/consider/consider_merging_isinstance.py index d3387bd5ce8..ba0b4c77a89 100644 --- a/tests/functional/c/consider/consider_merging_isinstance.py +++ b/tests/functional/c/consider/consider_merging_isinstance.py @@ -23,8 +23,8 @@ def isinstances(): result = isinstance(var[10], str) or isinstance(var[10], int) and var[8] * 14 or isinstance(var[10], float) and var[5] * 14.4 or isinstance(var[10], list) # [consider-merging-isinstance] result = isinstance(var[11], int) or isinstance(var[11], int) or isinstance(var[11], float) # [consider-merging-isinstance] - result = isinstance(var[20]) - result = isinstance() + result = isinstance(var[20]) # [no-value-for-parameter] + result = isinstance() # [no-value-for-parameter, no-value-for-parameter] # Combination merged and not merged result = isinstance(var[12], (int, float)) or isinstance(var[12], list) # [consider-merging-isinstance] diff --git a/tests/functional/c/consider/consider_merging_isinstance.txt b/tests/functional/c/consider/consider_merging_isinstance.txt index 3d4297fb88c..8bf618ad9de 100644 --- a/tests/functional/c/consider/consider_merging_isinstance.txt +++ b/tests/functional/c/consider/consider_merging_isinstance.txt @@ -4,4 +4,7 @@ consider-merging-isinstance:19:13:19:73:isinstances:Consider merging these isins consider-merging-isinstance:22:13:22:127:isinstances:Consider merging these isinstance calls to isinstance(var[6], (float, int)):UNDEFINED consider-merging-isinstance:23:13:23:158:isinstances:Consider merging these isinstance calls to isinstance(var[10], (list, str)):UNDEFINED consider-merging-isinstance:24:13:24:95:isinstances:Consider merging these isinstance calls to isinstance(var[11], (float, int)):UNDEFINED +no-value-for-parameter:26:13:26:32:isinstances:No value for argument '__class_or_tuple' in function call:HIGH +no-value-for-parameter:27:13:27:25:isinstances:No value for argument '__class_or_tuple' in function call:HIGH +no-value-for-parameter:27:13:27:25:isinstances:No value for argument '_obj' in function call:HIGH consider-merging-isinstance:30:13:30:75:isinstances:Consider merging these isinstance calls to isinstance(var[12], (float, int, list)):UNDEFINED diff --git a/tests/functional/c/consider/consider_using_get.py b/tests/functional/c/consider/consider_using_get.py index 728fccd5051..8a75f997270 100644 --- a/tests/functional/c/consider/consider_using_get.py +++ b/tests/functional/c/consider/consider_using_get.py @@ -16,10 +16,10 @@ if 'key' in dictionary: # not accessing the dictionary in assignment variable = "string" -if 'key' in dictionary: # is a match, but not obvious and we ignore it for now +if 'key' in dictionary: # is a match, but not obvious, and we ignore it for now variable = dictionary[key] -if 'key1' in dictionary: # dictionary querried for wrong key +if 'key1' in dictionary: # dictionary queried for wrong key variable = dictionary['key2'] if 'key' in dictionary: # body is not pure diff --git a/tests/functional/c/consider/consider_using_min_max_builtin.txt b/tests/functional/c/consider/consider_using_min_max_builtin.txt index ce1e98bd091..231d166f01c 100644 --- a/tests/functional/c/consider/consider_using_min_max_builtin.txt +++ b/tests/functional/c/consider/consider_using_min_max_builtin.txt @@ -8,7 +8,7 @@ consider-using-max-builtin:26:0:27:19::Consider using 'value3 = max(value3, valu consider-using-min-builtin:29:0:30:18::Consider using 'value2 = min(value2, value)' instead of unnecessary if block:UNDEFINED consider-using-min-builtin:32:0:33:25::Consider using 'value = min(value, float(value3))' instead of unnecessary if block:UNDEFINED consider-using-min-builtin:36:0:37:27::Consider using 'value2 = min(value2, offset + value)' instead of unnecessary if block:UNDEFINED -consider-using-min-builtin:45:0:46:17::Consider using 'value = min(value, 10)' instead of unnecessary if block:UNDEFINED +consider-using-min-builtin:45:0:46:17::Consider using 'A1.value = min(A1.value, 10)' instead of unnecessary if block:UNDEFINED consider-using-min-builtin:69:0:70:11::Consider using 'A1 = min(A1, A2)' instead of unnecessary if block:UNDEFINED consider-using-max-builtin:72:0:73:11::Consider using 'A2 = max(A2, A1)' instead of unnecessary if block:UNDEFINED consider-using-min-builtin:75:0:76:11::Consider using 'A1 = min(A1, A2)' instead of unnecessary if block:UNDEFINED diff --git a/tests/functional/d/deprecated/deprecated_methods_py3.py b/tests/functional/d/deprecated/deprecated_methods_py3.py deleted file mode 100644 index 80428c79f3b..00000000000 --- a/tests/functional/d/deprecated/deprecated_methods_py3.py +++ /dev/null @@ -1,50 +0,0 @@ -""" Functional tests for method deprecation. """ -# pylint: disable=missing-docstring, super-init-not-called, not-callable -import base64 -import cgi -import inspect -import logging -import nntplib -import platform -import unittest -import xml.etree.ElementTree - - -class MyTest(unittest.TestCase): - def test(self): - self.assert_(True) # [deprecated-method] - -xml.etree.ElementTree.Element('tag').getchildren() # [deprecated-method] -xml.etree.ElementTree.Element('tag').getiterator() # [deprecated-method] -xml.etree.ElementTree.XMLParser('tag', None, None).doctype(None, None, None) # [deprecated-method] -nntplib.NNTP(None).xpath(None) # [deprecated-method] - - -inspect.getargspec(None) # [deprecated-method] -logging.warn("a") # [deprecated-method] -platform.popen([]) # [deprecated-method] -base64.encodestring("42") # [deprecated-method] -base64.decodestring("42") # [deprecated-method] -cgi.escape("a") # [deprecated-method] - - -class SuperCrash(unittest.TestCase): - - def __init__(self): - # should not crash. - super()() - -xml.etree.ElementTree.iterparse(None) - - -class Tests(unittest.TestCase): - - def test_foo(self): - self.assertEquals(2 + 2, 4) # [deprecated-method] - self.assertNotEquals(2 + 2, 4) # [deprecated-method] - self.assertAlmostEquals(2 + 2, 4) # [deprecated-method] - self.assertNotAlmostEquals(2 + 2, 4) # [deprecated-method] - self.assert_("abc" == "2") # [deprecated-method] - - self.assertRaisesRegex(ValueError, "exception") - self.assertRegex("something", r".+") diff --git a/tests/functional/d/deprecated/deprecated_methods_py3.rc b/tests/functional/d/deprecated/deprecated_methods_py3.rc deleted file mode 100644 index b56440977e5..00000000000 --- a/tests/functional/d/deprecated/deprecated_methods_py3.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.7 diff --git a/tests/functional/d/deprecated/deprecated_methods_py3.txt b/tests/functional/d/deprecated/deprecated_methods_py3.txt deleted file mode 100644 index 9796114f666..00000000000 --- a/tests/functional/d/deprecated/deprecated_methods_py3.txt +++ /dev/null @@ -1,16 +0,0 @@ -deprecated-method:15::MyTest.test:Using deprecated method assert_() -deprecated-method:17:::Using deprecated method getchildren() -deprecated-method:18:::Using deprecated method getiterator() -deprecated-method:19:::Using deprecated method doctype() -deprecated-method:20:::Using deprecated method xpath() -deprecated-method:23:::Using deprecated method getargspec() -deprecated-method:24:::Using deprecated method warn() -deprecated-method:25:::Using deprecated method popen() -deprecated-method:26:::Using deprecated method encodestring() -deprecated-method:27:::Using deprecated method decodestring() -deprecated-method:28:::Using deprecated method escape() -deprecated-method:43::Tests.test_foo:Using deprecated method assertEquals() -deprecated-method:44::Tests.test_foo:Using deprecated method assertNotEquals() -deprecated-method:45::Tests.test_foo:Using deprecated method assertAlmostEquals() -deprecated-method:46::Tests.test_foo:Using deprecated method assertNotAlmostEquals() -deprecated-method:47::Tests.test_foo:Using deprecated method assert_() diff --git a/tests/functional/d/deprecated/deprecated_methods_py38.py b/tests/functional/d/deprecated/deprecated_methods_py38.py deleted file mode 100644 index 3a7dfe862bf..00000000000 --- a/tests/functional/d/deprecated/deprecated_methods_py38.py +++ /dev/null @@ -1,57 +0,0 @@ -""" Functional tests for method deprecation. """ -# pylint: disable=missing-docstring, super-init-not-called, not-callable, comparison-of-constants -import base64 -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] - -xml.etree.ElementTree.Element('tag').getchildren() # [deprecated-method] -xml.etree.ElementTree.Element('tag').getiterator() # [deprecated-method] -nntplib.NNTP(None).xpath(None) # [deprecated-method] - - -inspect.getargspec(None) # [deprecated-method] -logging.warn("a") # [deprecated-method] -base64.encodestring("42") # [deprecated-method] -base64.decodestring("42") # [deprecated-method] - - -class SuperCrash(unittest.TestCase): - - def __init__(self): - # should not crash. - super()() - -xml.etree.ElementTree.iterparse(None) - - -class Tests(unittest.TestCase): - - def test_foo(self): - self.assertEquals(2 + 2, 4) # [deprecated-method] - self.assertNotEquals(2 + 2, 4) # [deprecated-method] - self.assertAlmostEquals(2 + 2, 4) # [deprecated-method] - self.assertNotAlmostEquals(2 + 2, 4) # [deprecated-method] - self.assert_("abc" == "2") # [deprecated-method] - - self.assertRaisesRegex(ValueError, "exception") - self.assertRegex("something", r".+") - - -class Deprecated: # pylint: disable=too-few-public-methods - deprecated_method = logging.warn - - -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_py38.rc b/tests/functional/d/deprecated/deprecated_methods_py38.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/d/deprecated/deprecated_methods_py38.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/d/deprecated/deprecated_methods_py38.txt b/tests/functional/d/deprecated/deprecated_methods_py38.txt deleted file mode 100644 index afed0c6b47b..00000000000 --- a/tests/functional/d/deprecated/deprecated_methods_py38.txt +++ /dev/null @@ -1,14 +0,0 @@ -deprecated-method:13:8:13:26:MyTest.test:Using deprecated method assert_():UNDEFINED -deprecated-method:15:0:15:50::Using deprecated method getchildren():UNDEFINED -deprecated-method:16:0:16:50::Using deprecated method getiterator():UNDEFINED -deprecated-method:17:0:17:30::Using deprecated method xpath():UNDEFINED -deprecated-method:20:0:20:24::Using deprecated method getargspec():UNDEFINED -deprecated-method:21:0:21:17::Using deprecated method warn():UNDEFINED -deprecated-method:22:0:22:25::Using deprecated method encodestring():UNDEFINED -deprecated-method:23:0:23:25::Using deprecated method decodestring():UNDEFINED -deprecated-method:38:8:38:35:Tests.test_foo:Using deprecated method assertEquals():UNDEFINED -deprecated-method:39:8:39:38:Tests.test_foo:Using deprecated method assertNotEquals():UNDEFINED -deprecated-method:40:8:40:41:Tests.test_foo:Using deprecated method assertAlmostEquals():UNDEFINED -deprecated-method:41:8:41:44:Tests.test_foo:Using deprecated method assertNotAlmostEquals():UNDEFINED -deprecated-method:42:8:42:34:Tests.test_foo:Using deprecated method assert_():UNDEFINED -deprecated-method:53:0:53:21::Using deprecated method deprecated_method():UNDEFINED diff --git a/tests/functional/d/deprecated/deprecated_methods_py39.rc b/tests/functional/d/deprecated/deprecated_methods_py39.rc index 062f6df19c1..4e2b7483131 100644 --- a/tests/functional/d/deprecated/deprecated_methods_py39.rc +++ b/tests/functional/d/deprecated/deprecated_methods_py39.rc @@ -1,3 +1,2 @@ [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 062f6df19c1..4e2b7483131 100644 --- a/tests/functional/d/deprecated/deprecated_module_py39.rc +++ b/tests/functional/d/deprecated/deprecated_module_py39.rc @@ -1,3 +1,2 @@ [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 09ceaa5e544..5dc39b1a65b 100644 --- a/tests/functional/d/deprecated/deprecated_module_py39_earlier_pyversion.rc +++ b/tests/functional/d/deprecated/deprecated_module_py39_earlier_pyversion.rc @@ -2,5 +2,4 @@ py-version=3.8 [testoptions] -min_pyver=3.9 max_pyver=3.10 diff --git a/tests/functional/ext/docparams/missing_param_doc.py b/tests/functional/ext/docparams/missing_param_doc.py index dbd5846fc6b..c039c07ffc3 100644 --- a/tests/functional/ext/docparams/missing_param_doc.py +++ b/tests/functional/ext/docparams/missing_param_doc.py @@ -1,4 +1,8 @@ -#pylint: disable=missing-module-docstring +#pylint: disable=missing-module-docstring, too-few-public-methods + + +from typing import overload, Union + def foobar1(arg1, arg2): #[missing-any-param-doc] """function foobar ... @@ -207,3 +211,31 @@ def foobar19(one, two, **kwargs): """ print(one, two, kwargs) return 1 + + +class Word: + """ + Methods decorated with `typing.overload` are excluded + from the docparam checks. For example: `missing-param-doc` and + `missing-type-doc`. + """ + def __init__(self, word): + self.word = word + + @overload + def starts_with(self, letter: None) -> None: ... + + @overload + def starts_with(self, letter: str) -> bool: ... + + def starts_with(self, letter: Union[str, None]) -> Union[bool, None]: + """ + Returns: + True if `self.word` begins with `letter` + + Args: + letter: str + """ + if self.word: + return self.word.startswith(letter) + return None diff --git a/tests/functional/ext/docparams/missing_param_doc.txt b/tests/functional/ext/docparams/missing_param_doc.txt index fdf4da93f4b..0fddc295f95 100644 --- a/tests/functional/ext/docparams/missing_param_doc.txt +++ b/tests/functional/ext/docparams/missing_param_doc.txt @@ -1,15 +1,15 @@ -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 +missing-any-param-doc:7:0:7:11:foobar1:"Missing any documentation in ""foobar1""":HIGH +missing-any-param-doc:12:0:12:11:foobar2:"Missing any documentation in ""foobar2""":HIGH +missing-param-doc:19:0:19:11:foobar3:"""arg2"" missing in parameter documentation":HIGH +missing-type-doc:19:0:19:11:foobar3:"""arg2"" missing in parameter type documentation":HIGH +missing-param-doc:28:0:28:11:foobar4:"""arg2"" missing in parameter documentation":HIGH +missing-type-doc:28:0:28:11:foobar4:"""arg2"" missing in parameter type documentation":HIGH +missing-type-doc:37:0:37:11:foobar5:"""arg1"" missing in parameter type documentation":HIGH +missing-param-doc:47:0:47:11:foobar6:"""arg3"" missing in parameter documentation":HIGH +missing-type-doc:47:0:47:11:foobar6:"""arg3"" missing in parameter type documentation":HIGH +missing-any-param-doc:57:0:57:11:foobar7:"Missing any documentation in ""foobar7""":HIGH +missing-any-param-doc:65:0:65:11:foobar8:"Missing any documentation in ""foobar8""":HIGH +missing-type-doc:80:0:80:12:foobar10:"""arg1, arg3"" missing in parameter type documentation":HIGH +missing-any-param-doc:92:0:92:12:foobar11:"Missing any documentation in ""foobar11""":HIGH +missing-param-doc:101:0:101:12:foobar12:"""arg3"" missing in parameter documentation":HIGH +missing-type-doc:101:0:101:12:foobar12:"""arg2, arg3"" 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 0fd82307fb3..05db0cd5007 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 @@ -383,7 +383,7 @@ def test_finds_args_with_xref_type_google(named_arg, **kwargs): def test_ignores_optional_specifier_google( param1, param2, param3=(), param4=[], param5=[], param6=True -): +): # pylint: disable=too-many-positional-arguments """Do something. Args: @@ -411,7 +411,7 @@ def test_finds_multiple_complex_types_google( named_arg_eight, named_arg_nine, named_arg_ten, -): +): # pylint: disable=too-many-positional-arguments """The google docstring Args: 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 83fa9700d36..3d84fdef243 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 @@ -344,7 +344,7 @@ def my_func( named_arg_six, named_arg_seven, named_arg_eight, -): +): # pylint: disable=too-many-positional-arguments """The docstring Args diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_Google.py b/tests/functional/ext/docparams/raise/missing_raises_doc_Google.py index 22dbcadaa38..f4ccea31334 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Google.py +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Google.py @@ -17,33 +17,33 @@ def test_find_google_attr_raises_exact_exc(self): """This is a google docstring. Raises: - re.error: Sometimes + calendar.IllegalMonthError: Sometimes """ - import re + import calendar - raise re.error("hi") + raise calendar.IllegalMonthError(-1) def test_find_google_attr_raises_substr_exc(self): """This is a google docstring. Raises: - re.error: Sometimes + calendar.IllegalMonthError: Sometimes """ - from re import error + from calendar import IllegalMonthError - raise error("hi") + raise IllegalMonthError(-1) def test_find_valid_missing_google_attr_raises(self): # [missing-raises-doc] """This is a google docstring. Raises: - re.anothererror: Sometimes + calendar.anothererror: Sometimes """ - from re import error + from calendar import IllegalMonthError - raise error("hi") + raise IllegalMonthError(-1) def test_find_invalid_missing_google_attr_raises(self): 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 f59d2717697..f3bbcad7fb9 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Google.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Google.txt @@ -1,6 +1,6 @@ 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 +missing-raises-doc:38:0:38:46:test_find_valid_missing_google_attr_raises:"""IllegalMonthError"" 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 diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.py b/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.py index 8cf8e041f1d..44e047ceb34 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.py +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.py @@ -87,12 +87,12 @@ def test_find_numpy_attr_raises_exact_exc(self): Raises ------ - re.error + calendar.IllegalMonthError Sometimes """ - import re + import calendar - raise re.error("hi") + raise calendar.IllegalMonthError(-1) def test_find_numpy_attr_raises_substr_exc(self): @@ -100,12 +100,12 @@ def test_find_numpy_attr_raises_substr_exc(self): Raises ------ - re.error + calendar.IllegalMonthError Sometimes """ - from re import error + from calendar import IllegalMonthError - raise error("hi") + raise IllegalMonthError(-1) def test_find_valid_missing_numpy_attr_raises(self): # [missing-raises-doc] @@ -113,12 +113,12 @@ def test_find_valid_missing_numpy_attr_raises(self): # [missing-raises-doc] Raises ------ - re.anothererror + calendar.anothererror Sometimes """ - from re import error + from calendar import IllegalMonthError - raise error("hi") + raise IllegalMonthError(-1) def test_find_invalid_missing_numpy_attr_raises(self): 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 43c6ba89b95..1cfdc062a48 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.txt @@ -3,7 +3,7 @@ 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:111:0:111:45:test_find_valid_missing_numpy_attr_raises:"""IllegalMonthError"" 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 diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.py b/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.py index 91a603b7115..b9a301ed6f6 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.py +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.py @@ -123,21 +123,21 @@ def test_find_sphinx_attr_raises_exact_exc(self): def test_find_sphinx_attr_raises_substr_exc(self): """This is a sphinx docstring. - :raises re.error: Sometimes + :raises calendar.IllegalMonthError: Sometimes """ - from re import error + from calendar import IllegalMonthError - raise error("hi") + raise IllegalMonthError(-1) def test_find_valid_missing_sphinx_attr_raises(self): # [missing-raises-doc] """This is a sphinx docstring. - :raises re.anothererror: Sometimes + :raises calendar.anothererror: Sometimes """ - from re import error + from calendar import IllegalMonthError - raise error("hi") + raise IllegalMonthError(-1) def test_find_invalid_missing_sphinx_attr_raises(self): @@ -145,11 +145,11 @@ def test_find_invalid_missing_sphinx_attr_raises(self): pylint allows this to pass since the comparison between Raises and raise are based on the class name, not the qualified name. - :raises bogusmodule.error: Sometimes + :raises bogusmodule.IllegalMonthError: Sometimes """ - from re import error + from calendar import IllegalMonthError - raise error("hi") + raise IllegalMonthError(-1) class Foo: 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 599c8beda34..568de3c4b3b 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_Sphinx.txt @@ -10,4 +10,4 @@ missing-raises-doc:90:0:90:55:test_find_missing_sphinx_raises_infer_from_instanc 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 +missing-raises-doc:133:0:133:46:test_find_valid_missing_sphinx_attr_raises:"""IllegalMonthError"" not documented as being raised":HIGH diff --git a/tests/functional/ext/typing/typing_consider_using_alias.py b/tests/functional/ext/typing/typing_consider_using_alias.py index 070451cf0a8..783de1822ef 100644 --- a/tests/functional/ext/typing/typing_consider_using_alias.py +++ b/tests/functional/ext/typing/typing_consider_using_alias.py @@ -28,7 +28,6 @@ var7: typing.Hashable # [consider-using-alias] var8: typing.ContextManager[str] # [consider-using-alias] var9: typing.Pattern[str] # [consider-using-alias] -var10: typing.re.Match[str] # [consider-using-alias] var11: list[int] var12: collections.abc var13: Awaitable[None] diff --git a/tests/functional/ext/typing/typing_consider_using_alias.txt b/tests/functional/ext/typing/typing_consider_using_alias.txt index 2cd299d9043..2a556159788 100644 --- a/tests/functional/ext/typing/typing_consider_using_alias.txt +++ b/tests/functional/ext/typing/typing_consider_using_alias.txt @@ -6,15 +6,14 @@ consider-using-alias:27:6:27:21::'typing.Iterable' will be deprecated with PY39, 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 +consider-using-alias:39:9:39:13::'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:41:7:41:11::'typing.Type' will be deprecated with PY39, consider using 'type' instead:INFERENCE +consider-using-alias:42:7:42:12::'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead:INFERENCE +consider-using-alias:43:7:43:15::'typing.Callable' will be deprecated with PY39, consider using 'collections.abc.Callable' instead:INFERENCE +consider-using-alias:49:74:49:78:func1:'typing.Dict' will be deprecated with PY39, consider using 'dict' instead:INFERENCE +consider-using-alias:49:16:49:20:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:49:37:49:41:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:49:93:49:105:func1:'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead:INFERENCE +consider-using-alias:65:12:65:16:CustomNamedTuple:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:70:12:70:16:CustomTypedDict2:'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:74:12:74: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 b597e955eb7..b382b9ac9fa 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 @@ -26,7 +26,6 @@ var7: typing.Hashable # [consider-using-alias] var8: typing.ContextManager[str] # [consider-using-alias] var9: typing.Pattern[str] # [consider-using-alias] -var10: typing.re.Match[str] # [consider-using-alias] var11: list[int] var12: collections.abc var13: Awaitable[None] 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 7cf15a63c80..8ae56f810a6 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 @@ -6,15 +6,14 @@ consider-using-alias:25:6:25:21::'typing.Iterable' will be deprecated with PY39, 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 +consider-using-alias:37:9:37:13::'typing.List' will be deprecated with PY39, consider using 'list' instead:INFERENCE +consider-using-alias:39:7:39:11::'typing.Type' will be deprecated with PY39, consider using 'type' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:40:7:40:12::'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:41:7:41: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:47:74:47:78:func1:'typing.Dict' will be deprecated with PY39, consider using 'dict' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:47:16:47:20:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:47:37:47:41:func1:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:47:93:47:105:func1:'typing.Tuple' will be deprecated with PY39, consider using 'tuple' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:63:12:63:16:CustomNamedTuple:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:68:12:68:16:CustomTypedDict2:'typing.List' will be deprecated with PY39, consider using 'list' instead. Add 'from __future__ import annotations' as well:INFERENCE +consider-using-alias:72:12:72: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.txt b/tests/functional/ext/typing/typing_consider_using_union.txt index 5f2746815f5..4584ad5f300 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: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 +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_py310.txt b/tests/functional/ext/typing/typing_consider_using_union_py310.txt index fde1d7e2b26..011cff572ba 100644 --- a/tests/functional/ext/typing/typing_consider_using_union_py310.txt +++ b/tests/functional/ext/typing/typing_consider_using_union_py310.txt @@ -1,18 +1,18 @@ -consider-alternative-union-syntax:11:6:11:11::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:12:11:12:16::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:13:16:13:28::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:14:6:14:14::Consider using alternative Union syntax instead of 'Optional':INFERENCE -consider-alternative-union-syntax:16:9:16:14::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:17:14:17:19::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:18:19:18:31::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:19:9:19:17::Consider using alternative Union syntax instead of 'Optional':INFERENCE -consider-alternative-union-syntax:22:10:22:18:func1:Consider using alternative Union syntax instead of 'Optional':INFERENCE -consider-alternative-union-syntax:23:24:23:29:func1:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:24:5:24:10:func1:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:27:19:27:24:Custom1:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:31:28:31:33::Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:33:14:33:22::Consider using alternative Union syntax instead of 'Optional':INFERENCE -consider-alternative-union-syntax:36:12:36:17:CustomNamedTuple:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:38:56:38:64::Consider using alternative Union syntax instead of 'Optional':INFERENCE -consider-alternative-union-syntax:41:27:41:32:CustomTypedDict2:Consider using alternative Union syntax instead of 'Union':INFERENCE -consider-alternative-union-syntax:45:12:45:20:CustomDataClass:Consider using alternative Union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:11:6:11:11::Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:12:11:12:16::Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:13:16:13:28::Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:14:6:14:14::Consider using alternative union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:16:9:16:14::Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:17:14:17:19::Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:18:19:18:31::Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:19:9:19:17::Consider using alternative union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:22:10:22:18:func1:Consider using alternative union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:23:24:23:29:func1:Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:24:5:24:10:func1:Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:27:19:27:24:Custom1:Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:31:28:31:33::Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:33:14:33:22::Consider using alternative union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:36:12:36:17:CustomNamedTuple:Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:38:56:38:64::Consider using alternative union syntax instead of 'Optional':INFERENCE +consider-alternative-union-syntax:41:27:41:32:CustomTypedDict2:Consider using alternative union syntax instead of 'Union':INFERENCE +consider-alternative-union-syntax:45:12:45:20:CustomDataClass:Consider using alternative union syntax instead of 'Optional':INFERENCE 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 5d48e9afcad..58ea839c1e4 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: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 +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/ext/typing/typing_deprecated_alias.py b/tests/functional/ext/typing/typing_deprecated_alias.py index 80c132ebd90..a84afc47a10 100644 --- a/tests/functional/ext/typing/typing_deprecated_alias.py +++ b/tests/functional/ext/typing/typing_deprecated_alias.py @@ -19,7 +19,6 @@ var7: typing.Hashable # [deprecated-typing-alias] var8: typing.ContextManager[str] # [deprecated-typing-alias] var9: typing.Pattern[str] # [deprecated-typing-alias] -var10: typing.re.Match[str] # [deprecated-typing-alias] var11: list[int] var12: collections.abc var13: Awaitable[None] diff --git a/tests/functional/ext/typing/typing_deprecated_alias.rc b/tests/functional/ext/typing/typing_deprecated_alias.rc index a4a4c9022cc..d075a593efa 100644 --- a/tests/functional/ext/typing/typing_deprecated_alias.rc +++ b/tests/functional/ext/typing/typing_deprecated_alias.rc @@ -1,8 +1,3 @@ [main] py-version=3.9 load-plugins=pylint.extensions.typing - -[testoptions] -min_pyver=3.9 - -[typing] diff --git a/tests/functional/ext/typing/typing_deprecated_alias.txt b/tests/functional/ext/typing/typing_deprecated_alias.txt index 62cf9902ad7..b08faa95708 100644 --- a/tests/functional/ext/typing/typing_deprecated_alias.txt +++ b/tests/functional/ext/typing/typing_deprecated_alias.txt @@ -6,23 +6,22 @@ deprecated-typing-alias:18:6:18:21::'typing.Iterable' is deprecated, use 'collec deprecated-typing-alias:19:6:19:21::'typing.Hashable' is deprecated, use 'collections.abc.Hashable' instead:INFERENCE deprecated-typing-alias:20:6:20:27::'typing.ContextManager' is deprecated, use 'contextlib.AbstractContextManager' instead:INFERENCE deprecated-typing-alias:21:6:21:20::'typing.Pattern' is deprecated, use 're.Pattern' instead:INFERENCE -deprecated-typing-alias:22:7:22:22::'typing.Match' is deprecated, use 're.Match' instead:INFERENCE -deprecated-typing-alias:28:9:28:12::'typing.Set' is deprecated, use 'set' instead:INFERENCE -deprecated-typing-alias:29:9:29:13::'typing.Dict' is deprecated, use 'dict' instead:INFERENCE -deprecated-typing-alias:29:19:29:23::'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:30:20:30:31::'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:31:9:31:13::'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:33:7:33:11::'typing.Type' is deprecated, use 'type' instead:INFERENCE -deprecated-typing-alias:34:7:34:12::'typing.Tuple' is deprecated, use 'tuple' instead:INFERENCE -deprecated-typing-alias:35:7:35:15::'typing.Callable' is deprecated, use 'collections.abc.Callable' instead:INFERENCE -deprecated-typing-alias:41:74:41:78:func1:'typing.Dict' is deprecated, use 'dict' instead:INFERENCE -deprecated-typing-alias:41:16:41:20:func1:'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:41:37:41:41:func1:'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:41:93:41:105:func1:'typing.Tuple' is deprecated, use 'tuple' instead:INFERENCE -deprecated-typing-alias:48:20:48:31:CustomIntList:'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:52:28:52:32::'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:54:14:54:18::'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:57:12:57:16:CustomNamedTuple:'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:59:56:59:60::'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:62:12:62:16:CustomTypedDict2:'typing.List' is deprecated, use 'list' instead:INFERENCE -deprecated-typing-alias:66:12:66:16:CustomDataClass:'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:27:9:27:12::'typing.Set' is deprecated, use 'set' instead:INFERENCE +deprecated-typing-alias:28:9:28:13::'typing.Dict' is deprecated, use 'dict' instead:INFERENCE +deprecated-typing-alias:28:19:28:23::'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:29:20:29:31::'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:30:9:30:13::'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:32:7:32:11::'typing.Type' is deprecated, use 'type' instead:INFERENCE +deprecated-typing-alias:33:7:33:12::'typing.Tuple' is deprecated, use 'tuple' instead:INFERENCE +deprecated-typing-alias:34:7:34:15::'typing.Callable' is deprecated, use 'collections.abc.Callable' instead:INFERENCE +deprecated-typing-alias:40:74:40:78:func1:'typing.Dict' is deprecated, use 'dict' instead:INFERENCE +deprecated-typing-alias:40:16:40:20:func1:'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:40:37:40:41:func1:'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:40:93:40:105:func1:'typing.Tuple' is deprecated, use 'tuple' instead:INFERENCE +deprecated-typing-alias:47:20:47:31:CustomIntList:'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:51:28:51:32::'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:53:14:53:18::'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:56:12:56:16:CustomNamedTuple:'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:58:56:58:60::'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:61:12:61:16:CustomTypedDict2:'typing.List' is deprecated, use 'list' instead:INFERENCE +deprecated-typing-alias:65:12:65:16:CustomDataClass:'typing.List' is deprecated, use 'list' instead:INFERENCE diff --git a/tests/functional/ext/typing/unnecessary_default_type_args.py b/tests/functional/ext/typing/unnecessary_default_type_args.py new file mode 100644 index 00000000000..e2d1d700de2 --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args.py @@ -0,0 +1,17 @@ +# pylint: disable=missing-docstring,deprecated-typing-alias +import collections.abc as ca +import typing as t + +a1: t.Generator[int, str, str] +a2: t.Generator[int, None, None] +a3: t.Generator[int] +b1: t.AsyncGenerator[int, str] +b2: t.AsyncGenerator[int, None] +b3: t.AsyncGenerator[int] + +c1: ca.Generator[int, str, str] +c2: ca.Generator[int, None, None] # [unnecessary-default-type-args] +c3: ca.Generator[int] +d1: ca.AsyncGenerator[int, str] +d2: ca.AsyncGenerator[int, None] # [unnecessary-default-type-args] +d3: ca.AsyncGenerator[int] diff --git a/tests/functional/ext/typing/unnecessary_default_type_args.rc b/tests/functional/ext/typing/unnecessary_default_type_args.rc new file mode 100644 index 00000000000..63e11a4e6b7 --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args.rc @@ -0,0 +1,3 @@ +[main] +py-version=3.10 +load-plugins=pylint.extensions.typing diff --git a/tests/functional/ext/typing/unnecessary_default_type_args.txt b/tests/functional/ext/typing/unnecessary_default_type_args.txt new file mode 100644 index 00000000000..2d36ba46a66 --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args.txt @@ -0,0 +1,2 @@ +unnecessary-default-type-args:13:4:13:33::Type `ca.Generator[int, None, None]` has unnecessary default type args. Change it to `ca.Generator[int]`.:HIGH +unnecessary-default-type-args:16:4:16:32::Type `ca.AsyncGenerator[int, None]` has unnecessary default type args. Change it to `ca.AsyncGenerator[int]`.:HIGH diff --git a/tests/functional/ext/typing/unnecessary_default_type_args_py313.py b/tests/functional/ext/typing/unnecessary_default_type_args_py313.py new file mode 100644 index 00000000000..9dec4c4075a --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args_py313.py @@ -0,0 +1,17 @@ +# pylint: disable=missing-docstring,deprecated-typing-alias +import collections.abc as ca +import typing as t + +a1: t.Generator[int, str, str] +a2: t.Generator[int, None, None] # [unnecessary-default-type-args] +a3: t.Generator[int] +b1: t.AsyncGenerator[int, str] +b2: t.AsyncGenerator[int, None] # [unnecessary-default-type-args] +b3: t.AsyncGenerator[int] + +c1: ca.Generator[int, str, str] +c2: ca.Generator[int, None, None] # [unnecessary-default-type-args] +c3: ca.Generator[int] +d1: ca.AsyncGenerator[int, str] +d2: ca.AsyncGenerator[int, None] # [unnecessary-default-type-args] +d3: ca.AsyncGenerator[int] diff --git a/tests/functional/ext/typing/unnecessary_default_type_args_py313.rc b/tests/functional/ext/typing/unnecessary_default_type_args_py313.rc new file mode 100644 index 00000000000..d2db5fe7caf --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args_py313.rc @@ -0,0 +1,3 @@ +[main] +py-version=3.13 +load-plugins=pylint.extensions.typing diff --git a/tests/functional/ext/typing/unnecessary_default_type_args_py313.txt b/tests/functional/ext/typing/unnecessary_default_type_args_py313.txt new file mode 100644 index 00000000000..228f4996638 --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args_py313.txt @@ -0,0 +1,4 @@ +unnecessary-default-type-args:6:4:6:32::Type `t.Generator[int, None, None]` has unnecessary default type args. Change it to `t.Generator[int]`.:HIGH +unnecessary-default-type-args:9:4:9:31::Type `t.AsyncGenerator[int, None]` has unnecessary default type args. Change it to `t.AsyncGenerator[int]`.:HIGH +unnecessary-default-type-args:13:4:13:33::Type `ca.Generator[int, None, None]` has unnecessary default type args. Change it to `ca.Generator[int]`.:HIGH +unnecessary-default-type-args:16:4:16:32::Type `ca.AsyncGenerator[int, None]` has unnecessary default type args. Change it to `ca.AsyncGenerator[int]`.:HIGH diff --git a/tests/functional/f/fixme.py b/tests/functional/f/fixme.py index e3d420f8ef9..7ff749d94cb 100644 --- a/tests/functional/f/fixme.py +++ b/tests/functional/f/fixme.py @@ -1,9 +1,16 @@ """Tests for fixme and its disabling and enabling.""" -# pylint: disable=missing-function-docstring, unused-variable +# pylint: disable=missing-function-docstring, unused-variable, pointless-string-statement # +1: [fixme] # FIXME: beep +# +1: [fixme] + # TODO: don't forget indented ones should trigger +# +1: [fixme] +# TODO: that precedes another TODO: is treated as one and the message starts after the first +# +1: [fixme] +# TODO: large indentations after hash are okay +# but things cannot precede the TODO: do this def function(): variable = "FIXME: Ignore me!" @@ -35,6 +42,8 @@ def function(): # pylint: disable-next=fixme # FIXME: Don't raise when the message is disabled +"""TODO: Don't raise when docstring fixmes are disabled""" + # This line needs to be at the end of the file to make sure it doesn't end with a comment # Pragma's compare against the 'lineno' attribute of the respective nodes which # would stop too soon otherwise. diff --git a/tests/functional/f/fixme.txt b/tests/functional/f/fixme.txt index 53b66802026..e70b4d74614 100644 --- a/tests/functional/f/fixme.txt +++ b/tests/functional/f/fixme.txt @@ -1,9 +1,12 @@ fixme:5:1:None:None::"FIXME: beep":UNDEFINED -fixme:11:20:None:None::"FIXME: Valid test":UNDEFINED -fixme:14:5:None:None::"TODO: Do something with the variables":UNDEFINED -fixme:16:18:None:None::"XXX: Fix this later":UNDEFINED -fixme:18:5:None:None::"FIXME: no space after hash":UNDEFINED -fixme:20:5:None:None::"todo: no space after hash":UNDEFINED -fixme:23:2:None:None::"FIXME: this is broken":UNDEFINED -fixme:25:5:None:None::"./TODO: find with notes":UNDEFINED -fixme:27:5:None:None::"TO make something DO: find with regex":UNDEFINED +fixme:7:5:None:None::"TODO: don't forget indented ones should trigger":UNDEFINED +fixme:9:1:None:None::"TODO: that precedes another TODO: is treated as one and the message starts after the first":UNDEFINED +fixme:11:1:None:None::"TODO: large indentations after hash are okay":UNDEFINED +fixme:18:20:None:None::"FIXME: Valid test":UNDEFINED +fixme:21:5:None:None::"TODO: Do something with the variables":UNDEFINED +fixme:23:18:None:None::"XXX: Fix this later":UNDEFINED +fixme:25:5:None:None::"FIXME: no space after hash":UNDEFINED +fixme:27:5:None:None::"todo: no space after hash":UNDEFINED +fixme:30:2:None:None::"FIXME: this is broken":UNDEFINED +fixme:32:5:None:None::"./TODO: find with notes":UNDEFINED +fixme:34:5:None:None::"TO make something DO: find with regex":UNDEFINED diff --git a/tests/functional/f/fixme_docstring.py b/tests/functional/f/fixme_docstring.py new file mode 100644 index 00000000000..918ec00b078 --- /dev/null +++ b/tests/functional/f/fixme_docstring.py @@ -0,0 +1,56 @@ +"""Tests for fixme in docstrings""" +# pylint: disable=missing-function-docstring, pointless-string-statement + +# +1: [fixme] +"""TODO resolve this""" +# +1: [fixme] +""" TODO: indentations are permitted """ +# +1: [fixme] +''' TODO: indentations are permitted ''' +# +1: [fixme] +""" TODO: indentations are permitted""" + +""" preceding text TODO: is not permitted""" + +""" +FIXME don't forget this # [fixme] +XXX also remember this # [fixme] +FIXME: and this line, but treat it as one FIXME TODO # [fixme] +text cannot precede the TODO: it must be at the start + XXX indentations are okay # [fixme] +??? the codetag must be recognized +""" + +# +1: [fixme] +# FIXME should still work + +# +1: [fixme] +# TODO """ should work + +# """ TODO will not work +"""# TODO will not work""" + +"""TODOist API should not result in a message""" + +# +2: [fixme] +""" +TO make something DO: look a regex +""" + +# pylint: disable-next=fixme +"""TODO won't work anymore""" + +# +2: [fixme] +def function(): + """./TODO implement this""" + + +''' + XXX single quotes should be no different # [fixme] +''' +def function2(): + ''' + ./TODO implement this # [fixme] + FIXME and this # [fixme] + ''' + '''FIXME one more for good measure''' # [fixme] diff --git a/tests/functional/f/fixme_docstring.rc b/tests/functional/f/fixme_docstring.rc new file mode 100644 index 00000000000..de8a8941f9b --- /dev/null +++ b/tests/functional/f/fixme_docstring.rc @@ -0,0 +1,7 @@ +[MISCELLANEOUS] +# List of note tags to take in consideration, separated by a comma. +notes=XXX,TODO,./TODO +# Regular expression of note tags to take in consideration. +notes-rgx=FIXME(?!.*ISSUE-\d+)|TO.*DO +# enable checking for fixme's in docstrings +check-fixme-in-docstring=yes diff --git a/tests/functional/f/fixme_docstring.txt b/tests/functional/f/fixme_docstring.txt new file mode 100644 index 00000000000..b0b19a9ebb1 --- /dev/null +++ b/tests/functional/f/fixme_docstring.txt @@ -0,0 +1,16 @@ +fixme:5:1:None:None::TODO resolve this:UNDEFINED +fixme:7:1:None:None::"TODO: indentations are permitted ":UNDEFINED +fixme:9:1:None:None::"TODO: indentations are permitted ":UNDEFINED +fixme:11:1:None:None::"TODO: indentations are permitted":UNDEFINED +fixme:16:1:None:None::FIXME don't forget this # [fixme]:UNDEFINED +fixme:17:1:None:None::XXX also remember this # [fixme]:UNDEFINED +fixme:18:1:None:None::"FIXME: and this line, but treat it as one FIXME TODO # [fixme]":UNDEFINED +fixme:20:1:None:None::XXX indentations are okay # [fixme]:UNDEFINED +fixme:25:1:None:None::FIXME should still work:UNDEFINED +fixme:28:1:None:None::"TODO """""" should work":UNDEFINED +fixme:37:1:None:None::"TO make something DO: look a regex":UNDEFINED +fixme:45:5:None:None::./TODO implement this:UNDEFINED +fixme:49:1:None:None::XXX single quotes should be no different # [fixme]:UNDEFINED +fixme:53:5:None:None::./TODO implement this # [fixme]:UNDEFINED +fixme:54:5:None:None::FIXME and this # [fixme]:UNDEFINED +fixme:56:5:None:None::FIXME one more for good measure:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_collections.rc b/tests/functional/g/generic_alias/generic_alias_collections.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_collections.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37.py b/tests/functional/g/generic_alias/generic_alias_collections_py37.py deleted file mode 100644 index eaa153cedb2..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Test generic alias support for stdlib types (added in PY39). - -Raise [unsubscriptable-object] error for PY37 and PY38. -""" -# flake8: noqa -# pylint: disable=missing-docstring,pointless-statement -# pylint: disable=too-few-public-methods,multiple-statements,line-too-long -import abc -import collections -import collections.abc -import contextlib -import re - -# special -tuple[int, int] # [unsubscriptable-object] -type[int] # [unsubscriptable-object] -collections.abc.Callable[[int], str] # [unsubscriptable-object] - -# builtins -dict[int, str] # [unsubscriptable-object] -list[int] # [unsubscriptable-object] -set[int] # [unsubscriptable-object] -frozenset[int] # [unsubscriptable-object] - -# collections -collections.defaultdict[int, str] # [unsubscriptable-object] -collections.OrderedDict[int, str] # [unsubscriptable-object] -collections.ChainMap[int, str] # [unsubscriptable-object] -collections.Counter[int] # [unsubscriptable-object] -collections.deque[int] # [unsubscriptable-object] - -# collections.abc -collections.abc.Set[int] # [unsubscriptable-object] -collections.abc.Collection[int] # [unsubscriptable-object] -collections.abc.Container[int] # [unsubscriptable-object] -collections.abc.ItemsView[int, str] # [unsubscriptable-object] -collections.abc.KeysView[int] # [unsubscriptable-object] -collections.abc.Mapping[int, str] # [unsubscriptable-object] -collections.abc.MappingView[int] # [unsubscriptable-object] -collections.abc.MutableMapping[int, str] # [unsubscriptable-object] -collections.abc.MutableSequence[int] # [unsubscriptable-object] -collections.abc.MutableSet[int] # [unsubscriptable-object] -collections.abc.Sequence[int] # [unsubscriptable-object] -collections.abc.ValuesView[int] # [unsubscriptable-object] - -collections.abc.Iterable[int] # [unsubscriptable-object] -collections.abc.Iterator[int] # [unsubscriptable-object] -collections.abc.Generator[int, None, None] # [unsubscriptable-object] -collections.abc.Reversible[int] # [unsubscriptable-object] - -collections.abc.Coroutine[list[str], str, int] # [unsubscriptable-object,unsubscriptable-object] -collections.abc.AsyncGenerator[int, None] # [unsubscriptable-object] -collections.abc.AsyncIterable[int] # [unsubscriptable-object] -collections.abc.AsyncIterator[int] # [unsubscriptable-object] -collections.abc.Awaitable[int] # [unsubscriptable-object] - -# contextlib -contextlib.AbstractContextManager[int] # [unsubscriptable-object] -contextlib.AbstractAsyncContextManager[int] # [unsubscriptable-object] - -# re -re.Pattern[str] # [unsubscriptable-object] -re.Match[str] # [unsubscriptable-object] - - -# unsubscriptable types -collections.abc.Hashable -collections.abc.Sized -collections.abc.Hashable[int] # [unsubscriptable-object] -collections.abc.Sized[int] # [unsubscriptable-object] - -# subscriptable with Python 3.9 -collections.abc.ByteString[int] # [unsubscriptable-object] - - -# Missing implementation for 'collections.abc' derived classes -class DerivedHashable(collections.abc.Hashable): # [abstract-method] # __hash__ - pass - -class DerivedIterable(collections.abc.Iterable[int]): # [unsubscriptable-object] - pass - -class DerivedCollection(collections.abc.Collection[int]): # [unsubscriptable-object] - pass - - -# No implementation required for 'builtins' and 'collections' types -class DerivedList(list[int]): # [unsubscriptable-object] - pass - -class DerivedSet(set[int]): # [unsubscriptable-object] - pass - -class DerivedOrderedDict(collections.OrderedDict[int, str]): # [unsubscriptable-object] - pass - -class DerivedListIterable(list[collections.abc.Iterable[int]]): # [unsubscriptable-object,unsubscriptable-object] - pass - - -# Multiple generic base classes -class DerivedMultiple(collections.abc.Sized, collections.abc.Hashable): # [abstract-method,abstract-method] - pass - -class CustomAbstractCls1(abc.ABC): - pass -class CustomAbstractCls2(collections.abc.Sized, collections.abc.Iterable[CustomAbstractCls1]): # [abstract-method,unsubscriptable-object] # __len__ - pass -class CustomImplementation(CustomAbstractCls2): # [abstract-method] # __len__ - pass - - -# Type annotations -var_tuple: tuple[int, int] # [unsubscriptable-object] -var_dict: dict[int, str] # [unsubscriptable-object] -var_orderedDict: collections.OrderedDict[int, str] # [unsubscriptable-object] -var_container: collections.abc.Container[int] # [unsubscriptable-object] -var_sequence: collections.abc.Sequence[int] # [unsubscriptable-object] -var_iterable: collections.abc.Iterable[int] # [unsubscriptable-object] -var_awaitable: collections.abc.Awaitable[int] # [unsubscriptable-object] -var_contextmanager: contextlib.AbstractContextManager[int] # [unsubscriptable-object] -var_pattern: re.Pattern[int] # [unsubscriptable-object] -var_bytestring: collections.abc.ByteString -var_hashable: collections.abc.Hashable -var_sized: collections.abc.Sized - -# Type annotation with unsubscriptable type -var_int: int[int] # [unsubscriptable-object] -var_hashable2: collections.abc.Hashable[int] # [unsubscriptable-object] -var_sized2: collections.abc.Sized[int] # [unsubscriptable-object] - -# subscriptable with Python 3.9 -var_bytestring2: collections.abc.ByteString[int] # [unsubscriptable-object] diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37.rc b/tests/functional/g/generic_alias/generic_alias_collections_py37.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37.txt b/tests/functional/g/generic_alias/generic_alias_collections_py37.txt deleted file mode 100644 index 72104b4bed0..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37.txt +++ /dev/null @@ -1,67 +0,0 @@ -unsubscriptable-object:15:0:15:5::Value 'tuple' is unsubscriptable:UNDEFINED -unsubscriptable-object:16:0:16:4::Value 'type' is unsubscriptable:UNDEFINED -unsubscriptable-object:17:0:17:24::Value 'collections.abc.Callable' is unsubscriptable:UNDEFINED -unsubscriptable-object:20:0:20:4::Value 'dict' is unsubscriptable:UNDEFINED -unsubscriptable-object:21:0:21:4::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:22:0:22:3::Value 'set' is unsubscriptable:UNDEFINED -unsubscriptable-object:23:0:23:9::Value 'frozenset' is unsubscriptable:UNDEFINED -unsubscriptable-object:26:0:26:23::Value 'collections.defaultdict' is unsubscriptable:UNDEFINED -unsubscriptable-object:27:0:27:23::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED -unsubscriptable-object:28:0:28:20::Value 'collections.ChainMap' is unsubscriptable:UNDEFINED -unsubscriptable-object:29:0:29:19::Value 'collections.Counter' is unsubscriptable:UNDEFINED -unsubscriptable-object:30:0:30:17::Value 'collections.deque' is unsubscriptable:UNDEFINED -unsubscriptable-object:33:0:33:19::Value 'collections.abc.Set' is unsubscriptable:UNDEFINED -unsubscriptable-object:34:0:34:26::Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED -unsubscriptable-object:35:0:35:25::Value 'collections.abc.Container' is unsubscriptable:UNDEFINED -unsubscriptable-object:36:0:36:25::Value 'collections.abc.ItemsView' is unsubscriptable:UNDEFINED -unsubscriptable-object:37:0:37:24::Value 'collections.abc.KeysView' is unsubscriptable:UNDEFINED -unsubscriptable-object:38:0:38:23::Value 'collections.abc.Mapping' is unsubscriptable:UNDEFINED -unsubscriptable-object:39:0:39:27::Value 'collections.abc.MappingView' is unsubscriptable:UNDEFINED -unsubscriptable-object:40:0:40:30::Value 'collections.abc.MutableMapping' is unsubscriptable:UNDEFINED -unsubscriptable-object:41:0:41:31::Value 'collections.abc.MutableSequence' is unsubscriptable:UNDEFINED -unsubscriptable-object:42:0:42:26::Value 'collections.abc.MutableSet' is unsubscriptable:UNDEFINED -unsubscriptable-object:43:0:43:24::Value 'collections.abc.Sequence' is unsubscriptable:UNDEFINED -unsubscriptable-object:44:0:44:26::Value 'collections.abc.ValuesView' is unsubscriptable:UNDEFINED -unsubscriptable-object:46:0:46:24::Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -unsubscriptable-object:47:0:47:24::Value 'collections.abc.Iterator' is unsubscriptable:UNDEFINED -unsubscriptable-object:48:0:48:25::Value 'collections.abc.Generator' is unsubscriptable:UNDEFINED -unsubscriptable-object:49:0:49:26::Value 'collections.abc.Reversible' is unsubscriptable:UNDEFINED -unsubscriptable-object:51:0:51:25::Value 'collections.abc.Coroutine' is unsubscriptable:UNDEFINED -unsubscriptable-object:51:26:51:30::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:52:0:52:30::Value 'collections.abc.AsyncGenerator' is unsubscriptable:UNDEFINED -unsubscriptable-object:53:0:53:29::Value 'collections.abc.AsyncIterable' is unsubscriptable:UNDEFINED -unsubscriptable-object:54:0:54:29::Value 'collections.abc.AsyncIterator' is unsubscriptable:UNDEFINED -unsubscriptable-object:55:0:55:25::Value 'collections.abc.Awaitable' is unsubscriptable:UNDEFINED -unsubscriptable-object:58:0:58:33::Value 'contextlib.AbstractContextManager' is unsubscriptable:UNDEFINED -unsubscriptable-object:59:0:59:38::Value 'contextlib.AbstractAsyncContextManager' is unsubscriptable:UNDEFINED -unsubscriptable-object:62:0:62:10::Value 're.Pattern' is unsubscriptable:UNDEFINED -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 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 -unsubscriptable-object:91:17:91:20:DerivedSet:Value 'set' is unsubscriptable:UNDEFINED -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 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 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 -unsubscriptable-object:117:15:117:40::Value 'collections.abc.Container' is unsubscriptable:UNDEFINED -unsubscriptable-object:118:14:118:38::Value 'collections.abc.Sequence' is unsubscriptable:UNDEFINED -unsubscriptable-object:119:14:119:38::Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -unsubscriptable-object:120:15:120:40::Value 'collections.abc.Awaitable' is unsubscriptable:UNDEFINED -unsubscriptable-object:121:20:121:53::Value 'contextlib.AbstractContextManager' is unsubscriptable:UNDEFINED -unsubscriptable-object:122:13:122:23::Value 're.Pattern' is unsubscriptable:UNDEFINED -unsubscriptable-object:128:9:128:12::Value 'int' is unsubscriptable:UNDEFINED -unsubscriptable-object:129:15:129:39::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED -unsubscriptable-object:130:12:130:33::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED -unsubscriptable-object:133:17:133:43::Value 'collections.abc.ByteString' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.py b/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.py deleted file mode 100644 index 5319f13b629..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Test generic alias support for stdlib types (added in PY39). - -Raise [unsubscriptable-object] error for PY37 and PY38. -Make sure `import typing` doesn't change anything. -""" -# flake8: noqa -# pylint: disable=missing-docstring,pointless-statement,unused-import -# pylint: disable=too-few-public-methods,multiple-statements,line-too-long -import abc -import collections -import collections.abc -import contextlib -import re -import typing - -# special -tuple[int, int] # [unsubscriptable-object] -type[int] # [unsubscriptable-object] -collections.abc.Callable[[int], str] # [unsubscriptable-object] - -# builtins -dict[int, str] # [unsubscriptable-object] -list[int] # [unsubscriptable-object] -set[int] # [unsubscriptable-object] -frozenset[int] # [unsubscriptable-object] - -# collections -collections.defaultdict[int, str] # [unsubscriptable-object] -collections.OrderedDict[int, str] # [unsubscriptable-object] -collections.ChainMap[int, str] # [unsubscriptable-object] -collections.Counter[int] # [unsubscriptable-object] -collections.deque[int] # [unsubscriptable-object] - -# collections.abc -collections.abc.Set[int] # [unsubscriptable-object] -collections.abc.Collection[int] # [unsubscriptable-object] -collections.abc.Container[int] # [unsubscriptable-object] -collections.abc.ItemsView[int, str] # [unsubscriptable-object] -collections.abc.KeysView[int] # [unsubscriptable-object] -collections.abc.Mapping[int, str] # [unsubscriptable-object] -collections.abc.MappingView[int] # [unsubscriptable-object] -collections.abc.MutableMapping[int, str] # [unsubscriptable-object] -collections.abc.MutableSequence[int] # [unsubscriptable-object] -collections.abc.MutableSet[int] # [unsubscriptable-object] -collections.abc.Sequence[int] # [unsubscriptable-object] -collections.abc.ValuesView[int] # [unsubscriptable-object] - -collections.abc.Iterable[int] # [unsubscriptable-object] -collections.abc.Iterator[int] # [unsubscriptable-object] -collections.abc.Generator[int, None, None] # [unsubscriptable-object] -collections.abc.Reversible[int] # [unsubscriptable-object] - -collections.abc.Coroutine[list[str], str, int] # [unsubscriptable-object,unsubscriptable-object] -collections.abc.AsyncGenerator[int, None] # [unsubscriptable-object] -collections.abc.AsyncIterable[int] # [unsubscriptable-object] -collections.abc.AsyncIterator[int] # [unsubscriptable-object] -collections.abc.Awaitable[int] # [unsubscriptable-object] - -# contextlib -contextlib.AbstractContextManager[int] # [unsubscriptable-object] -contextlib.AbstractAsyncContextManager[int] # [unsubscriptable-object] - -# re -re.Pattern[str] # [unsubscriptable-object] -re.Match[str] # [unsubscriptable-object] - - -# unsubscriptable types -collections.abc.Hashable -collections.abc.Sized -collections.abc.Hashable[int] # [unsubscriptable-object] -collections.abc.Sized[int] # [unsubscriptable-object] - -# subscriptable with Python 3.9 -collections.abc.ByteString[int] # [unsubscriptable-object] - - -# Missing implementation for 'collections.abc' derived classes -class DerivedHashable(collections.abc.Hashable): # [abstract-method] # __hash__ - pass - -class DerivedIterable(collections.abc.Iterable[int]): # [unsubscriptable-object] - pass - -class DerivedCollection(collections.abc.Collection[int]): # [unsubscriptable-object] - pass - - -# No implementation required for 'builtins' and 'collections' types -class DerivedList(list[int]): # [unsubscriptable-object] - pass - -class DerivedSet(set[int]): # [unsubscriptable-object] - pass - -class DerivedOrderedDict(collections.OrderedDict[int, str]): # [unsubscriptable-object] - pass - -class DerivedListIterable(list[collections.abc.Iterable[int]]): # [unsubscriptable-object,unsubscriptable-object] - pass - - -# Multiple generic base classes -class DerivedMultiple(collections.abc.Sized, collections.abc.Hashable): # [abstract-method,abstract-method] - pass - -class CustomAbstractCls1(abc.ABC): - pass -class CustomAbstractCls2(collections.abc.Sized, collections.abc.Iterable[CustomAbstractCls1]): # [abstract-method,unsubscriptable-object] # __len__ - pass -class CustomImplementation(CustomAbstractCls2): # [abstract-method] # __len__ - pass - - -# Type annotations -var_tuple: tuple[int, int] # [unsubscriptable-object] -var_dict: dict[int, str] # [unsubscriptable-object] -var_orderedDict: collections.OrderedDict[int, str] # [unsubscriptable-object] -var_container: collections.abc.Container[int] # [unsubscriptable-object] -var_sequence: collections.abc.Sequence[int] # [unsubscriptable-object] -var_iterable: collections.abc.Iterable[int] # [unsubscriptable-object] -var_awaitable: collections.abc.Awaitable[int] # [unsubscriptable-object] -var_contextmanager: contextlib.AbstractContextManager[int] # [unsubscriptable-object] -var_pattern: re.Pattern[int] # [unsubscriptable-object] -var_bytestring: collections.abc.ByteString -var_hashable: collections.abc.Hashable -var_sized: collections.abc.Sized - -# Type annotation with unsubscriptable type -var_int: int[int] # [unsubscriptable-object] -var_hashable2: collections.abc.Hashable[int] # [unsubscriptable-object] -var_sized2: collections.abc.Sized[int] # [unsubscriptable-object] - -# subscriptable with Python 3.9 -var_bytestring2: collections.abc.ByteString[int] # [unsubscriptable-object] diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.rc b/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 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 deleted file mode 100644 index 0dd989f2e89..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt +++ /dev/null @@ -1,67 +0,0 @@ -unsubscriptable-object:17:0:17:5::Value 'tuple' is unsubscriptable:UNDEFINED -unsubscriptable-object:18:0:18:4::Value 'type' is unsubscriptable:UNDEFINED -unsubscriptable-object:19:0:19:24::Value 'collections.abc.Callable' is unsubscriptable:UNDEFINED -unsubscriptable-object:22:0:22:4::Value 'dict' is unsubscriptable:UNDEFINED -unsubscriptable-object:23:0:23:4::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:24:0:24:3::Value 'set' is unsubscriptable:UNDEFINED -unsubscriptable-object:25:0:25:9::Value 'frozenset' is unsubscriptable:UNDEFINED -unsubscriptable-object:28:0:28:23::Value 'collections.defaultdict' is unsubscriptable:UNDEFINED -unsubscriptable-object:29:0:29:23::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED -unsubscriptable-object:30:0:30:20::Value 'collections.ChainMap' is unsubscriptable:UNDEFINED -unsubscriptable-object:31:0:31:19::Value 'collections.Counter' is unsubscriptable:UNDEFINED -unsubscriptable-object:32:0:32:17::Value 'collections.deque' is unsubscriptable:UNDEFINED -unsubscriptable-object:35:0:35:19::Value 'collections.abc.Set' is unsubscriptable:UNDEFINED -unsubscriptable-object:36:0:36:26::Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED -unsubscriptable-object:37:0:37:25::Value 'collections.abc.Container' is unsubscriptable:UNDEFINED -unsubscriptable-object:38:0:38:25::Value 'collections.abc.ItemsView' is unsubscriptable:UNDEFINED -unsubscriptable-object:39:0:39:24::Value 'collections.abc.KeysView' is unsubscriptable:UNDEFINED -unsubscriptable-object:40:0:40:23::Value 'collections.abc.Mapping' is unsubscriptable:UNDEFINED -unsubscriptable-object:41:0:41:27::Value 'collections.abc.MappingView' is unsubscriptable:UNDEFINED -unsubscriptable-object:42:0:42:30::Value 'collections.abc.MutableMapping' is unsubscriptable:UNDEFINED -unsubscriptable-object:43:0:43:31::Value 'collections.abc.MutableSequence' is unsubscriptable:UNDEFINED -unsubscriptable-object:44:0:44:26::Value 'collections.abc.MutableSet' is unsubscriptable:UNDEFINED -unsubscriptable-object:45:0:45:24::Value 'collections.abc.Sequence' is unsubscriptable:UNDEFINED -unsubscriptable-object:46:0:46:26::Value 'collections.abc.ValuesView' is unsubscriptable:UNDEFINED -unsubscriptable-object:48:0:48:24::Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -unsubscriptable-object:49:0:49:24::Value 'collections.abc.Iterator' is unsubscriptable:UNDEFINED -unsubscriptable-object:50:0:50:25::Value 'collections.abc.Generator' is unsubscriptable:UNDEFINED -unsubscriptable-object:51:0:51:26::Value 'collections.abc.Reversible' is unsubscriptable:UNDEFINED -unsubscriptable-object:53:0:53:25::Value 'collections.abc.Coroutine' is unsubscriptable:UNDEFINED -unsubscriptable-object:53:26:53:30::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:54:0:54:30::Value 'collections.abc.AsyncGenerator' is unsubscriptable:UNDEFINED -unsubscriptable-object:55:0:55:29::Value 'collections.abc.AsyncIterable' is unsubscriptable:UNDEFINED -unsubscriptable-object:56:0:56:29::Value 'collections.abc.AsyncIterator' is unsubscriptable:UNDEFINED -unsubscriptable-object:57:0:57:25::Value 'collections.abc.Awaitable' is unsubscriptable:UNDEFINED -unsubscriptable-object:60:0:60:33::Value 'contextlib.AbstractContextManager' is unsubscriptable:UNDEFINED -unsubscriptable-object:61:0:61:38::Value 'contextlib.AbstractAsyncContextManager' is unsubscriptable:UNDEFINED -unsubscriptable-object:64:0:64:10::Value 're.Pattern' is unsubscriptable:UNDEFINED -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 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 -unsubscriptable-object:93:17:93:20:DerivedSet:Value 'set' is unsubscriptable:UNDEFINED -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 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 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 -unsubscriptable-object:119:15:119:40::Value 'collections.abc.Container' is unsubscriptable:UNDEFINED -unsubscriptable-object:120:14:120:38::Value 'collections.abc.Sequence' is unsubscriptable:UNDEFINED -unsubscriptable-object:121:14:121:38::Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -unsubscriptable-object:122:15:122:40::Value 'collections.abc.Awaitable' is unsubscriptable:UNDEFINED -unsubscriptable-object:123:20:123:53::Value 'contextlib.AbstractContextManager' is unsubscriptable:UNDEFINED -unsubscriptable-object:124:13:124:23::Value 're.Pattern' is unsubscriptable:UNDEFINED -unsubscriptable-object:130:9:130:12::Value 'int' is unsubscriptable:UNDEFINED -unsubscriptable-object:131:15:131:39::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED -unsubscriptable-object:132:12:132:33::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED -unsubscriptable-object:135:17:135:43::Value 'collections.abc.ByteString' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_mixed_py37.py b/tests/functional/g/generic_alias/generic_alias_mixed_py37.py deleted file mode 100644 index cb7a4d0f43a..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_mixed_py37.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Test generic alias support with mix of typing.py and stdlib types. - -Possible with postponed evaluation enabled, starting with PY37. -""" -# flake8: noqa -# pylint: disable=missing-docstring,pointless-statement -# pylint: disable=too-few-public-methods,multiple-statements,line-too-long -from __future__ import annotations - -import collections -import collections.abc -import contextlib -import re -import typing - -# Type annotations -var_orderedDict: collections.OrderedDict[int, str] -var_container: collections.abc.Container[int] -var_sequence: collections.abc.Sequence[int] -var_iterable: collections.abc.Iterable[int] -var_awaitable: collections.abc.Awaitable[int] -var_pattern: re.Pattern[int] -var_bytestring: collections.abc.ByteString -var_hashable: collections.abc.Hashable -var_ContextManager: contextlib.AbstractContextManager[int] - - -# No implementation required for 'builtins' -class DerivedListIterable(typing.List[typing.Iterable[int]]): - pass - - -# Missing implementation for 'collections.abc' derived classes -class DerivedHashable(typing.Hashable): # [abstract-method] # __hash__ - pass - -class DerivedIterable(typing.Iterable[int]): # [abstract-method] # __iter__ - pass - -class DerivedCollection(typing.Collection[int]): # [abstract-method,abstract-method,abstract-method] # __contains__, __iter__, __len__ - pass diff --git a/tests/functional/g/generic_alias/generic_alias_mixed_py37.rc b/tests/functional/g/generic_alias/generic_alias_mixed_py37.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_mixed_py37.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt b/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt deleted file mode 100644 index 188039c60d7..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt +++ /dev/null @@ -1,5 +0,0 @@ -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.rc b/tests/functional/g/generic_alias/generic_alias_mixed_py39.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_mixed_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.py b/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.py deleted file mode 100644 index 02cbcf081cd..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Test generic alias support for stdlib types (added in PY39). - -In type annotation context, they can be used with postponed evaluation enabled, -starting with PY37. -""" -# flake8: noqa -# pylint: disable=missing-docstring,pointless-statement,invalid-name -# pylint: disable=too-few-public-methods,multiple-statements,line-too-long -from __future__ import annotations - -import abc -import collections -import collections.abc -import contextlib -import re - - -# ----- unsubscriptable (even with postponed evaluation) ----- -# special -tuple[int, int] # [unsubscriptable-object] -type[int] # [unsubscriptable-object] -collections.abc.Callable[[int], str] # [unsubscriptable-object] - -# builtins -dict[int, str] # [unsubscriptable-object] -list[int] # [unsubscriptable-object] -set[int] # [unsubscriptable-object] -frozenset[int] # [unsubscriptable-object] - -# collections -collections.defaultdict[int, str] # [unsubscriptable-object] -collections.OrderedDict[int, str] # [unsubscriptable-object] -collections.ChainMap[int, str] # [unsubscriptable-object] -collections.Counter[int] # [unsubscriptable-object] -collections.deque[int] # [unsubscriptable-object] - -# collections.abc -collections.abc.Set[int] # [unsubscriptable-object] -collections.abc.Collection[int] # [unsubscriptable-object] -collections.abc.Container[int] # [unsubscriptable-object] -collections.abc.ItemsView[int, str] # [unsubscriptable-object] -collections.abc.KeysView[int] # [unsubscriptable-object] -collections.abc.Mapping[int, str] # [unsubscriptable-object] -collections.abc.MappingView[int] # [unsubscriptable-object] -collections.abc.MutableMapping[int, str] # [unsubscriptable-object] -collections.abc.MutableSequence[int] # [unsubscriptable-object] -collections.abc.MutableSet[int] # [unsubscriptable-object] -collections.abc.Sequence[int] # [unsubscriptable-object] -collections.abc.ValuesView[int] # [unsubscriptable-object] - -collections.abc.Iterable[int] # [unsubscriptable-object] -collections.abc.Iterator[int] # [unsubscriptable-object] -collections.abc.Generator[int, None, None] # [unsubscriptable-object] -collections.abc.Reversible[int] # [unsubscriptable-object] - -collections.abc.Coroutine[list[str], str, int] # [unsubscriptable-object,unsubscriptable-object] -collections.abc.AsyncGenerator[int, None] # [unsubscriptable-object] -collections.abc.AsyncIterable[int] # [unsubscriptable-object] -collections.abc.AsyncIterator[int] # [unsubscriptable-object] -collections.abc.Awaitable[int] # [unsubscriptable-object] - -# contextlib -contextlib.AbstractContextManager[int] # [unsubscriptable-object] -contextlib.AbstractAsyncContextManager[int] # [unsubscriptable-object] - -# re -re.Pattern[str] # [unsubscriptable-object] -re.Match[str] # [unsubscriptable-object] - - -# unsubscriptable types -collections.abc.Hashable -collections.abc.Sized -collections.abc.Hashable[int] # [unsubscriptable-object] -collections.abc.Sized[int] # [unsubscriptable-object] - -# subscriptable with Python 3.9 -collections.abc.ByteString[int] # [unsubscriptable-object] - - -# Missing implementation for 'collections.abc' derived classes -class DerivedHashable(collections.abc.Hashable): # [abstract-method] # __hash__ - pass - -class DerivedIterable(collections.abc.Iterable[int]): # [unsubscriptable-object] - pass - -class DerivedCollection(collections.abc.Collection[int]): # [unsubscriptable-object] - pass - - -# No implementation required for 'builtins' and 'collections' types -class DerivedList(list[int]): # [unsubscriptable-object] - pass - -class DerivedSet(set[int]): # [unsubscriptable-object] - pass - -class DerivedOrderedDict(collections.OrderedDict[int, str]): # [unsubscriptable-object] - pass - -class DerivedListIterable(list[collections.abc.Iterable[int]]): # [unsubscriptable-object,unsubscriptable-object] - pass - - -# Multiple generic base classes -class DerivedMultiple(collections.abc.Sized, collections.abc.Hashable): # [abstract-method,abstract-method] - pass - -class CustomAbstractCls1(abc.ABC): - pass -class CustomAbstractCls2(collections.abc.Sized, collections.abc.Iterable[CustomAbstractCls1]): # [abstract-method,unsubscriptable-object] # __len__ - pass -class CustomImplementation(CustomAbstractCls2): # [abstract-method] # __len__ - pass - - - -# ----- subscriptable (with postponed evaluation) ----- -# special -var_tuple: tuple[int, int] -var_type: type[int] -var_callable: collections.abc.Callable[[int], str] - -# builtins -var_dict: dict[int, str] -var_list: list[int] -var_set: set[int] -var_frozenset: frozenset[int] - -# collections -var_defaultdict: collections.defaultdict[int, str] -var_OrderedDict: collections.OrderedDict[int, str] -var_ChainMap: collections.ChainMap[int, str] -var_Counter: collections.Counter[int] -var_deque: collections.deque[int] - -# collections.abc -var_abc_set: collections.abc.Set[int] -var_abc_collection: collections.abc.Collection[int] -var_abc_container: collections.abc.Container[int] -var_abc_ItemsView: collections.abc.ItemsView[int, str] -var_abc_KeysView: collections.abc.KeysView[int] -var_abc_Mapping: collections.abc.Mapping[int, str] -var_abc_MappingView: collections.abc.MappingView[int] -var_abc_MutableMapping: collections.abc.MutableMapping[int, str] -var_abc_MutableSequence: collections.abc.MutableSequence[int] -var_abc_MutableSet: collections.abc.MutableSet[int] -var_abc_Sequence: collections.abc.Sequence[int] -var_abc_ValuesView: collections.abc.ValuesView[int] - -var_abc_Iterable: collections.abc.Iterable[int] -var_abc_Iterator: collections.abc.Iterator[int] -var_abc_Generator: collections.abc.Generator[int, None, None] -var_abc_Reversible: collections.abc.Reversible[int] - -var_abc_Coroutine: collections.abc.Coroutine[list[str], str, int] -var_abc_AsyncGenerator: collections.abc.AsyncGenerator[int, None] -var_abc_AsyncIterable: collections.abc.AsyncIterable[int] -var_abc_AsyncIterator: collections.abc.AsyncIterator[int] -var_abc_Awaitable: collections.abc.Awaitable[int] - -# contextlib -var_ContextManager: contextlib.AbstractContextManager[int] -var_AsyncContextManager: contextlib.AbstractAsyncContextManager[int] - -# re -var_re_Pattern: re.Pattern[str] -var_re_Match: re.Match[str] - - -# unsubscriptable types -var_abc_Hashable: collections.abc.Hashable -var_abc_Sized: collections.abc.Sized -var_abc_Hashable2: collections.abc.Hashable[int] # string annotations aren't checked -var_abc_Sized2: collections.abc.Sized[int] # string annotations aren't checked - -# subscriptable with Python 3.9 -var_abc_ByteString: collections.abc.ByteString[int] - - -# Generic in type stubs only -> string annotations aren't checked -class A: - ... - -var_a1: A[str] # string annotations aren't checked -var_a2: "A[str]" # string annotations aren't checked -class B(A[str]): # [unsubscriptable-object] - ... diff --git a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.rc b/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 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 deleted file mode 100644 index cbf46bfef46..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt +++ /dev/null @@ -1,55 +0,0 @@ -unsubscriptable-object:20:0:20:5::Value 'tuple' is unsubscriptable:UNDEFINED -unsubscriptable-object:21:0:21:4::Value 'type' is unsubscriptable:UNDEFINED -unsubscriptable-object:22:0:22:24::Value 'collections.abc.Callable' is unsubscriptable:UNDEFINED -unsubscriptable-object:25:0:25:4::Value 'dict' is unsubscriptable:UNDEFINED -unsubscriptable-object:26:0:26:4::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:27:0:27:3::Value 'set' is unsubscriptable:UNDEFINED -unsubscriptable-object:28:0:28:9::Value 'frozenset' is unsubscriptable:UNDEFINED -unsubscriptable-object:31:0:31:23::Value 'collections.defaultdict' is unsubscriptable:UNDEFINED -unsubscriptable-object:32:0:32:23::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED -unsubscriptable-object:33:0:33:20::Value 'collections.ChainMap' is unsubscriptable:UNDEFINED -unsubscriptable-object:34:0:34:19::Value 'collections.Counter' is unsubscriptable:UNDEFINED -unsubscriptable-object:35:0:35:17::Value 'collections.deque' is unsubscriptable:UNDEFINED -unsubscriptable-object:38:0:38:19::Value 'collections.abc.Set' is unsubscriptable:UNDEFINED -unsubscriptable-object:39:0:39:26::Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED -unsubscriptable-object:40:0:40:25::Value 'collections.abc.Container' is unsubscriptable:UNDEFINED -unsubscriptable-object:41:0:41:25::Value 'collections.abc.ItemsView' is unsubscriptable:UNDEFINED -unsubscriptable-object:42:0:42:24::Value 'collections.abc.KeysView' is unsubscriptable:UNDEFINED -unsubscriptable-object:43:0:43:23::Value 'collections.abc.Mapping' is unsubscriptable:UNDEFINED -unsubscriptable-object:44:0:44:27::Value 'collections.abc.MappingView' is unsubscriptable:UNDEFINED -unsubscriptable-object:45:0:45:30::Value 'collections.abc.MutableMapping' is unsubscriptable:UNDEFINED -unsubscriptable-object:46:0:46:31::Value 'collections.abc.MutableSequence' is unsubscriptable:UNDEFINED -unsubscriptable-object:47:0:47:26::Value 'collections.abc.MutableSet' is unsubscriptable:UNDEFINED -unsubscriptable-object:48:0:48:24::Value 'collections.abc.Sequence' is unsubscriptable:UNDEFINED -unsubscriptable-object:49:0:49:26::Value 'collections.abc.ValuesView' is unsubscriptable:UNDEFINED -unsubscriptable-object:51:0:51:24::Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -unsubscriptable-object:52:0:52:24::Value 'collections.abc.Iterator' is unsubscriptable:UNDEFINED -unsubscriptable-object:53:0:53:25::Value 'collections.abc.Generator' is unsubscriptable:UNDEFINED -unsubscriptable-object:54:0:54:26::Value 'collections.abc.Reversible' is unsubscriptable:UNDEFINED -unsubscriptable-object:56:0:56:25::Value 'collections.abc.Coroutine' is unsubscriptable:UNDEFINED -unsubscriptable-object:56:26:56:30::Value 'list' is unsubscriptable:UNDEFINED -unsubscriptable-object:57:0:57:30::Value 'collections.abc.AsyncGenerator' is unsubscriptable:UNDEFINED -unsubscriptable-object:58:0:58:29::Value 'collections.abc.AsyncIterable' is unsubscriptable:UNDEFINED -unsubscriptable-object:59:0:59:29::Value 'collections.abc.AsyncIterator' is unsubscriptable:UNDEFINED -unsubscriptable-object:60:0:60:25::Value 'collections.abc.Awaitable' is unsubscriptable:UNDEFINED -unsubscriptable-object:63:0:63:33::Value 'contextlib.AbstractContextManager' is unsubscriptable:UNDEFINED -unsubscriptable-object:64:0:64:38::Value 'contextlib.AbstractAsyncContextManager' is unsubscriptable:UNDEFINED -unsubscriptable-object:67:0:67:10::Value 're.Pattern' is unsubscriptable:UNDEFINED -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 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 -unsubscriptable-object:96:17:96:20:DerivedSet:Value 'set' is unsubscriptable:UNDEFINED -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 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 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_py39.rc b/tests/functional/g/generic_alias/generic_alias_related_py39.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/g/generic_alias/generic_alias_related_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/i/import_error.rc b/tests/functional/i/import_error.rc index 7278ff8f175..efd9369c9ee 100644 --- a/tests/functional/i/import_error.rc +++ b/tests/functional/i/import_error.rc @@ -4,7 +4,3 @@ enable=multiple-imports [TYPECHECK] ignored-modules=external_module,fake_module.submodule,foo,bar,*_ignored - -[testoptions] -# TODO: PY3.9: This does pass on PyPy 3.9 -except_implementations=PyPy diff --git a/tests/functional/i/inconsistent/inconsistent_quotes_fstring.py b/tests/functional/i/inconsistent/inconsistent_quotes_fstring.py index dd6f9fbe12d..e6e285341b2 100644 --- a/tests/functional/i/inconsistent/inconsistent_quotes_fstring.py +++ b/tests/functional/i/inconsistent/inconsistent_quotes_fstring.py @@ -1,4 +1,4 @@ # pylint: disable=missing-module-docstring dictionary = {'0': 0} -f_string = f'{dictionary["0"]}' +F_STRING = f'{dictionary["0"]}' diff --git a/tests/functional/i/inconsistent/inconsistent_quotes_fstring_py312.py b/tests/functional/i/inconsistent/inconsistent_quotes_fstring_py312.py index 5e952af1968..323bc6a07b7 100644 --- a/tests/functional/i/inconsistent/inconsistent_quotes_fstring_py312.py +++ b/tests/functional/i/inconsistent/inconsistent_quotes_fstring_py312.py @@ -1,5 +1,5 @@ # pylint: disable=missing-module-docstring dictionary = {'0': 0} -# quotes are inconsistent when targetting Python 3.12 (use single quotes) -f_string = f'{dictionary["0"]}' # [inconsistent-quotes] +# quotes are inconsistent when targeting Python 3.12 (use single quotes) +F_STRING = f'{dictionary["0"]}' # [inconsistent-quotes] diff --git a/tests/functional/i/inconsistent/inconsistent_quotes_fstring_py312_311.py b/tests/functional/i/inconsistent/inconsistent_quotes_fstring_py312_311.py index 5b53a19bb5c..c7099e4710a 100644 --- a/tests/functional/i/inconsistent/inconsistent_quotes_fstring_py312_311.py +++ b/tests/functional/i/inconsistent/inconsistent_quotes_fstring_py312_311.py @@ -1,5 +1,5 @@ # pylint: disable=missing-module-docstring dictionary = {'0': 0} -# quotes are consistent when targetting 3.11 and earlier (cannot use single quotes here) -f_string = f'{dictionary["0"]}' +# quotes are consistent when targeting 3.11 and earlier (cannot use single quotes here) +F_STRING = f'{dictionary["0"]}' diff --git a/tests/functional/i/inconsistent/inconsistent_returns_noreturn.py b/tests/functional/i/inconsistent/inconsistent_returns_noreturn.py index 13878aad696..ea5154752ae 100644 --- a/tests/functional/i/inconsistent/inconsistent_returns_noreturn.py +++ b/tests/functional/i/inconsistent/inconsistent_returns_noreturn.py @@ -4,6 +4,14 @@ import sys import typing +from pylint.constants import PY311_PLUS + +if PY311_PLUS: + from typing import assert_never # pylint: disable=no-name-in-module +else: + from typing_extensions import assert_never + + def parser_error(msg) -> typing.NoReturn: # pylint: disable=unused-argument sys.exit(1) @@ -11,7 +19,7 @@ def parser_error_nortype(msg): # pylint: disable=unused-argument sys.exit(2) -from typing import NoReturn # pylint: disable=wrong-import-position +from typing import NoReturn # pylint: disable=wrong-import-position,wrong-import-order def parser_error_name(msg) -> NoReturn: # pylint: disable=unused-argument sys.exit(3) @@ -95,3 +103,34 @@ def bug_pylint_8747_incorrect_annotation(self, s: str) -> int: return n except ValueError: self._falsely_no_return_method() + +# https://github.com/pylint-dev/pylint/issues/7565 +def never_is_handled_like_noreturn(arg: typing.Union[int, str]) -> int: + if isinstance(arg, int): + return 1 + if isinstance(arg, str): + return 2 + assert_never(arg) + + +def declared_to_not_return() -> None: + return + + +def config_takes_precedence_over_inference(arg: typing.Union[int, str]) -> int: + if isinstance(arg, int): + return 1 + if isinstance(arg, str): + return 2 + declared_to_not_return() + + +def unrelated() -> None: + return + + +# +1: [inconsistent-return-statements] +def ensure_message(arg: typing.Union[int, str]) -> int: + if isinstance(arg, int): + return 1 + unrelated() diff --git a/tests/functional/i/inconsistent/inconsistent_returns_noreturn.rc b/tests/functional/i/inconsistent/inconsistent_returns_noreturn.rc index dd6cbb59899..fdeed8ed4d0 100644 --- a/tests/functional/i/inconsistent/inconsistent_returns_noreturn.rc +++ b/tests/functional/i/inconsistent/inconsistent_returns_noreturn.rc @@ -1,2 +1,2 @@ [REFACTORING] -never-returning-functions=sys.exit,sys.getdefaultencoding +never-returning-functions=sys.exit,sys.getdefaultencoding,inconsistent_returns_noreturn.declared_to_not_return diff --git a/tests/functional/i/inconsistent/inconsistent_returns_noreturn.txt b/tests/functional/i/inconsistent/inconsistent_returns_noreturn.txt index e2ad258f72d..da3cfcc7f5f 100644 --- a/tests/functional/i/inconsistent/inconsistent_returns_noreturn.txt +++ b/tests/functional/i/inconsistent/inconsistent_returns_noreturn.txt @@ -1,2 +1,3 @@ -inconsistent-return-statements:32:0:32:25:bug_pylint_4122_wrong:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED -inconsistent-return-statements:77:4:77:29:ClassUnderTest.bug_pylint_8747_wrong:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED +inconsistent-return-statements:40:0:40:25:bug_pylint_4122_wrong:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED +inconsistent-return-statements:85:4:85:29:ClassUnderTest.bug_pylint_8747_wrong:Either all return statements in a function should return an expression, or none of them should.:UNDEFINED +inconsistent-return-statements:133:0:133:18:ensure_message:Either all return statements in a function should return an expression, or none of them should.: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 aa37a45306e..3189fb8f440 100644 --- a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py +++ b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py @@ -1,17 +1,13 @@ # pylint:disable=too-few-public-methods,import-error,missing-docstring, not-callable, import-outside-toplevel -"""test pb with exceptions and old/new style classes""" +"""test pb with exceptions and classes""" class ValidException(Exception): """Valid Exception.""" -class OldStyleClass: - """Not an exception.""" - class NewStyleClass: """Not an exception.""" - def good_case(): """raise""" raise ValidException('hop') @@ -31,22 +27,10 @@ def good_case3(): import io raise io.BlockingIOError -def bad_case0(): - """raise""" - # +2:<3.0:[nonstandard-exception] - # +1:>=3.0:[raising-non-exception] - raise OldStyleClass('hop') - def bad_case1(): """raise""" raise NewStyleClass() # [raising-non-exception] -def bad_case2(): - """raise""" - # +2:<3.0:[nonstandard-exception] - # +1:>=3.0:[raising-non-exception] - raise OldStyleClass('hop') - def bad_case3(): """raise""" raise NewStyleClass # [raising-non-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 f2ccd8a052d..863e5baf372 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,9 @@ -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 +raising-non-exception:32:4:32:25:bad_case1:Raising a class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:36:4:36:23:bad_case3:Raising a class which doesn't inherit from BaseException:INFERENCE +notimplemented-raised:40:4:40:31:bad_case4:NotImplemented raised - should raise NotImplementedError:HIGH +raising-bad-type:44:4:44:11:bad_case5:Raising int while only classes or instances are allowed:INFERENCE +raising-bad-type:48:4:48:14:bad_case6:Raising NoneType while only classes or instances are allowed:INFERENCE +raising-non-exception:52:4:52:14:bad_case7:Raising a class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:56:4:56:15:bad_case8:Raising a class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:60:4:60:14:bad_case9:Raising a class which doesn't inherit from BaseException:INFERENCE +raising-bad-type:94:4:94:18:bad_case10:Raising str while only classes or instances are allowed:INFERENCE diff --git a/tests/functional/i/invalid/invalid_field_call.py b/tests/functional/i/invalid/invalid_field_call.py index 43ff41c4295..62d62733ae7 100644 --- a/tests/functional/i/invalid/invalid_field_call.py +++ b/tests/functional/i/invalid/invalid_field_call.py @@ -34,7 +34,6 @@ class DC: mc.field() a: float = field(init=False) b: float = dc.field(init=False) - # TODO(remove py3.9 min) pylint: disable-next=unsubscriptable-object c: list[float] = [field(), field()] # [invalid-field-call, invalid-field-call] @dc.dataclass @@ -42,7 +41,6 @@ class IsAlsoDC: field() # [invalid-field-call] a: float = field(init=False) b: float = dc.field(init=False) - # TODO(remove py3.9 min) pylint: disable-next=unsubscriptable-object c: list[float] = [field(), field()] # [invalid-field-call, invalid-field-call] @dc.dataclass(frozen=True) diff --git a/tests/functional/i/invalid/invalid_field_call.txt b/tests/functional/i/invalid/invalid_field_call.txt index c672a2bfea1..5e4f0fe2448 100644 --- a/tests/functional/i/invalid/invalid_field_call.txt +++ b/tests/functional/i/invalid/invalid_field_call.txt @@ -6,9 +6,9 @@ invalid-field-call:27:4:27:14:NotADataClass:Invalid usage of field(), it should invalid-field-call:28:15:28:35:NotADataClass:Invalid usage of field(), it should be used within a dataclass or the make_dataclass() function.:INFERENCE invalid-field-call:32:4:32:11:DC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE invalid-field-call:33:4:33:14:DC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE -invalid-field-call:38:22:38:29:DC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE -invalid-field-call:38:31:38:38:DC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE -invalid-field-call:42:4:42:11:IsAlsoDC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE -invalid-field-call:46:22:46:29:IsAlsoDC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE -invalid-field-call:46:31:46:38:IsAlsoDC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE -invalid-field-call:61:15:61:32:AlsoNotADataClass:Invalid usage of field(), it should be used within a dataclass or the make_dataclass() function.:INFERENCE +invalid-field-call:37:22:37:29:DC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE +invalid-field-call:37:31:37:38:DC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE +invalid-field-call:41:4:41:11:IsAlsoDC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE +invalid-field-call:44:22:44:29:IsAlsoDC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE +invalid-field-call:44:31:44:38:IsAlsoDC:Invalid usage of field(), it should be the value of an assignment within a dataclass.:INFERENCE +invalid-field-call:59:15:59:32:AlsoNotADataClass:Invalid usage of field(), it should be used within a dataclass or the make_dataclass() function.:INFERENCE diff --git a/tests/functional/i/invalid/invalid_name.py b/tests/functional/i/invalid/invalid_name.py index 66ad1e77cd6..28638bcb4c5 100644 --- a/tests/functional/i/invalid/invalid_name.py +++ b/tests/functional/i/invalid/invalid_name.py @@ -102,3 +102,10 @@ def test_disable_mixed( """Invalid-name will still be raised for other arguments.""" self.foo_bar = fooBar self.foo_bar2 = fooBar2 + + def tearDown(self): ... # pylint: disable=invalid-name + + +class FooBarSubclass(FooBar): + tearDown = FooBar.tearDown + tearDownNotInAncestor = None # [invalid-name] diff --git a/tests/functional/i/invalid/invalid_name.rc b/tests/functional/i/invalid/invalid_name.rc new file mode 100644 index 00000000000..b2444c2d15c --- /dev/null +++ b/tests/functional/i/invalid/invalid_name.rc @@ -0,0 +1,2 @@ +[MAIN] +class-attribute-naming-style=snake_case diff --git a/tests/functional/i/invalid/invalid_name.txt b/tests/functional/i/invalid/invalid_name.txt index 72f8fcd9060..e8622f864a9 100644 --- a/tests/functional/i/invalid/invalid_name.txt +++ b/tests/functional/i/invalid/invalid_name.txt @@ -6,3 +6,4 @@ invalid-name:66:0:66:68:a_very_very_very_long_function_name_WithCamelCase_to_mak invalid-name:74:23:74:29:FooBar.__init__:"Argument name ""fooBar"" doesn't conform to snake_case naming style":HIGH invalid-name:80:8:80:14:FooBar.func1:"Argument name ""fooBar"" doesn't conform to snake_case naming style":HIGH invalid-name:100:8:100:15:FooBar.test_disable_mixed:"Argument name ""fooBar2"" doesn't conform to snake_case naming style":HIGH +invalid-name:111:4:111:25:FooBarSubclass:"Class attribute name ""tearDownNotInAncestor"" doesn't conform to snake_case naming style":HIGH diff --git a/tests/functional/m/method_cache_max_size_none_py39.py b/tests/functional/m/method_cache_max_size_none_py39.py deleted file mode 100644 index d0bd65755ec..00000000000 --- a/tests/functional/m/method_cache_max_size_none_py39.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Tests for method-cache-max-size-none""" -# pylint: disable=missing-function-docstring, reimported, too-few-public-methods -# pylint: disable=missing-class-docstring, function-redefined - -import functools -import functools as aliased_functools -from functools import cache -from functools import cache as aliased_cache - - -@cache -def my_func(param): - return param + 1 - - -class MyClassWithMethods: - @cache - @staticmethod - def my_func(param): - return param + 1 - - @cache - @classmethod - def my_func(cls, param): - return param + 1 - - @cache # [method-cache-max-size-none] - def my_func(self, param): - return param + 1 - - @functools.cache # [method-cache-max-size-none] - def my_func(self, param): - return param + 1 - - @aliased_functools.cache # [method-cache-max-size-none] - def my_func(self, param): - return param + 1 - - @aliased_cache # [method-cache-max-size-none] - def my_func(self, param): - return param + 1 - - # Check double decorating to check robustness of checker itself - @functools.lru_cache(maxsize=1) - @aliased_cache # [method-cache-max-size-none] - def my_func(self, param): - return param + 1 diff --git a/tests/functional/m/method_cache_max_size_none_py39.rc b/tests/functional/m/method_cache_max_size_none_py39.rc deleted file mode 100644 index 15ad50f5ab0..00000000000 --- a/tests/functional/m/method_cache_max_size_none_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver = 3.9 diff --git a/tests/functional/m/method_cache_max_size_none_py39.txt b/tests/functional/m/method_cache_max_size_none_py39.txt deleted file mode 100644 index e364e50ef57..00000000000 --- a/tests/functional/m/method_cache_max_size_none_py39.txt +++ /dev/null @@ -1,5 +0,0 @@ -method-cache-max-size-none:27:5:27:10:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE -method-cache-max-size-none:31:5:31: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:35:5:35:28:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE -method-cache-max-size-none:39:5:39:18:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE -method-cache-max-size-none:45:5:45:18:MyClassWithMethods.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 19fd60c7225..31bba74bf56 100644 --- a/tests/functional/m/method_hidden.py +++ b/tests/functional/m/method_hidden.py @@ -134,3 +134,9 @@ def __init__(self): class ChildTwo(ParentTwo): def __private(self): pass + + +class ChildHidingAncestorAttribute(Parent): + @functools().cached_property + def _protected(self): + pass diff --git a/tests/functional/m/method_hidden_py39.py b/tests/functional/m/method_hidden_py39.py deleted file mode 100644 index ac087d0d6c9..00000000000 --- a/tests/functional/m/method_hidden_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -# pylint: disable=too-few-public-methods,missing-docstring -"""check method hiding ancestor attribute -""" -import something_else as functools # pylint: disable=import-error - - -class Parent: - def __init__(self): - self._protected = None - - -class Child(Parent): - @functools().cached_property - def _protected(self): - # This test case is only valid for python3.9 and above - pass diff --git a/tests/functional/m/method_hidden_py39.rc b/tests/functional/m/method_hidden_py39.rc deleted file mode 100644 index 15ad50f5ab0..00000000000 --- a/tests/functional/m/method_hidden_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver = 3.9 diff --git a/tests/functional/m/misplaced_format_function.py b/tests/functional/m/misplaced_format_function.py index 679f5985820..a1372102418 100644 --- a/tests/functional/m/misplaced_format_function.py +++ b/tests/functional/m/misplaced_format_function.py @@ -6,7 +6,7 @@ print("value: {}").format(123) # [misplaced-format-function] print('value: {}'.format(123)) print('{} Come Forth!'.format('Lazarus')) -print('Der Hem ist mein Licht und mein Heil, vor wem sollte ich mich furchten? => {}'.format('Psalm 27, 1')) +print('He is my light and my salvation, of whom should I be afraid? => {}'.format('Psalm 27, 1')) print('123') print() s = 'value: {}'.format(123) diff --git a/tests/functional/m/multiple_statements.py b/tests/functional/m/multiple_statements.py index c3252f797c8..6279462cbf0 100644 --- a/tests/functional/m/multiple_statements.py +++ b/tests/functional/m/multiple_statements.py @@ -4,13 +4,28 @@ from typing import overload +if True: print("Golfing sure is nice") # [multiple-statements] if True: pass # [multiple-statements] +if True: ... # [multiple-statements] + +if True: print("Golfing sure is nice") # [multiple-statements] +else: + pass if True: pass # [multiple-statements] else: pass +if True: ... # [multiple-statements] +else: + pass + +# The following difference in behavior is due to black 2024's style +# that reformat pass on multiple line but reformat "..." on a single line +# (only for classes, not for the examples above) +class MyException(Exception): print("Golfing sure is nice") # [multiple-statements] class MyError(Exception): pass # [multiple-statements] +class DebugTrueDetected(Exception): ... class MyError(Exception): a='a' # [multiple-statements] @@ -28,3 +43,23 @@ class MyError(Exception): a='a'; b='b' # [multiple-statements] def concat2(arg1: str) -> str: ... def concat2(arg1: str) -> str: ... + +# Test for multiple statements on finally line +try: + pass +finally: pass # [multiple-statements] + +# Test for multiple statements on else line +try: + pass +except: + pass +else: pass # [multiple-statements] + +# Test for multiple statements on else and finally lines +try: + pass +except: + pass +else: pass # [multiple-statements] +finally: pass # [multiple-statements] diff --git a/tests/functional/m/multiple_statements.txt b/tests/functional/m/multiple_statements.txt index 661314268de..343298e25f8 100644 --- a/tests/functional/m/multiple_statements.txt +++ b/tests/functional/m/multiple_statements.txt @@ -1,5 +1,14 @@ -multiple-statements:7:9:7:13::More than one statement on a single line:UNDEFINED -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:7:9:7:38::More than one statement on a single line:HIGH +multiple-statements:8:9:8:13::More than one statement on a single line:HIGH +multiple-statements:9:9:9:12::More than one statement on a single line:HIGH +multiple-statements:11:9:11:38::More than one statement on a single line:HIGH +multiple-statements:15:9:15:13::More than one statement on a single line:HIGH +multiple-statements:19:9:19:12::More than one statement on a single line:HIGH +multiple-statements:26:30:26:59:MyException:More than one statement on a single line:HIGH +multiple-statements:27:26:27:30:MyError:More than one statement on a single line:HIGH +multiple-statements:30:26:30:31:MyError:More than one statement on a single line:HIGH +multiple-statements:32:26:32:31:MyError:More than one statement on a single line:HIGH +multiple-statements:50:9:50:13::More than one statement on a single line:HIGH +multiple-statements:57:6:57:10::More than one statement on a single line:HIGH +multiple-statements:64:6:64:10::More than one statement on a single line:HIGH +multiple-statements:65:9:65:13::More than one statement on a single line:HIGH diff --git a/tests/functional/m/multiple_statements_single_line.py b/tests/functional/m/multiple_statements_single_line.py index 93a470702c4..8b39d631bd9 100644 --- a/tests/functional/m/multiple_statements_single_line.py +++ b/tests/functional/m/multiple_statements_single_line.py @@ -4,19 +4,32 @@ from typing import overload +if True: print("Golfing sure is nice") if True: pass +if True: ... + +if True: print("Golfing sure is nice") # [multiple-statements] +else: + pass if True: pass # [multiple-statements] else: pass +if True: ... # [multiple-statements] +else: + pass + +class MyException(Exception): print("Golfing sure is nice") class MyError(Exception): pass +class DebugTrueDetected(Exception): ... + class MyError(Exception): a='a' class MyError(Exception): a='a'; b='b' # [multiple-statements] -try: +try: #@ pass except: pass diff --git a/tests/functional/m/multiple_statements_single_line.txt b/tests/functional/m/multiple_statements_single_line.txt index cac2f7eb2e7..8d19c72516c 100644 --- a/tests/functional/m/multiple_statements_single_line.txt +++ b/tests/functional/m/multiple_statements_single_line.txt @@ -1,2 +1,4 @@ -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:11:9:11:38::More than one statement on a single line:HIGH +multiple-statements:15:9:15:13::More than one statement on a single line:HIGH +multiple-statements:19:9:19:12::More than one statement on a single line:HIGH +multiple-statements:30:26:30:31:MyError:More than one statement on a single line:HIGH diff --git a/tests/functional/n/names_in__all__.py b/tests/functional/n/names_in__all__.py index 38ed18a2e78..76fbd833aed 100644 --- a/tests/functional/n/names_in__all__.py +++ b/tests/functional/n/names_in__all__.py @@ -1,7 +1,7 @@ # 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 +* NonExistent is not defined in this module, and it is listed in __all__. An error is expected. * This module imports path and republished it in __all__. No errors @@ -16,7 +16,7 @@ '', # [undefined-all-variable] Missing, SomeUndefined, # [undefined-variable] - 'NonExistant', # [undefined-all-variable] + 'NonExistent', # [undefined-all-variable] 'path', 'func', # [undefined-all-variable] 'inner', # [undefined-all-variable] diff --git a/tests/functional/n/names_in__all__.txt b/tests/functional/n/names_in__all__.txt index 720942df3db..29c0e11391b 100644 --- a/tests/functional/n/names_in__all__.txt +++ b/tests/functional/n/names_in__all__.txt @@ -1,6 +1,6 @@ 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:19:4:19:17::Undefined variable name 'NonExistent' 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_py39.rc b/tests/functional/n/nested_min_max_py39.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/n/nested_min_max_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/n/no/no_name_in_module.py b/tests/functional/n/no/no_name_in_module.py index ef9fb03d171..6d34245c84b 100644 --- a/tests/functional/n/no/no_name_in_module.py +++ b/tests/functional/n/no/no_name_in_module.py @@ -7,8 +7,8 @@ toto.yo() from xml.etree import ElementTree -ElementTree.nonexistant_function() # [no-member] -ElementTree.another.nonexistant.function() # [no-member] +ElementTree.nonexistent_function() # [no-member] +ElementTree.another.nonexistent.function() # [no-member] import sys diff --git a/tests/functional/n/no/no_name_in_module.txt b/tests/functional/n/no/no_name_in_module.txt index 878793bc739..ed8628108df 100644 --- a/tests/functional/n/no/no_name_in_module.txt +++ b/tests/functional/n/no/no_name_in_module.txt @@ -1,6 +1,6 @@ no-name-in-module:5:0:5:23::No name 'tutu' in module 'collections':UNDEFINED no-name-in-module:6:0:6:28::No name 'toto' in module 'collections':UNDEFINED -no-member:10:0:10:32::Module 'xml.etree.ElementTree' has no 'nonexistant_function' member:INFERENCE +no-member:10:0:10:32::Module 'xml.etree.ElementTree' has no 'nonexistent_function' member:INFERENCE no-member:11:0:11:19::Module 'xml.etree.ElementTree' has no 'another' member:INFERENCE no-member:16:6:16:17::Module 'sys' has no 'stdoout' member; maybe 'stdout'?:INFERENCE no-name-in-module:23:0:23:34::No name 'compiile' in module 're':UNDEFINED diff --git a/tests/functional/n/non/non_init_parent_called.py b/tests/functional/n/non/non_init_parent_called.py index 7a6c94eadd7..5c27a45aa12 100644 --- a/tests/functional/n/non/non_init_parent_called.py +++ b/tests/functional/n/non/non_init_parent_called.py @@ -3,7 +3,7 @@ """test for call to __init__ from a non ancestor class """ from . import non_init_parent_called -import nonexistant # [import-error] +import nonexistent # [import-error] class AAAA: @@ -19,13 +19,13 @@ class BBBBMixin: def __init__(self): print('init', self) -class CCC(BBBBMixin, non_init_parent_called.AAAA, non_init_parent_called.BBBB, nonexistant.AClass): # [no-member] +class CCC(BBBBMixin, non_init_parent_called.AAAA, non_init_parent_called.BBBB, nonexistent.AClass): # [no-member] """mix different things, some inferable some not""" def __init__(self): BBBBMixin.__init__(self) non_init_parent_called.AAAA.__init__(self) non_init_parent_called.BBBB.__init__(self) # [no-member] - nonexistant.AClass.__init__(self) + nonexistent.AClass.__init__(self) class DDDD(AAAA): """call superclass constructor in disjunct branches""" diff --git a/tests/functional/n/non/non_init_parent_called.txt b/tests/functional/n/non/non_init_parent_called.txt index d6ef10e9f85..85aad5f6e46 100644 --- a/tests/functional/n/non/non_init_parent_called.txt +++ b/tests/functional/n/non/non_init_parent_called.txt @@ -1,4 +1,4 @@ -import-error:6:0:6:18::Unable to import 'nonexistant':UNDEFINED +import-error:6:0:6:18::Unable to import 'nonexistent':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 diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_assignment_expressions.py b/tests/functional/n/non_ascii_name/non_ascii_name_assignment_expressions.py index ac51b9c4dc3..a0fa108f7fd 100644 --- a/tests/functional/n/non_ascii_name/non_ascii_name_assignment_expressions.py +++ b/tests/functional/n/non_ascii_name/non_ascii_name_assignment_expressions.py @@ -1,4 +1,4 @@ -"""Assigment Expression as defined in https://www.python.org/dev/peps/pep-0572/""" +"""Assignment Expression as defined in https://www.python.org/dev/peps/pep-0572/""" if (loł := __name__) == "__main__": # [non-ascii-name] print(loł) diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_decorator.rc b/tests/functional/n/non_ascii_name/non_ascii_name_decorator.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_decorator.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.py b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument.py similarity index 89% rename from tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.py rename to tests/functional/n/non_ascii_name/non_ascii_name_function_argument.py index a75e1c6b78a..377bf865e6b 100644 --- a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.py +++ b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument.py @@ -1,7 +1,5 @@ """ non ascii variable defined in a function - -This test is only for 3.8 as the starting column is incorrect """ diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument.txt b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument.txt new file mode 100644 index 00000000000..8a26db2e12a --- /dev/null +++ b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument.txt @@ -0,0 +1,2 @@ +non-ascii-name:9:4:9:13:okay:"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH +non-ascii-name:21:4:21:12::"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.rc b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.txt b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.txt deleted file mode 100644 index 7813364747c..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py38.txt +++ /dev/null @@ -1,2 +0,0 @@ -non-ascii-name:11:4:11:13:okay:"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH -non-ascii-name:23:0:None:None::"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.py b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.py deleted file mode 100644 index a2d87ac4d75..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -non ascii variable defined in a function - -This test is 3.9+ and not using 'min_pyver_end_position' -as the starting column is also incorrect on < 3.9 -""" - - -def okay( - just_some_thing_long_again: str, - lol_very_long_argument: str, - łol: str, # [non-ascii-name] -) -> bool: - """Be okay, yeah?""" - # Usage should not raise a second error - print(just_some_thing_long_again, lol_very_long_argument, łol) - return True - - -# Usage should raise a second error -okay( - "A VVVVVVVEEEERRRRRRRRRRYYYYYYYYYY LONG TIME ", - lol_very_long_argument="a", - łol="b", # [non-ascii-name] -) diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.rc b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.txt b/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.txt deleted file mode 100644 index 0222308af63..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_function_argument_py39plus.txt +++ /dev/null @@ -1,2 +0,0 @@ -non-ascii-name:12:4:12:13:okay:"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH -non-ascii-name:24:4:24:12::"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.py b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs.py similarity index 79% rename from tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.py rename to tests/functional/n/non_ascii_name/non_ascii_name_kwargs.py index 3c7b8c68359..fbd382f41db 100644 --- a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.py +++ b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs.py @@ -1,7 +1,5 @@ """ Defining non ASCII variables in a function call - -This test is only for 3.8 as the starting column is incorrect """ diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs.txt b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs.txt new file mode 100644 index 00000000000..b5ab4d5bc84 --- /dev/null +++ b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs.txt @@ -0,0 +1 @@ +non-ascii-name:14:4:14:10::"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.rc b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.txt b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.txt deleted file mode 100644 index b43189658fd..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py38.txt +++ /dev/null @@ -1 +0,0 @@ -non-ascii-name:16:0:None:None::"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.py b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.py deleted file mode 100644 index 0dfceb38f47..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Defining non ASCII variables in a function call - -This test is 3.9+ and not using 'min_pyver_end_position' -as the starting column is also incorrect on < 3.9 -""" - - -def okay(**kwargs): - """Print kwargs""" - print(kwargs) - - -okay( - a_long_attribute_that_is_very_okay=1, - b_belongs_to_yet_another_okay_attributed=2, - łol=3, # [non-ascii-name] -) diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.rc b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.txt b/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.txt deleted file mode 100644 index 7595a9d0829..00000000000 --- a/tests/functional/n/non_ascii_name/non_ascii_name_kwargs_py39plus.txt +++ /dev/null @@ -1 +0,0 @@ -non-ascii-name:17:4:17:10::"Argument name ""łol"" contains a non-ASCII character, consider renaming it.":HIGH diff --git a/tests/functional/n/non_ascii_name_class/non_ascii_name_class.py b/tests/functional/n/non_ascii_name_class/non_ascii_name_class.py index edcdae64352..8de521a7f57 100644 --- a/tests/functional/n/non_ascii_name_class/non_ascii_name_class.py +++ b/tests/functional/n/non_ascii_name_class/non_ascii_name_class.py @@ -9,7 +9,7 @@ class НoldIt: # [non-ascii-name] - """nice classs""" + """Nice class.""" def public(self): """do something""" diff --git a/tests/functional/n/not_async_context_manager_py37.py b/tests/functional/n/not_async_context_manager_py37.py index c1ca26976d0..9bf1cf046ee 100644 --- a/tests/functional/n/not_async_context_manager_py37.py +++ b/tests/functional/n/not_async_context_manager_py37.py @@ -8,8 +8,8 @@ async def context_manager(value): yield value -async with context_manager(42) as ans: - assert ans == 42 +async with context_manager(42) as answer: + assert answer == 42 def async_context_manager(): diff --git a/tests/functional/p/postponed/postponed_evaluation_pep585.py b/tests/functional/p/postponed/postponed_evaluation_pep585.py index 2317228e554..5354e081fdf 100644 --- a/tests/functional/p/postponed/postponed_evaluation_pep585.py +++ b/tests/functional/p/postponed/postponed_evaluation_pep585.py @@ -1,18 +1,5 @@ -"""Test PEP 585 in combination with postponed evaluation PEP 563. - -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 -# 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/pylint-dev/pylint/pull/7918#issuecomment-1352737369 -# pylint: disable=multiple-statements - -from __future__ import annotations +"""Test PEP 585 works as expected, starting with Python 3.9""" +# 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 import collections import dataclasses import typing @@ -20,25 +7,25 @@ from typing import Any, Dict, NamedTuple, TypedDict, Union, Tuple -AliasInvalid = list[int] # [unsubscriptable-object] +AliasValid = list[int] class CustomIntList(typing.List[int]): pass -class CustomIntListError(list[int]): # [unsubscriptable-object] +class CustomIntListError(list[int]): pass cast_variable = [1, 2, 3] -cast_variable = typing.cast(list[int], cast_variable) # [unsubscriptable-object] +cast_variable = typing.cast(list[int], cast_variable) -T = typing.TypeVar("T", list[int], str) # [unsubscriptable-object] +T = typing.TypeVar("T", list[int], str) -(lambda x: 2)(list[int]) # [unsubscriptable-object] +(lambda x: 2)(list[int]) # Check typing.NamedTuple CustomNamedTuple = typing.NamedTuple( - "CustomNamedTuple", [("my_var", list[int])]) # [unsubscriptable-object] + "CustomNamedTuple", [("my_var", list[int])]) class CustomNamedTuple2(NamedTuple): my_var: list[int] @@ -48,9 +35,9 @@ class CustomNamedTuple3(typing.NamedTuple): # Check typing.TypedDict -CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) # [unsubscriptable-object] +CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) -CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) # [unsubscriptable-object] +CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) class CustomTypedDict3(TypedDict): my_var: list[int] @@ -101,12 +88,12 @@ def func(arg: list[int]): def func2() -> list[int]: pass -Alias2 = Union[list[str], None] # [unsubscriptable-object] -Alias3 = Union[Union[list[int], int]] # [unsubscriptable-object] -Alias4 = Tuple[list[int]] # [unsubscriptable-object] -Alias5 = Dict[str, list[int]] # [unsubscriptable-object] -Alias6 = int | list[int] # [unsubscriptable-object] -Alias7 = list[list[int]] # [unsubscriptable-object,unsubscriptable-object] +Alias2 = Union[list[str], None] +Alias3 = Union[Union[list[int], int]] +Alias4 = Tuple[list[int]] +Alias5 = Dict[str, list[int]] +Alias6 = int | list[int] +Alias7 = list[list[int]] import collections.abc @@ -116,7 +103,7 @@ def func2() -> list[int]: class OrderedDict: pass -var12: OrderedDict[str, int] # string annotations aren't checked +var12: OrderedDict[str, int] # [unsubscriptable-object] var13: list[int] var14: collections.OrderedDict[str, int] var15: collections.Counter[int] @@ -126,15 +113,15 @@ class OrderedDict: def func3(): - AliasInvalid2 = list[int] # [unsubscriptable-object] + AliasInvalid2 = list[int] cast_variable2 = [1, 2, 3] - cast_variable2 = typing.cast(list[int], cast_variable2) # [unsubscriptable-object] + cast_variable2 = typing.cast(list[int], cast_variable2) var19: list[int] -def func4(arg=list[int]): # [unsubscriptable-object] +def func4(var=list[int]): pass -def func5(arg1: list[int], arg2=set[int]): # [unsubscriptable-object] +def func5(arg1: list[int], arg2=set[int]): pass def func6(arg1: list[int], /, *args: tuple[str], arg2: set[int], **kwargs: dict[str, Any]): diff --git a/tests/functional/p/postponed/postponed_evaluation_pep585.rc b/tests/functional/p/postponed/postponed_evaluation_pep585.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/p/postponed/postponed_evaluation_pep585.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/p/postponed/postponed_evaluation_pep585.txt b/tests/functional/p/postponed/postponed_evaluation_pep585.txt index 899dc59774d..127e252a539 100644 --- a/tests/functional/p/postponed/postponed_evaluation_pep585.txt +++ b/tests/functional/p/postponed/postponed_evaluation_pep585.txt @@ -1,19 +1 @@ -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 +unsubscriptable-object:106:7:106:18::Value 'OrderedDict' is unsubscriptable:UNDEFINED diff --git a/tests/functional/p/postponed/postponed_evaluation_pep585_error.py b/tests/functional/p/postponed/postponed_evaluation_pep585_error.py deleted file mode 100644 index 9810c0c14b8..00000000000 --- a/tests/functional/p/postponed/postponed_evaluation_pep585_error.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Test PEP 585 without postponed evaluation. Everything should fail. - -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 -# 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/pylint-dev/pylint/pull/7918#issuecomment-1352737369 -# pylint: disable=multiple-statements - -import collections -import dataclasses -import typing -from dataclasses import dataclass -from typing import Any, Dict, NamedTuple, TypedDict, Union - - -AliasInvalid = list[int] # [unsubscriptable-object] - -class CustomIntList(typing.List[int]): - pass - -class CustomIntListError(list[int]): # [unsubscriptable-object] - pass - -cast_variable = [1, 2, 3] -cast_variable = typing.cast(list[int], cast_variable) # [unsubscriptable-object] - -T = typing.TypeVar("T", list[int], str) # [unsubscriptable-object] - -(lambda x: 2)(list[int]) # [unsubscriptable-object] - - -# Check typing.NamedTuple -CustomNamedTuple = typing.NamedTuple( - "CustomNamedTuple", [("my_var", list[int])]) # [unsubscriptable-object] - -class CustomNamedTuple2(NamedTuple): - my_var: list[int] # [unsubscriptable-object] - -class CustomNamedTuple3(typing.NamedTuple): - my_var: list[int] # [unsubscriptable-object] - - -# Check typing.TypedDict -CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) # [unsubscriptable-object] - -CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) # [unsubscriptable-object] - -class CustomTypedDict3(TypedDict): - my_var: list[int] # [unsubscriptable-object] - -class CustomTypedDict4(typing.TypedDict): - my_var: list[int] # [unsubscriptable-object] - - -# Check dataclasses -def my_decorator(*args, **kwargs): - def wraps(*args, **kwargs): - pass - return wraps - -@dataclass -class CustomDataClass: - my_var: list[int] # [unsubscriptable-object] - -@dataclasses.dataclass -class CustomDataClass2: - my_var: list[int] # [unsubscriptable-object] - -@dataclass() -class CustomDataClass3: - my_var: list[int] # [unsubscriptable-object] - -@my_decorator -@dataclasses.dataclass -class CustomDataClass4: - my_var: list[int] # [unsubscriptable-object] - - -var1: set[int] # [unsubscriptable-object] -var2: collections.OrderedDict[str, int] # [unsubscriptable-object] -var3: dict[str, list[int]] # [unsubscriptable-object,unsubscriptable-object] -var4: Dict[str, list[int]] # [unsubscriptable-object] -var5: dict[tuple[int, int], str] # [unsubscriptable-object,unsubscriptable-object] -var6: Dict[tuple[int, int], str] # [unsubscriptable-object] -var7: list[list[int]] # [unsubscriptable-object,unsubscriptable-object] -var8: tuple[list[int]] # [unsubscriptable-object,unsubscriptable-object] -var9: int | list[str | int] # [unsubscriptable-object] -var10: Union[list[str], None] # [unsubscriptable-object] -var11: Union[Union[list[int], int]] # [unsubscriptable-object] - -def func(arg: list[int]): # [unsubscriptable-object] - pass - -def func2() -> list[int]: # [unsubscriptable-object] - pass - -Alias2 = Union[list[str], None] # [unsubscriptable-object] -Alias3 = Union[Union[list[int], int]] # [unsubscriptable-object] -Alias5 = Dict[str, list[int]] # [unsubscriptable-object] -Alias6 = int | list[int] # [unsubscriptable-object] -Alias7 = list[list[int]] # [unsubscriptable-object,unsubscriptable-object] - - -def func3(): - AliasInvalid2 = list[int] # [unsubscriptable-object] - cast_variable2 = [1, 2, 3] - cast_variable2 = typing.cast(list[int], cast_variable2) # [unsubscriptable-object] - var12: list[int] # [unsubscriptable-object] - -def func4(var=list[int]): # [unsubscriptable-object] - pass - -def func5(arg1: list[int], arg2=set[int]): # [unsubscriptable-object,unsubscriptable-object] - pass - -def func6(arg1: list[int], /, *args: tuple[str], arg2: set[int], **kwargs: dict[str, Any]): - # -1:[unsubscriptable-object,unsubscriptable-object,unsubscriptable-object,unsubscriptable-object] - pass diff --git a/tests/functional/p/postponed/postponed_evaluation_pep585_error.rc b/tests/functional/p/postponed/postponed_evaluation_pep585_error.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/p/postponed/postponed_evaluation_pep585_error.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/p/postponed/postponed_evaluation_pep585_error.txt b/tests/functional/p/postponed/postponed_evaluation_pep585_error.txt deleted file mode 100644 index 406081dfae6..00000000000 --- a/tests/functional/p/postponed/postponed_evaluation_pep585_error.txt +++ /dev/null @@ -1,49 +0,0 @@ -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/postponed/postponed_evaluation_pep585_py39.py b/tests/functional/p/postponed/postponed_evaluation_pep585_py39.py deleted file mode 100644 index 5354e081fdf..00000000000 --- a/tests/functional/p/postponed/postponed_evaluation_pep585_py39.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Test PEP 585 works as expected, starting with Python 3.9""" -# 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 -import collections -import dataclasses -import typing -from dataclasses import dataclass -from typing import Any, Dict, NamedTuple, TypedDict, Union, Tuple - - -AliasValid = list[int] - -class CustomIntList(typing.List[int]): - pass - -class CustomIntListError(list[int]): - pass - -cast_variable = [1, 2, 3] -cast_variable = typing.cast(list[int], cast_variable) - -T = typing.TypeVar("T", list[int], str) - -(lambda x: 2)(list[int]) - - -# Check typing.NamedTuple -CustomNamedTuple = typing.NamedTuple( - "CustomNamedTuple", [("my_var", list[int])]) - -class CustomNamedTuple2(NamedTuple): - my_var: list[int] - -class CustomNamedTuple3(typing.NamedTuple): - my_var: list[int] - - -# Check typing.TypedDict -CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) - -CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) - -class CustomTypedDict3(TypedDict): - my_var: list[int] - -class CustomTypedDict4(typing.TypedDict): - my_var: list[int] - - -# Check dataclasses -def my_decorator(*args, **kwargs): - def wraps(*args, **kwargs): - pass - return wraps - -@dataclass -class CustomDataClass: - my_var: list[int] - -@dataclasses.dataclass -class CustomDataClass2: - my_var: list[int] - -@dataclass() -class CustomDataClass3: - my_var: list[int] - -@my_decorator -@dataclasses.dataclass -class CustomDataClass4: - my_var: list[int] - - -var1: set[int] -var2: collections.OrderedDict[str, int] -var3: dict[str, list[int]] -var4: Dict[str, list[int]] -var5: dict[tuple[int, int], str] -var6: Dict[tuple[int, int], str] -var7: list[list[int]] -var8: tuple[list[int]] -var9: int | list[str | int] -var10: Union[list[str], None] -var11: Union[Union[list[int], int]] - -def func(arg: list[int]): - pass - -def func2() -> list[int]: - pass - -Alias2 = Union[list[str], None] -Alias3 = Union[Union[list[int], int]] -Alias4 = Tuple[list[int]] -Alias5 = Dict[str, list[int]] -Alias6 = int | list[int] -Alias7 = list[list[int]] - - -import collections.abc -import contextlib -import re - -class OrderedDict: - pass - -var12: OrderedDict[str, int] # [unsubscriptable-object] -var13: list[int] -var14: collections.OrderedDict[str, int] -var15: collections.Counter[int] -var16: collections.abc.Iterable[int] -var17: contextlib.AbstractContextManager[int] -var18: re.Pattern[str] - - -def func3(): - AliasInvalid2 = list[int] - cast_variable2 = [1, 2, 3] - cast_variable2 = typing.cast(list[int], cast_variable2) - var19: list[int] - -def func4(var=list[int]): - pass - -def func5(arg1: list[int], arg2=set[int]): - pass - -def func6(arg1: list[int], /, *args: tuple[str], arg2: set[int], **kwargs: dict[str, Any]): - pass diff --git a/tests/functional/p/postponed/postponed_evaluation_pep585_py39.rc b/tests/functional/p/postponed/postponed_evaluation_pep585_py39.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/p/postponed/postponed_evaluation_pep585_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/p/postponed/postponed_evaluation_pep585_py39.txt b/tests/functional/p/postponed/postponed_evaluation_pep585_py39.txt deleted file mode 100644 index 127e252a539..00000000000 --- a/tests/functional/p/postponed/postponed_evaluation_pep585_py39.txt +++ /dev/null @@ -1 +0,0 @@ -unsubscriptable-object:106:7:106:18::Value 'OrderedDict' is unsubscriptable:UNDEFINED diff --git a/tests/functional/p/protected_access.py b/tests/functional/p/protected_access.py index 6587e50a7b7..92efda59d8f 100644 --- a/tests/functional/p/protected_access.py +++ b/tests/functional/p/protected_access.py @@ -19,18 +19,6 @@ def __init__(self): OBJ._teta # [protected-access] -# Make sure protect-access doesn't raise an exception Uninferable attributes -class MC: - @property - def nargs(self): - return 1 if self._nargs else 2 - - -class Application(metaclass=MC): - def __no_special__(cls): - nargs = obj._nargs # [protected-access] - - class Light: @property def _light_internal(self) -> None: diff --git a/tests/functional/p/protected_access.txt b/tests/functional/p/protected_access.txt index e0bebb76a0f..bb2cdc2aba0 100644 --- a/tests/functional/p/protected_access.txt +++ b/tests/functional/p/protected_access.txt @@ -1,4 +1,3 @@ protected-access:19:0:19:9::Access to a protected member _teta of a client class:UNDEFINED -protected-access:31:16:31:26:Application.__no_special__:Access to a protected member _nargs of a client class:UNDEFINED -protected-access:41:14:41:35:Light.func:Access to a protected member _light_internal of a client class:UNDEFINED -protected-access:45:10:45:31:func:Access to a protected member _light_internal of a client class:UNDEFINED +protected-access:29:14:29:35:Light.func:Access to a protected member _light_internal of a client class:UNDEFINED +protected-access:33:10:33:31:func:Access to a protected member _light_internal of a client class:UNDEFINED diff --git a/tests/functional/r/raising/raising_non_exception.txt b/tests/functional/r/raising/raising_non_exception.txt index 5cab168463c..86c4f70b1a3 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:INFERENCE +raising-non-exception:13:0:13:22::Raising a class which doesn't inherit from BaseException:INFERENCE diff --git a/tests/functional/r/recursion/recursion_error_3152.py b/tests/functional/r/recursion/recursion_error_3152.py index 97ec190e8f7..2d7de55079d 100644 --- a/tests/functional/r/recursion/recursion_error_3152.py +++ b/tests/functional/r/recursion/recursion_error_3152.py @@ -2,6 +2,6 @@ import setuptools -# pylint: disable=missing-docstring,too-few-public-methods +# pylint: disable=missing-docstring,too-few-public-methods,abstract-method class Custom(setuptools.Command): pass diff --git a/tests/functional/r/recursion/recursion_error_3159.py b/tests/functional/r/recursion/recursion_error_3159.py index d905ad862a1..9abfed98780 100644 --- a/tests/functional/r/recursion/recursion_error_3159.py +++ b/tests/functional/r/recursion/recursion_error_3159.py @@ -13,8 +13,7 @@ def initialize_options(self): def finalize_options(self): pass - @staticmethod - def run(): + def run(self): print("Do anything") diff --git a/tests/functional/r/regression/regression_9865_calling_bound_lambda.py b/tests/functional/r/regression/regression_9865_calling_bound_lambda.py new file mode 100644 index 00000000000..2a8dae1b0b2 --- /dev/null +++ b/tests/functional/r/regression/regression_9865_calling_bound_lambda.py @@ -0,0 +1,8 @@ +"""Regression for https://github.com/pylint-dev/pylint/issues/9865.""" +# pylint: disable=missing-docstring,too-few-public-methods,unnecessary-lambda-assignment +class C: + eq = lambda self, y: self == y + +def test_lambda_method(): + ret = C().eq(1) + return ret diff --git a/tests/functional/r/regression/regression_9875_enumerate.py b/tests/functional/r/regression/regression_9875_enumerate.py new file mode 100644 index 00000000000..1eca3f7811f --- /dev/null +++ b/tests/functional/r/regression/regression_9875_enumerate.py @@ -0,0 +1,7 @@ +"""https://github.com/pylint-dev/pylint/issues/9875""" +# value = 0 +for idx, value in enumerate(iterable=[1, 2, 3]): + print(f'{idx=} {value=}') +# +1: [undefined-loop-variable, undefined-loop-variable] +for idx, value in enumerate(iterable=[value-1, value-2*1]): + print(f'{idx=} {value=}') diff --git a/tests/functional/r/regression/regression_9875_enumerate.txt b/tests/functional/r/regression/regression_9875_enumerate.txt new file mode 100644 index 00000000000..dad9a0f0aac --- /dev/null +++ b/tests/functional/r/regression/regression_9875_enumerate.txt @@ -0,0 +1,2 @@ +undefined-loop-variable:6:38:6:43::Using possibly undefined loop variable 'value':UNDEFINED +undefined-loop-variable:6:47:6:52::Using possibly undefined loop variable 'value':UNDEFINED diff --git a/tests/functional/r/regression_02/regression_5408.rc b/tests/functional/r/regression_02/regression_5408.rc index 7b414202d3b..b47a745259c 100644 --- a/tests/functional/r/regression_02/regression_5408.rc +++ b/tests/functional/r/regression_02/regression_5408.rc @@ -1,3 +1,2 @@ [testoptions] -min_pyver=3.9 except_implementations=PyPy diff --git a/tests/functional/r/regression_02/regression_5479.py b/tests/functional/r/regression_02/regression_5479.py index 9955ea68049..9b493f19293 100644 --- a/tests/functional/r/regression_02/regression_5479.py +++ b/tests/functional/r/regression_02/regression_5479.py @@ -1,7 +1,7 @@ """Test for a regression on slots and annotated assignments. Reported in https://github.com/pylint-dev/pylint/issues/5479 """ -# pylint: disable=too-few-public-methods, unused-private-member, missing-class-docstring, missing-function-docstring +# pylint: disable=too-few-public-methods, unused-private-member, missing-class-docstring, missing-function-docstring, declare-non-slot from __future__ import annotations diff --git a/tests/functional/r/regression_02/regression_9751.py b/tests/functional/r/regression_02/regression_9751.py new file mode 100644 index 00000000000..c3b9d787f22 --- /dev/null +++ b/tests/functional/r/regression_02/regression_9751.py @@ -0,0 +1,15 @@ +""" +pylint 3.2.4 regression +https://github.com/pylint-dev/pylint/issues/9751 +""" + +# pylint: disable=missing-function-docstring + +from typing import Any + +def repro() -> Any: + return 5 + +def main(): + x = repro() + 5 + print(x) diff --git a/tests/functional/s/slots_checks.py b/tests/functional/s/slots_checks.py index 2c22e968ed6..e0c76dbe45a 100644 --- a/tests/functional/s/slots_checks.py +++ b/tests/functional/s/slots_checks.py @@ -128,3 +128,76 @@ class Parent: class ChildNotAffectedByValueInSlot(Parent): __slots__ = ('first', ) + + +class ClassTypeHintNotInSlotsWithoutDict: + __slots__ = ("a", "b") + + a: int + b: str + c: bool # [declare-non-slot] + + +class ClassTypeHintNotInSlotsWithDict: + __slots__ = ("a", "b", "__dict__") + + a: int + b: str + c: bool + + +class BaseNoSlots: + pass + + +class DerivedWithSlots(BaseNoSlots): + __slots__ = ("age",) + + price: int + + +class BaseWithSlots: + __slots__ = ("a", "b",) + + +class DerivedWithMoreSlots(BaseWithSlots): + __slots__ = ("c",) + + # Is in base __slots__ + a: int + + # Not in any base __slots__ + d: int # [declare-non-slot] + e: str= "AnnAssign.value is not None" + + +class BaseWithSlotsDict: + __slots__ = ("__dict__", ) + +class DerivedTypeHintNotInSlots(BaseWithSlotsDict): + __slots__ = ("other", ) + + a: int + def __init__(self) -> None: + super().__init__() + self.a = 42 + + +class ClassWithEmptySlotsAndAnnotation: + __slots__ = () + + a: int + + +# https://github.com/pylint-dev/pylint/issues/9814 +class SlotsManipulationTest: + __slots__ = ["a", "b", "c"] + + +class TestChild(SlotsManipulationTest): + __slots__ += ["d", "e", "f"] # pylint: disable=undefined-variable + + +t = TestChild() + +print(t.__slots__) diff --git a/tests/functional/s/slots_checks.txt b/tests/functional/s/slots_checks.txt index d63ad251730..127cb465fc9 100644 --- a/tests/functional/s/slots_checks.txt +++ b/tests/functional/s/slots_checks.txt @@ -14,3 +14,5 @@ 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 +declare-non-slot:138:4:138:5:ClassTypeHintNotInSlotsWithoutDict:No such name 'c' in __slots__:INFERENCE +declare-non-slot:170:4:170:5:DerivedWithMoreSlots:No such name 'd' in __slots__:INFERENCE diff --git a/tests/functional/s/star/star_needs_assignment_target_py38.py b/tests/functional/s/star/star_needs_assignment_target_py38.py deleted file mode 100644 index fb5eea86a74..00000000000 --- a/tests/functional/s/star/star_needs_assignment_target_py38.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Test PEP 0448 -- Additional Unpacking Generalizations -https://www.python.org/dev/peps/pep-0448/ -""" - -# pylint: disable=superfluous-parens, unnecessary-comprehension - -UNPACK_TUPLE = (*range(4), 4) -UNPACK_LIST = [*range(4), 4] -UNPACK_SET = {*range(4), 4} -UNPACK_DICT = {'a': 1, **{'b': '2'}} -UNPACK_DICT2 = {**UNPACK_DICT, "x": 1, "y": 2} -UNPACK_DICT3 = {**{'a': 1}, 'a': 2, **{'a': 3}} - -UNPACK_IN_COMP = {elem for elem in (*range(10))} # [star-needs-assignment-target] diff --git a/tests/functional/s/star/star_needs_assignment_target_py38.rc b/tests/functional/s/star/star_needs_assignment_target_py38.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/s/star/star_needs_assignment_target_py38.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/s/star/star_needs_assignment_target_py38.txt b/tests/functional/s/star/star_needs_assignment_target_py38.txt deleted file mode 100644 index fb5a5faa6b5..00000000000 --- a/tests/functional/s/star/star_needs_assignment_target_py38.txt +++ /dev/null @@ -1 +0,0 @@ -star-needs-assignment-target:15:36:15:46::Can use starred expression only in assignment target:UNDEFINED diff --git a/tests/functional/s/super/super_init_not_called_extensions.py b/tests/functional/s/super/super_init_not_called_extensions.py deleted file mode 100644 index 241e0008a16..00000000000 --- a/tests/functional/s/super/super_init_not_called_extensions.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Tests for super-init-not-called.""" -# pylint: disable=too-few-public-methods - -from typing_extensions import Protocol as ExtensionProtocol - - -class TestProto(ExtensionProtocol): - """A protocol without __init__ using Protocol from typing_extensions.""" - - -class TestParent(TestProto): - """An implementation.""" - - def __init__(self): - ... - - -class TestChild(TestParent): - """An implementation which should call the init of TestParent.""" - - def __init__(self): # [super-init-not-called] - ... diff --git a/tests/functional/s/super/super_init_not_called_extensions.rc b/tests/functional/s/super/super_init_not_called_extensions.rc deleted file mode 100644 index d584aa9595d..00000000000 --- a/tests/functional/s/super/super_init_not_called_extensions.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -max_pyver=3.9 diff --git a/tests/functional/s/super/super_init_not_called_extensions.txt b/tests/functional/s/super/super_init_not_called_extensions.txt deleted file mode 100644 index b80cb80be4e..00000000000 --- a/tests/functional/s/super/super_init_not_called_extensions.txt +++ /dev/null @@ -1 +0,0 @@ -super-init-not-called:21:4:21:16:TestChild.__init__:__init__ method from base class 'TestParent' is not called:INFERENCE diff --git a/tests/functional/t/too/too_many_arguments.py b/tests/functional/t/too/too_many_arguments.py index 9c7f3ab0879..0d427c7e491 100644 --- a/tests/functional/t/too/too_many_arguments.py +++ b/tests/functional/t/too/too_many_arguments.py @@ -1,6 +1,7 @@ # pylint: disable=missing-docstring,wrong-import-position,unnecessary-dunder-call -def stupid_function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9): # [too-many-arguments] +# +1: [too-many-arguments, too-many-positional-arguments] +def stupid_function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9): return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 diff --git a/tests/functional/t/too/too_many_arguments.txt b/tests/functional/t/too/too_many_arguments.txt index 6d727813ea3..b996058196f 100644 --- a/tests/functional/t/too/too_many_arguments.txt +++ b/tests/functional/t/too/too_many_arguments.txt @@ -1,2 +1,3 @@ -too-many-arguments:3:0:3:19:stupid_function:Too many arguments (9/5):UNDEFINED -too-many-arguments:36:0:36:9:name1:Too many arguments (6/5):UNDEFINED +too-many-arguments:4:0:4:19:stupid_function:Too many arguments (9/5):UNDEFINED +too-many-positional-arguments:4:0:4:19:stupid_function:Too many positional arguments (9/5):HIGH +too-many-arguments:37:0:37:9:name1:Too many arguments (6/5):UNDEFINED diff --git a/tests/functional/t/too/too_many_function_args.py b/tests/functional/t/too/too_many_function_args.py index 9ba49565eb6..848cbd1a0c9 100644 --- a/tests/functional/t/too/too_many_function_args.py +++ b/tests/functional/t/too/too_many_function_args.py @@ -17,3 +17,8 @@ def main(param): if param == 0: tmp = add return tmp(1, 1.01) + + +# Negative case, see `_check_isinstance_args` in `./pylint/checkers/typecheck.py` +isinstance(1, int, int) # [too-many-function-args] +isinstance(1, 1, int) # [too-many-function-args, isinstance-second-argument-not-valid-type] diff --git a/tests/functional/t/too/too_many_function_args.txt b/tests/functional/t/too/too_many_function_args.txt new file mode 100644 index 00000000000..fbc0c97814a --- /dev/null +++ b/tests/functional/t/too/too_many_function_args.txt @@ -0,0 +1,3 @@ +too-many-function-args:23:0:23:23::Too many positional arguments for function call:HIGH +isinstance-second-argument-not-valid-type:24:0:24:21::Second argument of isinstance is not a type:INFERENCE +too-many-function-args:24:0:24:21::Too many positional arguments for function call:HIGH diff --git a/tests/functional/t/too/too_many_locals.py b/tests/functional/t/too/too_many_locals.py index 34395871d7f..8b141039f88 100644 --- a/tests/functional/t/too/too_many_locals.py +++ b/tests/functional/t/too/too_many_locals.py @@ -29,7 +29,8 @@ def too_many_locals_function(): # [too-many-locals] args15 = args14 * 15 return args15 -def too_many_arguments_function(arga, argu, argi, arge, argt, args): # [too-many-arguments] +# +1: [too-many-arguments, too-many-positional-arguments] +def too_many_arguments_function(arga, argu, argi, arge, argt, args): """pylint will complain about too many arguments.""" arga = argu arga += argi diff --git a/tests/functional/t/too/too_many_locals.txt b/tests/functional/t/too/too_many_locals.txt index 19272626f1f..6617661e3c9 100644 --- a/tests/functional/t/too/too_many_locals.txt +++ b/tests/functional/t/too/too_many_locals.txt @@ -1,3 +1,4 @@ too-many-locals:4:0:4:12:function:Too many local variables (16/15):UNDEFINED too-many-locals:12:0:12:28:too_many_locals_function:Too many local variables (16/15):UNDEFINED -too-many-arguments:32:0:32:31:too_many_arguments_function:Too many arguments (6/5):UNDEFINED +too-many-arguments:33:0:33:31:too_many_arguments_function:Too many arguments (6/5):UNDEFINED +too-many-positional-arguments:33:0:33:31:too_many_arguments_function:Too many positional arguments (6/5):HIGH diff --git a/tests/functional/t/too/too_many_positional_arguments.py b/tests/functional/t/too/too_many_positional_arguments.py new file mode 100644 index 00000000000..e373324818f --- /dev/null +++ b/tests/functional/t/too/too_many_positional_arguments.py @@ -0,0 +1,9 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring +class FiveArgumentMethods: + """The max positional arguments default is 5.""" + def fail1(self, a, b, c, d, e): # [too-many-arguments, too-many-positional-arguments] + pass + def fail2(self, a, b, c, d, /, e): # [too-many-arguments, too-many-positional-arguments] + pass + def okay1(self, a, b, c, d, *, e=True): # [too-many-arguments] + pass diff --git a/tests/functional/t/too/too_many_positional_arguments.txt b/tests/functional/t/too/too_many_positional_arguments.txt new file mode 100644 index 00000000000..80591cd3710 --- /dev/null +++ b/tests/functional/t/too/too_many_positional_arguments.txt @@ -0,0 +1,5 @@ +too-many-arguments:4:4:4:13:FiveArgumentMethods.fail1:Too many arguments (6/5):UNDEFINED +too-many-positional-arguments:4:4:4:13:FiveArgumentMethods.fail1:Too many positional arguments (6/5):HIGH +too-many-arguments:6:4:6:13:FiveArgumentMethods.fail2:Too many arguments (6/5):UNDEFINED +too-many-positional-arguments:6:4:6:13:FiveArgumentMethods.fail2:Too many positional arguments (6/5):HIGH +too-many-arguments:8:4:8:13:FiveArgumentMethods.okay1:Too many arguments (6/5):UNDEFINED diff --git a/tests/functional/u/undefined/undefined_variable.py b/tests/functional/u/undefined/undefined_variable.py index 194de114d3b..19c134e77b0 100644 --- a/tests/functional/u/undefined/undefined_variable.py +++ b/tests/functional/u/undefined/undefined_variable.py @@ -380,3 +380,7 @@ def y(self) -> RepeatedReturnAnnotations: # [undefined-variable] pass def z(self) -> RepeatedReturnAnnotations: # [undefined-variable] pass + +class A: + def say_hello(self) -> __module__: # [undefined-variable] + ... diff --git a/tests/functional/u/undefined/undefined_variable.txt b/tests/functional/u/undefined/undefined_variable.txt index ea707c910a2..c527c76d96f 100644 --- a/tests/functional/u/undefined/undefined_variable.txt +++ b/tests/functional/u/undefined/undefined_variable.txt @@ -37,3 +37,4 @@ undefined-variable:365:10:365:20:global_var_mixed_assignment:Undefined variable 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 +undefined-variable:385:27:385:37:A.say_hello:Undefined variable '__module__':UNDEFINED diff --git a/tests/functional/u/unexpected_keyword_arg.py b/tests/functional/u/unexpected_keyword_arg.py index 07b242ec49e..cd736005ffc 100644 --- a/tests/functional/u/unexpected_keyword_arg.py +++ b/tests/functional/u/unexpected_keyword_arg.py @@ -163,3 +163,35 @@ def ambiguous_func6(arg1=42): # Two functions with same keyword argument but mixed defaults (names, constant) func5 = ambiguous_func3 if unknown else ambiguous_func5 func5() + + +# pylint: disable=unused-argument +if do_something(): + class AmbiguousClass: + def __init__(self, feeling="fine"): + ... +else: + class AmbiguousClass: + def __init__(self, feeling="fine", thinking="hard"): + ... + + +AmbiguousClass(feeling="so-so") +AmbiguousClass(thinking="carefully") +AmbiguousClass(worrying="little") # we could raise here if we infer_all() + + +if do_something(): + class NotAmbiguousClass: + def __init__(self, feeling="fine"): + ... +else: + class NotAmbiguousClass: + def __init__(self, feeling="fine"): + ... + + +NotAmbiguousClass(feeling="so-so") +NotAmbiguousClass(worrying="little") # [unexpected-keyword-arg] + +# pylint: enable=unused-argument diff --git a/tests/functional/u/unexpected_keyword_arg.txt b/tests/functional/u/unexpected_keyword_arg.txt index 3cc968e8830..94d3f71bc89 100644 --- a/tests/functional/u/unexpected_keyword_arg.txt +++ b/tests/functional/u/unexpected_keyword_arg.txt @@ -2,3 +2,4 @@ unexpected-keyword-arg:43:0:43:28::Unexpected keyword argument 'internal_arg' in unexpected-keyword-arg:73:0:73:45::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED unexpected-keyword-arg:96:0:96:26::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED unexpected-keyword-arg:118:0:118:30::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED +unexpected-keyword-arg:195:0:195:36::Unexpected keyword argument 'worrying' in constructor call:UNDEFINED diff --git a/tests/functional/u/unexpected_special_method_signature.py b/tests/functional/u/unexpected_special_method_signature.py index 146308b0356..31ea161fafd 100644 --- a/tests/functional/u/unexpected_special_method_signature.py +++ b/tests/functional/u/unexpected_special_method_signature.py @@ -74,6 +74,7 @@ class Valid: def __new__(cls, test, multiple, args): pass + # pylint: disable-next=too-many-positional-arguments def __init__(self, this, can, have, multiple, args, as_well): pass diff --git a/tests/functional/u/uninferable_all_object.py b/tests/functional/u/uninferable_all_object.py index 3e565f9ebf9..9c49c2ab6e6 100644 --- a/tests/functional/u/uninferable_all_object.py +++ b/tests/functional/u/uninferable_all_object.py @@ -2,7 +2,7 @@ __all__ = sorted([ 'Dummy', - 'NonExistant', + 'NonExistent', 'path', 'func', 'inner', diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38.py b/tests/functional/u/unnecessary/unnecessary_dunder_call.py similarity index 100% rename from tests/functional/u/unnecessary/unnecessary_dunder_call_py38.py rename to tests/functional/u/unnecessary/unnecessary_dunder_call.py diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38.txt b/tests/functional/u/unnecessary/unnecessary_dunder_call.txt similarity index 100% rename from tests/functional/u/unnecessary/unnecessary_dunder_call_py38.txt rename to tests/functional/u/unnecessary/unnecessary_dunder_call.txt diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38.rc b/tests/functional/u/unnecessary/unnecessary_dunder_call_py38.rc deleted file mode 100644 index afbcb7d4323..00000000000 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38.rc +++ /dev/null @@ -1,3 +0,0 @@ -[testoptions] -max_pyver=3.8 -except_implementations=PyPy diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.py b/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.py deleted file mode 100644 index f86e14651d6..00000000000 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Checks for unnecessary-dunder-call.""" -# pylint: disable=too-few-public-methods, undefined-variable -# pylint: disable=missing-class-docstring, missing-function-docstring -# pylint: disable=protected-access, unnecessary-lambda-assignment, unnecessary-lambda -from collections import OrderedDict -from typing import Any - -# Test includelisted dunder methods raise lint when manually called. -num_str = some_num.__str__() # [unnecessary-dunder-call] -num_repr = some_num.__add__(2) # [unnecessary-dunder-call] -my_repr = my_module.my_object.__repr__() # [unnecessary-dunder-call] - -MY_CONTAINS_BAD = {1, 2, 3}.__contains__(1) # [unnecessary-dunder-call] -MY_CONTAINS_GOOD = 1 in {1, 2, 3} - -# Just instantiate like a normal person please -my_list_bad = [] -my_list_bad.__init__({1, 2, 3}) # [unnecessary-dunder-call] -my_list_good = list({1, 2, 3}) - -# Test unknown/user-defined dunder methods don't raise lint. -my_woohoo = my_object.__woohoo__() - -# Test lint raised within function. -def is_bigger_than_two(val): - return val.__gt__(2) # [unnecessary-dunder-call] - -# Test dunder methods don't raise lint -# if within a dunder method definition. -class Foo1: - def __init__(self): - object.__init__(self) - -class Foo2: - def __init__(self): - super().__init__(self) - -class Bar1: - def __new__(cls): - object.__new__(cls) - -class Bar2: - def __new__(cls): - super().__new__(cls) - -class CustomRegistry(dict): - def __init__(self) -> None: - super().__init__() - self._entry_ids = {} - - def __setitem__(self, key, entry) -> None: - super().__setitem__(key, entry) - self._entry_ids.__setitem__(entry.id, entry) - self._entry_ids.__delitem__(entry.id) - - def __delitem__(self, key: str) -> None: - entry = self[key] - self._entry_ids.__delitem__(entry.id) - super().__delitem__(key) - -class CustomState: - def __init__(self, state): - self._state = state - - def __eq__(self, other: Any) -> bool: - return self._state.__eq__(other) - -class CustomDict(OrderedDict): - def __init__(self, *args, **kwds): - OrderedDict.__init__(self, *args, **kwds) - - def __setitem__(self, key, value): - OrderedDict.__setitem__(self, key, value) - - -class MyClass(list): - def __contains__(self, item): - print("do some special checks") - return super().__contains__(item) - -class PluginBase: - subclasses = [] - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - cls.subclasses.append(cls) - -# Validate that dunder call is allowed -# at any depth within dunder definition -class SomeClass: - def __init__(self): - self.my_attr = object() - - def __setattr__(self, name, value): - def nested_function(): - self.my_attr.__setattr__(name, value) - - nested_function() - -# Allow use of dunder methods that don't -# have an alternate method of being called -class Base: - @classmethod - def get_first_subclass(cls): - for subklass in cls.__subclasses__(): - return subklass - return object - -# Test no lint raised for attributes. -my_instance_name = x.__class__.__name__ -my_pkg_version = pkg.__version__ - -# Allow use of dunder methods on non instantiated classes -MANUAL_SELF = int.__add__(1, 1) -MY_DICT = {"a": 1, "b": 2} -dict.__setitem__(MY_DICT, "key", "value") - -# Still flag instantiated classes -INSTANTIATED_SELF = int("1").__add__(1) # [unnecessary-dunder-call] -{"a": 1, "b": 2}.__setitem__("key", "value") # [unnecessary-dunder-call] - -# We also exclude dunder methods called on super -# 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__() - -# Test no lint raised for these dunders within lambdas -lambda1 = lambda x: x.__setitem__(1,2) -lambda2 = lambda x: x.__del__(1) -lambda3 = lambda x,y: x.__ipow__(y) -lambda4 = lambda u,v: u.__setitem__(v()) - -# Test lint raised for these dunders within lambdas -lambda5 = lambda x: x.__gt__(3) # [unnecessary-dunder-call] -lambda6 = lambda x,y: x.__or__(y) # [unnecessary-dunder-call] -lambda7 = lambda x: x.__iter__() # [unnecessary-dunder-call] -lambda8 = lambda z: z.__hash__() # [unnecessary-dunder-call] -lambda9 = lambda n: (4).__rmul__(n) # [unnecessary-dunder-call] diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.rc b/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.rc deleted file mode 100644 index 413b991518a..00000000000 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.rc +++ /dev/null @@ -1,3 +0,0 @@ -[testoptions] -max_pyver=3.8 -except_implementations=CPython diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.txt b/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.txt deleted file mode 100644 index 2152103f8c2..00000000000 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call_py38_pypy.txt +++ /dev/null @@ -1,13 +0,0 @@ -unnecessary-dunder-call:9:10:None:None::Unnecessarily calls dunder method __str__. Use str built-in function.:HIGH -unnecessary-dunder-call:10:11:None:None::Unnecessarily calls dunder method __add__. Use + operator.:HIGH -unnecessary-dunder-call:11:10:None:None::Unnecessarily calls dunder method __repr__. Use repr built-in function.:HIGH -unnecessary-dunder-call:13:18:None:None::Unnecessarily calls dunder method __contains__. Use in keyword.:HIGH -unnecessary-dunder-call:18:0:None:None::Unnecessarily calls dunder method __init__. Instantiate class directly.:HIGH -unnecessary-dunder-call:26:11:None:None:is_bigger_than_two:Unnecessarily calls dunder method __gt__. Use > operator.:HIGH -unnecessary-dunder-call:119:20:None:None::Unnecessarily calls dunder method __add__. Use + operator.:HIGH -unnecessary-dunder-call:120:0:None:None::Unnecessarily calls dunder method __setitem__. Set item via subscript.:HIGH -unnecessary-dunder-call:140:20:None:None::Unnecessarily calls dunder method __gt__. Use > operator.:HIGH -unnecessary-dunder-call:141:22:None:None::Unnecessarily calls dunder method __or__. Use | operator.:HIGH -unnecessary-dunder-call:142:20:None:None::Unnecessarily calls dunder method __iter__. Use iter built-in function.:HIGH -unnecessary-dunder-call:143:20:None:None::Unnecessarily calls dunder method __hash__. Use hash built-in function.:HIGH -unnecessary-dunder-call:144:21:None:None::Unnecessarily calls dunder method __rmul__. Use * operator.:HIGH diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.py b/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.py deleted file mode 100644 index f86e14651d6..00000000000 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Checks for unnecessary-dunder-call.""" -# pylint: disable=too-few-public-methods, undefined-variable -# pylint: disable=missing-class-docstring, missing-function-docstring -# pylint: disable=protected-access, unnecessary-lambda-assignment, unnecessary-lambda -from collections import OrderedDict -from typing import Any - -# Test includelisted dunder methods raise lint when manually called. -num_str = some_num.__str__() # [unnecessary-dunder-call] -num_repr = some_num.__add__(2) # [unnecessary-dunder-call] -my_repr = my_module.my_object.__repr__() # [unnecessary-dunder-call] - -MY_CONTAINS_BAD = {1, 2, 3}.__contains__(1) # [unnecessary-dunder-call] -MY_CONTAINS_GOOD = 1 in {1, 2, 3} - -# Just instantiate like a normal person please -my_list_bad = [] -my_list_bad.__init__({1, 2, 3}) # [unnecessary-dunder-call] -my_list_good = list({1, 2, 3}) - -# Test unknown/user-defined dunder methods don't raise lint. -my_woohoo = my_object.__woohoo__() - -# Test lint raised within function. -def is_bigger_than_two(val): - return val.__gt__(2) # [unnecessary-dunder-call] - -# Test dunder methods don't raise lint -# if within a dunder method definition. -class Foo1: - def __init__(self): - object.__init__(self) - -class Foo2: - def __init__(self): - super().__init__(self) - -class Bar1: - def __new__(cls): - object.__new__(cls) - -class Bar2: - def __new__(cls): - super().__new__(cls) - -class CustomRegistry(dict): - def __init__(self) -> None: - super().__init__() - self._entry_ids = {} - - def __setitem__(self, key, entry) -> None: - super().__setitem__(key, entry) - self._entry_ids.__setitem__(entry.id, entry) - self._entry_ids.__delitem__(entry.id) - - def __delitem__(self, key: str) -> None: - entry = self[key] - self._entry_ids.__delitem__(entry.id) - super().__delitem__(key) - -class CustomState: - def __init__(self, state): - self._state = state - - def __eq__(self, other: Any) -> bool: - return self._state.__eq__(other) - -class CustomDict(OrderedDict): - def __init__(self, *args, **kwds): - OrderedDict.__init__(self, *args, **kwds) - - def __setitem__(self, key, value): - OrderedDict.__setitem__(self, key, value) - - -class MyClass(list): - def __contains__(self, item): - print("do some special checks") - return super().__contains__(item) - -class PluginBase: - subclasses = [] - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - cls.subclasses.append(cls) - -# Validate that dunder call is allowed -# at any depth within dunder definition -class SomeClass: - def __init__(self): - self.my_attr = object() - - def __setattr__(self, name, value): - def nested_function(): - self.my_attr.__setattr__(name, value) - - nested_function() - -# Allow use of dunder methods that don't -# have an alternate method of being called -class Base: - @classmethod - def get_first_subclass(cls): - for subklass in cls.__subclasses__(): - return subklass - return object - -# Test no lint raised for attributes. -my_instance_name = x.__class__.__name__ -my_pkg_version = pkg.__version__ - -# Allow use of dunder methods on non instantiated classes -MANUAL_SELF = int.__add__(1, 1) -MY_DICT = {"a": 1, "b": 2} -dict.__setitem__(MY_DICT, "key", "value") - -# Still flag instantiated classes -INSTANTIATED_SELF = int("1").__add__(1) # [unnecessary-dunder-call] -{"a": 1, "b": 2}.__setitem__("key", "value") # [unnecessary-dunder-call] - -# We also exclude dunder methods called on super -# 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__() - -# Test no lint raised for these dunders within lambdas -lambda1 = lambda x: x.__setitem__(1,2) -lambda2 = lambda x: x.__del__(1) -lambda3 = lambda x,y: x.__ipow__(y) -lambda4 = lambda u,v: u.__setitem__(v()) - -# Test lint raised for these dunders within lambdas -lambda5 = lambda x: x.__gt__(3) # [unnecessary-dunder-call] -lambda6 = lambda x,y: x.__or__(y) # [unnecessary-dunder-call] -lambda7 = lambda x: x.__iter__() # [unnecessary-dunder-call] -lambda8 = lambda z: z.__hash__() # [unnecessary-dunder-call] -lambda9 = lambda n: (4).__rmul__(n) # [unnecessary-dunder-call] diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.rc b/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.txt b/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.txt deleted file mode 100644 index 65ab199829f..00000000000 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call_py39.txt +++ /dev/null @@ -1,13 +0,0 @@ -unnecessary-dunder-call:9:10:9:28::Unnecessarily calls dunder method __str__. Use str built-in function.:HIGH -unnecessary-dunder-call:10:11:10:30::Unnecessarily calls dunder method __add__. Use + operator.:HIGH -unnecessary-dunder-call:11:10:11:40::Unnecessarily calls dunder method __repr__. Use repr built-in function.:HIGH -unnecessary-dunder-call:13:18:13:43::Unnecessarily calls dunder method __contains__. Use in keyword.:HIGH -unnecessary-dunder-call:18:0:18:31::Unnecessarily calls dunder method __init__. Instantiate class directly.:HIGH -unnecessary-dunder-call:26:11:26:24:is_bigger_than_two:Unnecessarily calls dunder method __gt__. Use > operator.:HIGH -unnecessary-dunder-call:119:20:119:39::Unnecessarily calls dunder method __add__. Use + operator.:HIGH -unnecessary-dunder-call:120:0:120:44::Unnecessarily calls dunder method __setitem__. Set item via subscript.:HIGH -unnecessary-dunder-call:140:20:140:31::Unnecessarily calls dunder method __gt__. Use > operator.:HIGH -unnecessary-dunder-call:141:22:141:33::Unnecessarily calls dunder method __or__. Use | operator.:HIGH -unnecessary-dunder-call:142:20:142:32::Unnecessarily calls dunder method __iter__. Use iter built-in function.:HIGH -unnecessary-dunder-call:143:20:143:32::Unnecessarily calls dunder method __hash__. Use hash built-in function.:HIGH -unnecessary-dunder-call:144:20:144:35::Unnecessarily calls dunder method __rmul__. Use * operator.:HIGH diff --git a/tests/functional/u/unnecessary/unnecessary_lambda.py b/tests/functional/u/unnecessary/unnecessary_lambda.py index 85c30fef284..6b379165fad 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda.py +++ b/tests/functional/u/unnecessary/unnecessary_lambda.py @@ -1,4 +1,4 @@ -# pylint: disable=undefined-variable, use-list-literal, unnecessary-lambda-assignment, use-dict-literal +# pylint: disable=undefined-variable, use-list-literal, unnecessary-lambda-assignment, use-dict-literal, disallowed-name """test suspicious lambda expressions """ @@ -65,3 +65,23 @@ def f(d): _ = lambda x: x(x) _ = lambda x, y: x(x, y) _ = lambda x: z(lambda y: x + y)(x) + + +# https://github.com/pylint-dev/pylint/issues/8192 + +# foo does not yet exist, so replacing lambda x: foo.get(x) with +# foo.get will raise NameError +g = lambda x: foo.get(x) # [unnecessary-lambda] FALSE POSITIVE + +# an object is created and given the name 'foo' +foo = {1: 2} +assert g(1) == 2 + +# a new object is created and given the name 'foo'; first object is lost +foo = {1: 3} +assert g(1) == 3 + +# the name 'foo' is deleted; second object is lost; there is no foo +del foo + +assert g(1) == 3 # NameError: name 'foo' is not defined diff --git a/tests/functional/u/unnecessary/unnecessary_lambda.txt b/tests/functional/u/unnecessary/unnecessary_lambda.txt index 87f80872cf1..4278090250d 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda.txt +++ b/tests/functional/u/unnecessary/unnecessary_lambda.txt @@ -7,3 +7,4 @@ unnecessary-lambda:23:4:23:53::Lambda may not be necessary:UNDEFINED unnecessary-lambda:25:4:25:71::Lambda may not be necessary:UNDEFINED unnecessary-lambda:29:4:29:31::Lambda may not be necessary:UNDEFINED unnecessary-lambda:31:4:31:44::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:74:4:74:24::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 3201d5407df..b9f8b73251b 100644 --- a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py @@ -156,3 +156,14 @@ def _get_extra_attrs(self, extra_columns): for idx, val in enumerate(my_list): if (val := 42) and my_list[idx] == 'b': print(1) + +def regression_9078(apples, cant_infer_this): + """Regression test for https://github.com/pylint-dev/pylint/issues/9078.""" + for _, _ in enumerate(apples, int(cant_infer_this)): + ... + +def random_uninferrable_start(pears): + import random # pylint: disable=import-outside-toplevel + + for _, _ in enumerate(pears, random.choice([5, 42])): + ... diff --git a/tests/functional/u/unreachable.py b/tests/functional/u/unreachable.py index e59ad218feb..2c9d5ce97ee 100644 --- a/tests/functional/u/unreachable.py +++ b/tests/functional/u/unreachable.py @@ -4,6 +4,7 @@ import os import signal import sys +from typing import NoReturn def func1(): return 1 @@ -79,3 +80,28 @@ def func12(): incognito_function() var = 2 + 2 # [unreachable] print(var) + +def func13(): + def inner() -> NoReturn: + while True: + pass + + inner() + print("unreachable") # [unreachable] + +async def func14(): + async def inner() -> NoReturn: + while True: + pass + + await inner() + print("unreachable") # [unreachable] + +async def func15(): + async def inner() -> NoReturn: + while True: + pass + + coro = inner() + print("reachable") + await coro diff --git a/tests/functional/u/unreachable.txt b/tests/functional/u/unreachable.txt index 82f9797aa1d..4c2b586456c 100644 --- a/tests/functional/u/unreachable.txt +++ b/tests/functional/u/unreachable.txt @@ -1,10 +1,12 @@ -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 +unreachable:11:4:11:24:func1:Unreachable code:HIGH +unreachable:16:8:16:28:func2:Unreachable code:HIGH +unreachable:22:8:22:28:func3:Unreachable code:HIGH +unreachable:26:4:26:16:func4:Unreachable code:HIGH +unreachable:39:4:39:24:func6:Unreachable code:HIGH +unreachable:43:4:43:15:func7:Unreachable code:INFERENCE +unreachable:65:4:65:15:func9:Unreachable code:INFERENCE +unreachable:70:4:70:15:func10:Unreachable code:INFERENCE +unreachable:75:4:75:15:func11:Unreachable code:INFERENCE +unreachable:81:4:81:15:func12:Unreachable code:INFERENCE +unreachable:90:4:90:24:func13:Unreachable code:INFERENCE +unreachable:98:4:98:24:func14:Unreachable code:INFERENCE diff --git a/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.py b/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.py new file mode 100644 index 00000000000..56cce47fb28 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.py @@ -0,0 +1,4 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring +import random +if zero_or_one := random.randint(0, 1): # [using-assignment-expression-in-unsupported-version] + assert zero_or_one == 1 diff --git a/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.rc b/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.rc new file mode 100644 index 00000000000..77eb3be6456 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.rc @@ -0,0 +1,2 @@ +[main] +py-version=3.7 diff --git a/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.txt b/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.txt new file mode 100644 index 00000000000..54e68c2d5c2 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_assignment_expression.txt @@ -0,0 +1 @@ +using-assignment-expression-in-unsupported-version:3:3:3:38::Assignment expression is not supported by all versions included in the py-version setting:HIGH diff --git a/tests/functional/u/unsupported/unsupported_version_for_exception_group.py b/tests/functional/u/unsupported/unsupported_version_for_exception_group.py new file mode 100644 index 00000000000..6327e82f1dd --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_exception_group.py @@ -0,0 +1,21 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring +def f(): + excs = [OSError("error 1"), SystemError("error 2")] + # +1: [using-exception-groups-in-unsupported-version] + raise ExceptionGroup("there were problems", excs) + + +try: # [using-exception-groups-in-unsupported-version] + f() +except* OSError as e: + print("There were OSErrors") +except* SystemError as e: + print("There were SystemErrors") + + +try: + f() +except ExceptionGroup as group: # [using-exception-groups-in-unsupported-version] + # https://github.com/pylint-dev/pylint/issues/8985 + for exc in group.exceptions: # pylint: disable=not-an-iterable + print("ERROR: ", exc) diff --git a/tests/functional/u/used/used_before_assignment_py311.rc b/tests/functional/u/unsupported/unsupported_version_for_exception_group.rc similarity index 54% rename from tests/functional/u/used/used_before_assignment_py311.rc rename to tests/functional/u/unsupported/unsupported_version_for_exception_group.rc index 56e6770585d..4885accdeb0 100644 --- a/tests/functional/u/used/used_before_assignment_py311.rc +++ b/tests/functional/u/unsupported/unsupported_version_for_exception_group.rc @@ -1,2 +1,5 @@ +[main] +py-version=3.10 + [testoptions] min_pyver=3.11 diff --git a/tests/functional/u/unsupported/unsupported_version_for_exception_group.txt b/tests/functional/u/unsupported/unsupported_version_for_exception_group.txt new file mode 100644 index 00000000000..0e48220e980 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_exception_group.txt @@ -0,0 +1,3 @@ +using-exception-groups-in-unsupported-version:5:4:5:53:f:Exception groups are not supported by all versions included in the py-version setting:HIGH +using-exception-groups-in-unsupported-version:8:0:13:36::Exception groups are not supported by all versions included in the py-version setting:HIGH +using-exception-groups-in-unsupported-version:18:0:21:29::Exception groups are not supported by all versions included in the py-version setting:HIGH diff --git a/tests/functional/u/unsupported/unsupported_version_for_f_string.txt b/tests/functional/u/unsupported/unsupported_version_for_f_string.txt index 3c05a79c0a3..634abd3dc3a 100644 --- a/tests/functional/u/unsupported/unsupported_version_for_f_string.txt +++ b/tests/functional/u/unsupported/unsupported_version_for_f_string.txt @@ -1,2 +1,2 @@ -using-f-string-in-unsupported-version:4:6:4:26::F-strings are not supported by all versions included in the py-version setting:UNDEFINED -using-f-string-in-unsupported-version:5:10:5:53::F-strings are not supported by all versions included in the py-version setting:UNDEFINED +using-f-string-in-unsupported-version:4:6:4:26::F-strings are not supported by all versions included in the py-version setting:HIGH +using-f-string-in-unsupported-version:5:10:5:53::F-strings are not supported by all versions included in the py-version setting:HIGH diff --git a/tests/functional/u/unsupported/unsupported_version_for_final.txt b/tests/functional/u/unsupported/unsupported_version_for_final.txt index 51c55b9206d..326bffd4c2d 100644 --- a/tests/functional/u/unsupported/unsupported_version_for_final.txt +++ b/tests/functional/u/unsupported/unsupported_version_for_final.txt @@ -1,9 +1,9 @@ -using-final-decorator-in-unsupported-version:10:1:10:6:MyClass1:typing.final is not supported by all versions included in the py-version setting:UNDEFINED -using-final-decorator-in-unsupported-version:12:5:12:10:MyClass1.my_method:typing.final is not supported by all versions included in the py-version setting:UNDEFINED -using-final-decorator-in-unsupported-version:13:5:13:10:MyClass1.my_method:typing.final is not supported by all versions included in the py-version setting:UNDEFINED -using-final-decorator-in-unsupported-version:18:1:18:8:MyClass2:typing.final is not supported by all versions included in the py-version setting:UNDEFINED -using-final-decorator-in-unsupported-version:20:5:20:12:MyClass2.my_method:typing.final is not supported by all versions included in the py-version setting:UNDEFINED -using-final-decorator-in-unsupported-version:25:1:25:13:MyClass3:typing.final is not supported by all versions included in the py-version setting:UNDEFINED -using-final-decorator-in-unsupported-version:27:5:27:17:MyClass3.my_method:typing.final is not supported by all versions included in the py-version setting:UNDEFINED -using-final-decorator-in-unsupported-version:32:1:32:15:MyClass4:typing.final is not supported by all versions included in the py-version setting:UNDEFINED -using-final-decorator-in-unsupported-version:34:5:34:19:MyClass4.my_method:typing.final is not supported by all versions included in the py-version setting:UNDEFINED +using-final-decorator-in-unsupported-version:10:1:10:6:MyClass1:typing.final is not supported by all versions included in the py-version setting:HIGH +using-final-decorator-in-unsupported-version:12:5:12:10:MyClass1.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH +using-final-decorator-in-unsupported-version:13:5:13:10:MyClass1.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH +using-final-decorator-in-unsupported-version:18:1:18:8:MyClass2:typing.final is not supported by all versions included in the py-version setting:HIGH +using-final-decorator-in-unsupported-version:20:5:20:12:MyClass2.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH +using-final-decorator-in-unsupported-version:25:1:25:13:MyClass3:typing.final is not supported by all versions included in the py-version setting:HIGH +using-final-decorator-in-unsupported-version:27:5:27:17:MyClass3.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH +using-final-decorator-in-unsupported-version:32:1:32:15:MyClass4:typing.final is not supported by all versions included in the py-version setting:HIGH +using-final-decorator-in-unsupported-version:34:5:34:19:MyClass4.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH diff --git a/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.py b/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.py new file mode 100644 index 00000000000..66d56bd4c73 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.py @@ -0,0 +1,5 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring, line-too-long +# +1: [using-generic-type-syntax-in-unsupported-version, using-generic-type-syntax-in-unsupported-version] +type Point[T] = tuple[float, float] +# +1: [using-generic-type-syntax-in-unsupported-version, using-generic-type-syntax-in-unsupported-version] +type Alias[*Ts] = tuple[*Ts] diff --git a/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.rc b/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.rc new file mode 100644 index 00000000000..884bba53e2f --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.rc @@ -0,0 +1,5 @@ +[main] +py-version=3.11 + +[testoptions] +min_pyver=3.12 diff --git a/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.txt b/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.txt new file mode 100644 index 00000000000..7b7deed82c2 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_generic_type_syntax.txt @@ -0,0 +1,4 @@ +using-generic-type-syntax-in-unsupported-version:3:0:3:35::Generic type syntax (PEP 695) is not supported by all versions included in the py-version setting:HIGH +using-generic-type-syntax-in-unsupported-version:3:11:3:12::Generic type syntax (PEP 695) is not supported by all versions included in the py-version setting:HIGH +using-generic-type-syntax-in-unsupported-version:5:0:5:28::Generic type syntax (PEP 695) is not supported by all versions included in the py-version setting:HIGH +using-generic-type-syntax-in-unsupported-version:5:11:5:14::Generic type syntax (PEP 695) is not supported by all versions included in the py-version setting:HIGH diff --git a/tests/functional/u/unsupported/unsupported_version_for_posonly_args.py b/tests/functional/u/unsupported/unsupported_version_for_posonly_args.py new file mode 100644 index 00000000000..27c507d2d08 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_posonly_args.py @@ -0,0 +1,3 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring +def add(x, y, /): # [using-positional-only-args-in-unsupported-version] + return x + y diff --git a/tests/functional/u/unsupported/unsupported_version_for_posonly_args.rc b/tests/functional/u/unsupported/unsupported_version_for_posonly_args.rc new file mode 100644 index 00000000000..77eb3be6456 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_posonly_args.rc @@ -0,0 +1,2 @@ +[main] +py-version=3.7 diff --git a/tests/functional/u/unsupported/unsupported_version_for_posonly_args.txt b/tests/functional/u/unsupported/unsupported_version_for_posonly_args.txt new file mode 100644 index 00000000000..4ae1b275689 --- /dev/null +++ b/tests/functional/u/unsupported/unsupported_version_for_posonly_args.txt @@ -0,0 +1 @@ +using-positional-only-args-in-unsupported-version:2:0:None:None:add:Positional-only arguments are not supported by all versions included in the py-version setting:HIGH diff --git a/tests/functional/u/unused/unused_import.py b/tests/functional/u/unused/unused_import.py index c8e7c5640ba..a2e3ceca3f9 100644 --- a/tests/functional/u/unused/unused_import.py +++ b/tests/functional/u/unused/unused_import.py @@ -39,6 +39,8 @@ class SomeClass: import xml +example: t.Annotated[str, "Path"] = "/foo/bar" + def get_ordered_dict() -> "collections.OrderedDict": return [] diff --git a/tests/functional/u/unused/unused_import.txt b/tests/functional/u/unused/unused_import.txt index f356843fa9d..b8f1b2f8fec 100644 --- a/tests/functional/u/unused/unused_import.txt +++ b/tests/functional/u/unused/unused_import.txt @@ -7,8 +7,8 @@ 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:17:0:17:40::Unused SomeOtherName imported from fake:UNDEFINED -unused-import:54:0:54:9::Unused import os:UNDEFINED -unused-import:89:4:89:19::Unused import unittest:UNDEFINED -unused-import:91:4:91:15::Unused import uuid:UNDEFINED -unused-import:93:4:93:19::Unused import warnings:UNDEFINED -unused-import:95:4:95:21::Unused import compileall:UNDEFINED +unused-import:56:0:56:9::Unused import os:UNDEFINED +unused-import:91:4:91:19::Unused import unittest:UNDEFINED +unused-import:93:4:93:15::Unused import uuid:UNDEFINED +unused-import:95:4:95:19::Unused import warnings:UNDEFINED +unused-import:97:4:97:21::Unused import compileall:UNDEFINED diff --git a/tests/functional/u/unused/unused_import_py39.py b/tests/functional/u/unused/unused_import_py39.py deleted file mode 100644 index 2a897b1741d..00000000000 --- a/tests/functional/u/unused/unused_import_py39.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -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 deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/u/unused/unused_import_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[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 deleted file mode 100644 index 50e5ad5a96f..00000000000 --- a/tests/functional/u/unused/unused_import_py39.txt +++ /dev/null @@ -1 +0,0 @@ -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_py39.rc b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc deleted file mode 100644 index 16b75eea755..00000000000 --- a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.9 diff --git a/tests/functional/u/use/use_implicit_booleaness_not_comparison_to_string.txt b/tests/functional/u/use/use_implicit_booleaness_not_comparison_to_string.txt index 5f07a683c61..191de3a386e 100644 --- a/tests/functional/u/use/use_implicit_booleaness_not_comparison_to_string.txt +++ b/tests/functional/u/use/use_implicit_booleaness_not_comparison_to_string.txt @@ -1,6 +1,6 @@ -use-implicit-booleaness-not-comparison-to-string:6:3:6:10::"""X is ''"" can be simplified to ""not X"", if it is striclty a string, as an empty string is falsey":HIGH -use-implicit-booleaness-not-comparison-to-string:9:3:9:14::"""Y is not ''"" can be simplified to ""Y"", if it is striclty a string, as an empty string is falsey":HIGH -use-implicit-booleaness-not-comparison-to-string:12:3:12:10::"""X == ''"" can be simplified to ""not X"", if it is striclty a string, as an empty string is falsey":HIGH -use-implicit-booleaness-not-comparison-to-string:15:3:15:10::"""Y != ''"" can be simplified to ""Y"", if it is striclty a string, as an empty string is falsey":HIGH -use-implicit-booleaness-not-comparison-to-string:18:3:18:10::"""'' == Y"" can be simplified to ""not Y"", if it is striclty a string, as an empty string is falsey":HIGH -use-implicit-booleaness-not-comparison-to-string:21:3:21:10::"""'' != X"" can be simplified to ""X"", if it is striclty a string, as an empty string is falsey":HIGH +use-implicit-booleaness-not-comparison-to-string:6:3:6:10::"""X is ''"" can be simplified to ""not X"", if it is strictly a string, as an empty string is falsey":HIGH +use-implicit-booleaness-not-comparison-to-string:9:3:9:14::"""Y is not ''"" can be simplified to ""Y"", if it is strictly a string, as an empty string is falsey":HIGH +use-implicit-booleaness-not-comparison-to-string:12:3:12:10::"""X == ''"" can be simplified to ""not X"", if it is strictly a string, as an empty string is falsey":HIGH +use-implicit-booleaness-not-comparison-to-string:15:3:15:10::"""Y != ''"" can be simplified to ""Y"", if it is strictly a string, as an empty string is falsey":HIGH +use-implicit-booleaness-not-comparison-to-string:18:3:18:10::"""'' == Y"" can be simplified to ""not Y"", if it is strictly a string, as an empty string is falsey":HIGH +use-implicit-booleaness-not-comparison-to-string:21:3:21:10::"""'' != X"" can be simplified to ""X"", if it is strictly a string, as an empty string is falsey":HIGH diff --git a/tests/functional/u/use/use_yield_from.py b/tests/functional/u/use/use_yield_from.py index 2ccbb6d77e8..f4dcdc6b9e1 100644 --- a/tests/functional/u/use/use_yield_from.py +++ b/tests/functional/u/use/use_yield_from.py @@ -57,3 +57,13 @@ async def async_for_yield(agen): async def async_yield(agen): for item in agen: yield item + + +# If the return from `yield` is used inline, don't suggest delegation. + +def yield_use_send(): + for item in (1, 2, 3): + _ = yield item + total = 0 + for item in (1, 2, 3): + total += yield item diff --git a/tests/functional/u/used/used_before_assignment.py b/tests/functional/u/used/used_before_assignment.py index c706af043fc..360be6a4f3a 100644 --- a/tests/functional/u/used/used_before_assignment.py +++ b/tests/functional/u/used/used_before_assignment.py @@ -1,7 +1,8 @@ """Miscellaneous used-before-assignment cases""" -# pylint: disable=consider-using-f-string, missing-function-docstring +# pylint: disable=consider-using-f-string, missing-function-docstring, bare-except import datetime import sys +from typing import NoReturn MSG = "hello %s" % MSG # [used-before-assignment] @@ -146,6 +147,14 @@ def turn_on2(**kwargs): if 1 in []: print(PERCENT) + +# Always true +if always_true := True: + ONE = 1 + +print(ONE if always_true else 2) + + # Different test if 1 in [1]: print(SALE) # [used-before-assignment] @@ -205,3 +214,78 @@ def inner_if_continues_outer_if_has_no_other_statements(): else: order = None print(order) + + +class PlatformChecks: # pylint: disable=missing-docstring + """https://github.com/pylint-dev/pylint/issues/9674""" + def skip(self, msg) -> NoReturn: + raise Exception(msg) # pylint: disable=broad-exception-raised + + def print_platform_specific_command(self): + if sys.platform == "linux": + cmd = "ls" + elif sys.platform == "win32": + cmd = "dir" + else: + self.skip("only runs on Linux/Windows") + + print(cmd) + + +# https://github.com/pylint-dev/pylint/issues/9941 +try: + x = 1 / 0 +except ZeroDivisionError: + print(x) # [used-before-assignment] + +try: + y = 1 / 0 + print(y) +except ZeroDivisionError: + print(y) # FALSE NEGATIVE + + +# https://github.com/pylint-dev/pylint/issues/9642 +def __(): + for i in []: + if i: + fail1 = 42 + print(fail1) # [possibly-used-before-assignment] + + for i in []: + fail2 = 42 + print(fail2) # FALSE NEGATIVE + + +# https://github.com/pylint-dev/pylint/issues/9689 +def outer_(): + a = 1 + + def inner_try(): + try: + nonlocal a + print(a) # [used-before-assignment] FALSE POSITIVE + a = 2 + print(a) + except: + pass + + def inner_while(): + i = 0 + while i < 2: + i += 1 + nonlocal a + print(a) # [used-before-assignment] FALSE POSITIVE + a = 2 + print(a) + + def inner_for(): + for _ in range(2): + nonlocal a + print(a) + a = 2 + print(a) + + inner_try() + inner_while() + inner_for() diff --git a/tests/functional/u/used/used_before_assignment.txt b/tests/functional/u/used/used_before_assignment.txt index 6f97f4324cc..3fd2fa6afee 100644 --- a/tests/functional/u/used/used_before_assignment.txt +++ b/tests/functional/u/used/used_before_assignment.txt @@ -1,15 +1,19 @@ -used-before-assignment:6:19:6:22::Using variable 'MSG' before assignment:HIGH -used-before-assignment:8:20:8:24::Using variable 'MSG2' before assignment:HIGH -used-before-assignment:11:4:11:9:outer:Using variable 'inner' before assignment:HIGH -used-before-assignment:20:20:20:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH -used-before-assignment:24:0:24:9::Using variable 'calculate' before assignment:HIGH -used-before-assignment:32:10:32:14:redefine_time_import:Using variable 'time' before assignment:HIGH -used-before-assignment:46:3:46:7::Using variable 'VAR2' before assignment:INFERENCE -possibly-used-before-assignment:64:3:64:7::Possibly using variable 'VAR4' before assignment:INFERENCE -possibly-used-before-assignment:74:3:74:7::Possibly using variable 'VAR5' before assignment:INFERENCE -used-before-assignment:79:3:79:7::Using variable 'VAR6' before assignment:INFERENCE -used-before-assignment:114:6:114:11::Using variable 'VAR10' before assignment:INFERENCE -possibly-used-before-assignment:120:6:120:11::Possibly using variable 'VAR12' before assignment:CONTROL_FLOW -used-before-assignment:151:10:151:14::Using variable 'SALE' before assignment:INFERENCE -used-before-assignment:183:10:183:18::Using variable 'ALL_DONE' before assignment:INFERENCE -used-before-assignment:194:6:194:24::Using variable 'NOT_ALWAYS_DEFINED' before assignment:INFERENCE +used-before-assignment:7:19:7:22::Using variable 'MSG' before assignment:HIGH +used-before-assignment:9:20:9:24::Using variable 'MSG2' before assignment:HIGH +used-before-assignment:12:4:12:9:outer:Using variable 'inner' before assignment:HIGH +used-before-assignment:21:20:21:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH +used-before-assignment:25:0:25:9::Using variable 'calculate' before assignment:HIGH +used-before-assignment:33:10:33:14:redefine_time_import:Using variable 'time' before assignment:HIGH +used-before-assignment:47:3:47:7::Using variable 'VAR2' before assignment:INFERENCE +possibly-used-before-assignment:65:3:65:7::Possibly using variable 'VAR4' before assignment:INFERENCE +possibly-used-before-assignment:75:3:75:7::Possibly using variable 'VAR5' before assignment:INFERENCE +used-before-assignment:80:3:80:7::Using variable 'VAR6' before assignment:INFERENCE +used-before-assignment:115:6:115:11::Using variable 'VAR10' before assignment:INFERENCE +possibly-used-before-assignment:121:6:121:11::Possibly using variable 'VAR12' before assignment:CONTROL_FLOW +used-before-assignment:160:10:160:14::Using variable 'SALE' before assignment:INFERENCE +used-before-assignment:192:10:192:18::Using variable 'ALL_DONE' before assignment:INFERENCE +used-before-assignment:203:6:203:24::Using variable 'NOT_ALWAYS_DEFINED' before assignment:INFERENCE +used-before-assignment:239:10:239:11::Using variable 'x' before assignment:CONTROL_FLOW +possibly-used-before-assignment:253:10:253:15:__:Possibly using variable 'fail1' before assignment:CONTROL_FLOW +used-before-assignment:267:18:267:19:outer_.inner_try:Using variable 'a' before assignment:HIGH +used-before-assignment:278:18:278:19:outer_.inner_while:Using variable 'a' before assignment:HIGH diff --git a/tests/functional/u/used/used_before_assignment_nonlocal.py b/tests/functional/u/used/used_before_assignment_nonlocal.py index 4d926e9eb0a..a3d8ca65173 100644 --- a/tests/functional/u/used/used_before_assignment_nonlocal.py +++ b/tests/functional/u/used/used_before_assignment_nonlocal.py @@ -106,3 +106,60 @@ def inner(): print(args) inner() print(args) + + +def nonlocal_in_outer_frame_fail(): + """Nonlocal declared in outer frame, bad usage and assignment in inner frame.""" + num = 1 + def outer(): + nonlocal num + def inner(): + print(num) # [used-before-assignment] + num = 2 + inner() + outer() + + +def nonlocal_in_outer_frame_ok(callback, condition_a, condition_b): + """Nonlocal declared in outer frame, usage and definition in different frames, + both enclosed in outer frame. + """ + def outer(): + nonlocal callback + if condition_a: + def inner(): + callback() # should not emit possibly-used-before-assignment + inner() + else: + if condition_b: + def callback(): + pass + outer() + + +def nonlocal_in_distant_outer_frame_fail(callback, condition_a, condition_b): + """Nonlocal declared in outer frame, both usage and definition immediately enclosed + in intermediate frame. + """ + def outer(): + nonlocal callback + def intermediate(): + if condition_a: + def inner(): + callback() # [possibly-used-before-assignment] + inner() + else: + if condition_b: + def callback(): + pass + intermediate() + outer() + + +def nonlocal_after_bad_usage_fail(): + """Nonlocal declared after used-before-assignment.""" + num = 1 + def inner(): + num = num + 1 # [used-before-assignment] + nonlocal num + inner() diff --git a/tests/functional/u/used/used_before_assignment_nonlocal.txt b/tests/functional/u/used/used_before_assignment_nonlocal.txt index 2bdbf2fe1ea..d48443f3750 100644 --- a/tests/functional/u/used/used_before_assignment_nonlocal.txt +++ b/tests/functional/u/used/used_before_assignment_nonlocal.txt @@ -6,3 +6,6 @@ used-before-assignment:33:44:33:53:test_fail4:Using variable 'undefined' before 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 +used-before-assignment:117:18:117:21:nonlocal_in_outer_frame_fail.outer.inner:Using variable 'num' before assignment:HIGH +possibly-used-before-assignment:149:20:149:28:nonlocal_in_distant_outer_frame_fail.outer.intermediate.inner:Possibly using variable 'callback' before assignment:CONTROL_FLOW +used-before-assignment:163:14:163:17:nonlocal_after_bad_usage_fail.inner:Using variable 'num' before assignment:HIGH diff --git a/tests/functional/u/used/used_before_assignment_postponed_evaluation.py b/tests/functional/u/used/used_before_assignment_postponed_evaluation.py index 4ff22470cc9..5183e6b9707 100644 --- a/tests/functional/u/used/used_before_assignment_postponed_evaluation.py +++ b/tests/functional/u/used/used_before_assignment_postponed_evaluation.py @@ -1,5 +1,5 @@ """Tests for used-before-assignment when postponed evaluation of annotations is enabled""" -# pylint: disable=missing-function-docstring, invalid-name +# pylint: disable=missing-function-docstring, invalid-name, missing-class-docstring, too-few-public-methods from __future__ import annotations from typing import TYPE_CHECKING @@ -11,3 +11,29 @@ def function_one(m: math): # no error for annotations return m + +# https://github.com/pylint-dev/pylint/issues/8893 +if TYPE_CHECKING: + import datetime + +def f(): + return datetime.datetime.now() # [used-before-assignment] + +def g() -> datetime.datetime: + return datetime.datetime.now() # [used-before-assignment] + +if TYPE_CHECKING: + class X: + pass + +def h(): + return X() # [used-before-assignment] + +def i() -> X: + return X() # [used-before-assignment] + +if TYPE_CHECKING: + from mod import Y + +def j(): + return {Y() for _ in range(1)} # FALSE NEGATIVE diff --git a/tests/functional/u/used/used_before_assignment_postponed_evaluation.txt b/tests/functional/u/used/used_before_assignment_postponed_evaluation.txt index 15681c6dbad..74eaf9b6a5f 100644 --- a/tests/functional/u/used/used_before_assignment_postponed_evaluation.txt +++ b/tests/functional/u/used/used_before_assignment_postponed_evaluation.txt @@ -1 +1,5 @@ used-before-assignment:10:6:10:9::Using variable 'var' before assignment:INFERENCE +used-before-assignment:20:11:20:19:f:Using variable 'datetime' before assignment:INFERENCE +used-before-assignment:23:11:23:19:g:Using variable 'datetime' before assignment:INFERENCE +used-before-assignment:30:11:30:12:h:Using variable 'X' before assignment:INFERENCE +used-before-assignment:33:11:33:12:i:Using variable 'X' before assignment:INFERENCE diff --git a/tests/functional/u/used/used_before_assignment_py310.py b/tests/functional/u/used/used_before_assignment_py310.py index 14f46b61e9f..3734cc2d558 100644 --- a/tests/functional/u/used/used_before_assignment_py310.py +++ b/tests/functional/u/used/used_before_assignment_py310.py @@ -5,3 +5,72 @@ print("x used to cause used-before-assignment!") case _: print("good thing it doesn't now!") + + +# pylint: disable = missing-function-docstring, redefined-outer-name, missing-class-docstring + +# https://github.com/pylint-dev/pylint/issues/9668 +from enum import Enum +from pylint.constants import PY311_PLUS +if PY311_PLUS: + from typing import assert_never # pylint: disable=no-name-in-module +else: + from typing_extensions import assert_never + +class Example(Enum): + FOO = 1 + BAR = 2 + +def check_value_if_then_match_return(example: Example, should_check: bool) -> str | None: + if should_check: + result = None + else: + match example: + case Example.FOO: + result = "foo" + case Example.BAR: + result = "bar" + case _: + return None + + return result # [possibly-used-before-assignment] FALSE POSITIVE + +def check_value_if_then_match_raise(example: Example, should_check: bool) -> str | None: + if should_check: + result = None + else: + match example: + case Example.FOO: + result = "foo" + case Example.BAR: + result = "bar" + case _: + raise ValueError("Not a valid enum") + + return result # [possibly-used-before-assignment] FALSE POSITIVE + +def check_value_if_then_match_assert_never(example: Example, should_check: bool) -> str | None: + if should_check: + result = None + else: + match example: + case Example.FOO: + result = "foo" + case Example.BAR: + result = "bar" + case _: + assert_never(example) + + return result # [possibly-used-before-assignment] FALSE POSITIVE + +def g(x): + if x is None: + y = 0 + else: + match x: + case int(): + y = x + case _: + raise TypeError(type(x)) + + return y # [possibly-used-before-assignment] FALSE POSITIVE diff --git a/tests/functional/u/used/used_before_assignment_py310.txt b/tests/functional/u/used/used_before_assignment_py310.txt new file mode 100644 index 00000000000..75ae6e67744 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_py310.txt @@ -0,0 +1,4 @@ +possibly-used-before-assignment:36:11:36:17:check_value_if_then_match_return:Possibly using variable 'result' before assignment:CONTROL_FLOW +possibly-used-before-assignment:50:11:50:17:check_value_if_then_match_raise:Possibly using variable 'result' before assignment:CONTROL_FLOW +possibly-used-before-assignment:64:11:64:17:check_value_if_then_match_assert_never:Possibly using variable 'result' before assignment:CONTROL_FLOW +possibly-used-before-assignment:76:11:76:12:g:Possibly using variable 'y' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/used/used_before_assignment_py311.py b/tests/functional/u/used/used_before_assignment_py311.py index 2e46ff5fd67..5c69e1067d6 100644 --- a/tests/functional/u/used/used_before_assignment_py311.py +++ b/tests/functional/u/used/used_before_assignment_py311.py @@ -1,6 +1,12 @@ """assert_never() introduced in 3.11""" from enum import Enum -from typing import assert_never + +from pylint.constants import PY311_PLUS + +if PY311_PLUS: + from typing import assert_never # pylint: disable=no-name-in-module +else: + from typing_extensions import assert_never class MyEnum(Enum): diff --git a/tests/functional/u/used/used_before_assignment_py312.py b/tests/functional/u/used/used_before_assignment_py312.py index f47e005b856..7da2a8d0b14 100644 --- a/tests/functional/u/used/used_before_assignment_py312.py +++ b/tests/functional/u/used/used_before_assignment_py312.py @@ -4,3 +4,11 @@ type Point[T] = tuple[T, ...] type Alias[*Ts] = tuple[*Ts] type Alias[**P] = Callable[P] + +# pylint: disable = invalid-name, missing-class-docstring, too-few-public-methods + +# https://github.com/pylint-dev/pylint/issues/9815 +type IntOrX = int | X # [used-before-assignment] FALSE POSITIVE + +class X: + pass diff --git a/tests/functional/u/used/used_before_assignment_py312.txt b/tests/functional/u/used/used_before_assignment_py312.txt new file mode 100644 index 00000000000..e045f5ae43e --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_py312.txt @@ -0,0 +1 @@ +used-before-assignment:11:20:11:21::Using variable 'X' 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 9316285cda1..5229260d2f4 100644 --- a/tests/functional/u/used/used_before_assignment_typing.py +++ b/tests/functional/u/used/used_before_assignment_typing.py @@ -173,8 +173,8 @@ def defined_in_elif_branch(self) -> calendar.Calendar: # [possibly-used-before- def defined_in_else_branch(self) -> urlopen: print(zoneinfo) # [used-before-assignment] - print(pprint()) - print(collections()) + print(pprint()) # [used-before-assignment] + print(collections()) # [used-before-assignment] return urlopen def defined_in_nested_if_else(self) -> heapq: # [possibly-used-before-assignment] diff --git a/tests/functional/u/used/used_before_assignment_typing.txt b/tests/functional/u/used/used_before_assignment_typing.txt index 24900a3f95f..06e162e9c46 100644 --- a/tests/functional/u/used/used_before_assignment_typing.txt +++ b/tests/functional/u/used/used_before_assignment_typing.txt @@ -6,6 +6,8 @@ used-before-assignment:153:20:153:28:VariableAnnotationsGuardedByTypeChecking:Us possibly-used-before-assignment:170:40:170:48:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'calendar' before assignment:INFERENCE possibly-used-before-assignment:171:14:171:20:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'bisect' before assignment:INFERENCE used-before-assignment:175:14:175:22:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'zoneinfo' before assignment:INFERENCE +used-before-assignment:176:14:176:20:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'pprint' before assignment:INFERENCE +used-before-assignment:177:14:177:25:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'collections' before assignment:INFERENCE possibly-used-before-assignment:180:43:180:48:TypeCheckingMultiBranch.defined_in_nested_if_else:Possibly using variable 'heapq' before assignment:INFERENCE used-before-assignment:184:39:184:44:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'array' before assignment:INFERENCE used-before-assignment:185:14:185:19:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'types' before assignment:INFERENCE diff --git a/tests/functional/w/wrong_import_order.txt b/tests/functional/w/wrong_import_order.txt index 068d2140d88..9f143c292de 100644 --- a/tests/functional/w/wrong_import_order.txt +++ b/tests/functional/w/wrong_import_order.txt @@ -1,16 +1,16 @@ -wrong-import-order:12:0:12:14::"standard import ""os.path"" should be placed before third party import ""six""":UNDEFINED -wrong-import-order:14:0:14:10::"standard import ""sys"" should be placed before third party imports ""six"", ""astroid.are_exclusive""":UNDEFINED -wrong-import-order:15:0:15:15::"standard import ""datetime"" should be placed before third party imports ""six"", ""astroid.are_exclusive""":UNDEFINED -wrong-import-order:18:0:18:22::"third party import ""totally_missing"" should be placed before local import ""package.Class""":UNDEFINED -wrong-import-order:20:0:20:14::"third party import ""astroid"" should be placed before local imports ""package.Class"", "".package""":UNDEFINED -wrong-import-order:22:0:22:22::"first party import ""pylint.checkers"" should be placed before local imports ""package.Class"", "".package"", "".package2""":UNDEFINED -wrong-import-order:23:0:23:25::"first party import ""pylint.config"" should be placed before local imports ""package.Class"", "".package"", "".package2""":UNDEFINED -wrong-import-order:24:0:24:17::"first party import ""pylint.sys"" should be placed before local imports ""package.Class"", "".package"", "".package2""":UNDEFINED -wrong-import-order:25:0:25:28::"first party import ""pylint.pyreverse"" should be placed before local imports ""package.Class"", "".package"", "".package2""":UNDEFINED -wrong-import-order:30:0:30:40::"third party import ""six.moves.urllib.parse.quote"" should be placed before first party imports ""pylint.checkers"", ""pylint.config"", ""pylint.sys"", ""pylint.pyreverse"" and local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED -wrong-import-order:31:0:31:23::"first party import ""pylint.constants"" should be placed before local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED -wrong-import-order:32:0:32:19::"standard import ""re"" should be placed before third party imports ""six"", ""astroid.are_exclusive"", ""unused_import"", ""totally_missing"", ""astroid"", ""six.moves.urllib.parse.quote"", first party imports ""pylint.checkers"", ""pylint.config"", ""pylint.sys"", ""pylint.pyreverse"", ""pylint.constants"", and local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED -wrong-import-order:32:0:32:19::"third party import ""requests"" should be placed before first party imports ""pylint.checkers"", ""pylint.config"", ""pylint.sys"", ""pylint.pyreverse"", ""pylint.constants"" and local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED -wrong-import-order:33:0:33:24::"first party import ""pylint.exceptions"" should be placed before local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED -wrong-import-order:34:0:34:21::"first party import ""pylint.message"" should be placed before local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED -wrong-import-order:35:0:35:11::"standard import ""time"" should be placed before third party imports ""six"", ""astroid.are_exclusive"", ""unused_import"" (...) ""astroid"", ""six.moves.urllib.parse.quote"", ""requests"", first party imports ""pylint.checkers"", ""pylint.config"", ""pylint.sys"" (...) ""pylint.constants"", ""pylint.exceptions"", ""pylint.message"", and local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED +wrong-import-order:12:0:12:14::"standard import ""os.path"" should be placed before third party import ""six""":UNDEFINED +wrong-import-order:14:0:14:10::"standard import ""sys"" should be placed before third party imports ""six"", ""astroid.are_exclusive""":UNDEFINED +wrong-import-order:15:0:15:15::"standard import ""datetime"" should be placed before third party imports ""six"", ""astroid.are_exclusive""":UNDEFINED +wrong-import-order:18:0:18:22::"third party import ""totally_missing"" should be placed before local import ""package.Class""":UNDEFINED +wrong-import-order:20:0:20:14::"third party import ""astroid"" should be placed before local imports ""package.Class"", "".package""":UNDEFINED +wrong-import-order:22:0:22:22::"first party import ""pylint.checkers"" should be placed before local imports ""package.Class"", "".package"", "".package2""":UNDEFINED +wrong-import-order:23:0:23:25::"first party import ""pylint.config"" should be placed before local imports ""package.Class"", "".package"", "".package2""":UNDEFINED +wrong-import-order:24:0:24:17::"first party import ""pylint.sys"" should be placed before local imports ""package.Class"", "".package"", "".package2""":UNDEFINED +wrong-import-order:25:0:25:28::"first party import ""pylint.pyreverse"" should be placed before local imports ""package.Class"", "".package"", "".package2""":UNDEFINED +wrong-import-order:30:0:30:40::"third party import ""six.moves.urllib.parse.quote"" should be placed before first party imports ""pylint.checkers"", ""pylint.config"", ""pylint.sys"", ""pylint.pyreverse"" and local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED +wrong-import-order:31:0:31:23::"first party import ""pylint.constants"" should be placed before local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED +wrong-import-order:32:0:32:19::"standard import ""re"" should be placed before third party imports ""six"", ""astroid.are_exclusive"", ""unused_import"", ""totally_missing"", ""astroid"", ""six.moves.urllib.parse.quote"", first party imports ""pylint.checkers"", ""pylint.config"", ""pylint.sys"", ""pylint.pyreverse"", ""pylint.constants"", and local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED +wrong-import-order:32:0:32:19::"third party import ""requests"" should be placed before first party imports ""pylint.checkers"", ""pylint.config"", ""pylint.sys"", ""pylint.pyreverse"", ""pylint.constants"" and local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED +wrong-import-order:33:0:33:24::"first party import ""pylint.exceptions"" should be placed before local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED +wrong-import-order:34:0:34:21::"first party import ""pylint.message"" should be placed before local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED +wrong-import-order:35:0:35:11::"standard import ""time"" should be placed before third party imports ""six"", ""astroid.are_exclusive"", ""unused_import"" (...) ""astroid"", ""six.moves.urllib.parse.quote"", ""requests"", first party imports ""pylint.checkers"", ""pylint.config"", ""pylint.sys"" (...) ""pylint.constants"", ""pylint.exceptions"", ""pylint.message"", and local imports ""package.Class"", "".package"", "".package2"" (...) ""package3.Class3"", "".package4"", ""package4.Class4""":UNDEFINED diff --git a/tests/lint/unittest_expand_modules.py b/tests/lint/unittest_expand_modules.py index 34133d759bc..e8d38e6a576 100644 --- a/tests/lint/unittest_expand_modules.py +++ b/tests/lint/unittest_expand_modules.py @@ -114,6 +114,25 @@ def test__is_in_ignore_list_re_match() -> None: "path": INIT_PATH, } +# A directory that is not a python package. +REPORTERS_PATH = Path(__file__).parent.parent / "reporters" +test_reporters = { # pylint: disable=consider-using-namedtuple-or-dataclass + str(REPORTERS_PATH / "unittest_json_reporter.py"): { + "path": str(REPORTERS_PATH / "unittest_json_reporter.py"), + "name": "reporters.unittest_json_reporter", + "isarg": False, + "basepath": str(REPORTERS_PATH / "__init__.py"), + "basename": "reporters", + }, + str(REPORTERS_PATH / "unittest_reporting.py"): { + "path": str(REPORTERS_PATH / "unittest_reporting.py"), + "name": "reporters.unittest_reporting", + "isarg": False, + "basepath": str(REPORTERS_PATH / "__init__.py"), + "basename": "reporters", + }, +} + def _list_expected_package_modules( deduplicating: bool = False, @@ -174,6 +193,7 @@ class Checker(BaseChecker): for module in _list_expected_package_modules() }, ), + ([str(Path(__file__).parent.parent / "reporters")], test_reporters), ], ) @set_config(ignore_paths="") diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py index 086c1921105..7ba8879e9a7 100644 --- a/tests/lint/unittest_lint.py +++ b/tests/lint/unittest_lint.py @@ -859,7 +859,7 @@ def test_init_hooks_called_before_load_plugins() -> None: def test_analyze_explicit_script(linter: PyLinter) -> None: linter.set_reporter(testutils.GenericTestReporter()) - linter.check([os.path.join(DATA_DIR, "ascript")]) + linter.check([os.path.join(DATA_DIR, "a_script")]) assert len(linter.reporter.messages) == 1 assert linter.reporter.messages[0] == Message( msg_id="C0301", @@ -870,11 +870,11 @@ def test_analyze_explicit_script(linter: PyLinter) -> None: description="Warning without any associated confidence level.", ), location=MessageLocationTuple( - abspath=os.path.join(abspath(dirname(__file__)), "ascript").replace( - f"lint{os.path.sep}ascript", f"data{os.path.sep}ascript" + abspath=os.path.join(abspath(dirname(__file__)), "a_script").replace( + f"lint{os.path.sep}a_script", f"data{os.path.sep}a_script" ), - path=f"tests{os.path.sep}data{os.path.sep}ascript", - module="data.ascript", + path=f"tests{os.path.sep}data{os.path.sep}a_script", + module="data.a_script", obj="", line=2, column=0, diff --git a/tests/profile/test_profile_against_externals.py b/tests/profile/test_profile_against_externals.py deleted file mode 100644 index b646ede9013..00000000000 --- a/tests/profile/test_profile_against_externals.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Profiles basic -jX functionality.""" - -# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE -# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt - -# 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: str) -> list[str]: - assert os.path.exists(scanpath), f"Dir not found {scanpath}" - - filepaths: list[str] = [] - for dirpath, dirnames, filenames in os.walk(scanpath): - dirnames[:] = [dirname for dirname in dirnames if dirname != "__pycache__"] - filepaths.extend( - [ - os.path.join(dirpath, filename) - for filename in filenames - if filename.endswith(".py") - ] - ) - return filepaths - - -@pytest.mark.skipif( - not os.environ.get("PYTEST_PROFILE_EXTERNAL", False), - reason="PYTEST_PROFILE_EXTERNAL, not set, assuming not a profile run", -) -@pytest.mark.parametrize( - "name,git_repo", [("numpy", "https://github.com/numpy/numpy.git")] -) -def test_run(tmp_path: Path, name: str, git_repo: str) -> None: - """Runs pylint against external sources.""" - checkoutdir = tmp_path / name - checkoutdir.mkdir() - Repo.clone_from(url=git_repo, to_path=checkoutdir, depth=1) - filepaths = _get_py_files(scanpath=str(checkoutdir)) - print(f"Have {len(filepaths)} files") - - runner = Run(filepaths, reporter=Reporter(), exit=False) - - print( - f"Had {len(filepaths)} files with {len(runner.linter.reporter.messages)} messages" - ) - pprint.pprint(runner.linter.reporter.messages) diff --git a/tests/pyreverse/test_diadefs.py b/tests/pyreverse/test_diadefs.py index 2b8bd5e324e..06affe12e54 100644 --- a/tests/pyreverse/test_diadefs.py +++ b/tests/pyreverse/test_diadefs.py @@ -8,7 +8,6 @@ from __future__ import annotations -import sys from collections.abc import Iterator from pathlib import Path @@ -248,7 +247,6 @@ 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: GetProjectCallable ) -> None: diff --git a/tests/pyreverse/test_inspector.py b/tests/pyreverse/test_inspector.py index d28d99584fb..b388569ac78 100644 --- a/tests/pyreverse/test_inspector.py +++ b/tests/pyreverse/test_inspector.py @@ -26,7 +26,7 @@ @pytest.fixture -def project(get_project: GetProjectCallable) -> Generator[Project, None, None]: +def project(get_project: GetProjectCallable) -> Generator[Project]: with _test_cwd(TESTS): project = get_project("data", "data") linker = inspector.Linker(project) diff --git a/tests/pyreverse/test_main.py b/tests/pyreverse/test_main.py index e8e46df2c1a..37189c57835 100644 --- a/tests/pyreverse/test_main.py +++ b/tests/pyreverse/test_main.py @@ -65,6 +65,42 @@ def test_project_root_in_sys_path() -> None: assert sys.path == [PROJECT_ROOT_DIR] +def test_discover_package_path_source_root_as_parent(tmp_path: Any) -> None: + """Test discover_package_path when source root is a parent of the module.""" + # Create this temporary structure: + # /tmp_path/ + # └── project/ + # └── my-package/ + # └── __init__.py + project_dir = tmp_path / "project" + package_dir = project_dir / "mypackage" + package_dir.mkdir(parents=True) + (package_dir / "__init__.py").touch() + + # Test with project_dir as source root (parent of package) + result = discover_package_path(str(package_dir), [str(project_dir)]) + assert result == str(project_dir) + + +def test_discover_package_path_source_root_as_child(tmp_path: Any) -> None: + """Test discover_package_path when source root is a child of the module.""" + # Create this temporary structure: + # /tmp_path/ + # └── project/ + # └── src/ + # └── my-package/ + # └── __init__.py + project_dir = tmp_path / "project" + src_dir = project_dir / "src" + package_dir = src_dir / "mypackage" + package_dir.mkdir(parents=True) + (package_dir / "__init__.py").touch() + + # Test with src_dir as source root (child of project) + result = discover_package_path(str(project_dir), [str(src_dir)]) + assert result == str(src_dir) + + @mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock()) @mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock()) @mock.patch("pylint.pyreverse.main.writer") @@ -73,9 +109,7 @@ 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 - main.Run(["-o", "png", TEST_DATA_DIR]) + exit_code = main.Run(["-o", "png", TEST_DATA_DIR]).run() # Check that the right info message is shown to the user assert ( "Format png is not supported natively. Pyreverse will try to generate it using Graphviz..." @@ -83,7 +117,7 @@ def test_graphviz_supported_image_format( ) # Check that pyreverse actually made the call to create the diagram and we exit cleanly mock_writer.DiagramWriter().write.assert_called_once() - assert wrapped_sysexit.value.code == 0 + assert exit_code == 0 @mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock()) @@ -95,9 +129,7 @@ def test_graphviz_cant_determine_supported_formats( ) -> 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: - # we have to catch the SystemExit so the test execution does not stop - main.Run(["-o", "png", TEST_DATA_DIR]) + exit_code = main.Run(["-o", "png", TEST_DATA_DIR]).run() # Check that the right info message is shown to the user assert ( "Unable to determine Graphviz supported output formats." @@ -105,7 +137,7 @@ def test_graphviz_cant_determine_supported_formats( ) # Check that pyreverse actually made the call to create the diagram and we exit cleanly mock_writer.DiagramWriter().write.assert_called_once() - assert wrapped_sysexit.value.code == 0 + assert exit_code == 0 @mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock()) @@ -134,9 +166,7 @@ def test_graphviz_unsupported_image_format(capsys: CaptureFixture) -> None: @pytest.mark.usefixtures("mock_graphviz") def test_verbose(_: mock.MagicMock, capsys: CaptureFixture[str]) -> None: """Test the --verbose flag.""" - with pytest.raises(SystemExit): - # we have to catch the SystemExit so the test execution does not stop - main.Run(["--verbose", TEST_DATA_DIR]) + main.Run(["--verbose", TEST_DATA_DIR]).run() assert "parsing" in capsys.readouterr().out @@ -164,7 +194,7 @@ def test_verbose(_: mock.MagicMock, capsys: CaptureFixture[str]) -> None: @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]) # type: ignore[var-annotated] + run = main.Run([TEST_DATA_DIR]) assert getattr(run.config, arg) == expected_default @@ -177,9 +207,8 @@ def test_command_line_arguments_yes_no( Make sure that we support --module-names=yes syntax instead of using it as a flag. """ - with pytest.raises(SystemExit) as wrapped_sysexit: - main.Run(["--module-names=yes", TEST_DATA_DIR]) - assert wrapped_sysexit.value.code == 0 + exit_code = main.Run(["--module-names=yes", TEST_DATA_DIR]).run() + assert exit_code == 0 @mock.patch("pylint.pyreverse.main.writer") @@ -191,7 +220,7 @@ def test_class_command( Make sure that we append multiple --class arguments to one option destination. """ - runner = main.Run( # type: ignore[var-annotated] + runner = main.Run( [ "--class", "data.clientmodule_test.Ancestor", diff --git a/tests/pyreverse/test_pyreverse_functional.py b/tests/pyreverse/test_pyreverse_functional.py index 715ad3dada0..ac8dab1b196 100644 --- a/tests/pyreverse/test_pyreverse_functional.py +++ b/tests/pyreverse/test_pyreverse_functional.py @@ -38,14 +38,13 @@ def test_class_diagrams(testfile: FunctionalPyreverseTestfile, tmp_path: Path) - else: source_roots = "" for output_format in testfile.options["output_formats"]: - with pytest.raises(SystemExit) as sys_exit: - args = ["-o", f"{output_format}", "-d", str(tmp_path)] - if source_roots: - args += ["--source-roots", source_roots] - args.extend(testfile.options["command_line_args"]) - args += [str(input_file)] - Run(args) - assert sys_exit.value.code == 0 + args = ["-o", f"{output_format}", "-d", str(tmp_path)] + if source_roots: + args += ["--source-roots", source_roots] + args.extend(testfile.options["command_line_args"]) + args += [str(input_file)] + exit_code = Run(args).run() + assert exit_code == 0 assert testfile.source.with_suffix(f".{output_format}").read_text( encoding="utf8" ) == (tmp_path / f"classes.{output_format}").read_text(encoding="utf8") diff --git a/tests/regrtest_data/dummy_wildcard.py b/tests/regrtest_data/dummy_wildcard.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_func.py b/tests/test_func.py index 99805d160eb..cbdc17d1d24 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -113,7 +113,7 @@ def gen_tests( if filter_rgx: is_to_run = re.compile(filter_rgx).search else: - is_to_run = ( # noqa: E731, We're going to throw all this anyway + is_to_run = ( # noqa: E731 we're going to throw all this anyway lambda x: 1 # type: ignore[assignment] # pylint: disable=unnecessary-lambda-assignment ) tests: list[tuple[str, str, list[tuple[str, str]]]] = [] diff --git a/tests/test_self.py b/tests/test_self.py index 1c72bc95f85..d01821c34c5 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -219,6 +219,26 @@ def test_disable_all(self) -> None: self._runtest([UNNECESSARY_LAMBDA, "--disable=all"], out=out, code=32) assert "No files to lint: exiting." in out.getvalue().strip() + def test_disable_all_enable_invalid(self) -> None: + # Reproduces issue #9403. If disable=all is used no error was raised for invalid messages unless + # unknown-option-value was manually enabled. + out = StringIO() + self._runtest( + # Enable one valid message to not run into "No files to lint: exiting." + [ + UNNECESSARY_LAMBDA, + "--disable=all", + "--enable=import-error", + "--enable=foo", + ], + out=out, + code=0, + ) + assert ( + "W0012: Unknown option value for '--enable', expected a valid pylint message and got 'foo'" + in out.getvalue().strip() + ) + def test_output_with_verbose(self) -> None: out = StringIO() self._runtest([UNNECESSARY_LAMBDA, "--verbose"], out=out, code=4) @@ -1387,7 +1407,7 @@ def test_output_of_callback_options( [ [["--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"], "--help-msg: expected at least one argument", True], [["--help-msg", "C0102,C0103"], ":invalid-name (C0103):", False], ], ) diff --git a/tests/test_similar.py b/tests/test_similar.py index 4c12d4366c4..c5357f4b6d4 100644 --- a/tests/test_similar.py +++ b/tests/test_similar.py @@ -22,7 +22,7 @@ CLEAN_PATH = re.escape(dirname(dirname(__file__)) + os.path.sep) -class TestSimilarCodeChecker: +class TestSymilarCodeChecker: def _runtest(self, args: list[str], code: int) -> None: """Runs the tests and sees if output code is as expected.""" out = StringIO() @@ -161,7 +161,7 @@ def test_duplicate_code_raw_strings_disable_line_disable_all(self) -> None: code=0, ) - def test_duplicate_code_raw_strings_disable_line_midle(self) -> None: + def test_duplicate_code_raw_strings_disable_line_middle(self) -> None: """Tests disabling duplicate-code at a line in the middle of a piece of similar code.""" path = join(DATA, "raw_strings_disable_line_middle") self._runtest( diff --git a/tests/testutils/test_lint_module_output_update.py b/tests/testutils/test_lint_module_output_update.py index 038dc77ed35..6d732a57ab7 100644 --- a/tests/testutils/test_lint_module_output_update.py +++ b/tests/testutils/test_lint_module_output_update.py @@ -12,7 +12,6 @@ import pytest -from pylint.constants import IS_PYPY, PY39_PLUS from pylint.testutils import FunctionalTestFile, LintModuleTest from pylint.testutils.functional import LintModuleOutputUpdate @@ -72,10 +71,6 @@ def test_lint_module_output_update_remove_useless_txt( assert not expected_output_file.exists() -@pytest.mark.skipif( - IS_PYPY and not PY39_PLUS, - reason="Requires accurate 'end_col' value to update output", -) @pytest.mark.parametrize( "directory_path", DIRECTORIES, ids=[str(p) for p in DIRECTORIES] ) diff --git a/towncrier.toml b/towncrier.toml index d9c1481d94d..32c1c05023b 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,7 +1,7 @@ [tool.towncrier] -version = "3.3.0" +version = "4.0.0" directory = "doc/whatsnew/fragments" -filename = "doc/whatsnew/3/3.3/index.rst" +filename = "doc/whatsnew/4/4.0/index.rst" template = "doc/whatsnew/fragments/_template.rst" issue_format = "`#{issue} `_" wrap = false # doesn't wrap links correctly if beginning with indentation diff --git a/tox.ini b/tox.ini index b3e429cc8c9..e44d81759fb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.0 -envlist = formatting, py38, py39, py310, py311, py312, pypy, benchmark +envlist = formatting, py39, py310, py311, py312, pypy, benchmark skip_missing_interpreters = true requires = pip >=21.3.1 isolated_build = true @@ -84,14 +84,3 @@ commands = --benchmark-autosave {toxinidir}/tests \ --benchmark-group-by="group" \ {posargs:} - -[testenv:profile_against_external] -setenv = - PYTEST_PROFILE_EXTERNAL = 1 -deps = - -r {toxinidir}/requirements_test.txt - gprof2dot -commands = - pytest --exitfirst \ - --profile-svg \ - {toxinidir}/tests/profile/test_profile_against_externals.py