diff options
701 files changed, 6814 insertions, 2571 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..7b08ae365 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,44 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in the repo. +# Right now there is not default owner to avoid spam +# * @pierre-sassoulas @DanielNoord @cdce8p @jacobtylerwalls @hippo91 + +# Order is important. The last matching pattern has the most precedence. + +### Core components + +# internal message handling +pylint/message/* @pierre-sassoulas +tests/message/* @pierre-sassoulas + +# typing +pylint/typing.py @DanielNoord + +# multiprocessing (doublethefish is not yet a contributor with write access) +# pylint/lint/parallel.py @doublethefish +# tests/test_check_parallel.py @doublethefish + +### Pyreverse +pylint/pyreverse/* @DudeNr33 +tests/pyreverse/* @DudeNr33 + +### Extensions + +# For any all +pylint/extensions/for_any_all.py @areveny +tests/functional/ext/for_any_all/* @areveny + +# Private import +pylint/extensions/private_import.py @areveny +tests/extensions/test_private_import.py @areveny +tests/functional/ext/private_import/* @areveny + +# CodeStyle +pylint/extensions/code_style.* @cdce8p +tests/functional/ext/code_style/* @cdce8p + +# Typing +pylint/extensions/typing.* @cdce8p +tests/functional/ext/typing/* @cdce8p diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 82bd53ea5..dfa06dbbb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,9 +5,9 @@ To ease the process of reviewing your PR, do make sure to complete the following - [ ] Write a good description on what the PR does. - [ ] Create a news fragment with `towncrier create <IssueNumber>.<type>` which will be - included in the changelog. `<type>` can be one of: new_check, removed_check, extension, - false_positive, false_negative, bugfix, other, internal. If necessary you can write - details or offer examples on how the new change is supposed to work. + included in the changelog. `<type>` can be one of: breaking, user_action, feature, + new_check, removed_check, extension, false_positive, false_negative, bugfix, other, internal. + If necessary you can write details or offer examples on how the new change is supposed to work. - [ ] If you used multiple emails or multiple names when contributing, add your mails and preferred name in ``script/.contributors_aliases.json`` --> diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1cce434b2..58f5c1d41 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,6 +20,9 @@ on: schedule: - cron: "44 16 * * 4" +permissions: + contents: read + jobs: analyze: name: Analyze @@ -39,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/primer_comment.yaml b/.github/workflows/primer_comment.yaml index 97d546ccb..3f901278f 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: 1 KEY_PREFIX: venv-primer - DEFAULT_PYTHON: "3.10" + DEFAULT_PYTHON: "3.11" permissions: contents: read diff --git a/.github/workflows/primer_run_main.yaml b/.github/workflows/primer_run_main.yaml index 4f89b9e6e..e8e8f983c 100644 --- a/.github/workflows/primer_run_main.yaml +++ b/.github/workflows/primer_run_main.yaml @@ -19,6 +19,9 @@ env: CACHE_VERSION: 1 KEY_PREFIX: venv-primer +permissions: + contents: read + jobs: run-primer: name: Run / ${{ matrix.python-version }} @@ -26,13 +29,13 @@ jobs: timeout-minutes: 60 strategy: matrix: - python-version: ["3.7", "3.10"] + python-version: ["3.7", "3.11"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -40,7 +43,7 @@ jobs: # Create a re-usable virtual environment - name: Create Python virtual environment cache id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.11 with: path: venv key: @@ -62,10 +65,10 @@ jobs: . venv/bin/activate python tests/primer/__main__.py prepare --make-commit-string output=$(python tests/primer/__main__.py prepare --read-commit-string) - echo "::set-output name=commitstring::$output" + echo "commitstring=$output" >> $GITHUB_OUTPUT - name: Restore projects cache id: cache-projects - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.11 with: path: tests/.pylint_primer_tests/ key: >- @@ -76,10 +79,11 @@ jobs: . venv/bin/activate python tests/primer/__main__.py prepare --clone - name: Upload output diff - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.1 with: name: primer_commitstring - path: tests/.pylint_primer_tests/commit_string.txt + path: + tests/.pylint_primer_tests/commit_string_${{ matrix.python-version }}.txt # Run primer - name: Run pylint primer @@ -92,7 +96,7 @@ jobs: then echo "::warning ::$WARNINGS" fi - name: Upload output - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.1 with: name: primer_output path: >- diff --git a/.github/workflows/primer_run_pr.yaml b/.github/workflows/primer_run_pr.yaml index 4edfefc44..b8f0815dd 100644 --- a/.github/workflows/primer_run_pr.yaml +++ b/.github/workflows/primer_run_pr.yaml @@ -28,6 +28,9 @@ env: CACHE_VERSION: 1 KEY_PREFIX: venv-primer +permissions: + contents: read + jobs: run-primer: name: Run / ${{ matrix.python-version }} @@ -35,15 +38,15 @@ jobs: timeout-minutes: 120 strategy: matrix: - python-version: ["3.7", "3.10"] + python-version: ["3.7", "3.11"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -55,7 +58,7 @@ jobs: # Restore cached Python environment - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.11 with: path: venv key: @@ -123,7 +126,7 @@ jobs: - name: Copy and unzip the commit string run: | unzip primer_commitstring.zip - cp commit_string.txt tests/.pylint_primer_tests/commit_string.txt + cp commit_string_${{ matrix.python-version }}.txt tests/.pylint_primer_tests/commit_string_${{ matrix.python-version }}.txt - name: Unzip the output of 'main' run: unzip primer_output_main.zip - name: Get commit string @@ -131,10 +134,10 @@ jobs: run: | . venv/bin/activate output=$(python tests/primer/__main__.py prepare --read-commit-string) - echo "::set-output name=commitstring::$output" + echo "commitstring=$output" >> $GITHUB_OUTPUT - name: Restore projects cache id: cache-projects - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.11 with: path: tests/.pylint_primer_tests/ key: >- @@ -164,14 +167,14 @@ jobs: then echo "::warning ::$WARNINGS" fi - name: Upload output of PR - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.1 with: name: primer_output_pr path: tests/.pylint_primer_tests/output_${{ steps.python.outputs.python-version }}_pr.txt - name: Upload output of 'main' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.1 with: name: primer_output_main path: output_${{ steps.python.outputs.python-version }}_main.txt @@ -181,7 +184,7 @@ jobs: run: | echo ${{ github.event.pull_request.number }} | tee pr_number.txt - name: Upload PR number - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.1 with: name: pr_number path: pr_number.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9708980d9..6dbe0562f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,10 @@ on: - published env: - DEFAULT_PYTHON: "3.10" + DEFAULT_PYTHON: "3.11" + +permissions: + contents: read jobs: release-pypi: @@ -17,10 +20,10 @@ jobs: url: https://pypi.org/project/pylint/ steps: - name: Check out code from Github - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a440ec26d..b28eab353 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.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 @@ -14,8 +14,8 @@ repos: tests/functional/t/trailing_newlines.py| doc/data/messages/t/trailing-newlines/bad.py| )$ - - repo: https://github.com/myint/autoflake - rev: v1.4 + - repo: https://github.com/PyCQA/autoflake + rev: v2.0.0 hooks: - id: autoflake exclude: &fixtures tests(/\w*)*/functional/|tests/input|doc/data/messages|tests(/\w*)*data/ @@ -33,7 +33,7 @@ repos: exclude: tests(/\w*)*/functional/|tests/input|doc/data/messages|examples/|setup.py|tests(/\w*)*data/ types: [python] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v3.2.2 hooks: - id: pyupgrade args: [--py37-plus] @@ -44,7 +44,7 @@ repos: - id: isort exclude: doc/data/messages/(r/reimported|w/wrong-import-order|u/ungrouped-imports|m/misplaced-future|m/multiple-imports)/bad.py - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.10.0 hooks: - id: black args: [--safe, --quiet] @@ -57,7 +57,8 @@ repos: rev: 5.0.4 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.12.0] + additional_dependencies: + [flake8-bugbear==22.10.27, flake8-typing-imports==1.14.0] exclude: *fixtures - repo: local hooks: @@ -93,14 +94,14 @@ repos: files: ^(doc/whatsnew/fragments) exclude: doc/whatsnew/fragments/_.*.rst - repo: https://github.com/rstcheck/rstcheck - rev: "v6.1.0" + rev: "v6.1.1" hooks: - id: rstcheck args: ["--report-level=warning"] files: ^(doc/(.*/)*.*\.rst) additional_dependencies: [Sphinx==5.0.1] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v0.991 hooks: - id: mypy name: mypy @@ -109,16 +110,23 @@ repos: types: [python] args: [] require_serial: true - additional_dependencies: ["platformdirs==2.2.0", "types-pkg_resources==0.1.3"] + additional_dependencies: + [ + "isort>=5", + "platformdirs==2.2.0", + "py==1.11", + "tomlkit>=0.10.1", + "types-pkg_resources==0.1.3", + ] exclude: tests(/\w*)*/functional/|tests/input|tests(/.*)+/conftest.py|doc/data/messages|tests(/\w*)*data/ - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.0 + rev: v3.0.0-alpha.4 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] exclude: tests(/\w*)*data/ - repo: https://github.com/DanielNoord/pydocstringformatter - rev: v0.7.0 + rev: v0.7.2 hooks: - id: pydocstringformatter exclude: *fixtures diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt index 2aca384fa..29f42e332 100644 --- a/.pyenchant_pylint_custom_dict.txt +++ b/.pyenchant_pylint_custom_dict.txt @@ -31,6 +31,7 @@ bla bom bool boolean +booleaness boolop boundmethod builtins @@ -107,6 +108,7 @@ epytext erroring etree expr +falsey favour filepath filestream @@ -283,6 +285,7 @@ sep setcomp shortstrings singledispatch +singledispatchmethod spammy sqlalchemy src @@ -326,6 +329,7 @@ toplevel towncrier tp truthness +truthey tryexcept txt typecheck @@ -359,6 +363,7 @@ vcg's vectorisation virtualized wc +whitespaces xfails xml xyz diff --git a/Dockerfile b/Dockerfile index 7bde0a292..976a56d47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,6 @@ FROM python:3.10.0-alpine3.15 COPY ./ /tmp/build WORKDIR /tmp/build -RUN python -m pip install . && rm -rf /tmp/build +RUN python -m pip install --no-cache-dir . && rm -rf /tmp/build ENTRYPOINT ["pylint"] diff --git a/README.rst b/README.rst index b432b4fb3..f41ad3e23 100644 --- a/README.rst +++ b/README.rst @@ -33,10 +33,15 @@ :target: https://bestpractices.coreinfrastructure.org/projects/6328 :alt: CII Best Practices +.. image:: https://api.securityscorecards.dev/projects/github.com/PyCQA/pylint/badge + :target: https://api.securityscorecards.dev/projects/github.com/PyCQA/pylint + :alt: OpenSSF Scorecard + .. image:: https://img.shields.io/discord/825463413634891776.svg :target: https://discord.gg/qYxpadCgkx :alt: Discord + What is Pylint? ================ @@ -54,10 +59,19 @@ will know that ``argparse.error(...)`` is in fact a logging call and not an argp .. _`code smells`: https://martinfowler.com/bliki/CodeSmell.html Pylint is highly configurable and permits to write plugins in order to add your -own checks (for example, for internal libraries or an internal rule). Pylint has an -ecosystem of existing plugins for popular frameworks such as `pylint-django`_ or -`pylint-sonarjson`_. +own checks (for example, for internal libraries or an internal rule). Pylint also has an +ecosystem of existing plugins for popular frameworks and third party libraries. + +.. note:: + Pylint supports the Python standard library out of the box. Third-party + libraries are not always supported, so a plugin might be needed. A good place + to start is ``PyPI`` which often returns a plugin by searching for + ``pylint <library>``. `pylint-pydantic`_, `pylint-django`_ and + `pylint-sonarjson`_ are examples of such plugins. More information about plugins + and how to load them can be found at :ref:`plugins <plugins>`. + +.. _`pylint-pydantic`: https://pypi.org/project/pylint-pydantic .. _`pylint-django`: https://github.com/PyCQA/pylint-django .. _`pylint-sonarjson`: https://github.com/omegacen/pylint-sonarjson @@ -72,11 +86,15 @@ Pylint ships with three additional tools: - pyreverse_ (standalone tool that generates package and class diagrams.) - symilar_ (duplicate code finder that is also integrated in pylint) -- epylint_ (Emacs and Flymake compatible Pylint) .. _pyreverse: https://pylint.pycqa.org/en/latest/pyreverse.html .. _symilar: https://pylint.pycqa.org/en/latest/symilar.html + +The epylint_ Emacs package, which includes Flymake support, is now maintained +in `its own repository`_. + .. _epylint: https://pylint.pycqa.org/en/latest/user_guide/ide_integration/flymake-emacs.html +.. _its own repository: https://github.com/emacsorphanage/pylint Projects that you might want to use alongside pylint include flake8_ (faster and simpler checks with very few false positives), mypy_, pyright_ or pyre_ (typing checks), bandit_ (security @@ -84,7 +102,7 @@ oriented checks), black_ and isort_ (auto-formatting), autoflake_ (automated rem unused imports or variables), pyupgrade_ (automated upgrade to newer python syntax) and pydocstringformatter_ (automated pep257). -.. _flake8: https://gitlab.com/pycqa/flake8/ +.. _flake8: https://github.com/PyCQA/flake8 .. _bandit: https://github.com/PyCQA/bandit .. _mypy: https://github.com/python/mypy .. _pyright: https://github.com/microsoft/pyright diff --git a/doc/data/messages/a/anomalous-unicode-escape-in-string/bad.py b/doc/data/messages/a/anomalous-unicode-escape-in-string/bad.py index 40a2a0caf..40275f055 100644 --- a/doc/data/messages/a/anomalous-unicode-escape-in-string/bad.py +++ b/doc/data/messages/a/anomalous-unicode-escape-in-string/bad.py @@ -1 +1 @@ -print(b"\u{0}".format("0394")) # [anomalous-unicode-escape-in-string] +print(b"\u%b" % b"0394") # [anomalous-unicode-escape-in-string] diff --git a/doc/data/messages/a/anomalous-unicode-escape-in-string/good.py b/doc/data/messages/a/anomalous-unicode-escape-in-string/good.py index f2285d70c..c5f4cf46b 100644 --- a/doc/data/messages/a/anomalous-unicode-escape-in-string/good.py +++ b/doc/data/messages/a/anomalous-unicode-escape-in-string/good.py @@ -1 +1 @@ -print(b"\\u{0}".format("0394")) +print(b"\\u%b" % b"0394") diff --git a/doc/data/messages/a/astroid-error/details.rst b/doc/data/messages/a/astroid-error/details.rst index ab8204529..96e8f7ade 100644 --- a/doc/data/messages/a/astroid-error/details.rst +++ b/doc/data/messages/a/astroid-error/details.rst @@ -1 +1,2 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! +This is a message linked to an internal problem in pylint. There's nothing to change in your code, +but maybe in pylint's configuration or installation. diff --git a/doc/data/messages/a/astroid-error/good.py b/doc/data/messages/a/astroid-error/good.py deleted file mode 100644 index c40beb573..000000000 --- a/doc/data/messages/a/astroid-error/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/b/bad-dunder-name/bad.py b/doc/data/messages/b/bad-dunder-name/bad.py new file mode 100644 index 000000000..f01f65010 --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/bad.py @@ -0,0 +1,6 @@ +class Apples: + def _init_(self): # [bad-dunder-name] + pass + + def __hello__(self): # [bad-dunder-name] + print("hello") diff --git a/doc/data/messages/b/bad-dunder-name/good.py b/doc/data/messages/b/bad-dunder-name/good.py new file mode 100644 index 000000000..4f0adb9b6 --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/good.py @@ -0,0 +1,6 @@ +class Apples: + def __init__(self): + pass + + def hello(self): + print("hello") diff --git a/doc/data/messages/b/bad-dunder-name/pylintrc b/doc/data/messages/b/bad-dunder-name/pylintrc new file mode 100644 index 000000000..c70980544 --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.dunder diff --git a/doc/data/messages/b/bad-format-string-key/bad.py b/doc/data/messages/b/bad-format-string-key/bad.py new file mode 100644 index 000000000..346d02d14 --- /dev/null +++ b/doc/data/messages/b/bad-format-string-key/bad.py @@ -0,0 +1 @@ +print("%(one)d" % {"one": 1, 2: 2}) # [bad-format-string-key] diff --git a/doc/data/messages/b/bad-format-string-key/details.rst b/doc/data/messages/b/bad-format-string-key/details.rst index ab8204529..321b4a0ba 100644 --- a/doc/data/messages/b/bad-format-string-key/details.rst +++ b/doc/data/messages/b/bad-format-string-key/details.rst @@ -1 +1,6 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! +This check only works for old-style string formatting using the '%' operator. + +This check only works if the dictionary with the values to be formatted is defined inline. +Passing a variable will not trigger the check as the other keys in this dictionary may be +used in other contexts, while an inline defined dictionary is clearly only intended to hold +the values that should be formatted. diff --git a/doc/data/messages/b/bad-format-string-key/good.py b/doc/data/messages/b/bad-format-string-key/good.py index c40beb573..db7cfde04 100644 --- a/doc/data/messages/b/bad-format-string-key/good.py +++ b/doc/data/messages/b/bad-format-string-key/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +print("%(one)d, %(two)d" % {"one": 1, "two": 2}) diff --git a/doc/data/messages/b/bad-thread-instantiation/bad.py b/doc/data/messages/b/bad-thread-instantiation/bad.py new file mode 100644 index 000000000..580786d85 --- /dev/null +++ b/doc/data/messages/b/bad-thread-instantiation/bad.py @@ -0,0 +1,9 @@ +import threading + + +def thread_target(n): + print(n ** 2) + + +thread = threading.Thread(lambda: None) # [bad-thread-instantiation] +thread.start() diff --git a/doc/data/messages/b/bad-thread-instantiation/details.rst b/doc/data/messages/b/bad-thread-instantiation/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/b/bad-thread-instantiation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/b/bad-thread-instantiation/good.py b/doc/data/messages/b/bad-thread-instantiation/good.py index c40beb573..735fa4da1 100644 --- a/doc/data/messages/b/bad-thread-instantiation/good.py +++ b/doc/data/messages/b/bad-thread-instantiation/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +import threading + + +def thread_target(n): + print(n ** 2) + + +thread = threading.Thread(target=thread_target, args=(10,)) +thread.start() diff --git a/doc/data/messages/b/broad-except/bad.py b/doc/data/messages/b/broad-except/bad.py deleted file mode 100644 index f4946093e..000000000 --- a/doc/data/messages/b/broad-except/bad.py +++ /dev/null @@ -1,4 +0,0 @@ -try: - 1 / 0 -except Exception: # [broad-except] - pass diff --git a/doc/data/messages/b/broad-exception-caught/bad.py b/doc/data/messages/b/broad-exception-caught/bad.py new file mode 100644 index 000000000..3423925a6 --- /dev/null +++ b/doc/data/messages/b/broad-exception-caught/bad.py @@ -0,0 +1,4 @@ +try: + 1 / 0 +except Exception: # [broad-exception-caught] + pass diff --git a/doc/data/messages/b/broad-except/good.py b/doc/data/messages/b/broad-exception-caught/good.py index b02b365b0..b02b365b0 100644 --- a/doc/data/messages/b/broad-except/good.py +++ b/doc/data/messages/b/broad-exception-caught/good.py diff --git a/doc/data/messages/b/broad-exception-raised/bad.py b/doc/data/messages/b/broad-exception-raised/bad.py new file mode 100644 index 000000000..4c8ff3b5a --- /dev/null +++ b/doc/data/messages/b/broad-exception-raised/bad.py @@ -0,0 +1,4 @@ +def small_apple(apple, length): + if len(apple) < length: + raise Exception("Apple is too small!") # [broad-exception-raised] + print(f"{apple} is proper size.") diff --git a/doc/data/messages/b/broad-exception-raised/good.py b/doc/data/messages/b/broad-exception-raised/good.py new file mode 100644 index 000000000..a63b1b356 --- /dev/null +++ b/doc/data/messages/b/broad-exception-raised/good.py @@ -0,0 +1,4 @@ +def small_apple(apple, length): + if len(apple) < length: + raise ValueError("Apple is too small!") + print(f"{apple} is proper size.") diff --git a/doc/data/messages/b/broken-noreturn/bad.py b/doc/data/messages/b/broken-noreturn/bad.py new file mode 100644 index 000000000..77baf763b --- /dev/null +++ b/doc/data/messages/b/broken-noreturn/bad.py @@ -0,0 +1,5 @@ +from typing import NoReturn, Union + + +def exploding_apple(apple) -> Union[None, NoReturn]: # [broken-noreturn] + print(f"{apple} is about to explode") diff --git a/doc/data/messages/b/broken-noreturn/details.rst b/doc/data/messages/b/broken-noreturn/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/b/broken-noreturn/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/b/broken-noreturn/good.py b/doc/data/messages/b/broken-noreturn/good.py index c40beb573..ce4dc6e98 100644 --- a/doc/data/messages/b/broken-noreturn/good.py +++ b/doc/data/messages/b/broken-noreturn/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +from typing import NoReturn + + +def exploding_apple(apple) -> NoReturn: + print(f"{apple} is about to explode") + raise Exception("{apple} exploded !") diff --git a/doc/data/messages/b/broken-noreturn/pylintrc b/doc/data/messages/b/broken-noreturn/pylintrc new file mode 100644 index 000000000..eb28fc75b --- /dev/null +++ b/doc/data/messages/b/broken-noreturn/pylintrc @@ -0,0 +1,3 @@ +[main] +py-version=3.7 +load-plugins=pylint.extensions.typing diff --git a/doc/data/messages/c/condition-evals-to-constant/bad.py b/doc/data/messages/c/condition-evals-to-constant/bad.py new file mode 100644 index 000000000..f52b24fc0 --- /dev/null +++ b/doc/data/messages/c/condition-evals-to-constant/bad.py @@ -0,0 +1,2 @@ +def is_a_fruit(fruit): + return bool(fruit in {"apple", "orange"} or True) # [condition-evals-to-constant] diff --git a/doc/data/messages/c/condition-evals-to-constant/details.rst b/doc/data/messages/c/condition-evals-to-constant/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/c/condition-evals-to-constant/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/c/condition-evals-to-constant/good.py b/doc/data/messages/c/condition-evals-to-constant/good.py index c40beb573..37e975491 100644 --- a/doc/data/messages/c/condition-evals-to-constant/good.py +++ b/doc/data/messages/c/condition-evals-to-constant/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def is_a_fruit(fruit): + return fruit in {"apple", "orange"} diff --git a/doc/data/messages/c/config-parse-error/details.rst b/doc/data/messages/c/config-parse-error/details.rst index ab8204529..4fc0fe076 100644 --- a/doc/data/messages/c/config-parse-error/details.rst +++ b/doc/data/messages/c/config-parse-error/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! +This is a message linked to a problem in your configuration not your code. diff --git a/doc/data/messages/c/config-parse-error/good.py b/doc/data/messages/c/config-parse-error/good.py deleted file mode 100644 index c40beb573..000000000 --- a/doc/data/messages/c/config-parse-error/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/c/confusing-with-statement/bad.py b/doc/data/messages/c/confusing-with-statement/bad.py new file mode 100644 index 000000000..d84288058 --- /dev/null +++ b/doc/data/messages/c/confusing-with-statement/bad.py @@ -0,0 +1,2 @@ +with open('file.txt', 'w') as fh1, fh2: # [confusing-with-statement] + pass diff --git a/doc/data/messages/c/confusing-with-statement/details.rst b/doc/data/messages/c/confusing-with-statement/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/c/confusing-with-statement/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/c/confusing-with-statement/good.py b/doc/data/messages/c/confusing-with-statement/good.py index c40beb573..e8b39d500 100644 --- a/doc/data/messages/c/confusing-with-statement/good.py +++ b/doc/data/messages/c/confusing-with-statement/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +with open('file.txt', 'w', encoding="utf8") as fh1: + with open('file.txt', 'w', encoding="utf8") as fh2: + pass diff --git a/doc/data/messages/c/consider-using-assignment-expr/bad.py b/doc/data/messages/c/consider-using-assignment-expr/bad.py new file mode 100644 index 000000000..a700537fa --- /dev/null +++ b/doc/data/messages/c/consider-using-assignment-expr/bad.py @@ -0,0 +1,4 @@ +apples = 2 + +if apples: # [consider-using-assignment-expr] + print("God apples!") diff --git a/doc/data/messages/c/consider-using-assignment-expr/details.rst b/doc/data/messages/c/consider-using-assignment-expr/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/c/consider-using-assignment-expr/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/c/consider-using-assignment-expr/good.py b/doc/data/messages/c/consider-using-assignment-expr/good.py index c40beb573..a1e402701 100644 --- a/doc/data/messages/c/consider-using-assignment-expr/good.py +++ b/doc/data/messages/c/consider-using-assignment-expr/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +if apples := 2: + print("God apples!") diff --git a/doc/data/messages/c/consider-using-assignment-expr/pylintrc b/doc/data/messages/c/consider-using-assignment-expr/pylintrc new file mode 100644 index 000000000..14b316d48 --- /dev/null +++ b/doc/data/messages/c/consider-using-assignment-expr/pylintrc @@ -0,0 +1,3 @@ +[MAIN] +py-version=3.8 +load-plugins=pylint.extensions.code_style diff --git a/doc/data/messages/c/consider-using-augmented-assign/bad.py b/doc/data/messages/c/consider-using-augmented-assign/bad.py new file mode 100644 index 000000000..90b8931a6 --- /dev/null +++ b/doc/data/messages/c/consider-using-augmented-assign/bad.py @@ -0,0 +1,2 @@ +x = 1 +x = x + 1 # [consider-using-augmented-assign] diff --git a/doc/data/messages/c/consider-using-augmented-assign/good.py b/doc/data/messages/c/consider-using-augmented-assign/good.py new file mode 100644 index 000000000..3e34f6b26 --- /dev/null +++ b/doc/data/messages/c/consider-using-augmented-assign/good.py @@ -0,0 +1,2 @@ +x = 1 +x += 1 diff --git a/doc/data/messages/c/consider-using-augmented-assign/pylintrc b/doc/data/messages/c/consider-using-augmented-assign/pylintrc new file mode 100644 index 000000000..584602294 --- /dev/null +++ b/doc/data/messages/c/consider-using-augmented-assign/pylintrc @@ -0,0 +1,3 @@ +[MAIN] +load-plugins=pylint.extensions.code_style +enable=consider-using-augmented-assign diff --git a/doc/data/messages/d/dict-init-mutate/bad.py b/doc/data/messages/d/dict-init-mutate/bad.py new file mode 100644 index 000000000..d6d1cfe18 --- /dev/null +++ b/doc/data/messages/d/dict-init-mutate/bad.py @@ -0,0 +1,3 @@ +fruit_prices = {} # [dict-init-mutate] +fruit_prices['apple'] = 1 +fruit_prices['banana'] = 10 diff --git a/doc/data/messages/d/dict-init-mutate/good.py b/doc/data/messages/d/dict-init-mutate/good.py new file mode 100644 index 000000000..02137f287 --- /dev/null +++ b/doc/data/messages/d/dict-init-mutate/good.py @@ -0,0 +1 @@ +fruit_prices = {"apple": 1, "banana": 10} diff --git a/doc/data/messages/d/dict-init-mutate/pylintrc b/doc/data/messages/d/dict-init-mutate/pylintrc new file mode 100644 index 000000000..bbe6bd1f7 --- /dev/null +++ b/doc/data/messages/d/dict-init-mutate/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.dict_init_mutate, diff --git a/doc/data/messages/e/exec-used/details.rst b/doc/data/messages/e/exec-used/details.rst index 2a61975f2..246857f32 100644 --- a/doc/data/messages/e/exec-used/details.rst +++ b/doc/data/messages/e/exec-used/details.rst @@ -1 +1,10 @@ -The available methods and variables used in ``exec()`` may introduce a security hole. You can restrict the use of these variables and methods by passing optional globals and locals parameters (dictionaries) to the ``exec()`` method. +The available methods and variables used in ``exec()`` may introduce a security hole. +You can restrict the use of these variables and methods by passing optional globals +and locals parameters (dictionaries) to the ``exec()`` method. + +However, use of ``exec`` is still insecure. For example, consider the following call +that writes a file to the user's system: + +.. code-block:: python + + exec("""\nwith open("file.txt", "w", encoding="utf-8") as file:\n file.write("# code as nefarious as imaginable")\n""") diff --git a/doc/data/messages/f/fatal/details.rst b/doc/data/messages/f/fatal/details.rst index ab8204529..1c4303137 100644 --- a/doc/data/messages/f/fatal/details.rst +++ b/doc/data/messages/f/fatal/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! +This is a message linked to an internal problem in pylint. There's nothing to change in your code. diff --git a/doc/data/messages/f/fatal/good.py b/doc/data/messages/f/fatal/good.py deleted file mode 100644 index c40beb573..000000000 --- a/doc/data/messages/f/fatal/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/i/invalid-bool-returned/bad.py b/doc/data/messages/i/invalid-bool-returned/bad.py new file mode 100644 index 000000000..8e2df42d9 --- /dev/null +++ b/doc/data/messages/i/invalid-bool-returned/bad.py @@ -0,0 +1,5 @@ +class BadBool: + """__bool__ returns an int""" + + def __bool__(self): # [invalid-bool-returned] + return 1 diff --git a/doc/data/messages/i/invalid-bool-returned/details.rst b/doc/data/messages/i/invalid-bool-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-bool-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-bool-returned/good.py b/doc/data/messages/i/invalid-bool-returned/good.py index c40beb573..33e00c0e3 100644 --- a/doc/data/messages/i/invalid-bool-returned/good.py +++ b/doc/data/messages/i/invalid-bool-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodBool: + """__bool__ returns `bool`""" + + def __bool__(self): + return True diff --git a/doc/data/messages/i/invalid-bytes-returned/bad.py b/doc/data/messages/i/invalid-bytes-returned/bad.py new file mode 100644 index 000000000..5068c85f9 --- /dev/null +++ b/doc/data/messages/i/invalid-bytes-returned/bad.py @@ -0,0 +1,5 @@ +class BadBytes: + """__bytes__ returns <type 'str'>""" + + def __bytes__(self): # [invalid-bytes-returned] + return "123" diff --git a/doc/data/messages/i/invalid-bytes-returned/details.rst b/doc/data/messages/i/invalid-bytes-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-bytes-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-bytes-returned/good.py b/doc/data/messages/i/invalid-bytes-returned/good.py index c40beb573..3bc95489f 100644 --- a/doc/data/messages/i/invalid-bytes-returned/good.py +++ b/doc/data/messages/i/invalid-bytes-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodBytes: + """__bytes__ returns <type 'bytes'>""" + + def __bytes__(self): + return b"some bytes" diff --git a/doc/data/messages/i/invalid-characters-in-docstring/details.rst b/doc/data/messages/i/invalid-characters-in-docstring/details.rst index ab8204529..9977db144 100644 --- a/doc/data/messages/i/invalid-characters-in-docstring/details.rst +++ b/doc/data/messages/i/invalid-characters-in-docstring/details.rst @@ -1 +1,2 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! +This is a message linked to an internal problem in enchant. There's nothing to change in your code, +but maybe in pylint's configuration or the way you installed the 'enchant' system library. diff --git a/doc/data/messages/i/invalid-characters-in-docstring/good.py b/doc/data/messages/i/invalid-characters-in-docstring/good.py deleted file mode 100644 index c40beb573..000000000 --- a/doc/data/messages/i/invalid-characters-in-docstring/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/i/invalid-class-object/bad.py b/doc/data/messages/i/invalid-class-object/bad.py new file mode 100644 index 000000000..5c6a6f8df --- /dev/null +++ b/doc/data/messages/i/invalid-class-object/bad.py @@ -0,0 +1,5 @@ +class Apple: + pass + + +Apple.__class__ = 1 # [invalid-class-object] diff --git a/doc/data/messages/i/invalid-class-object/details.rst b/doc/data/messages/i/invalid-class-object/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-class-object/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-class-object/good.py b/doc/data/messages/i/invalid-class-object/good.py index c40beb573..3b50097f1 100644 --- a/doc/data/messages/i/invalid-class-object/good.py +++ b/doc/data/messages/i/invalid-class-object/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Apple: + pass + + +class RedDelicious: + pass + + +Apple.__class__ = RedDelicious diff --git a/doc/data/messages/i/invalid-format-returned/bad.py b/doc/data/messages/i/invalid-format-returned/bad.py new file mode 100644 index 000000000..21412d91b --- /dev/null +++ b/doc/data/messages/i/invalid-format-returned/bad.py @@ -0,0 +1,5 @@ +class BadFormat: + """__format__ returns <type 'int'>""" + + def __format__(self, format_spec): # [invalid-format-returned] + return 1 diff --git a/doc/data/messages/i/invalid-format-returned/details.rst b/doc/data/messages/i/invalid-format-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-format-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-format-returned/good.py b/doc/data/messages/i/invalid-format-returned/good.py index c40beb573..69ab6fc07 100644 --- a/doc/data/messages/i/invalid-format-returned/good.py +++ b/doc/data/messages/i/invalid-format-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodFormat: + """__format__ returns <type 'str'>""" + + def __format__(self, format_spec): + return "hello!" diff --git a/doc/data/messages/i/invalid-getnewargs-ex-returned/bad.py b/doc/data/messages/i/invalid-getnewargs-ex-returned/bad.py new file mode 100644 index 000000000..2f5c5742e --- /dev/null +++ b/doc/data/messages/i/invalid-getnewargs-ex-returned/bad.py @@ -0,0 +1,5 @@ +class BadGetNewArgsEx: + """__getnewargs_ex__ returns tuple with incorrect arg length""" + + def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] + return (tuple(1), dict(x="y"), 1) diff --git a/doc/data/messages/i/invalid-getnewargs-ex-returned/details.rst b/doc/data/messages/i/invalid-getnewargs-ex-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-getnewargs-ex-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-getnewargs-ex-returned/good.py b/doc/data/messages/i/invalid-getnewargs-ex-returned/good.py index c40beb573..b9cbb0288 100644 --- a/doc/data/messages/i/invalid-getnewargs-ex-returned/good.py +++ b/doc/data/messages/i/invalid-getnewargs-ex-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodGetNewArgsEx: + """__getnewargs_ex__ returns <type 'tuple'>""" + + def __getnewargs_ex__(self): + return ((1,), {"2": 2}) diff --git a/doc/data/messages/i/invalid-getnewargs-returned/bad.py b/doc/data/messages/i/invalid-getnewargs-returned/bad.py new file mode 100644 index 000000000..0864f7deb --- /dev/null +++ b/doc/data/messages/i/invalid-getnewargs-returned/bad.py @@ -0,0 +1,5 @@ +class BadGetNewArgs: + """__getnewargs__ returns an integer""" + + def __getnewargs__(self): # [invalid-getnewargs-returned] + return 1 diff --git a/doc/data/messages/i/invalid-getnewargs-returned/details.rst b/doc/data/messages/i/invalid-getnewargs-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-getnewargs-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-getnewargs-returned/good.py b/doc/data/messages/i/invalid-getnewargs-returned/good.py index c40beb573..bdc547d4d 100644 --- a/doc/data/messages/i/invalid-getnewargs-returned/good.py +++ b/doc/data/messages/i/invalid-getnewargs-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodGetNewArgs: + """__getnewargs__ returns <type 'tuple'>""" + + def __getnewargs__(self): + return (1, 2) diff --git a/doc/data/messages/i/invalid-hash-returned/bad.py b/doc/data/messages/i/invalid-hash-returned/bad.py new file mode 100644 index 000000000..ef0a9cb3f --- /dev/null +++ b/doc/data/messages/i/invalid-hash-returned/bad.py @@ -0,0 +1,5 @@ +class BadHash: + """__hash__ returns dict""" + + def __hash__(self): # [invalid-hash-returned] + return {} diff --git a/doc/data/messages/i/invalid-hash-returned/details.rst b/doc/data/messages/i/invalid-hash-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-hash-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-hash-returned/good.py b/doc/data/messages/i/invalid-hash-returned/good.py index c40beb573..c912bf5a4 100644 --- a/doc/data/messages/i/invalid-hash-returned/good.py +++ b/doc/data/messages/i/invalid-hash-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodHash: + """__hash__ returns `int`""" + + def __hash__(self): + return 19 diff --git a/doc/data/messages/i/invalid-index-returned/bad.py b/doc/data/messages/i/invalid-index-returned/bad.py new file mode 100644 index 000000000..197de0104 --- /dev/null +++ b/doc/data/messages/i/invalid-index-returned/bad.py @@ -0,0 +1,5 @@ +class BadIndex: + """__index__ returns a dict""" + + def __index__(self): # [invalid-index-returned] + return {"19": "19"} diff --git a/doc/data/messages/i/invalid-index-returned/details.rst b/doc/data/messages/i/invalid-index-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-index-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-index-returned/good.py b/doc/data/messages/i/invalid-index-returned/good.py index c40beb573..3455ac278 100644 --- a/doc/data/messages/i/invalid-index-returned/good.py +++ b/doc/data/messages/i/invalid-index-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodIndex: + """__index__ returns <type 'int'>""" + + def __index__(self): + return 19 diff --git a/doc/data/messages/i/invalid-length-hint-returned/bad.py b/doc/data/messages/i/invalid-length-hint-returned/bad.py new file mode 100644 index 000000000..9ec400ccc --- /dev/null +++ b/doc/data/messages/i/invalid-length-hint-returned/bad.py @@ -0,0 +1,5 @@ +class BadLengthHint: + """__length_hint__ returns non-int""" + + def __length_hint__(self): # [invalid-length-hint-returned] + return 3.0 diff --git a/doc/data/messages/i/invalid-length-hint-returned/details.rst b/doc/data/messages/i/invalid-length-hint-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-length-hint-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-length-hint-returned/good.py b/doc/data/messages/i/invalid-length-hint-returned/good.py index c40beb573..ec294183a 100644 --- a/doc/data/messages/i/invalid-length-hint-returned/good.py +++ b/doc/data/messages/i/invalid-length-hint-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class GoodLengthHint: + """__length_hint__ returns <type 'int'>""" + + def __length_hint__(self): + return 10 diff --git a/doc/data/messages/i/invalid-metaclass/bad.py b/doc/data/messages/i/invalid-metaclass/bad.py new file mode 100644 index 000000000..301b4f20e --- /dev/null +++ b/doc/data/messages/i/invalid-metaclass/bad.py @@ -0,0 +1,2 @@ +class Apple(metaclass=int): # [invalid-metaclass] + pass diff --git a/doc/data/messages/i/invalid-metaclass/details.rst b/doc/data/messages/i/invalid-metaclass/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-metaclass/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-metaclass/good.py b/doc/data/messages/i/invalid-metaclass/good.py index c40beb573..e8b90fc01 100644 --- a/doc/data/messages/i/invalid-metaclass/good.py +++ b/doc/data/messages/i/invalid-metaclass/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Plant: + pass + +class Apple(Plant): + pass diff --git a/doc/data/messages/i/invalid-repr-returned/bad.py b/doc/data/messages/i/invalid-repr-returned/bad.py new file mode 100644 index 000000000..33d22256c --- /dev/null +++ b/doc/data/messages/i/invalid-repr-returned/bad.py @@ -0,0 +1,5 @@ +class Repr: + """__repr__ returns <type 'int'>""" + + def __repr__(self): # [invalid-repr-returned] + return 1 diff --git a/doc/data/messages/i/invalid-repr-returned/details.rst b/doc/data/messages/i/invalid-repr-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-repr-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-repr-returned/good.py b/doc/data/messages/i/invalid-repr-returned/good.py index c40beb573..120fbc4d7 100644 --- a/doc/data/messages/i/invalid-repr-returned/good.py +++ b/doc/data/messages/i/invalid-repr-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Repr: + """__repr__ returns <type 'str'>""" + + def __repr__(self): + return "apples" diff --git a/doc/data/messages/i/invalid-slice-step/bad.py b/doc/data/messages/i/invalid-slice-step/bad.py new file mode 100644 index 000000000..a860ce14a --- /dev/null +++ b/doc/data/messages/i/invalid-slice-step/bad.py @@ -0,0 +1,3 @@ +LETTERS = ["a", "b", "c", "d"] + +LETTERS[::0] # [invalid-slice-step] diff --git a/doc/data/messages/i/invalid-slice-step/good.py b/doc/data/messages/i/invalid-slice-step/good.py new file mode 100644 index 000000000..c81d80331 --- /dev/null +++ b/doc/data/messages/i/invalid-slice-step/good.py @@ -0,0 +1,3 @@ +LETTERS = ["a", "b", "c", "d"] + +LETTERS[::2] diff --git a/doc/data/messages/i/invalid-str-returned/bad.py b/doc/data/messages/i/invalid-str-returned/bad.py new file mode 100644 index 000000000..6826ce325 --- /dev/null +++ b/doc/data/messages/i/invalid-str-returned/bad.py @@ -0,0 +1,5 @@ +class Str: + """__str__ returns int""" + + def __str__(self): # [invalid-str-returned] + return 1 diff --git a/doc/data/messages/i/invalid-str-returned/details.rst b/doc/data/messages/i/invalid-str-returned/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/invalid-str-returned/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/invalid-str-returned/good.py b/doc/data/messages/i/invalid-str-returned/good.py index c40beb573..bf2682b23 100644 --- a/doc/data/messages/i/invalid-str-returned/good.py +++ b/doc/data/messages/i/invalid-str-returned/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Str: + """__str__ returns <type 'str'>""" + + def __str__(self): + return "oranges" diff --git a/doc/data/messages/i/isinstance-second-argument-not-valid-type/bad.py b/doc/data/messages/i/isinstance-second-argument-not-valid-type/bad.py new file mode 100644 index 000000000..5fb3b8375 --- /dev/null +++ b/doc/data/messages/i/isinstance-second-argument-not-valid-type/bad.py @@ -0,0 +1 @@ +isinstance("apples and oranges", hex) # [isinstance-second-argument-not-valid-type] diff --git a/doc/data/messages/i/isinstance-second-argument-not-valid-type/details.rst b/doc/data/messages/i/isinstance-second-argument-not-valid-type/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/i/isinstance-second-argument-not-valid-type/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/i/isinstance-second-argument-not-valid-type/good.py b/doc/data/messages/i/isinstance-second-argument-not-valid-type/good.py index c40beb573..c1df5fca8 100644 --- a/doc/data/messages/i/isinstance-second-argument-not-valid-type/good.py +++ b/doc/data/messages/i/isinstance-second-argument-not-valid-type/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +isinstance("apples and oranges", str) diff --git a/doc/data/messages/m/magic-value-comparison/bad.py b/doc/data/messages/m/magic-value-comparison/bad.py new file mode 100644 index 000000000..536659abe --- /dev/null +++ b/doc/data/messages/m/magic-value-comparison/bad.py @@ -0,0 +1,10 @@ +import random + +measurement = random.randint(0, 200) +above_threshold = False +i = 0 +while i < 5: # [magic-value-comparison] + above_threshold = measurement > 100 # [magic-value-comparison] + if above_threshold: + break + measurement = random.randint(0, 200) diff --git a/doc/data/messages/m/magic-value-comparison/good.py b/doc/data/messages/m/magic-value-comparison/good.py new file mode 100644 index 000000000..4b8960906 --- /dev/null +++ b/doc/data/messages/m/magic-value-comparison/good.py @@ -0,0 +1,15 @@ +import random + +MAX_NUM_OF_ITERATIONS = 5 +THRESHOLD_VAL = 100 +MIN_MEASUREMENT_VAL = 0 +MAX_MEASUREMENT_VAL = 200 + +measurement = random.randint(MIN_MEASUREMENT_VAL, MAX_MEASUREMENT_VAL) +above_threshold = False +i = 0 +while i < MAX_NUM_OF_ITERATIONS: + above_threshold = measurement > THRESHOLD_VAL + if above_threshold: + break + measurement = random.randint(MIN_MEASUREMENT_VAL, MAX_MEASUREMENT_VAL) diff --git a/doc/data/messages/m/magic-value-comparison/pylintrc b/doc/data/messages/m/magic-value-comparison/pylintrc new file mode 100644 index 000000000..c4980c135 --- /dev/null +++ b/doc/data/messages/m/magic-value-comparison/pylintrc @@ -0,0 +1,2 @@ +[main] +load-plugins=pylint.extensions.magic_value diff --git a/doc/data/messages/m/method-check-failed/details.rst b/doc/data/messages/m/method-check-failed/details.rst index ab8204529..1c4303137 100644 --- a/doc/data/messages/m/method-check-failed/details.rst +++ b/doc/data/messages/m/method-check-failed/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! +This is a message linked to an internal problem in pylint. There's nothing to change in your code. diff --git a/doc/data/messages/m/method-check-failed/good.py b/doc/data/messages/m/method-check-failed/good.py deleted file mode 100644 index c40beb573..000000000 --- a/doc/data/messages/m/method-check-failed/good.py +++ /dev/null @@ -1 +0,0 @@ -# This is a placeholder for correct code for this message. diff --git a/doc/data/messages/m/misplaced-comparison-constant/bad.py b/doc/data/messages/m/misplaced-comparison-constant/bad.py new file mode 100644 index 000000000..1a5712a32 --- /dev/null +++ b/doc/data/messages/m/misplaced-comparison-constant/bad.py @@ -0,0 +1,8 @@ +def compare_apples(apples=20): + for i in range(10): + if 5 <= i: # [misplaced-comparison-constant] + pass + if 1 == i: # [misplaced-comparison-constant] + pass + if 20 < len(apples): # [misplaced-comparison-constant] + pass diff --git a/doc/data/messages/m/misplaced-comparison-constant/details.rst b/doc/data/messages/m/misplaced-comparison-constant/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/m/misplaced-comparison-constant/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/m/misplaced-comparison-constant/good.py b/doc/data/messages/m/misplaced-comparison-constant/good.py index c40beb573..ba00a7f23 100644 --- a/doc/data/messages/m/misplaced-comparison-constant/good.py +++ b/doc/data/messages/m/misplaced-comparison-constant/good.py @@ -1 +1,8 @@ -# This is a placeholder for correct code for this message. +def compare_apples(apples=20): + for i in range(10): + if i >= 5: + pass + if i == 1: + pass + if len(apples) > 20: + pass diff --git a/doc/data/messages/m/misplaced-comparison-constant/pylintrc b/doc/data/messages/m/misplaced-comparison-constant/pylintrc new file mode 100644 index 000000000..aa3b1f8eb --- /dev/null +++ b/doc/data/messages/m/misplaced-comparison-constant/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.comparison_placement diff --git a/doc/data/messages/m/missing-raises-doc/pylintrc b/doc/data/messages/m/missing-raises-doc/pylintrc index 4547f9811..7bdd1242d 100644 --- a/doc/data/messages/m/missing-raises-doc/pylintrc +++ b/doc/data/messages/m/missing-raises-doc/pylintrc @@ -1,2 +1,5 @@ [MAIN] load-plugins = pylint.extensions.docparams + +[BASIC] +accept-no-raise-doc = no diff --git a/doc/data/messages/m/modified-iterating-dict/bad.py b/doc/data/messages/m/modified-iterating-dict/bad.py new file mode 100644 index 000000000..cd31a62db --- /dev/null +++ b/doc/data/messages/m/modified-iterating-dict/bad.py @@ -0,0 +1,6 @@ +fruits = {"apple": 1, "orange": 2, "mango": 3} + +i = 0 +for fruit in fruits: + fruits["apple"] = i # [modified-iterating-dict] + i += 1 diff --git a/doc/data/messages/m/modified-iterating-dict/details.rst b/doc/data/messages/m/modified-iterating-dict/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/m/modified-iterating-dict/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/m/modified-iterating-dict/good.py b/doc/data/messages/m/modified-iterating-dict/good.py index c40beb573..8755a6c45 100644 --- a/doc/data/messages/m/modified-iterating-dict/good.py +++ b/doc/data/messages/m/modified-iterating-dict/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +fruits = {"apple": 1, "orange": 2, "mango": 3} + +i = 0 +for fruit in fruits.copy(): + fruits["apple"] = i + i += 1 diff --git a/doc/data/messages/m/modified-iterating-list/bad.py b/doc/data/messages/m/modified-iterating-list/bad.py new file mode 100644 index 000000000..57d77150e --- /dev/null +++ b/doc/data/messages/m/modified-iterating-list/bad.py @@ -0,0 +1,3 @@ +fruits = ["apple", "orange", "mango"] +for fruit in fruits: + fruits.append("pineapple") # [modified-iterating-list] diff --git a/doc/data/messages/m/modified-iterating-list/details.rst b/doc/data/messages/m/modified-iterating-list/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/m/modified-iterating-list/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/m/modified-iterating-list/good.py b/doc/data/messages/m/modified-iterating-list/good.py index c40beb573..0132f8b64 100644 --- a/doc/data/messages/m/modified-iterating-list/good.py +++ b/doc/data/messages/m/modified-iterating-list/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +fruits = ["apple", "orange", "mango"] +for fruit in fruits.copy(): + fruits.append("pineapple") diff --git a/doc/data/messages/m/modified-iterating-set/bad.py b/doc/data/messages/m/modified-iterating-set/bad.py new file mode 100644 index 000000000..bd82a564f --- /dev/null +++ b/doc/data/messages/m/modified-iterating-set/bad.py @@ -0,0 +1,3 @@ +fruits = {"apple", "orange", "mango"} +for fruit in fruits: + fruits.add(fruit + "yum") # [modified-iterating-set] diff --git a/doc/data/messages/m/modified-iterating-set/details.rst b/doc/data/messages/m/modified-iterating-set/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/m/modified-iterating-set/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/m/modified-iterating-set/good.py b/doc/data/messages/m/modified-iterating-set/good.py index c40beb573..0af8e2426 100644 --- a/doc/data/messages/m/modified-iterating-set/good.py +++ b/doc/data/messages/m/modified-iterating-set/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +fruits = {"apple", "orange", "mango"} +for fruit in fruits.copy(): + fruits.add(fruit + "yum") diff --git a/doc/data/messages/m/multiple-statements/bad.py b/doc/data/messages/m/multiple-statements/bad.py new file mode 100644 index 000000000..754eede36 --- /dev/null +++ b/doc/data/messages/m/multiple-statements/bad.py @@ -0,0 +1,5 @@ +fruits = ["apple", "orange", "mango"] + +if "apple" in fruits: pass # [multiple-statements] +else: + print("no apples!") diff --git a/doc/data/messages/m/multiple-statements/details.rst b/doc/data/messages/m/multiple-statements/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/m/multiple-statements/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/m/multiple-statements/good.py b/doc/data/messages/m/multiple-statements/good.py index c40beb573..9328c83fd 100644 --- a/doc/data/messages/m/multiple-statements/good.py +++ b/doc/data/messages/m/multiple-statements/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +fruits = ["apple", "orange", "mango"] + +if "apple" in fruits: + pass +else: + print("no apples!") diff --git a/doc/data/messages/n/named-expr-without-context/bad.py b/doc/data/messages/n/named-expr-without-context/bad.py new file mode 100644 index 000000000..c5d2ffba7 --- /dev/null +++ b/doc/data/messages/n/named-expr-without-context/bad.py @@ -0,0 +1 @@ +(a := 42) # [named-expr-without-context] diff --git a/doc/data/messages/n/named-expr-without-context/good.py b/doc/data/messages/n/named-expr-without-context/good.py new file mode 100644 index 000000000..50f6b2621 --- /dev/null +++ b/doc/data/messages/n/named-expr-without-context/good.py @@ -0,0 +1,2 @@ +if (a := 42): + print('Success') diff --git a/doc/data/messages/n/nan-comparison/bad.py b/doc/data/messages/n/nan-comparison/bad.py new file mode 100644 index 000000000..911686520 --- /dev/null +++ b/doc/data/messages/n/nan-comparison/bad.py @@ -0,0 +1,5 @@ +import numpy as np + + +def both_nan(x, y) -> bool: + return x == np.NaN and y == float("nan") # [nan-comparison, nan-comparison] diff --git a/doc/data/messages/n/nan-comparison/details.rst b/doc/data/messages/n/nan-comparison/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/n/nan-comparison/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/n/nan-comparison/good.py b/doc/data/messages/n/nan-comparison/good.py index c40beb573..31f54edf4 100644 --- a/doc/data/messages/n/nan-comparison/good.py +++ b/doc/data/messages/n/nan-comparison/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +import numpy as np + + +def both_nan(x, y) -> bool: + return np.isnan(x) and np.isnan(y) diff --git a/doc/data/messages/n/nested-min-max/bad.py b/doc/data/messages/n/nested-min-max/bad.py new file mode 100644 index 000000000..b3e13db3a --- /dev/null +++ b/doc/data/messages/n/nested-min-max/bad.py @@ -0,0 +1 @@ +print(min(1, min(2, 3))) # [nested-min-max] diff --git a/doc/data/messages/n/nested-min-max/good.py b/doc/data/messages/n/nested-min-max/good.py new file mode 100644 index 000000000..2d348b224 --- /dev/null +++ b/doc/data/messages/n/nested-min-max/good.py @@ -0,0 +1 @@ +print(min(1, 2, 3)) diff --git a/doc/data/messages/n/no-classmethod-decorator/bad.py b/doc/data/messages/n/no-classmethod-decorator/bad.py new file mode 100644 index 000000000..55c4f4d0f --- /dev/null +++ b/doc/data/messages/n/no-classmethod-decorator/bad.py @@ -0,0 +1,11 @@ +class Fruit: + COLORS = [] + + def __init__(self, color): + self.color = color + + def pick_colors(cls, *args): + """classmethod to pick fruit colors""" + cls.COLORS = args + + pick_colors = classmethod(pick_colors) # [no-classmethod-decorator] diff --git a/doc/data/messages/n/no-classmethod-decorator/details.rst b/doc/data/messages/n/no-classmethod-decorator/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/n/no-classmethod-decorator/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/n/no-classmethod-decorator/good.py b/doc/data/messages/n/no-classmethod-decorator/good.py index c40beb573..9b70c769d 100644 --- a/doc/data/messages/n/no-classmethod-decorator/good.py +++ b/doc/data/messages/n/no-classmethod-decorator/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +class Fruit: + COLORS = [] + + def __init__(self, color): + self.color = color + + @classmethod + def pick_colors(cls, *args): + """classmethod to pick fruit colors""" + cls.COLORS = args diff --git a/doc/data/messages/n/non-ascii-module-import/bad.py b/doc/data/messages/n/non-ascii-module-import/bad.py new file mode 100644 index 000000000..ce2e811c6 --- /dev/null +++ b/doc/data/messages/n/non-ascii-module-import/bad.py @@ -0,0 +1,3 @@ +from os.path import join as łos # [non-ascii-module-import] + +foo = łos("a", "b") diff --git a/doc/data/messages/n/non-ascii-module-import/details.rst b/doc/data/messages/n/non-ascii-module-import/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/n/non-ascii-module-import/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/n/non-ascii-module-import/good.py b/doc/data/messages/n/non-ascii-module-import/good.py index c40beb573..388a5c78e 100644 --- a/doc/data/messages/n/non-ascii-module-import/good.py +++ b/doc/data/messages/n/non-ascii-module-import/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +from os.path import join as os_join + +foo = os_join("a", "b") diff --git a/doc/data/messages/n/non-ascii-name/bad.py b/doc/data/messages/n/non-ascii-name/bad.py new file mode 100644 index 000000000..954532794 --- /dev/null +++ b/doc/data/messages/n/non-ascii-name/bad.py @@ -0,0 +1 @@ +ápple_count = 4444 # [non-ascii-name] diff --git a/doc/data/messages/n/non-ascii-name/details.rst b/doc/data/messages/n/non-ascii-name/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/n/non-ascii-name/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/n/non-ascii-name/good.py b/doc/data/messages/n/non-ascii-name/good.py index c40beb573..bbddb08d5 100644 --- a/doc/data/messages/n/non-ascii-name/good.py +++ b/doc/data/messages/n/non-ascii-name/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +apple_count = 4444 diff --git a/doc/data/messages/n/non-str-assignment-to-dunder-name/bad.py b/doc/data/messages/n/non-str-assignment-to-dunder-name/bad.py new file mode 100644 index 000000000..59f13a13c --- /dev/null +++ b/doc/data/messages/n/non-str-assignment-to-dunder-name/bad.py @@ -0,0 +1,5 @@ +class Fruit: + pass + + +Fruit.__name__ = 1 # [non-str-assignment-to-dunder-name] diff --git a/doc/data/messages/n/non-str-assignment-to-dunder-name/details.rst b/doc/data/messages/n/non-str-assignment-to-dunder-name/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/n/non-str-assignment-to-dunder-name/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/n/non-str-assignment-to-dunder-name/good.py b/doc/data/messages/n/non-str-assignment-to-dunder-name/good.py index c40beb573..ff55f1800 100644 --- a/doc/data/messages/n/non-str-assignment-to-dunder-name/good.py +++ b/doc/data/messages/n/non-str-assignment-to-dunder-name/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Fruit: + pass + + +Fruit.__name__ = "FRUIT" diff --git a/doc/data/messages/n/nonlocal-without-binding/bad.py b/doc/data/messages/n/nonlocal-without-binding/bad.py new file mode 100644 index 000000000..6a166e09f --- /dev/null +++ b/doc/data/messages/n/nonlocal-without-binding/bad.py @@ -0,0 +1,3 @@ +class Fruit: + def get_color(self): + nonlocal colors # [nonlocal-without-binding] diff --git a/doc/data/messages/n/nonlocal-without-binding/details.rst b/doc/data/messages/n/nonlocal-without-binding/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/n/nonlocal-without-binding/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/n/nonlocal-without-binding/good.py b/doc/data/messages/n/nonlocal-without-binding/good.py index c40beb573..cce884ac8 100644 --- a/doc/data/messages/n/nonlocal-without-binding/good.py +++ b/doc/data/messages/n/nonlocal-without-binding/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +class Fruit: + colors = ["red", "green"] + + def get_color(self): + nonlocal colors diff --git a/doc/data/messages/n/not-a-mapping/bad.py b/doc/data/messages/n/not-a-mapping/bad.py new file mode 100644 index 000000000..79ca9215e --- /dev/null +++ b/doc/data/messages/n/not-a-mapping/bad.py @@ -0,0 +1,5 @@ +def print_colors(**colors): + print(colors) + + +print_colors(**list("red", "black")) # [not-a-mapping] diff --git a/doc/data/messages/n/not-a-mapping/details.rst b/doc/data/messages/n/not-a-mapping/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/n/not-a-mapping/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/n/not-a-mapping/good.py b/doc/data/messages/n/not-a-mapping/good.py index c40beb573..3de53ac5d 100644 --- a/doc/data/messages/n/not-a-mapping/good.py +++ b/doc/data/messages/n/not-a-mapping/good.py @@ -1 +1,5 @@ -# This is a placeholder for correct code for this message. +def print_colors(**colors): + print(colors) + + +print_colors(**dict(red=1, black=2)) diff --git a/doc/data/messages/p/parse-error/details.rst b/doc/data/messages/p/parse-error/details.rst index ab8204529..1c4303137 100644 --- a/doc/data/messages/p/parse-error/details.rst +++ b/doc/data/messages/p/parse-error/details.rst @@ -1 +1 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! +This is a message linked to an internal problem in pylint. There's nothing to change in your code. diff --git a/doc/data/messages/p/possibly-unused-variable/bad.py b/doc/data/messages/p/possibly-unused-variable/bad.py new file mode 100644 index 000000000..b64aee88b --- /dev/null +++ b/doc/data/messages/p/possibly-unused-variable/bad.py @@ -0,0 +1,4 @@ +def choose_fruits(fruits): + print(fruits) + color = "red" # [possibly-unused-variable] + return locals() diff --git a/doc/data/messages/p/possibly-unused-variable/details.rst b/doc/data/messages/p/possibly-unused-variable/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/p/possibly-unused-variable/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/p/possibly-unused-variable/good.py b/doc/data/messages/p/possibly-unused-variable/good.py index c40beb573..095118328 100644 --- a/doc/data/messages/p/possibly-unused-variable/good.py +++ b/doc/data/messages/p/possibly-unused-variable/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +def choose_fruits(fruits): + current_locals = locals() + print(fruits) + color = "red" + print(color) + return current_locals diff --git a/doc/data/messages/p/preferred-module/bad.py b/doc/data/messages/p/preferred-module/bad.py new file mode 100644 index 000000000..a047ff36d --- /dev/null +++ b/doc/data/messages/p/preferred-module/bad.py @@ -0,0 +1 @@ +import urllib # [preferred-module] diff --git a/doc/data/messages/p/preferred-module/details.rst b/doc/data/messages/p/preferred-module/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/p/preferred-module/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/p/preferred-module/good.py b/doc/data/messages/p/preferred-module/good.py index c40beb573..20b15530d 100644 --- a/doc/data/messages/p/preferred-module/good.py +++ b/doc/data/messages/p/preferred-module/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +import requests diff --git a/doc/data/messages/p/preferred-module/pylintrc b/doc/data/messages/p/preferred-module/pylintrc new file mode 100644 index 000000000..00ee49930 --- /dev/null +++ b/doc/data/messages/p/preferred-module/pylintrc @@ -0,0 +1,2 @@ +[IMPORTS] +preferred-modules=urllib:requests, diff --git a/doc/data/messages/r/redefined-outer-name/bad.py b/doc/data/messages/r/redefined-outer-name/bad.py new file mode 100644 index 000000000..3d03c9cd5 --- /dev/null +++ b/doc/data/messages/r/redefined-outer-name/bad.py @@ -0,0 +1,6 @@ +count = 10 + + +def count_it(count): # [redefined-outer-name] + for i in range(count): + print(i) diff --git a/doc/data/messages/r/redefined-outer-name/details.rst b/doc/data/messages/r/redefined-outer-name/details.rst index ab8204529..475e6a344 100644 --- a/doc/data/messages/r/redefined-outer-name/details.rst +++ b/doc/data/messages/r/redefined-outer-name/details.rst @@ -1 +1,23 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! +A common issue is that this message is triggered when using `pytest` `fixtures <https://docs.pytest.org/en/7.1.x/how-to/fixtures.html>`_: + +.. code-block:: python + + import pytest + + @pytest.fixture + def setup(): + ... + + + def test_something(setup): # [redefined-outer-name] + ... + +One solution to this problem is to explicitly name the fixture: + +.. code-block:: python + + @pytest.fixture(name="setup") + def setup_fixture(): + ... + +Alternatively `pylint` plugins like `pylint-pytest <https://pypi.org/project/pylint-pytest/>`_ can be used. diff --git a/doc/data/messages/r/redefined-outer-name/good.py b/doc/data/messages/r/redefined-outer-name/good.py index c40beb573..135059838 100644 --- a/doc/data/messages/r/redefined-outer-name/good.py +++ b/doc/data/messages/r/redefined-outer-name/good.py @@ -1 +1,6 @@ -# This is a placeholder for correct code for this message. +count = 10 + + +def count_it(limit): + for i in range(limit): + print(i) diff --git a/doc/data/messages/r/redundant-returns-doc/bad.py b/doc/data/messages/r/redundant-returns-doc/bad.py new file mode 100644 index 000000000..5d018db4c --- /dev/null +++ b/doc/data/messages/r/redundant-returns-doc/bad.py @@ -0,0 +1,9 @@ +def print_fruits(fruits): # [redundant-returns-doc] + """Print list of fruits + + Returns + ------- + str + """ + print(fruits) + return None diff --git a/doc/data/messages/r/redundant-returns-doc/details.rst b/doc/data/messages/r/redundant-returns-doc/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/r/redundant-returns-doc/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/r/redundant-returns-doc/good.py b/doc/data/messages/r/redundant-returns-doc/good.py index c40beb573..7f3eeebbb 100644 --- a/doc/data/messages/r/redundant-returns-doc/good.py +++ b/doc/data/messages/r/redundant-returns-doc/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +def print_fruits(fruits): + """Print list of fruits + + Returns + ------- + str + """ + print(fruits) + return ",".join(fruits) diff --git a/doc/data/messages/r/redundant-returns-doc/pylintrc b/doc/data/messages/r/redundant-returns-doc/pylintrc new file mode 100644 index 000000000..4547f9811 --- /dev/null +++ b/doc/data/messages/r/redundant-returns-doc/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins = pylint.extensions.docparams diff --git a/doc/data/messages/r/redundant-u-string-prefix/bad.py b/doc/data/messages/r/redundant-u-string-prefix/bad.py new file mode 100644 index 000000000..bae9738a5 --- /dev/null +++ b/doc/data/messages/r/redundant-u-string-prefix/bad.py @@ -0,0 +1,2 @@ +def print_fruit(): + print(u"Apple") # [redundant-u-string-prefix] diff --git a/doc/data/messages/r/redundant-u-string-prefix/details.rst b/doc/data/messages/r/redundant-u-string-prefix/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/r/redundant-u-string-prefix/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/r/redundant-u-string-prefix/good.py b/doc/data/messages/r/redundant-u-string-prefix/good.py index c40beb573..64b2d500d 100644 --- a/doc/data/messages/r/redundant-u-string-prefix/good.py +++ b/doc/data/messages/r/redundant-u-string-prefix/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def print_fruit(): + print("Apple") diff --git a/doc/data/messages/r/redundant-yields-doc/bad.py b/doc/data/messages/r/redundant-yields-doc/bad.py new file mode 100644 index 000000000..c2d1e6875 --- /dev/null +++ b/doc/data/messages/r/redundant-yields-doc/bad.py @@ -0,0 +1,9 @@ +def give_fruits(fruits): # [redundant-yields-doc] + """Something about fruits + + Yields + ------- + list + fruits + """ + return fruits diff --git a/doc/data/messages/r/redundant-yields-doc/details.rst b/doc/data/messages/r/redundant-yields-doc/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/r/redundant-yields-doc/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/r/redundant-yields-doc/good.py b/doc/data/messages/r/redundant-yields-doc/good.py index c40beb573..1055a0c60 100644 --- a/doc/data/messages/r/redundant-yields-doc/good.py +++ b/doc/data/messages/r/redundant-yields-doc/good.py @@ -1 +1,10 @@ -# This is a placeholder for correct code for this message. +def give_fruits(fruits): + """Something about fruits + + Yields + ------- + str + fruit + """ + for fruit in fruits: + yield fruit diff --git a/doc/data/messages/r/redundant-yields-doc/pylintrc b/doc/data/messages/r/redundant-yields-doc/pylintrc new file mode 100644 index 000000000..4547f9811 --- /dev/null +++ b/doc/data/messages/r/redundant-yields-doc/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins = pylint.extensions.docparams diff --git a/doc/data/messages/s/self-cls-assignment/bad.py b/doc/data/messages/s/self-cls-assignment/bad.py new file mode 100644 index 000000000..64541405f --- /dev/null +++ b/doc/data/messages/s/self-cls-assignment/bad.py @@ -0,0 +1,9 @@ +class Fruit: + @classmethod + def list_fruits(cls): + cls = 'apple' # [self-cls-assignment] + + def print_color(self, *colors): + self = "red" # [self-cls-assignment] + color = colors[1] + print(color) diff --git a/doc/data/messages/s/self-cls-assignment/details.rst b/doc/data/messages/s/self-cls-assignment/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/s/self-cls-assignment/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/s/self-cls-assignment/good.py b/doc/data/messages/s/self-cls-assignment/good.py index c40beb573..ae8b172fa 100644 --- a/doc/data/messages/s/self-cls-assignment/good.py +++ b/doc/data/messages/s/self-cls-assignment/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Fruit: + @classmethod + def list_fruits(cls): + fruit = 'apple' + print(fruit) + + def print_color(self, *colors): + color = colors[1] + print(color) diff --git a/doc/data/messages/s/simplifiable-condition/bad.py b/doc/data/messages/s/simplifiable-condition/bad.py new file mode 100644 index 000000000..e3ffe5de9 --- /dev/null +++ b/doc/data/messages/s/simplifiable-condition/bad.py @@ -0,0 +1,2 @@ +def has_apples(apples) -> bool: + return bool(apples or False) # [simplifiable-condition] diff --git a/doc/data/messages/s/simplifiable-condition/details.rst b/doc/data/messages/s/simplifiable-condition/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/s/simplifiable-condition/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/s/simplifiable-condition/good.py b/doc/data/messages/s/simplifiable-condition/good.py index c40beb573..400a2788c 100644 --- a/doc/data/messages/s/simplifiable-condition/good.py +++ b/doc/data/messages/s/simplifiable-condition/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def has_apples(apples) -> bool: + return bool(apples) diff --git a/doc/data/messages/s/simplify-boolean-expression/bad.py b/doc/data/messages/s/simplify-boolean-expression/bad.py new file mode 100644 index 000000000..06806d350 --- /dev/null +++ b/doc/data/messages/s/simplify-boolean-expression/bad.py @@ -0,0 +1,2 @@ +def has_oranges(oranges, apples=None) -> bool: + return apples and False or oranges # [simplify-boolean-expression] diff --git a/doc/data/messages/s/simplify-boolean-expression/details.rst b/doc/data/messages/s/simplify-boolean-expression/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/s/simplify-boolean-expression/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/s/simplify-boolean-expression/good.py b/doc/data/messages/s/simplify-boolean-expression/good.py index c40beb573..ca1c8a26f 100644 --- a/doc/data/messages/s/simplify-boolean-expression/good.py +++ b/doc/data/messages/s/simplify-boolean-expression/good.py @@ -1 +1,2 @@ -# This is a placeholder for correct code for this message. +def has_oranges(oranges, apples=None) -> bool: + return oranges diff --git a/doc/data/messages/s/singledispatch-method/bad.py b/doc/data/messages/s/singledispatch-method/bad.py new file mode 100644 index 000000000..49e545b92 --- /dev/null +++ b/doc/data/messages/s/singledispatch-method/bad.py @@ -0,0 +1,19 @@ +from functools import singledispatch + + +class Board: + @singledispatch # [singledispatch-method] + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/singledispatch-method/details.rst b/doc/data/messages/s/singledispatch-method/details.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/doc/data/messages/s/singledispatch-method/details.rst diff --git a/doc/data/messages/s/singledispatch-method/good.py b/doc/data/messages/s/singledispatch-method/good.py new file mode 100644 index 000000000..f38047cd1 --- /dev/null +++ b/doc/data/messages/s/singledispatch-method/good.py @@ -0,0 +1,19 @@ +from functools import singledispatch + + +class Board: + @singledispatch + @staticmethod + def convert_position(position): + pass + + @convert_position.register + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/singledispatchmethod-function/bad.py b/doc/data/messages/s/singledispatchmethod-function/bad.py new file mode 100644 index 000000000..d2255f865 --- /dev/null +++ b/doc/data/messages/s/singledispatchmethod-function/bad.py @@ -0,0 +1,19 @@ +from functools import singledispatchmethod + + +class Board: + @singledispatchmethod # [singledispatchmethod-function] + @staticmethod + def convert_position(position): + pass + + @convert_position.register # [singledispatchmethod-function] + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatchmethod-function] + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/singledispatchmethod-function/details.rst b/doc/data/messages/s/singledispatchmethod-function/details.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/doc/data/messages/s/singledispatchmethod-function/details.rst diff --git a/doc/data/messages/s/singledispatchmethod-function/good.py b/doc/data/messages/s/singledispatchmethod-function/good.py new file mode 100644 index 000000000..1bc3570b5 --- /dev/null +++ b/doc/data/messages/s/singledispatchmethod-function/good.py @@ -0,0 +1,18 @@ +from functools import singledispatchmethod + + +class Board: + @singledispatchmethod + def convert_position(cls, position): + pass + + @singledispatchmethod + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @singledispatchmethod + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/syntax-error/bad.py b/doc/data/messages/s/syntax-error/bad.py new file mode 100644 index 000000000..6a34478e1 --- /dev/null +++ b/doc/data/messages/s/syntax-error/bad.py @@ -0,0 +1,5 @@ +fruit_stock = { + 'apple': 42, + 'orange': 21 # [syntax-error] + 'banana': 12 +} diff --git a/doc/data/messages/s/syntax-error/good.py b/doc/data/messages/s/syntax-error/good.py new file mode 100644 index 000000000..eccab8746 --- /dev/null +++ b/doc/data/messages/s/syntax-error/good.py @@ -0,0 +1,5 @@ +fruit_stock = { + 'apple': 42, + 'orange': 21, + 'banana': 12 +} diff --git a/doc/data/messages/t/too-many-function-args/bad.py b/doc/data/messages/t/too-many-function-args/bad.py new file mode 100644 index 000000000..97eedb944 --- /dev/null +++ b/doc/data/messages/t/too-many-function-args/bad.py @@ -0,0 +1,6 @@ +class Fruit: + def __init__(self, color): + self.color = color + + +apple = Fruit("red", "apple", [1, 2, 3]) # [too-many-function-args] diff --git a/doc/data/messages/t/too-many-function-args/details.rst b/doc/data/messages/t/too-many-function-args/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/t/too-many-function-args/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/t/too-many-function-args/good.py b/doc/data/messages/t/too-many-function-args/good.py index c40beb573..338b8e1e8 100644 --- a/doc/data/messages/t/too-many-function-args/good.py +++ b/doc/data/messages/t/too-many-function-args/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self, color, name): + self.color = color + self.name = name + + +apple = Fruit("red", "apple") diff --git a/doc/data/messages/t/too-many-try-statements/bad.py b/doc/data/messages/t/too-many-try-statements/bad.py new file mode 100644 index 000000000..4e816ad39 --- /dev/null +++ b/doc/data/messages/t/too-many-try-statements/bad.py @@ -0,0 +1,10 @@ +FRUITS = {"apple": 1, "orange": 10} + + +def pick_fruit(name): + try: # [too-many-try-statements] + count = FRUITS[name] + count += 1 + print(f"Got fruit count {count}") + except KeyError: + return diff --git a/doc/data/messages/t/too-many-try-statements/details.rst b/doc/data/messages/t/too-many-try-statements/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/t/too-many-try-statements/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/t/too-many-try-statements/good.py b/doc/data/messages/t/too-many-try-statements/good.py index c40beb573..faea966a1 100644 --- a/doc/data/messages/t/too-many-try-statements/good.py +++ b/doc/data/messages/t/too-many-try-statements/good.py @@ -1 +1,11 @@ -# This is a placeholder for correct code for this message. +FRUITS = {"apple": 1, "orange": 10} + + +def pick_fruit(name): + try: + count = FRUITS[name] + except KeyError: + return + + count += 1 + print(f"Got fruit count {count}") diff --git a/doc/data/messages/t/too-many-try-statements/pylintrc b/doc/data/messages/t/too-many-try-statements/pylintrc new file mode 100644 index 000000000..438a80b6d --- /dev/null +++ b/doc/data/messages/t/too-many-try-statements/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.broad_try_clause, diff --git a/doc/data/messages/u/unbalanced-dict-unpacking/bad.py b/doc/data/messages/u/unbalanced-dict-unpacking/bad.py new file mode 100644 index 000000000..9162ccc45 --- /dev/null +++ b/doc/data/messages/u/unbalanced-dict-unpacking/bad.py @@ -0,0 +1,4 @@ +FRUITS = {"apple": 2, "orange": 3, "mellon": 10} + +for fruit, price in FRUITS.values(): # [unbalanced-dict-unpacking] + print(fruit) diff --git a/doc/data/messages/u/unbalanced-dict-unpacking/good.py b/doc/data/messages/u/unbalanced-dict-unpacking/good.py new file mode 100644 index 000000000..450e03489 --- /dev/null +++ b/doc/data/messages/u/unbalanced-dict-unpacking/good.py @@ -0,0 +1,4 @@ +FRUITS = {"apple": 2, "orange": 3, "mellon": 10} + +for fruit, price in FRUITS.items(): + print(fruit) diff --git a/doc/data/messages/u/unnecessary-dict-index-lookup/bad.py b/doc/data/messages/u/unnecessary-dict-index-lookup/bad.py new file mode 100644 index 000000000..047175861 --- /dev/null +++ b/doc/data/messages/u/unnecessary-dict-index-lookup/bad.py @@ -0,0 +1,4 @@ +FRUITS = {"apple": 1, "orange": 10, "berry": 22} + +for fruit_name, fruit_count in FRUITS.items(): + print(FRUITS[fruit_name]) # [unnecessary-dict-index-lookup] diff --git a/doc/data/messages/u/unnecessary-dict-index-lookup/details.rst b/doc/data/messages/u/unnecessary-dict-index-lookup/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/u/unnecessary-dict-index-lookup/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/u/unnecessary-dict-index-lookup/good.py b/doc/data/messages/u/unnecessary-dict-index-lookup/good.py index c40beb573..c2ee5ed57 100644 --- a/doc/data/messages/u/unnecessary-dict-index-lookup/good.py +++ b/doc/data/messages/u/unnecessary-dict-index-lookup/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +FRUITS = {"apple": 1, "orange": 10, "berry": 22} + +for fruit_name, fruit_count in FRUITS.items(): + print(fruit_count) diff --git a/doc/data/messages/u/unrecognized-inline-option/bad.py b/doc/data/messages/u/unrecognized-inline-option/bad.py new file mode 100644 index 000000000..ff49aa920 --- /dev/null +++ b/doc/data/messages/u/unrecognized-inline-option/bad.py @@ -0,0 +1,2 @@ +# +1: [unrecognized-inline-option] +# pylint:applesoranges=1 diff --git a/doc/data/messages/u/unrecognized-inline-option/details.rst b/doc/data/messages/u/unrecognized-inline-option/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/u/unrecognized-inline-option/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/u/unrecognized-inline-option/good.py b/doc/data/messages/u/unrecognized-inline-option/good.py index c40beb573..2fdb3780a 100644 --- a/doc/data/messages/u/unrecognized-inline-option/good.py +++ b/doc/data/messages/u/unrecognized-inline-option/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +# pylint: enable=too-many-public-methods diff --git a/doc/data/messages/u/unsubscriptable-object/bad.py b/doc/data/messages/u/unsubscriptable-object/bad.py new file mode 100644 index 000000000..8b168a0af --- /dev/null +++ b/doc/data/messages/u/unsubscriptable-object/bad.py @@ -0,0 +1,5 @@ +class Fruit: + pass + + +Fruit()[1] # [unsubscriptable-object] diff --git a/doc/data/messages/u/unsubscriptable-object/details.rst b/doc/data/messages/u/unsubscriptable-object/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/u/unsubscriptable-object/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/u/unsubscriptable-object/good.py b/doc/data/messages/u/unsubscriptable-object/good.py index c40beb573..56e91444e 100644 --- a/doc/data/messages/u/unsubscriptable-object/good.py +++ b/doc/data/messages/u/unsubscriptable-object/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Fruit: + def __init__(self): + self.colors = ["red", "orange", "yellow"] + + def __getitem__(self, idx): + return self.colors[idx] + + +Fruit()[1] diff --git a/doc/data/messages/u/unsupported-assignment-operation/bad.py b/doc/data/messages/u/unsupported-assignment-operation/bad.py new file mode 100644 index 000000000..26ee1a993 --- /dev/null +++ b/doc/data/messages/u/unsupported-assignment-operation/bad.py @@ -0,0 +1,6 @@ +def pick_fruits(fruits): + for fruit in fruits: + print(fruit) + + +pick_fruits(["apple"])[0] = "orange" # [unsupported-assignment-operation] diff --git a/doc/data/messages/u/unsupported-assignment-operation/details.rst b/doc/data/messages/u/unsupported-assignment-operation/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/u/unsupported-assignment-operation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/u/unsupported-assignment-operation/good.py b/doc/data/messages/u/unsupported-assignment-operation/good.py index c40beb573..13fe34c05 100644 --- a/doc/data/messages/u/unsupported-assignment-operation/good.py +++ b/doc/data/messages/u/unsupported-assignment-operation/good.py @@ -1 +1,8 @@ -# This is a placeholder for correct code for this message. +def pick_fruits(fruits): + for fruit in fruits: + print(fruit) + + return [] + + +pick_fruits(["apple"])[0] = "orange" diff --git a/doc/data/messages/u/unsupported-delete-operation/bad.py b/doc/data/messages/u/unsupported-delete-operation/bad.py new file mode 100644 index 000000000..a7870e3a8 --- /dev/null +++ b/doc/data/messages/u/unsupported-delete-operation/bad.py @@ -0,0 +1,3 @@ +FRUITS = ("apple", "orange", "berry") + +del FRUITS[0] # [unsupported-delete-operation] diff --git a/doc/data/messages/u/unsupported-delete-operation/details.rst b/doc/data/messages/u/unsupported-delete-operation/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/u/unsupported-delete-operation/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/u/unsupported-delete-operation/good.py b/doc/data/messages/u/unsupported-delete-operation/good.py index c40beb573..8143c4fee 100644 --- a/doc/data/messages/u/unsupported-delete-operation/good.py +++ b/doc/data/messages/u/unsupported-delete-operation/good.py @@ -1 +1,3 @@ -# This is a placeholder for correct code for this message. +FRUITS = ["apple", "orange", "berry"] + +del FRUITS[0] diff --git a/doc/data/messages/u/unsupported-membership-test/bad.py b/doc/data/messages/u/unsupported-membership-test/bad.py new file mode 100644 index 000000000..37502ecd3 --- /dev/null +++ b/doc/data/messages/u/unsupported-membership-test/bad.py @@ -0,0 +1,5 @@ +class Fruit: + pass + + +apple = "apple" in Fruit() # [unsupported-membership-test] diff --git a/doc/data/messages/u/unsupported-membership-test/details.rst b/doc/data/messages/u/unsupported-membership-test/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/u/unsupported-membership-test/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/u/unsupported-membership-test/good.py b/doc/data/messages/u/unsupported-membership-test/good.py index c40beb573..96b96d4d5 100644 --- a/doc/data/messages/u/unsupported-membership-test/good.py +++ b/doc/data/messages/u/unsupported-membership-test/good.py @@ -1 +1,7 @@ -# This is a placeholder for correct code for this message. +class Fruit: + FRUITS = ["apple", "orange"] + def __contains__(self, name): + return name in self.FRUITS + + +apple = "apple" in Fruit() diff --git a/doc/data/messages/u/unused-private-member/bad.py b/doc/data/messages/u/unused-private-member/bad.py new file mode 100644 index 000000000..b56bcaad3 --- /dev/null +++ b/doc/data/messages/u/unused-private-member/bad.py @@ -0,0 +1,5 @@ +class Fruit: + FRUITS = {"apple": "red", "orange": "orange"} + + def __print_color(self): # [unused-private-member] + pass diff --git a/doc/data/messages/u/unused-private-member/details.rst b/doc/data/messages/u/unused-private-member/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/u/unused-private-member/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/u/unused-private-member/good.py b/doc/data/messages/u/unused-private-member/good.py index c40beb573..02df36c44 100644 --- a/doc/data/messages/u/unused-private-member/good.py +++ b/doc/data/messages/u/unused-private-member/good.py @@ -1 +1,9 @@ -# This is a placeholder for correct code for this message. +class Fruit: + FRUITS = {"apple": "red", "orange": "orange"} + + def __print_color(self, name, color): + print(f"{name}: {color}") + + def print(self): + for fruit, color in self.FRUITS.items(): + self.__print_color(fruit, color) diff --git a/doc/data/messages/u/use-dict-literal/bad.py b/doc/data/messages/u/use-dict-literal/bad.py index 6c3056b6f..2d90a91e8 100644 --- a/doc/data/messages/u/use-dict-literal/bad.py +++ b/doc/data/messages/u/use-dict-literal/bad.py @@ -1 +1,3 @@ empty_dict = dict() # [use-dict-literal] +new_dict = dict(foo="bar") # [use-dict-literal] +new_dict = dict(**another_dict) # [use-dict-literal] diff --git a/doc/data/messages/u/use-dict-literal/details.rst b/doc/data/messages/u/use-dict-literal/details.rst new file mode 100644 index 000000000..f07532ead --- /dev/null +++ b/doc/data/messages/u/use-dict-literal/details.rst @@ -0,0 +1,4 @@ +https://gist.github.com/hofrob/ad143aaa84c096f42489c2520a3875f9
+
+This example script shows an 18% increase in performance when using a literal over the
+constructor in python version 3.10.6.
diff --git a/doc/data/messages/u/use-dict-literal/good.py b/doc/data/messages/u/use-dict-literal/good.py index 5f7d64deb..237d2c881 100644 --- a/doc/data/messages/u/use-dict-literal/good.py +++ b/doc/data/messages/u/use-dict-literal/good.py @@ -1 +1,7 @@ empty_dict = {} + +# create using a literal dict +new_dict = {"foo": "bar"} + +# shallow copy a dict +new_dict = {**another_dict} diff --git a/doc/data/messages/u/use-implicit-booleaness-not-comparison/bad.py b/doc/data/messages/u/use-implicit-booleaness-not-comparison/bad.py new file mode 100644 index 000000000..78411ec2a --- /dev/null +++ b/doc/data/messages/u/use-implicit-booleaness-not-comparison/bad.py @@ -0,0 +1,4 @@ +z = [] + +if z != []: # [use-implicit-booleaness-not-comparison] + print("z is not an empty sequence") diff --git a/doc/data/messages/u/use-implicit-booleaness-not-comparison/details.rst b/doc/data/messages/u/use-implicit-booleaness-not-comparison/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/u/use-implicit-booleaness-not-comparison/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/u/use-implicit-booleaness-not-comparison/good.py b/doc/data/messages/u/use-implicit-booleaness-not-comparison/good.py index c40beb573..6801d91eb 100644 --- a/doc/data/messages/u/use-implicit-booleaness-not-comparison/good.py +++ b/doc/data/messages/u/use-implicit-booleaness-not-comparison/good.py @@ -1 +1,4 @@ -# This is a placeholder for correct code for this message. +z = [] + +if z: + print("z is not an empty sequence") diff --git a/doc/data/messages/w/wrong-spelling-in-comment/bad.py b/doc/data/messages/w/wrong-spelling-in-comment/bad.py new file mode 100644 index 000000000..becaf40e5 --- /dev/null +++ b/doc/data/messages/w/wrong-spelling-in-comment/bad.py @@ -0,0 +1 @@ +# There's a mistkae in this string # [wrong-spelling-in-comment] diff --git a/doc/data/messages/w/wrong-spelling-in-comment/details.rst b/doc/data/messages/w/wrong-spelling-in-comment/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/w/wrong-spelling-in-comment/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/w/wrong-spelling-in-comment/good.py b/doc/data/messages/w/wrong-spelling-in-comment/good.py index c40beb573..84af67f5a 100644 --- a/doc/data/messages/w/wrong-spelling-in-comment/good.py +++ b/doc/data/messages/w/wrong-spelling-in-comment/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +# There's no mistake in this string diff --git a/doc/data/messages/w/wrong-spelling-in-comment/pylintrc b/doc/data/messages/w/wrong-spelling-in-comment/pylintrc new file mode 100644 index 000000000..dd11bf811 --- /dev/null +++ b/doc/data/messages/w/wrong-spelling-in-comment/pylintrc @@ -0,0 +1,3 @@ +[main] +# This might not run in your env if you don't have the en_US dict installed. +spelling-dict=en_US diff --git a/doc/data/messages/w/wrong-spelling-in-docstring/bad.py b/doc/data/messages/w/wrong-spelling-in-docstring/bad.py new file mode 100644 index 000000000..b5c9149ae --- /dev/null +++ b/doc/data/messages/w/wrong-spelling-in-docstring/bad.py @@ -0,0 +1 @@ +"""There's a mistkae in this string""" # [wrong-spelling-in-docstring] diff --git a/doc/data/messages/w/wrong-spelling-in-docstring/details.rst b/doc/data/messages/w/wrong-spelling-in-docstring/details.rst deleted file mode 100644 index ab8204529..000000000 --- a/doc/data/messages/w/wrong-spelling-in-docstring/details.rst +++ /dev/null @@ -1 +0,0 @@ -You can help us make the doc better `by contributing <https://github.com/PyCQA/pylint/issues/5953>`_ ! diff --git a/doc/data/messages/w/wrong-spelling-in-docstring/good.py b/doc/data/messages/w/wrong-spelling-in-docstring/good.py index c40beb573..10d19e91e 100644 --- a/doc/data/messages/w/wrong-spelling-in-docstring/good.py +++ b/doc/data/messages/w/wrong-spelling-in-docstring/good.py @@ -1 +1 @@ -# This is a placeholder for correct code for this message. +"""There's no mistake in this string""" diff --git a/doc/data/messages/w/wrong-spelling-in-docstring/pylintrc b/doc/data/messages/w/wrong-spelling-in-docstring/pylintrc new file mode 100644 index 000000000..dd11bf811 --- /dev/null +++ b/doc/data/messages/w/wrong-spelling-in-docstring/pylintrc @@ -0,0 +1,3 @@ +[main] +# This might not run in your env if you don't have the en_US dict installed. +spelling-dict=en_US diff --git a/doc/development_guide/api/index.rst b/doc/development_guide/api/index.rst index 373c49866..00e6e1a9f 100644 --- a/doc/development_guide/api/index.rst +++ b/doc/development_guide/api/index.rst @@ -7,10 +7,9 @@ Python program thanks to their APIs: .. sourcecode:: python - from pylint import run_pylint, run_epylint, run_pyreverse, run_symilar + from pylint import run_pylint, run_pyreverse, run_symilar run_pylint("--disable=C", "myfile.py") - run_epylint(...) run_pyreverse(...) run_symilar(...) diff --git a/doc/development_guide/contributor_guide/contribute.rst b/doc/development_guide/contributor_guide/contribute.rst index 32de20106..492f8966b 100644 --- a/doc/development_guide/contributor_guide/contribute.rst +++ b/doc/development_guide/contributor_guide/contribute.rst @@ -67,9 +67,9 @@ your patch gets accepted: .. keep this in sync with the description of PULL_REQUEST_TEMPLATE.md! - Create a news fragment with `towncrier create <IssueNumber>.<type>` which will be - included in the changelog. `<type>` can be one of: new_check, removed_check, extension, - false_positive, false_negative, bugfix, other, internal. If necessary you can write - details or offer examples on how the new change is supposed to work. + included in the changelog. `<type>` can be one of: breaking, user_action, feature, + new_check, removed_check, extension, false_positive, false_negative, bugfix, other, internal. + If necessary you can write details or offer examples on how the new change is supposed to work. - Document your change, if it is a non-trivial one. diff --git a/doc/development_guide/contributor_guide/release.md b/doc/development_guide/contributor_guide/release.md index ee5fa2b7a..a076838fd 100644 --- a/doc/development_guide/contributor_guide/release.md +++ b/doc/development_guide/contributor_guide/release.md @@ -49,17 +49,20 @@ branch ## Back-porting a fix from `main` to the maintenance branch -Whenever a commit on `main` should be released in a patch release on the current -maintenance branch we cherry-pick the commit from `main`. - -- During the merge request on `main`, make sure that the changelog is for the patch - version `X.Y-1.Z'`. (For example: `v2.3.5`) -- After the PR is merged on `main` cherry-pick the commits on the - `release-branch-X.Y-1.Z'` branch created from `maintenance/X.Y-1.x` then cherry-pick - the commit from the `main` branch. (For example: `release-branch-2.3.5` from - `maintenance/2.3.x`) -- Remove the "need backport" label from cherry-picked issues - +Whenever a PR on `main` should be released in a patch release on the current maintenance +branch: + +- Label the PR with `backport maintenance/X.Y-1.x`. (For example + `backport maintenance/2.3.x`) +- Squash the PR before merging (alternatively rebase if there's a single commit) +- (If the automated cherry-pick has conflicts) + - Add a `Needs backport` label and do it manually. + - You might alternatively also: + - Cherry-pick the changes that create the conflict if it's not a new feature before + doing the original PR cherry-pick manually. + - Decide to wait for the next minor to release the PR + - In any case upgrade the milestones in the original PR and newly cherry-picked PR + to match reality. - Release a patch version ## Releasing a patch version diff --git a/doc/development_guide/contributor_guide/tests/launching_test.rst b/doc/development_guide/contributor_guide/tests/launching_test.rst index b982bbbeb..02114f01f 100644 --- a/doc/development_guide/contributor_guide/tests/launching_test.rst +++ b/doc/development_guide/contributor_guide/tests/launching_test.rst @@ -57,16 +57,25 @@ Primer tests Pylint also uses what we refer to as ``primer`` tests. These are tests that are run automatically in our Continuous Integration and check whether any changes in Pylint lead to crashes or fatal errors -on the ``stdlib`` and a selection of external repositories. +on the ``stdlib``, and also assess a pull request's impact on the linting of a selection of external +repositories by posting the diff against ``pylint``'s current output as a comment. -To run the ``primer`` tests you can add either ``--primer-stdlib`` or ``--primer-external`` to the -pytest_ command. If you want to only run the ``primer`` you can add either of their marks, for example:: +To run the primer test for the ``stdlib``, which only checks for crashes and fatal errors, you can add +``--primer-stdlib`` to the pytest_ command. For example:: pytest -m primer_stdlib --primer-stdlib -The external ``primer`` can be run with:: +To produce the output generated on Continuous Integration for the linting of external repositories, +run these commands:: - pytest -m primer_external_batch_one --primer-external # Runs batch one + python tests/primer/__main__.py prepare --clone + python tests/primer/__main__.py run --type=pr + +To fully simulate the process on Continuous Integration, you should then checkout ``main``, and +then run these commands:: + + python tests/primer/__main__.py run --type=main + python tests/primer/__main__.py compare The list of repositories is created on the basis of three criteria: 1) projects need to use a diverse range of language features, 2) projects need to be well maintained and 3) projects should not have a codebase diff --git a/doc/development_guide/how_tos/custom_checkers.rst b/doc/development_guide/how_tos/custom_checkers.rst index 7a9c567db..6d36b0ec6 100644 --- a/doc/development_guide/how_tos/custom_checkers.rst +++ b/doc/development_guide/how_tos/custom_checkers.rst @@ -218,10 +218,28 @@ Now we can debug our checker! .. Note:: ``my_plugin`` refers to a module called ``my_plugin.py``. - This module can be made available to pylint by putting this - module's parent directory in your ``PYTHONPATH`` - environment variable or by adding the ``my_plugin.py`` - file to the ``pylint/checkers`` directory if running from source. + The preferred way of making this plugin available to pylint is + by installing it as a package. This can be done either from a packaging index like + ``PyPI`` or by installing it from a local source such as with ``pip install``. + + Alternatively, the plugin module can be made available to pylint by + putting this module's parent directory in your ``PYTHONPATH`` + environment variable. + + If your pylint config has an ``init-hook`` that modifies + ``sys.path`` to include the module's parent directory, this + will also work, but only if either: + + * the ``init-hook`` and the ``load-plugins`` list are both + defined in a configuration file, or... + * the ``init-hook`` is passed as a command-line argument and + the ``load-plugins`` list is in the configuration file + + So, you cannot load a custom plugin by modifying ``sys.path`` if you + supply the ``init-hook`` in a configuration file, but pass the module name + in via ``--load-plugins`` on the command line. + This is because pylint loads plugins specified on command + line before loading any configuration from other sources. Defining a Message ------------------ diff --git a/doc/development_guide/how_tos/plugins.rst b/doc/development_guide/how_tos/plugins.rst index bc2c0f14c..3940f2481 100644 --- a/doc/development_guide/how_tos/plugins.rst +++ b/doc/development_guide/how_tos/plugins.rst @@ -1,5 +1,7 @@ .. -*- coding: utf-8 -*- +.. _plugins: + How To Write a Pylint Plugin ============================ diff --git a/doc/exts/pylint_extensions.py b/doc/exts/pylint_extensions.py index a973e1b13..406d2d39d 100755 --- a/doc/exts/pylint_extensions.py +++ b/doc/exts/pylint_extensions.py @@ -6,22 +6,42 @@ """Script used to generate the extensions file before building the actual documentation.""" +from __future__ import annotations + import os import re import sys import warnings -from typing import Optional +from typing import Any import sphinx from sphinx.application import Sphinx +from pylint.checkers import BaseChecker from pylint.constants import MAIN_CHECKER_NAME from pylint.lint import PyLinter +from pylint.typing import MessageDefinitionTuple, OptionDict, ReportsCallable from pylint.utils import get_rst_title +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + + +class _CheckerInfo(TypedDict): + """Represents data about a checker.""" + + checker: BaseChecker + options: list[tuple[str, OptionDict, Any]] + msgs: dict[str, MessageDefinitionTuple] + reports: list[tuple[str, str, ReportsCallable]] + doc: str + module: str + # pylint: disable-next=unused-argument -def builder_inited(app: Optional[Sphinx]) -> None: +def builder_inited(app: Sphinx | None) -> None: """Output full documentation in ReST format for all extension modules.""" # PACKAGE/docs/exts/pylint_extensions.py --> PACKAGE/ base_path = os.path.dirname( @@ -30,7 +50,7 @@ def builder_inited(app: Optional[Sphinx]) -> None: # PACKAGE/ --> PACKAGE/pylint/extensions ext_path = os.path.join(base_path, "pylint", "extensions") modules = [] - doc_files = {} + doc_files: dict[str, str] = {} for filename in os.listdir(ext_path): name, ext = os.path.splitext(filename) if name[0] == "_": @@ -79,18 +99,26 @@ def builder_inited(app: Optional[Sphinx]) -> None: checker, information = checker_information j = -1 checker = information["checker"] - del information["checker"] if i == max_len - 1: # Remove the \n\n at the end of the file j = -3 print( - checker.get_full_documentation(**information, show_options=False)[:j], + checker.get_full_documentation( + msgs=information["msgs"], + options=information["options"], + reports=information["reports"], + doc=information["doc"], + module=information["module"], + show_options=False, + )[:j], file=stream, ) -def get_plugins_info(linter, doc_files): - by_checker = {} +def get_plugins_info( + linter: PyLinter, doc_files: dict[str, str] +) -> dict[BaseChecker, _CheckerInfo]: + by_checker: dict[BaseChecker, _CheckerInfo] = {} for checker in linter.get_checkers(): if checker.name == MAIN_CHECKER_NAME: continue @@ -116,18 +144,18 @@ def get_plugins_info(linter, doc_files): except KeyError: with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) - by_checker[checker] = { - "checker": checker, - "options": list(checker.options_and_values()), - "msgs": dict(checker.msgs), - "reports": list(checker.reports), - "doc": doc, - "module": module, - } + by_checker[checker] = _CheckerInfo( + checker=checker, + options=list(checker.options_and_values()), + msgs=dict(checker.msgs), + reports=list(checker.reports), + doc=doc, + module=module, + ) return by_checker -def setup(app): +def setup(app: Sphinx) -> dict[str, str]: app.connect("builder-inited", builder_inited) return {"version": sphinx.__display_version__} diff --git a/doc/exts/pylint_features.py b/doc/exts/pylint_features.py index 8654046d3..fcf4f01c8 100755 --- a/doc/exts/pylint_features.py +++ b/doc/exts/pylint_features.py @@ -8,8 +8,9 @@ documentation. """ +from __future__ import annotations + import os -from typing import Optional import sphinx from sphinx.application import Sphinx @@ -19,7 +20,7 @@ from pylint.utils import get_rst_title, print_full_documentation # pylint: disable-next=unused-argument -def builder_inited(app: Optional[Sphinx]) -> None: +def builder_inited(app: Sphinx | None) -> None: # PACKAGE/docs/exts/pylint_extensions.py --> PACKAGE/ base_path = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -39,7 +40,7 @@ def builder_inited(app: Optional[Sphinx]) -> None: print_full_documentation(linter, stream, False) -def setup(app): +def setup(app: Sphinx) -> dict[str, str]: app.connect("builder-inited", builder_inited) return {"version": sphinx.__display_version__} diff --git a/doc/exts/pylint_messages.py b/doc/exts/pylint_messages.py index 0fcacf804..cef7c83a4 100644 --- a/doc/exts/pylint_messages.py +++ b/doc/exts/pylint_messages.py @@ -44,6 +44,7 @@ class MessageData(NamedTuple): checker_module_name: str checker_module_path: str shared: bool = False + default_enabled: bool = True MessagesDict = Dict[str, List[MessageData]] @@ -194,6 +195,7 @@ def _get_all_messages( checker_module.__name__, checker_module.__file__, message.shared, + message.default_enabled, ) msg_type = MSG_TYPES_DOC[message.msgid[0]] messages_dict[msg_type].append(message_data) @@ -271,7 +273,15 @@ def _generate_single_message_body(message: MessageData) -> str: **Description:** *{message.definition.description}* +""" + if not message.default_enabled: + body += f""" +.. caution:: + This message is disabled by default. To enable it, add ``{message.name}`` to the ``enable`` option. + +""" + body += f""" {message.bad_code} {message.good_code} {message.details} diff --git a/doc/requirements.txt b/doc/requirements.txt index 512d311aa..e9c849eb7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ -Sphinx==5.1.1 +Sphinx==5.3.0 sphinx-reredirects<1 myst-parser~=0.18 -towncrier~=21.9 -furo==2022.6.21 +towncrier~=22.8 +furo==2022.9.29 -e . diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 173b255cd..fce472234 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -4,55 +4,27 @@ Tutorial ======== -:Author: Robert Kirkpatrick - - -Intro ------ - -Beginner to coding standards? Pylint can be your guide to reveal what's really -going on behind the scenes and help you to become a more aware programmer. - -Sharing code is a rewarding endeavor. Putting your code ``out there`` can be -either an act of philanthropy, ``coming of age``, or a basic extension of belief -in open source. Whatever the motivation, your good intentions may not have the -desired outcome if people find your code hard to use or understand. The Python -community has formalized some recommended programming styles to help everyone -write code in a common, agreed-upon style that makes the most sense for shared -code. This style is captured in `PEP 8`_, the "Style Guide for Python Code". -Pylint can be a quick and easy way of -seeing if your code has captured the essence of `PEP 8`_ and is therefore -``friendly`` to other potential users. - -Perhaps you're not ready to share your code but you'd like to learn a bit more -about writing better code and don't know where to start. Pylint can tell you -where you may have run astray and point you in the direction to figure out what -you have done and how to do better. - This tutorial is all about approaching coding standards with little or no knowledge of in-depth programming or the code standards themselves. It's the equivalent of skipping the manual and jumping right in. -My command line prompt for these examples is: +The command line prompt for these examples is: .. sourcecode:: console - robertk01 Desktop$ + tutor Desktop$ .. _PEP 8: https://peps.python.org/pep-0008/ Getting Started --------------- -Running Pylint with no arguments will invoke the help dialogue and give you an -idea of the arguments available to you. Do that now, i.e.: +Running Pylint with the ``--help`` arguments will give you an idea of the arguments +available. Do that now, i.e.: .. sourcecode:: console - robertk01 Desktop$ pylint - ... - a bunch of stuff - ... + pylint --help A couple of the options that we'll focus on here are: :: @@ -66,17 +38,12 @@ A couple of the options that we'll focus on here are: :: --reports=<y or n> --output-format=<format> -If you need more detail, you can also ask for an even longer help message, -like so: :: +If you need more detail, you can also ask for an even longer help message: :: - robertk01 Desktop$ pylint --long-help - ... - Even more stuff - ... + pylint --long-help -Pay attention to the last bit of this longer help output. This gives you a -hint of what -Pylint is going to ``pick on``: :: +Pay attention to the last bit of this longer help output. This gives you a +hint of what Pylint is going to ``pick on``: :: Output: Using the default text output, the message format is : @@ -90,155 +57,148 @@ Pylint is going to ``pick on``: :: further processing. When Pylint is first run on a fresh piece of code, a common complaint is that it -is too ``noisy``. The current default configuration is set to enforce all possible -warnings. We'll use some of the options I noted above to make it suit your -preferences a bit better (and thus make it emit messages only when needed). - +is too ``noisy``. The default configuration enforce a lot of warnings. +We'll use some of the options we noted above to make it suit your +preferences a bit better. Your First Pylint'ing --------------------- -We'll use a basic Python script as fodder for our tutorial. -The starting code we will use is called simplecaesar.py and is here in its -entirety: +We'll use a basic Python script with ``black`` already applied on it, +as fodder for our tutorial. The starting code we will use is called +``simplecaesar.py`` and is here in its entirety: .. sourcecode:: python - #!/usr/bin/env python3 - - import string; + #!/usr/bin/env python3 - shift = 3 - choice = input("would you like to encode or decode?") - word = input("Please enter text") - letters = string.ascii_letters + string.punctuation + string.digits - encoded = '' - if choice == "encode": - for letter in word: - if letter == ' ': - encoded = encoded + ' ' - else: - x = letters.index(letter) + shift - encoded = encoded + letters[x] - if choice == "decode": - for letter in word: - if letter == ' ': - encoded = encoded + ' ' - else: - x = letters.index(letter) - shift - encoded = encoded + letters[x] + import string - print(encoded) + shift = 3 + choice = input("would you like to encode or decode?") + word = input("Please enter text") + letters = string.ascii_letters + string.punctuation + string.digits + encoded = "" + if choice == "encode": + for letter in word: + if letter == " ": + encoded = encoded + " " + else: + x = letters.index(letter) + shift + encoded = encoded + letters[x] + if choice == "decode": + for letter in word: + if letter == " ": + encoded = encoded + " " + else: + x = letters.index(letter) - shift + encoded = encoded + letters[x] + print(encoded) -Let's get started. -If we run this: +Let's get started. If we run this: .. sourcecode:: console - robertk01 Desktop$ pylint simplecaesar.py - ************* Module simplecaesar - simplecaesar.py:3:0: W0301: Unnecessary semicolon (unnecessary-semicolon) - simplecaesar.py:1:0: C0114: Missing module docstring (missing-module-docstring) - simplecaesar.py:5:0: C0103: Constant name "shift" doesn't conform to UPPER_CASE naming style (invalid-name) - simplecaesar.py:9:0: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) - simplecaesar.py:13:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + tutor Desktop$ pylint simplecaesar.py + ************* Module simplecaesar + simplecaesar.py:1:0: C0114: Missing module docstring (missing-module-docstring) + simplecaesar.py:5:0: C0103: Constant name "shift" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:8:0: C0103: Constant name "letters" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:9:0: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:13:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:15:12: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:16:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:20:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:22:12: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:23:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) - ----------------------------------- - Your code has been rated at 7.37/10 + ----------------------------------- + Your code has been rated at 4.74/10 -Previous experience taught me that the default output for the messages -needed a bit more info. We can see the second line is: :: +We can see the second line is: :: "simplecaesar.py:1:0: C0114: Missing module docstring (missing-module-docstring)" -This basically means that line 1 violates a convention ``C0114``. It's telling me I really should have a docstring. -I agree, but what if I didn't fully understand what rule I violated. Knowing only that I violated a convention -isn't much help if I'm a newbie. Another piece of information there is the -message symbol between parens, ``missing-module-docstring`` here. +This basically means that line 1 at column 0 violates the convention ``C0114``. +Another piece of information is the message symbol between parens, +``missing-module-docstring``. -If I want to read up a bit more about that, I can go back to the +If we want to read up a bit more about that, we can go back to the command line and try this: .. sourcecode:: console - robertk01 Desktop$ pylint --help-msg=missing-module-docstring + tutor Desktop$ pylint --help-msg=missing-module-docstring :missing-module-docstring (C0114): *Missing module docstring* Used when a module has no docstring.Empty modules do not require a docstring. This message belongs to the basic checker. - -Yeah, ok. That one was a bit of a no-brainer, but I have run into error messages -that left me with no clue about what went wrong, simply because I was unfamiliar -with the underlying mechanism of code theory. One error that puzzled my newbie -mind was: :: - - :too-many-instance-attributes (R0902): *Too many instance attributes (%s/%s)* - -I get it now thanks to Pylint pointing it out to me. If you don't get that one, -pour a fresh cup of coffee and look into it - let your programmer mind grow! - +That one was a bit of a no-brainer, but we can also run into error messages +where we are unfamiliar with the underlying code theory. The Next Step ------------- Now that we got some configuration stuff out of the way, let's see what we can -do with the remaining warnings. - -If we add a docstring to describe what the code is meant to do that will help. -There are 5 ``invalid-name`` messages that we will get to later. Lastly, I -put an unnecessary semicolon at the end of the import line so I'll -fix that too. To sum up, I'll add a docstring to line 2, and remove the ``;`` -from line 3. - -Here is the updated code: +do with the remaining warnings. If we add a docstring to describe what the code +is meant to do that will help. There are ``invalid-name`` messages that we will +get to later. Here is the updated code: .. sourcecode:: python - #!/usr/bin/env python3 - """This script prompts a user to enter a message to encode or decode - using a classic Caesar shift substitution (3 letter shift)""" - - import string - - shift = 3 - choice = input("would you like to encode or decode?") - word = input("Please enter text") - letters = string.ascii_letters + string.punctuation + string.digits - encoded = '' - if choice == "encode": - for letter in word: - if letter == ' ': - encoded = encoded + ' ' - else: - x = letters.index(letter) + shift - encoded = encoded + letters[x] - if choice == "decode": - for letter in word: - if letter == ' ': - encoded = encoded + ' ' - else: - x = letters.index(letter) - shift - encoded = encoded + letters[x] - - print(encoded) + #!/usr/bin/env python3 + + """This script prompts a user to enter a message to encode or decode + using a classic Caesar shift substitution (3 letter shift)""" + + import string + + shift = 3 + choice = input("would you like to encode or decode?") + word = input("Please enter text") + letters = string.ascii_letters + string.punctuation + string.digits + encoded = "" + if choice == "encode": + for letter in word: + if letter == " ": + encoded = encoded + " " + else: + x = letters.index(letter) + shift + encoded = encoded + letters[x] + if choice == "decode": + for letter in word: + if letter == " ": + encoded = encoded + " " + else: + x = letters.index(letter) - shift + encoded = encoded + letters[x] + + print(encoded) Here is what happens when we run it: .. sourcecode:: console - robertk01 Desktop$ pylint simplecaesar.py - ************* Module simplecaesar - simplecaesar.py:7:0: C0103: Constant name "shift" doesn't conform to UPPER_CASE naming style (invalid-name) - simplecaesar.py:11:0: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) - simplecaesar.py:15:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + tutor Desktop$ pylint simplecaesar.py + ************* Module simplecaesar + simplecaesar.py:8:0: C0103: Constant name "shift" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:11:0: C0103: Constant name "letters" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:12:0: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:16:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:18:12: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:19:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:23:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:25:12: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name) + simplecaesar.py:26:12: C0103: Constant name "encoded" doesn't conform to UPPER_CASE naming style (invalid-name) - ------------------------------------------------------------------ - Your code has been rated at 8.42/10 (previous run: 7.37/10, +1.05) + ------------------------------------------------------------------ + Your code has been rated at 5.26/10 (previous run: 4.74/10, +0.53) -Nice! Pylint told us how much our code rating has improved since our last run, and we're down to just the ``invalid-name`` messages. +Nice! Pylint told us how much our code rating has improved since our last run, +and we're down to just the ``invalid-name`` messages. There are fairly well defined conventions around naming things like instance variables, functions, classes, etc. The conventions focus on the use of @@ -246,11 +206,11 @@ UPPERCASE and lowercase as well as the characters that separate multiple words in the name. This lends itself well to checking via a regular expression, thus the **should match (([A-Z\_][A-Z1-9\_]*)|(__.*__))$**. -In this case Pylint is telling me that those variables appear to be constants +In this case Pylint is telling us that those variables appear to be constants and should be all UPPERCASE. This is an in-house convention that has lived with Pylint since its inception. You too can create your own in-house naming conventions but for the purpose of this tutorial, we want to stick to the `PEP 8`_ -standard. In this case, the variables I declared should follow the convention +standard. In this case, the variables we declared should follow the convention of all lowercase. The appropriate rule would be something like: "should match [a-z\_][a-z0-9\_]{2,30}$". Notice the lowercase letters in the regular expression (a-z versus A-Z). @@ -260,14 +220,15 @@ will now be quite quiet: .. sourcecode:: console - robertk01 Desktop$ pylint --const-rgx='[a-z_][a-z0-9_]{2,30}$' simplecaesar.py + tutor Desktop$ pylint simplecaesar.py --const-rgx='[a-z\_][a-z0-9\_]{2,30}$' + ************* Module simplecaesar + simplecaesar.py:18:12: C0103: Constant name "x" doesn't conform to '[a-z\\_][a-z0-9\\_]{2,30}$' pattern (invalid-name) + simplecaesar.py:25:12: C0103: Constant name "x" doesn't conform to '[a-z\\_][a-z0-9\\_]{2,30}$' pattern (invalid-name) - ------------------------------------------------------------------- - Your code has been rated at 10.00/10 (previous run: 8.42/10, +1.58) + ------------------------------------------------------------------ + Your code has been rated at 8.95/10 (previous run: 5.26/10, +3.68) - -Regular expressions can be quite a beast so take my word on this particular -example but go ahead and `read up`_ on them if you want. +You can `read up`_ on regular expressions or use `a website to help you`_. .. tip:: It would really be a pain to specify that regex on the command line all the time, particularly if we're using many other options. @@ -276,6 +237,5 @@ example but go ahead and `read up`_ on them if you want. quickly sharing them with others. Invoking ``pylint --generate-toml-config`` will create a sample ``.toml`` section with all the options set and explained in comments. This can then be added to your ``pyproject.toml`` file or any other ``.toml`` file pointed to with the ``--rcfile`` option. -That's it for the basic intro. More tutorials will follow. - .. _`read up`: https://docs.python.org/library/re.html +.. _`a website to help you`: https://regex101.com/ diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index d52c9c704..0eaf22792 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -14,12 +14,15 @@ Pylint provides the following optional plugins: - :ref:`pylint.extensions.comparison_placement` - :ref:`pylint.extensions.confusing_elif` - :ref:`pylint.extensions.consider_ternary_expression` +- :ref:`pylint.extensions.dict_init_mutate` - :ref:`pylint.extensions.docparams` - :ref:`pylint.extensions.docstyle` +- :ref:`pylint.extensions.dunder` - :ref:`pylint.extensions.empty_comment` - :ref:`pylint.extensions.emptystring` - :ref:`pylint.extensions.eq_without_hash` - :ref:`pylint.extensions.for_any_all` +- :ref:`pylint.extensions.magic_value` - :ref:`pylint.extensions.mccabe` - :ref:`pylint.extensions.no_self_use` - :ref:`pylint.extensions.overlapping_exceptions` @@ -79,6 +82,9 @@ Code Style checker Messages Emitted when an if assignment is directly followed by an if statement and both can be combined by using an assignment expression ``:=``. Requires Python 3.8 and ``py-version >= 3.8``. +:consider-using-augmented-assign (R6104): *Use '%s' to do an augmented assign directly* + Emitted when an assignment is referring to the object that it is assigning + to. This can be changed to be an augmented assign. Disabled by default! .. _pylint.extensions.emptystring: @@ -91,7 +97,7 @@ Verbatim name of the checker is ``compare-to-empty-string``. Compare-To-Empty-String checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:compare-to-empty-string (C1901): *Avoid comparisons to empty string* +:compare-to-empty-string (C1901): *"%s" can be simplified to "%s" as an empty string is falsey* Used when Pylint detects comparison to an empty string constant. @@ -105,7 +111,7 @@ Verbatim name of the checker is ``compare-to-zero``. Compare-To-Zero checker Messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:compare-to-zero (C2001): *Avoid comparisons to zero* +:compare-to-zero (C2001): *"%s" can be simplified to "%s" as 0 is falsey* Used when Pylint detects comparison to a 0 constant. @@ -259,6 +265,21 @@ Design checker Messages Cyclomatic +.. _pylint.extensions.dict_init_mutate: + +Dict-Init-Mutate checker +~~~~~~~~~~~~~~~~~~~~~~~~ + +This checker is provided by ``pylint.extensions.dict_init_mutate``. +Verbatim name of the checker is ``dict-init-mutate``. + +Dict-Init-Mutate checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:dict-init-mutate (C3401): *Dictionary mutated immediately after initialization* + Dictionaries can be initialized with a single statement using dictionary + literal syntax. + + .. _pylint.extensions.docstyle: Docstyle checker @@ -275,6 +296,23 @@ Docstyle checker Messages Used when a blank line is found at the beginning of a docstring. +.. _pylint.extensions.dunder: + +Dunder checker +~~~~~~~~~~~~~~ + +This checker is provided by ``pylint.extensions.dunder``. +Verbatim name of the checker is ``dunder``. + +See also :ref:`dunder checker's options' documentation <dunder-options>` + +Dunder checker Messages +^^^^^^^^^^^^^^^^^^^^^^^ +:bad-dunder-name (W3201): *Bad or misspelled dunder method name %s.* + Used when a dunder method is misspelled or defined with a name not within the + predefined list of dunder names. + + .. _pylint.extensions.check_elif: Else If Used checker @@ -335,6 +373,23 @@ Import-Private-Name checker Messages underscores should be considered private. +.. _pylint.extensions.magic_value: + +Magic-Value checker +~~~~~~~~~~~~~~~~~~~ + +This checker is provided by ``pylint.extensions.magic_value``. +Verbatim name of the checker is ``magic-value``. + +See also :ref:`magic-value checker's options' documentation <magic-value-options>` + +Magic-Value checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:magic-value-comparison (R2004): *Consider using a named constant or an enum instead of '%s'.* + Using named constants instead of magic values helps improve readability and + maintainability of your code, try to avoid them in comparisons. + + .. _pylint.extensions.redefined_variable_type: Multiple Types checker diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index bff61f93b..8b166855a 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -95,7 +95,7 @@ Basic checker Messages Used when a break or a return statement is found inside the finally clause of a try...finally block: the exceptions raised in the try clause will be silently swallowed instead of being re-raised. -:assert-on-tuple (W0199): *Assert called on a 2-item-tuple. Did you mean 'assert x,y'?* +:assert-on-tuple (W0199): *Assert called on a populated tuple. Did you mean 'assert x,y'?* A call of assert on a tuple will always evaluate to true if the tuple is not empty, and will always evaluate to false if it is. :assert-on-string-literal (W0129): *Assert statement has a string literal as its first argument. The assert will %s fail.* @@ -135,6 +135,9 @@ Basic checker Messages argument list as the lambda itself; such lambda expressions are in all but a few cases replaceable with the function being called in the body of the lambda. +:named-expr-without-context (W0131): *Named expression used without context* + Emitted if named expression is used to do a regular assignment outside a + context like if, for, while, or a comprehension. :redeclared-assigned-name (W0128): *Redeclared variable %r in assignment* Emitted when we detect that a variable was redeclared in the same assignment. :pointless-statement (W0104): *Statement seems to have no effect* @@ -241,12 +244,12 @@ Classes checker Messages Used when a class has an inconsistent method resolution order. :inherit-non-class (E0239): *Inheriting %r, which is not a class.* Used when a class inherits from something which is not a class. -:invalid-class-object (E0243): *Invalid __class__ object* - Used when an invalid object is assigned to a __class__ property. Only a class - is permitted. :invalid-slots (E0238): *Invalid __slots__ object* Used when an invalid __slots__ is found in class. Only a string, an iterable or a sequence is permitted. +:invalid-class-object (E0243): *Invalid assignment to '__class__'. Should be a class definition but got a '%s'* + Used when an invalid object is assigned to a __class__ property. Only a class + is permitted. :invalid-slots-object (E0236): *Invalid object %r in __slots__, must contain only non empty strings* Used when an invalid (non-string) object occurs in __slots__. :no-method-argument (E0211): *Method %r has no argument* @@ -305,7 +308,7 @@ Classes checker Messages Used when an instance attribute is defined outside the __init__ method. :subclassed-final-class (W0240): *Class %r is a subclass of a class decorated with typing.final: %r* Used when a class decorated with typing.final has been subclassed. -:abstract-method (W0223): *Method %r is abstract in class %r but is not overridden* +:abstract-method (W0223): *Method %r is abstract in class %r but is not overridden in child class %r* Used when an abstract method (i.e. raise NotImplementedError) is not overridden in concrete class. :overridden-final-method (W0239): *Method %r overrides a method decorated with typing.final which is defined in class %r* @@ -440,7 +443,7 @@ Exceptions checker Messages :duplicate-except (W0705): *Catching previously caught exception type %s* Used when an except catches a type that was already caught by a previous handler. -:broad-except (W0703): *Catching too general exception %s* +:broad-exception-caught (W0718): *Catching too general exception %s* Used when an except catches a too general exception, possibly burying unrelated errors. :raise-missing-from (W0707): *Consider explicitly re-raising using %s'%s from %s'* @@ -462,6 +465,8 @@ Exceptions checker Messages operations between exceptions in except handlers. :bare-except (W0702): *No exception type(s) specified* Used when an except clause doesn't specify exceptions type to catch. +:broad-exception-raised (W0719): *Raising too general exception: %s* + Used when an except raises a too general exception. :try-except-raise (W0706): *The except handler raises immediately* Used when an except handler uses raise as its first or only operator. This is useless because it raises back the exception immediately. Remove the raise @@ -737,6 +742,9 @@ Refactoring checker Messages verbose. :consider-merging-isinstance (R1701): *Consider merging these isinstance calls to isinstance(%s, (%s))* Used when multiple consecutive isinstance calls can be merged into one. +:use-dict-literal (R1735): *Consider using '%s' instead of a call to 'dict'.* + Emitted when using dict() to create a dictionary instead of a literal '{ ... + }'. The literal is faster as it avoids an additional function call. :consider-using-max-builtin (R1731): *Consider using '%s' instead of unnecessary if block* Using the max builtin instead of a conditional improves readability and conciseness. @@ -779,9 +787,6 @@ Refactoring checker Messages :consider-swap-variables (R1712): *Consider using tuple unpacking for swapping variables* You do not have to use a temporary variable in order to swap variables. Using "tuple unpacking" to directly swap variables makes the intention more clear. -:use-dict-literal (R1735): *Consider using {} instead of dict()* - Emitted when using dict() to create an empty dictionary instead of the - literal {}. The literal is faster as it avoids an additional function call. :trailing-comma-tuple (R1707): *Disallow trailing comma tuple* In Python, a tuple is actually created by the comma symbol, not by the parentheses. Unfortunately, one can actually create a tuple by misplacing a @@ -845,7 +850,7 @@ Refactoring checker Messages Emitted when a single "return" or "return None" statement is found at the end of function or method definition. This statement can safely be removed because Python will implicitly return None -:use-implicit-booleaness-not-comparison (C1803): *'%s' can be simplified to '%s' as an empty sequence is falsey* +:use-implicit-booleaness-not-comparison (C1803): *'%s' can be simplified to '%s' as an empty %s is falsey* Used when Pylint detects that collection literal comparison is being used to check for emptiness; Use implicit booleaness instead of a collection classes; empty collections are considered as false @@ -927,6 +932,12 @@ Stdlib checker Messages :invalid-envvar-value (E1507): *%s does not support %s type argument* Env manipulation functions support only string type arguments. See https://docs.python.org/3/library/os.html#os.getenv. +:singledispatch-method (E1519): *singledispatch decorator should not be used with methods, use singledispatchmethod instead.* + singledispatch should decorate functions and not class/instance methods. Use + singledispatchmethod for those cases. +:singledispatchmethod-function (E1520): *singledispatchmethod decorator should not be used with functions, use singledispatch instead.* + singledispatchmethod should decorate class/instance methods and not + functions. Use singledispatch for those cases. :bad-open-mode (W1501): *"%s" is not a valid mode for open.* Python supports: r, w, a[, x] modes with b, +, and U (only with r) options. See https://docs.python.org/3/library/functions.html#open @@ -979,8 +990,8 @@ Stdlib checker Messages https://docs.python.org/3/library/subprocess.html#subprocess.run :bad-thread-instantiation (W1506): *threading.Thread needs the target function* The warning is emitted when a threading.Thread class is instantiated without - the target function being passed. By default, the first parameter is the - group param, not the target param. + the target function being passed as a kwarg or as a second argument. By + default, the first parameter is the group param, not the target param. String checker @@ -1144,6 +1155,9 @@ Typecheck checker Messages :invalid-slice-index (E1127): *Slice index is not an int, None, or instance with __index__* Used when a slice index is not an integer, None, or an object with an __index__ method. +:invalid-slice-step (E1144): *Slice step cannot be 0* + Used when a slice step is 0 and the object doesn't implement a custom + __getitem__ method. :too-many-function-args (E1121): *Too many positional arguments for %s call* Used when a function call passes too many positional arguments. :unexpected-keyword-arg (E1123): *Unexpected keyword argument %r in %s call* @@ -1295,7 +1309,9 @@ Variables checker Messages variable is not defined in the module scope. :self-cls-assignment (W0642): *Invalid assignment to %s in method* Invalid assignment to self or cls in instance or class method respectively. -:unbalanced-tuple-unpacking (W0632): *Possible unbalanced tuple unpacking with sequence%s: left side has %d label(s), right side has %d value(s)* +:unbalanced-dict-unpacking (W0644): *Possible unbalanced dict unpacking with %s: left side has %d label%s, right side has %d value%s* + Used when there is an unbalanced dict unpacking in assignment or for loop +:unbalanced-tuple-unpacking (W0632): *Possible unbalanced tuple unpacking with sequence %s: left side has %d label%s, right side has %d value%s* Used when there is an unbalanced tuple unpacking in assignment :possibly-unused-variable (W0641): *Possibly unused variable %r* Used when a variable is defined but might not be used. The possibility comes diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index 41b58efba..c9e651565 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -99,7 +99,7 @@ Standard Checkers --ignore-paths """""""""""""" -*Add files or directories matching the regular expressions patterns to the ignore-list. The regex matches against paths and can be in Posix or Windows format. Because '\' represents the directory delimiter on Windows systems, it can't be used as an escape character.* +*Add files or directories matching the regular expressions patterns to the ignore-list. The regex matches against paths and can be in Posix or Windows format. Because '\\' represents the directory delimiter on Windows systems, it can't be used as an escape character.* **Default:** ``[]`` @@ -217,9 +217,9 @@ Standard Checkers confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] - # disable = + disable = ["consider-using-augmented-assign"] - # enable = + enable = [] evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))" @@ -620,7 +620,7 @@ Standard Checkers """"""""""""""""""""""""""""""""""""""" *List of valid names for the first argument in a metaclass class method.* -**Default:** ``('cls',)`` +**Default:** ``('mcs',)`` @@ -642,7 +642,7 @@ Standard Checkers valid-classmethod-first-arg = ["cls"] - valid-metaclass-classmethod-first-arg = ["cls"] + valid-metaclass-classmethod-first-arg = ["mcs"] @@ -798,7 +798,7 @@ Standard Checkers """""""""""""""""""""""" *Exceptions that will emit a warning when caught.* -**Default:** ``('BaseException', 'Exception')`` +**Default:** ``('builtins.BaseException', 'builtins.Exception')`` @@ -812,7 +812,7 @@ Standard Checkers .. code-block:: toml [tool.pylint.exceptions] - overgeneral-exceptions = ["BaseException", "Exception"] + overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] @@ -1673,6 +1673,68 @@ Extensions </details> +.. _dunder-options: + +``Dunder`` **Checker** +---------------------- +--good-dunder-names +""""""""""""""""""" +*Good dunder names which should always be accepted.* + +**Default:** ``[]`` + + + +.. raw:: html + + <details> + <summary><a>Example configuration section</a></summary> + +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. + +.. code-block:: toml + + [tool.pylint.dunder] + good-dunder-names = [] + + + +.. raw:: html + + </details> + + +.. _magic-value-options: + +``Magic-value`` **Checker** +--------------------------- +--valid-magic-values +"""""""""""""""""""" +* List of valid magic values that `magic-value-compare` will not detect.* + +**Default:** ``(0, -1, 1, '', '__main__')`` + + + +.. raw:: html + + <details> + <summary><a>Example configuration section</a></summary> + +**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values. + +.. code-block:: toml + + [tool.pylint.magic-value] + valid-magic-values = [0, -1, 1, "", "__main__"] + + + +.. raw:: html + + </details> + + .. _parameter_documentation-options: ``Parameter_documentation`` **Checker** diff --git a/doc/user_guide/configuration/index.rst b/doc/user_guide/configuration/index.rst index d039b4445..ffe8c51a3 100644 --- a/doc/user_guide/configuration/index.rst +++ b/doc/user_guide/configuration/index.rst @@ -9,7 +9,7 @@ various projects and a lot of checks to activate if they suit your style. You can generate a sample configuration file with ``--generate-toml-config`` or ``--generate-rcfile``. Every option present on the command line before this -will be included in the file +will be included in the file. For example:: @@ -18,6 +18,13 @@ For example:: In practice, it is often better to create a minimal configuration file which only contains configuration overrides. For all other options, Pylint will use its default values. +.. note:: + + The internals that create the configuration files fall back to the default values if + no other value was given. This means that some values depend on the interpreter that + was used to generate the file. Most notably ``py-version`` which defaults to the + current interpreter. + .. toctree:: :maxdepth: 2 :titlesonly: diff --git a/doc/user_guide/installation/ide_integration/flymake-emacs.rst b/doc/user_guide/installation/ide_integration/flymake-emacs.rst index 44b9b3262..79310ff59 100644 --- a/doc/user_guide/installation/ide_integration/flymake-emacs.rst +++ b/doc/user_guide/installation/ide_integration/flymake-emacs.rst @@ -4,12 +4,10 @@ Using Pylint through Flymake in Emacs ===================================== .. warning:: - If you're reading this doc and are actually using flymake please - open a support question at https://github.com/PyCQA/pylint/issues/new/choose - and tell us, we don't have any maintainers for emacs and are thinking about - dropping the support. - -.. TODO 3.0, do we still need to support flymake ? + The Emacs package now has its own repository and is looking for a maintainer. + If you're reading this doc and are interested in maintaining this package or + are actually using flymake please open an issue at + https://github.com/emacsorphanage/pylint/issues/new/choose To enable Flymake for Python, insert the following into your .emacs: diff --git a/doc/user_guide/installation/ide_integration/index.rst b/doc/user_guide/installation/ide_integration/index.rst index c1bf5eb4d..c359c8ee1 100644 --- a/doc/user_guide/installation/ide_integration/index.rst +++ b/doc/user_guide/installation/ide_integration/index.rst @@ -18,6 +18,7 @@ Below you can find tutorials for some of the most common ones. - PyDev_ - pyscripter_ in the `Tool -> Tools` menu. - Spyder_ in the `View -> Panes -> Static code analysis` +- `Sublime Text`_ - :ref:`TextMate <pylint_in_textmate>` - Vim_ - `Visual Studio Code`_ in the `Preferences -> Settings` menu @@ -36,6 +37,7 @@ Below you can find tutorials for some of the most common ones. .. _pydev: https://www.pydev.org/manual_adv_pylint.html .. _pyscripter: https://github.com/pyscripter/pyscripter .. _spyder: https://docs.spyder-ide.org/current/panes/pylint.html +.. _Sublime Text: https://packagecontrol.io/packages/SublimeLinter-pylint .. _Vim: https://www.vim.org/scripts/script.php?script_id=891 .. _Visual Studio: https://docs.microsoft.com/visualstudio/python/code-pylint .. _Visual Studio Code: https://code.visualstudio.com/docs/python/linting#_pylint diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index 558b3f1bf..d7c058823 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -102,6 +102,7 @@ All messages in the error category: error/invalid-repr-returned error/invalid-sequence-index error/invalid-slice-index + error/invalid-slice-step error/invalid-slots error/invalid-slots-object error/invalid-star-assignment-target @@ -145,6 +146,8 @@ All messages in the error category: error/return-arg-in-generator error/return-in-init error/return-outside-function + error/singledispatch-method + error/singledispatchmethod-function error/star-needs-assignment-target error/syntax-error error/too-few-format-args @@ -205,6 +208,7 @@ All messages in the warning category: warning/assert-on-tuple warning/attribute-defined-outside-init warning/bad-builtin + warning/bad-dunder-name warning/bad-format-string warning/bad-format-string-key warning/bad-indentation @@ -214,7 +218,8 @@ All messages in the warning category: warning/bare-except warning/binary-op-exception warning/boolean-datetime - warning/broad-except + warning/broad-exception-caught + warning/broad-exception-raised warning/cell-var-from-loop warning/comparison-with-callable warning/confusing-with-statement @@ -226,6 +231,7 @@ All messages in the warning category: warning/deprecated-method warning/deprecated-module warning/deprecated-typing-alias + warning/dict-init-mutate warning/differing-param-doc warning/differing-type-doc warning/duplicate-except @@ -273,6 +279,7 @@ All messages in the warning category: warning/missing-yield-type-doc warning/modified-iterating-list warning/multiple-constructor-doc + warning/named-expr-without-context warning/nan-comparison warning/non-ascii-file-name warning/non-parent-init-called @@ -307,6 +314,7 @@ All messages in the warning category: warning/super-without-brackets warning/too-many-try-statements warning/try-except-raise + warning/unbalanced-dict-unpacking warning/unbalanced-tuple-unpacking warning/undefined-loop-variable warning/unknown-option-value @@ -341,6 +349,7 @@ All renamed messages in the warning category: :maxdepth: 1 :titlesonly: + warning/broad-except warning/cache-max-size-none warning/implicit-str-concat-in-sequence warning/lru-cache-decorating-method @@ -460,6 +469,7 @@ All messages in the refactor category: refactor/consider-swap-variables refactor/consider-using-alias refactor/consider-using-assignment-expr + refactor/consider-using-augmented-assign refactor/consider-using-dict-comprehension refactor/consider-using-from-import refactor/consider-using-generator @@ -480,6 +490,7 @@ All messages in the refactor category: refactor/empty-comment refactor/inconsistent-return-statements refactor/literal-comparison + refactor/magic-value-comparison refactor/no-classmethod-decorator refactor/no-else-break refactor/no-else-continue diff --git a/doc/user_guide/usage/output.rst b/doc/user_guide/usage/output.rst index c98eb8476..50a7fa76e 100644 --- a/doc/user_guide/usage/output.rst +++ b/doc/user_guide/usage/output.rst @@ -131,7 +131,7 @@ Following the analysis message, Pylint can display a set of reports, each one focusing on a particular aspect of the project, such as number of messages by categories, modules dependencies. These features can be enabled through the ``--reports=y`` option, or its shorthand -version ``-rn``. +version ``-ry``. For instance, the metrics report displays summaries gathered from the current run. diff --git a/doc/whatsnew/2/2.15/index.rst b/doc/whatsnew/2/2.15/index.rst index 1e404e81e..b1e40d274 100644 --- a/doc/whatsnew/2/2.15/index.rst +++ b/doc/whatsnew/2/2.15/index.rst @@ -169,7 +169,7 @@ False Positives Fixed --------------------- - Fix the message for ``unnecessary-dunder-call`` for ``__aiter__`` and - ``__aneext__``. Also + ``__anext__``. Also only emit the warning when ``py-version`` >= 3.10. Closes #7529 (`#7529 <https://github.com/PyCQA/pylint/issues/7529>`_) diff --git a/doc/whatsnew/fragments/2374.false_negative b/doc/whatsnew/fragments/2374.false_negative new file mode 100644 index 000000000..251ffc396 --- /dev/null +++ b/doc/whatsnew/fragments/2374.false_negative @@ -0,0 +1,4 @@ +Emit ``used-before-assignment`` when function arguments are redefined inside +an inner function and accessed there before assignment. + +Closes #2374 diff --git a/doc/whatsnew/fragments/2876.new_check b/doc/whatsnew/fragments/2876.new_check new file mode 100644 index 000000000..a8353a32e --- /dev/null +++ b/doc/whatsnew/fragments/2876.new_check @@ -0,0 +1,4 @@ +Add new extension checker ``dict-init-mutate`` that flags mutating a dictionary immediately +after the dictionary was created. + +Closes #2876 diff --git a/doc/whatsnew/fragments/3038.new_check b/doc/whatsnew/fragments/3038.new_check new file mode 100644 index 000000000..8e61147fc --- /dev/null +++ b/doc/whatsnew/fragments/3038.new_check @@ -0,0 +1,4 @@ +Added ``bad-dunder-name`` extension check, which flags bad or misspelled dunder methods. +You can use the ``good-dunder-names`` option to allow specific dunder names. + +Closes #3038 diff --git a/doc/whatsnew/fragments/3044.bugfix b/doc/whatsnew/fragments/3044.bugfix new file mode 100644 index 000000000..9f764ca4b --- /dev/null +++ b/doc/whatsnew/fragments/3044.bugfix @@ -0,0 +1,3 @@ +Fix bug in detecting ``unused-variable`` when iterating on variable. + +Closes #3044 diff --git a/doc/whatsnew/fragments/3299.bugfix b/doc/whatsnew/fragments/3299.bugfix new file mode 100644 index 000000000..dd45d1978 --- /dev/null +++ b/doc/whatsnew/fragments/3299.bugfix @@ -0,0 +1,4 @@ +Fix bug in scanning of names inside arguments to `typing.Literal`. +See https://peps.python.org/pep-0586/#literals-enums-and-forward-references for details. + +Refs #3299 diff --git a/doc/whatsnew/fragments/3299.false_positive b/doc/whatsnew/fragments/3299.false_positive new file mode 100644 index 000000000..b1e61c931 --- /dev/null +++ b/doc/whatsnew/fragments/3299.false_positive @@ -0,0 +1,3 @@ +Fix false positive for ``unused-variable`` and ``unused-import`` when a name is only used in a string literal type annotation. + +Closes #3299 diff --git a/doc/whatsnew/fragments/3391.extension b/doc/whatsnew/fragments/3391.extension new file mode 100644 index 000000000..8879610b2 --- /dev/null +++ b/doc/whatsnew/fragments/3391.extension @@ -0,0 +1,6 @@ +Added ``consider-using-augmented-assign`` check for ``CodeStyle`` extension +which flags ``x = x + 1`` to simplify to ``x += 1``. +This check is disabled by default. To use it, load the code style extension +with ``load-plugins=pylint.extensions.code_style`` and add ``consider-using-augmented-assign`` in the ``enable`` option. + +Closes #3391 diff --git a/doc/whatsnew/fragments/3822.false_positive b/doc/whatsnew/fragments/3822.false_positive new file mode 100644 index 000000000..00a2143cf --- /dev/null +++ b/doc/whatsnew/fragments/3822.false_positive @@ -0,0 +1,3 @@ +``trailing-whitespaces`` is no longer reported within strings. + +Closes #3822 diff --git a/doc/whatsnew/fragments/4150.false_negative b/doc/whatsnew/fragments/4150.false_negative new file mode 100644 index 000000000..f4b18d814 --- /dev/null +++ b/doc/whatsnew/fragments/4150.false_negative @@ -0,0 +1,3 @@ +Fix a false negative for ``unused-import`` when one module used an import in a type annotation that was also used in another module. + +Closes #4150 diff --git a/doc/whatsnew/fragments/4354.bugfix b/doc/whatsnew/fragments/4354.bugfix new file mode 100644 index 000000000..09caf8d13 --- /dev/null +++ b/doc/whatsnew/fragments/4354.bugfix @@ -0,0 +1,3 @@ +Fix ignored files being linted when passed on stdin. + +Closes #4354 diff --git a/doc/whatsnew/fragments/4562.bugfix b/doc/whatsnew/fragments/4562.bugfix new file mode 100644 index 000000000..4e153d487 --- /dev/null +++ b/doc/whatsnew/fragments/4562.bugfix @@ -0,0 +1,3 @@ +Fix ``no-member`` false negative when augmented assign is done manually, without ``+=``. + +Closes #4562 diff --git a/doc/whatsnew/fragments/4655.bugfix b/doc/whatsnew/fragments/4655.bugfix new file mode 100644 index 000000000..e30433d47 --- /dev/null +++ b/doc/whatsnew/fragments/4655.bugfix @@ -0,0 +1,3 @@ +Any assertion on a populated tuple will now receive a ``assert-on-tuple`` warning. + +Closes #4655 diff --git a/doc/whatsnew/fragments/4743.bugfix b/doc/whatsnew/fragments/4743.bugfix new file mode 100644 index 000000000..1f8c30f1a --- /dev/null +++ b/doc/whatsnew/fragments/4743.bugfix @@ -0,0 +1,4 @@ +``missing-return-doc``, ``missing-raises-doc`` and ``missing-yields-doc`` now respect +the ``no-docstring-rgx`` option. + +Closes #4743 diff --git a/doc/whatsnew/fragments/4792.false_negative b/doc/whatsnew/fragments/4792.false_negative new file mode 100644 index 000000000..6d05ef565 --- /dev/null +++ b/doc/whatsnew/fragments/4792.false_negative @@ -0,0 +1,3 @@ +Flag ``superfluous-parens`` if parentheses are used during string concatenation. + +Closes #4792 diff --git a/doc/whatsnew/fragments/4809.false_positive b/doc/whatsnew/fragments/4809.false_positive new file mode 100644 index 000000000..a33be0ea5 --- /dev/null +++ b/doc/whatsnew/fragments/4809.false_positive @@ -0,0 +1,3 @@ +Fix false positive for ``global-variable-not-assigned`` when a global variable is re-assigned via an ``ImportFrom`` node. + +Closes #4809 diff --git a/doc/whatsnew/fragments/4913.false_negative b/doc/whatsnew/fragments/4913.false_negative new file mode 100644 index 000000000..bb8686347 --- /dev/null +++ b/doc/whatsnew/fragments/4913.false_negative @@ -0,0 +1,3 @@ +Emit ``used-before-assignment`` when relying on names only defined under conditions always testing false. + +Closes #4913 diff --git a/doc/whatsnew/fragments/519.false_negative b/doc/whatsnew/fragments/519.false_negative new file mode 100644 index 000000000..7c8a0f45c --- /dev/null +++ b/doc/whatsnew/fragments/519.false_negative @@ -0,0 +1,3 @@ +Code following a call to ``quit``, ``exit``, ``sys.exit`` or ``os._exit`` will be marked as `unreachable`. + +Refs #519 diff --git a/doc/whatsnew/fragments/5478.bugfix b/doc/whatsnew/fragments/5478.bugfix new file mode 100644 index 000000000..8cadb729c --- /dev/null +++ b/doc/whatsnew/fragments/5478.bugfix @@ -0,0 +1,3 @@ +``consider-iterating-dictionary`` will no longer be raised if bitwise operations are used. + +Closes #5478 diff --git a/doc/whatsnew/fragments/5797.new_check b/doc/whatsnew/fragments/5797.new_check new file mode 100644 index 000000000..f82abe09d --- /dev/null +++ b/doc/whatsnew/fragments/5797.new_check @@ -0,0 +1,4 @@ +Add new check called ``unbalanced-dict-unpacking`` to check for unbalanced dict unpacking +in assignment and for loops. + +Closes #5797 diff --git a/doc/whatsnew/fragments/5886.false_positive b/doc/whatsnew/fragments/5886.false_positive new file mode 100644 index 000000000..d6ab03940 --- /dev/null +++ b/doc/whatsnew/fragments/5886.false_positive @@ -0,0 +1,3 @@ +Fix ``deprecated-method`` false positive when alias for method is similar to name of deprecated method. + +Closes #5886 diff --git a/doc/whatsnew/fragments/5920.other b/doc/whatsnew/fragments/5920.other new file mode 100644 index 000000000..5bd356a9c --- /dev/null +++ b/doc/whatsnew/fragments/5920.other @@ -0,0 +1,3 @@ +Pylint now provides basic support for Python 3.11. + +Closes #5920 diff --git a/doc/whatsnew/fragments/6242.bugfix b/doc/whatsnew/fragments/6242.bugfix new file mode 100644 index 000000000..25d323e7e --- /dev/null +++ b/doc/whatsnew/fragments/6242.bugfix @@ -0,0 +1,4 @@ +Pylint will now filter duplicates given to it before linting. The output should +be the same whether a file is given/discovered multiple times or not. + +Closes #6242, #4053 diff --git a/doc/whatsnew/fragments/6543.feature b/doc/whatsnew/fragments/6543.feature new file mode 100644 index 000000000..0ebb9b19f --- /dev/null +++ b/doc/whatsnew/fragments/6543.feature @@ -0,0 +1,5 @@ +Update ``pyreverse`` to differentiate between aggregations and compositions. +``pyreverse`` checks if it's an Instance or a Call of an object via method parameters (via type hints) +to decide if it's a composition or an aggregation. + +Refs #6543 diff --git a/doc/whatsnew/fragments/6592.false_positive b/doc/whatsnew/fragments/6592.false_positive new file mode 100644 index 000000000..846ddce96 --- /dev/null +++ b/doc/whatsnew/fragments/6592.false_positive @@ -0,0 +1,3 @@ +Fix false positive for ``too-many-function-args`` when a function call is assigned to a class attribute inside the class where the function is defined. + +Closes #6592 diff --git a/doc/whatsnew/fragments/6639.false_negative b/doc/whatsnew/fragments/6639.false_negative new file mode 100644 index 000000000..7735c8536 --- /dev/null +++ b/doc/whatsnew/fragments/6639.false_negative @@ -0,0 +1,3 @@ +``consider-using-join`` can now be emitted for non-empty string separators. + +Closes #6639 diff --git a/doc/whatsnew/fragments/6795.bugfix b/doc/whatsnew/fragments/6795.bugfix new file mode 100644 index 000000000..20a29da35 --- /dev/null +++ b/doc/whatsnew/fragments/6795.bugfix @@ -0,0 +1,3 @@ +Remove ``__index__`` dunder method call from ``unnecessary-dunder-call`` check. + +Closes #6795 diff --git a/doc/whatsnew/fragments/6917.new_check b/doc/whatsnew/fragments/6917.new_check new file mode 100644 index 000000000..d36aa2c59 --- /dev/null +++ b/doc/whatsnew/fragments/6917.new_check @@ -0,0 +1,4 @@ +Added ``singledispatch-method`` which informs that ``@singledispatch`` should decorate functions and not class/instance methods. +Added ``singledispatchmethod-function`` which informs that ``@singledispatchmethod`` should decorate class/instance methods and not functions. + +Closes #6917 diff --git a/doc/whatsnew/fragments/7003.bugfix b/doc/whatsnew/fragments/7003.bugfix new file mode 100644 index 000000000..3e2806f6f --- /dev/null +++ b/doc/whatsnew/fragments/7003.bugfix @@ -0,0 +1,4 @@ +Fixed handling of ``--`` as separator between positional arguments and flags. +This was not actually fixed in 2.14.5. + +Closes #7003, Refs #7096 diff --git a/doc/whatsnew/fragments/7124.other b/doc/whatsnew/fragments/7124.other new file mode 100644 index 000000000..684cf2dc0 --- /dev/null +++ b/doc/whatsnew/fragments/7124.other @@ -0,0 +1,3 @@ +Update message for ``abstract-method`` to include child class name. + +Closes #7124 diff --git a/doc/whatsnew/fragments/7169.bugfix b/doc/whatsnew/fragments/7169.bugfix new file mode 100644 index 000000000..6ddf1a498 --- /dev/null +++ b/doc/whatsnew/fragments/7169.bugfix @@ -0,0 +1,3 @@ +Don't crash on ``OSError`` in config file discovery. + +Closes #7169 diff --git a/doc/whatsnew/fragments/7208.user_action b/doc/whatsnew/fragments/7208.user_action new file mode 100644 index 000000000..0492676b3 --- /dev/null +++ b/doc/whatsnew/fragments/7208.user_action @@ -0,0 +1,10 @@ +The ``accept-no-raise-doc`` option related to ``missing-raises-doc`` will now +be correctly taken into account all the time. + +Pylint will no longer raise missing-raises-doc (W9006) when no exceptions are +documented and accept-no-raise-doc is true (issue #7208). +If you were expecting missing-raises-doc errors to be raised in that case, you +will now have to add ``accept-no-raise-doc=no`` in your configuration to keep +the same behavior. + +Closes #7208 diff --git a/doc/whatsnew/fragments/7214.bugfix b/doc/whatsnew/fragments/7214.bugfix new file mode 100644 index 000000000..016d3dc3e --- /dev/null +++ b/doc/whatsnew/fragments/7214.bugfix @@ -0,0 +1,3 @@ +Messages sent to reporter are now copied so a reporter cannot modify the message sent to other reporters. + +Closes #7214 diff --git a/doc/whatsnew/fragments/7264.bugfix b/doc/whatsnew/fragments/7264.bugfix new file mode 100644 index 000000000..dc2aa5d40 --- /dev/null +++ b/doc/whatsnew/fragments/7264.bugfix @@ -0,0 +1,8 @@ +Fixed a case where custom plugins specified by command line could silently fail. + +Specifically, if a plugin relies on the ``init-hook`` option changing ``sys.path`` before +it can be imported, this will now emit a ``bad-plugin-value`` message. Before this +change, it would silently fail to register the plugin for use, but would load +any configuration, which could have unintended effects. + +Fixes part of #7264. diff --git a/doc/whatsnew/fragments/7264.internal b/doc/whatsnew/fragments/7264.internal new file mode 100644 index 000000000..2bd3337bd --- /dev/null +++ b/doc/whatsnew/fragments/7264.internal @@ -0,0 +1,9 @@ +Add and fix regression tests for plugin loading. + +This shores up the tests that cover the loading of custom plugins as affected +by any changes made to the ``sys.path`` during execution of an ``init-hook``. +Given the existing contract of allowing plugins to be loaded by fiddling with +the path in this way, this is now the last bit of work needed to close Github +issue #7264. + +Closes #7264 diff --git a/doc/whatsnew/fragments/7281.new_check b/doc/whatsnew/fragments/7281.new_check new file mode 100644 index 000000000..46c2f0551 --- /dev/null +++ b/doc/whatsnew/fragments/7281.new_check @@ -0,0 +1,4 @@ +Add ``magic-number`` plugin checker for comparison with constants instead of named constants or enums. +You can use it with ``--load-plugins=pylint.extensions.magic_value``. + +Closes #7281 diff --git a/doc/whatsnew/fragments/7290.false_positive b/doc/whatsnew/fragments/7290.false_positive new file mode 100644 index 000000000..cfb6a13b5 --- /dev/null +++ b/doc/whatsnew/fragments/7290.false_positive @@ -0,0 +1,3 @@ +Pylint now understands the ``kw_only`` keyword argument for ``dataclass``. + +Closes #7290, closes #6550, closes #5857 diff --git a/doc/whatsnew/fragments/7311.false_positive b/doc/whatsnew/fragments/7311.false_positive new file mode 100644 index 000000000..84d57502b --- /dev/null +++ b/doc/whatsnew/fragments/7311.false_positive @@ -0,0 +1,4 @@ +Fix false positive for ``undefined-loop-variable`` in ``for-else`` loops that use a function +having a return type annotation of ``NoReturn`` or ``Never``. + +Closes #7311 diff --git a/doc/whatsnew/fragments/7346.other b/doc/whatsnew/fragments/7346.other new file mode 100644 index 000000000..01d1a26d9 --- /dev/null +++ b/doc/whatsnew/fragments/7346.other @@ -0,0 +1,4 @@ +Update Pyreverse's dot and plantuml printers to detect when class methods are abstract and show them with italic font. +For the dot printer update the label to use html-like syntax. + +Closes #7346 diff --git a/doc/whatsnew/fragments/7368.false_positive b/doc/whatsnew/fragments/7368.false_positive new file mode 100644 index 000000000..4e9551a32 --- /dev/null +++ b/doc/whatsnew/fragments/7368.false_positive @@ -0,0 +1,3 @@ +Fix ``used-before-assignment`` for functions/classes defined in type checking guard. + +Closes #7368 diff --git a/doc/whatsnew/fragments/7380.bugfix b/doc/whatsnew/fragments/7380.bugfix new file mode 100644 index 000000000..dc5ea5fa6 --- /dev/null +++ b/doc/whatsnew/fragments/7380.bugfix @@ -0,0 +1,3 @@ +Update ``modified_iterating`` checker to fix a crash with ``for`` loops on empty list. + +Closes #7380 diff --git a/doc/whatsnew/fragments/7390.bugfix b/doc/whatsnew/fragments/7390.bugfix new file mode 100644 index 000000000..c1808eec0 --- /dev/null +++ b/doc/whatsnew/fragments/7390.bugfix @@ -0,0 +1,3 @@ +Update wording for ``arguments-differ`` and ``arguments-renamed`` to clarify overriding object. + +Closes #7390 diff --git a/doc/whatsnew/fragments/7398.other b/doc/whatsnew/fragments/7398.other new file mode 100644 index 000000000..e83974ccf --- /dev/null +++ b/doc/whatsnew/fragments/7398.other @@ -0,0 +1,4 @@ +The ``docparams`` extension now considers typing in Numpy style docstrings +as "documentation" for the ``missing-param-doc`` message. + +Refs #7398 diff --git a/doc/whatsnew/fragments/7401.bugfix b/doc/whatsnew/fragments/7401.bugfix new file mode 100644 index 000000000..8b0f0e2a8 --- /dev/null +++ b/doc/whatsnew/fragments/7401.bugfix @@ -0,0 +1,3 @@ +``disable-next`` is now correctly scoped to only the succeeding line. + +Closes #7401 diff --git a/doc/whatsnew/fragments/7453.bugfix b/doc/whatsnew/fragments/7453.bugfix new file mode 100644 index 000000000..94b5240dd --- /dev/null +++ b/doc/whatsnew/fragments/7453.bugfix @@ -0,0 +1,3 @@ +Fixed a crash in the ``unhashable-member`` checker when using a ``lambda`` as a dict key. + +Closes #7453 diff --git a/doc/whatsnew/fragments/7457.bugfix b/doc/whatsnew/fragments/7457.bugfix new file mode 100644 index 000000000..31b48d257 --- /dev/null +++ b/doc/whatsnew/fragments/7457.bugfix @@ -0,0 +1,3 @@ +Add `mailcap` to deprecated modules list. + +Closes #7457 diff --git a/doc/whatsnew/fragments/7461.bugfix b/doc/whatsnew/fragments/7461.bugfix new file mode 100644 index 000000000..1fb503c9a --- /dev/null +++ b/doc/whatsnew/fragments/7461.bugfix @@ -0,0 +1,3 @@ +Fix a crash in the ``modified-iterating-dict`` checker involving instance attributes. + +Closes #7461 diff --git a/doc/whatsnew/fragments/7463.other b/doc/whatsnew/fragments/7463.other new file mode 100644 index 000000000..5af1ed640 --- /dev/null +++ b/doc/whatsnew/fragments/7463.other @@ -0,0 +1,3 @@ +Relevant `DeprecationWarnings` are now raised with `stacklevel=2`, so they have the callsite attached in the message. + +Closes #7463 diff --git a/doc/whatsnew/fragments/7467.bugfix b/doc/whatsnew/fragments/7467.bugfix new file mode 100644 index 000000000..7e76f86a0 --- /dev/null +++ b/doc/whatsnew/fragments/7467.bugfix @@ -0,0 +1,3 @@ +``invalid-class-object`` does not crash anymore when ``__class__`` is assigned alongside another variable. + +Closes #7467 diff --git a/doc/whatsnew/fragments/7471.bugfix b/doc/whatsnew/fragments/7471.bugfix new file mode 100644 index 000000000..b1b6f369c --- /dev/null +++ b/doc/whatsnew/fragments/7471.bugfix @@ -0,0 +1,3 @@ +``--help-msg`` now accepts a comma-separated list of message IDs again. + +Closes #7471 diff --git a/doc/whatsnew/fragments/7485.other b/doc/whatsnew/fragments/7485.other new file mode 100644 index 000000000..e2f3d36d3 --- /dev/null +++ b/doc/whatsnew/fragments/7485.other @@ -0,0 +1,3 @@ +Add a ``minimal`` option to ``pylint-config`` and its toml generator. + +Closes #7485 diff --git a/doc/whatsnew/fragments/7494.new_check b/doc/whatsnew/fragments/7494.new_check new file mode 100644 index 000000000..fe62e1483 --- /dev/null +++ b/doc/whatsnew/fragments/7494.new_check @@ -0,0 +1,4 @@ +Rename ``broad-except`` to ``broad-exception-caught`` and add new checker ``broad-exception-raised`` +which will warn if general exceptions ``BaseException`` or ``Exception`` are raised. + +Closes #7494 diff --git a/doc/whatsnew/fragments/7495.bugfix b/doc/whatsnew/fragments/7495.bugfix new file mode 100644 index 000000000..cec2bcf53 --- /dev/null +++ b/doc/whatsnew/fragments/7495.bugfix @@ -0,0 +1,4 @@ +Allow specifying non-builtin exceptions in the ``overgeneral-exception`` option +using an exception's qualified name. + +Closes #7495 diff --git a/doc/whatsnew/fragments/7501.false_positive b/doc/whatsnew/fragments/7501.false_positive new file mode 100644 index 000000000..0c2d33a07 --- /dev/null +++ b/doc/whatsnew/fragments/7501.false_positive @@ -0,0 +1,3 @@ +Fix false positive for ``unhashable-member`` when subclassing ``dict`` and using the subclass as a dictionary key. + +Closes #7501 diff --git a/doc/whatsnew/fragments/7507.bugfix b/doc/whatsnew/fragments/7507.bugfix new file mode 100644 index 000000000..5ce05c658 --- /dev/null +++ b/doc/whatsnew/fragments/7507.bugfix @@ -0,0 +1,4 @@ +Report ``no-self-argument`` rather than ``no-method-argument`` for methods +with variadic arguments. + +Closes #7507 diff --git a/doc/whatsnew/fragments/7507.other b/doc/whatsnew/fragments/7507.other new file mode 100644 index 000000000..3cdca7465 --- /dev/null +++ b/doc/whatsnew/fragments/7507.other @@ -0,0 +1,3 @@ +Add method name to the error messages of ``no-method-argument`` and ``no-self-argument``. + +Closes #7507 diff --git a/doc/whatsnew/fragments/7522.bugfix b/doc/whatsnew/fragments/7522.bugfix new file mode 100644 index 000000000..f4fa9da1a --- /dev/null +++ b/doc/whatsnew/fragments/7522.bugfix @@ -0,0 +1,3 @@ +Fixed an issue where ``syntax-error`` couldn't be raised on files with invalid encodings. + +Closes #7522 diff --git a/doc/whatsnew/fragments/7524.bugfix b/doc/whatsnew/fragments/7524.bugfix new file mode 100644 index 000000000..8a9c5fc79 --- /dev/null +++ b/doc/whatsnew/fragments/7524.bugfix @@ -0,0 +1,4 @@ +Fix false positive for ``redefined-outer-name`` when aliasing ``typing`` +e.g. as ``t`` and guarding imports under ``t.TYPE_CHECKING``. + +Closes #7524 diff --git a/doc/whatsnew/fragments/7528.bugfix b/doc/whatsnew/fragments/7528.bugfix new file mode 100644 index 000000000..b06bf1570 --- /dev/null +++ b/doc/whatsnew/fragments/7528.bugfix @@ -0,0 +1,3 @@ +Fixed a crash of the ``modified_iterating`` checker when iterating on a set defined as a class attribute. + +Closes #7528 diff --git a/doc/whatsnew/fragments/7529.false_positive b/doc/whatsnew/fragments/7529.false_positive new file mode 100644 index 000000000..7775d9086 --- /dev/null +++ b/doc/whatsnew/fragments/7529.false_positive @@ -0,0 +1,4 @@ +Fix the message for ``unnecessary-dunder-call`` for ``__aiter__`` and ``__aneext__``. Also +only emit the warning when ``py-version`` >= 3.10. + +Closes #7529 diff --git a/doc/whatsnew/fragments/7544.other b/doc/whatsnew/fragments/7544.other new file mode 100644 index 000000000..b868f1265 --- /dev/null +++ b/doc/whatsnew/fragments/7544.other @@ -0,0 +1,3 @@ +Prevent leaving the pip install cache in the Docker image. + +Refs #7544 diff --git a/doc/whatsnew/fragments/7546.new_check b/doc/whatsnew/fragments/7546.new_check new file mode 100644 index 000000000..5b1f5a327 --- /dev/null +++ b/doc/whatsnew/fragments/7546.new_check @@ -0,0 +1,4 @@ +Added ``nested-min-max`` which flags ``min(1, min(2, 3))`` to simplify to +``min(1, 2, 3)``. + +Closes #7546 diff --git a/doc/whatsnew/fragments/7547.false_negative b/doc/whatsnew/fragments/7547.false_negative new file mode 100644 index 000000000..97abdf100 --- /dev/null +++ b/doc/whatsnew/fragments/7547.false_negative @@ -0,0 +1,3 @@ +Fix a false negative for ``unused-import`` when a constant inside ``typing.Annotated`` was treated as a reference to an import. + +Closes #7547 diff --git a/doc/whatsnew/fragments/7563.false_positive b/doc/whatsnew/fragments/7563.false_positive new file mode 100644 index 000000000..1a3252f35 --- /dev/null +++ b/doc/whatsnew/fragments/7563.false_positive @@ -0,0 +1,3 @@ +Fix ``used-before-assignment`` false positive when else branch calls ``sys.exit`` or similar terminating functions. + +Closes #7563 diff --git a/doc/whatsnew/fragments/7569.bugfix b/doc/whatsnew/fragments/7569.bugfix new file mode 100644 index 000000000..c6a84fc64 --- /dev/null +++ b/doc/whatsnew/fragments/7569.bugfix @@ -0,0 +1,3 @@ +Use ``py-version`` to determine if a message should be emitted for messages defined with ``max-version`` or ``min-version``. + +Closes #7569 diff --git a/doc/whatsnew/fragments/7570.bugfix b/doc/whatsnew/fragments/7570.bugfix new file mode 100644 index 000000000..13cbc2916 --- /dev/null +++ b/doc/whatsnew/fragments/7570.bugfix @@ -0,0 +1,4 @@ +Improve ``bad-thread-instantiation`` check to warn if ``target`` is not passed in as a keyword argument +or as a second argument. + +Closes #7570 diff --git a/doc/whatsnew/fragments/7609.false_positive b/doc/whatsnew/fragments/7609.false_positive new file mode 100644 index 000000000..5c91f396e --- /dev/null +++ b/doc/whatsnew/fragments/7609.false_positive @@ -0,0 +1,4 @@ +Fix a false positive for ``used-before-assignment`` for imports guarded by +``typing.TYPE_CHECKING`` later used in variable annotations. + +Closes #7609 diff --git a/doc/whatsnew/fragments/7610.bugfix b/doc/whatsnew/fragments/7610.bugfix new file mode 100644 index 000000000..3eb49fcbb --- /dev/null +++ b/doc/whatsnew/fragments/7610.bugfix @@ -0,0 +1,3 @@ +Fixes edge case of custom method named ``next`` raised an astroid error. + +Closes #7610 diff --git a/doc/whatsnew/fragments/7626.false_positive b/doc/whatsnew/fragments/7626.false_positive new file mode 100644 index 000000000..bfa56c107 --- /dev/null +++ b/doc/whatsnew/fragments/7626.false_positive @@ -0,0 +1,4 @@ +Fix a false positive for ``simplify-boolean-expression`` when multiple values +are inferred for a constant. + +Closes #7626 diff --git a/doc/whatsnew/fragments/7626.other b/doc/whatsnew/fragments/7626.other new file mode 100644 index 000000000..8020fc83f --- /dev/null +++ b/doc/whatsnew/fragments/7626.other @@ -0,0 +1,3 @@ +Add a keyword-only ``compare_constants`` argument to ``safe_infer``. + +Refs #7626 diff --git a/doc/whatsnew/fragments/7629.other b/doc/whatsnew/fragments/7629.other new file mode 100644 index 000000000..bac156ee5 --- /dev/null +++ b/doc/whatsnew/fragments/7629.other @@ -0,0 +1,4 @@ +Add ``default_enabled`` option to optional message dict. Provides an option to disable a checker message by default. +To use a disabled message, the user must enable it explicitly by adding the message to the ``enable`` option. + +Refs #7629 diff --git a/doc/whatsnew/fragments/7635.bugfix b/doc/whatsnew/fragments/7635.bugfix new file mode 100644 index 000000000..72085e029 --- /dev/null +++ b/doc/whatsnew/fragments/7635.bugfix @@ -0,0 +1,7 @@ +Fixed a multi-processing crash that prevents using any more than 1 thread on MacOS. + +The returned module objects and errors that were cached by the linter plugin loader +cannot be reliably pickled. This means that ``dill`` would throw an error when +attempting to serialise the linter object for multi-processing use. + +Closes #7635. diff --git a/doc/whatsnew/fragments/7655.other b/doc/whatsnew/fragments/7655.other new file mode 100644 index 000000000..9024fe38e --- /dev/null +++ b/doc/whatsnew/fragments/7655.other @@ -0,0 +1,3 @@ +Sort ``--generated-rcfile`` output. + +Refs #7655 diff --git a/doc/whatsnew/fragments/7661.bugfix b/doc/whatsnew/fragments/7661.bugfix new file mode 100644 index 000000000..2e58c861b --- /dev/null +++ b/doc/whatsnew/fragments/7661.bugfix @@ -0,0 +1,3 @@ +Fix crash that happened when parsing files with unexpected encoding starting with 'utf' like ``utf13``. + +Closes #7661 diff --git a/doc/whatsnew/fragments/7682.false_positive b/doc/whatsnew/fragments/7682.false_positive new file mode 100644 index 000000000..3f94f447f --- /dev/null +++ b/doc/whatsnew/fragments/7682.false_positive @@ -0,0 +1,3 @@ +``unnecessary-list-index-lookup`` will not be wrongly emitted if ``enumerate`` is called with ``start``. + +Closes #7682 diff --git a/doc/whatsnew/fragments/7690.new_check b/doc/whatsnew/fragments/7690.new_check new file mode 100644 index 000000000..2969428ed --- /dev/null +++ b/doc/whatsnew/fragments/7690.new_check @@ -0,0 +1,3 @@ +Extended ``use-dict-literal`` to also warn about call to ``dict()`` when passing keyword arguments. + +Closes #7690 diff --git a/doc/whatsnew/fragments/7699.false_negative b/doc/whatsnew/fragments/7699.false_negative new file mode 100644 index 000000000..128aeff29 --- /dev/null +++ b/doc/whatsnew/fragments/7699.false_negative @@ -0,0 +1,3 @@ +``consider-using-any-or-all`` message will now be raised in cases when boolean is initialized, reassigned during loop, and immediately returned. + +Closes #7699 diff --git a/doc/whatsnew/fragments/7737.other b/doc/whatsnew/fragments/7737.other new file mode 100644 index 000000000..7b7988759 --- /dev/null +++ b/doc/whatsnew/fragments/7737.other @@ -0,0 +1,5 @@ +epylint is now deprecated and will be removed in pylint 3.0.0. All emacs and flymake related +files were removed and their support will now happen in an external repository : +https://github.com/emacsorphanage/pylint. + +Closes #7737 diff --git a/doc/whatsnew/fragments/7742.bugfix b/doc/whatsnew/fragments/7742.bugfix new file mode 100644 index 000000000..7e3c93089 --- /dev/null +++ b/doc/whatsnew/fragments/7742.bugfix @@ -0,0 +1,3 @@ +Fix a crash when a child class with an ``__init__`` method inherits from a parent class with an ``__init__`` class attribute. + +Closes #7742 diff --git a/doc/whatsnew/fragments/7760.new_check b/doc/whatsnew/fragments/7760.new_check new file mode 100644 index 000000000..89cdf29f9 --- /dev/null +++ b/doc/whatsnew/fragments/7760.new_check @@ -0,0 +1,5 @@ +Add ``named-expr-without-context`` check to emit a warning if a named +expression is used outside a context like ``if``, ``for``, ``while``, or +a comprehension. + +Refs #7760 diff --git a/doc/whatsnew/fragments/7762.false_negative b/doc/whatsnew/fragments/7762.false_negative new file mode 100644 index 000000000..6b4083ff6 --- /dev/null +++ b/doc/whatsnew/fragments/7762.false_negative @@ -0,0 +1,4 @@ +Extend ``invalid-slice-index`` to emit an warning for invalid slice indices +used with string and byte sequences, and range objects. + +Refs #7762 diff --git a/doc/whatsnew/fragments/7762.new_check b/doc/whatsnew/fragments/7762.new_check new file mode 100644 index 000000000..4ff67326b --- /dev/null +++ b/doc/whatsnew/fragments/7762.new_check @@ -0,0 +1,4 @@ +Add ``invalid-slice-step`` check to warn about a slice step value of ``0`` +for common builtin sequences. + +Refs #7762 diff --git a/doc/whatsnew/fragments/7765.false_positive b/doc/whatsnew/fragments/7765.false_positive new file mode 100644 index 000000000..de7c44c5a --- /dev/null +++ b/doc/whatsnew/fragments/7765.false_positive @@ -0,0 +1,3 @@ +Don't warn about ``stop-iteration-return`` when using ``next()`` over ``itertools.cycle``. + +Closes #7765 diff --git a/doc/whatsnew/fragments/7770.false_negative b/doc/whatsnew/fragments/7770.false_negative new file mode 100644 index 000000000..d9a165390 --- /dev/null +++ b/doc/whatsnew/fragments/7770.false_negative @@ -0,0 +1,3 @@ +Fixes ``unnecessary-list-index-lookup`` false negative when ``enumerate`` is called with ``iterable`` as a kwarg. + +Closes #7770 diff --git a/doc/whatsnew/fragments/7782.bugfix b/doc/whatsnew/fragments/7782.bugfix new file mode 100644 index 000000000..e4c1498c4 --- /dev/null +++ b/doc/whatsnew/fragments/7782.bugfix @@ -0,0 +1,4 @@ +Fix ``valid-metaclass-classmethod-first-arg`` default config value from "cls" to "mcs" +which would cause both a false-positive and false-negative. + +Closes #7782 diff --git a/doc/whatsnew/fragments/7818.false_negative b/doc/whatsnew/fragments/7818.false_negative new file mode 100644 index 000000000..f1fe76ded --- /dev/null +++ b/doc/whatsnew/fragments/7818.false_negative @@ -0,0 +1,3 @@ +Fix ``dangerous-default-value`` false negative when `*` is used. + +Closes #7818 diff --git a/doc/whatsnew/fragments/7821.bugfix b/doc/whatsnew/fragments/7821.bugfix new file mode 100644 index 000000000..af48814db --- /dev/null +++ b/doc/whatsnew/fragments/7821.bugfix @@ -0,0 +1,3 @@ +Fixes a crash in the ``unnecessary_list_index_lookup`` check when using ``enumerate`` with ``start`` and a class attribute. + +Closes #7821 diff --git a/doc/whatsnew/fragments/7828.bugfix b/doc/whatsnew/fragments/7828.bugfix new file mode 100644 index 000000000..f47cc3cc9 --- /dev/null +++ b/doc/whatsnew/fragments/7828.bugfix @@ -0,0 +1,3 @@ +Fixes a crash in ``stop-iteration-return`` when the ``next`` builtin is called without arguments. + +Closes #7828 diff --git a/doc/whatsnew/fragments/7860.false_positive b/doc/whatsnew/fragments/7860.false_positive new file mode 100644 index 000000000..c76425c54 --- /dev/null +++ b/doc/whatsnew/fragments/7860.false_positive @@ -0,0 +1,3 @@ +``multiple-statements`` no longer triggers for function stubs using inlined ``...``. + +Closes #7860 diff --git a/elisp/pylint-flymake.el b/elisp/pylint-flymake.el deleted file mode 100644 index ed213bf46..000000000 --- a/elisp/pylint-flymake.el +++ /dev/null @@ -1,15 +0,0 @@ -;; Configure Flymake for python -(when (load "flymake" t) - (defun flymake-pylint-init () - (let* ((temp-file (flymake-init-create-temp-buffer-copy - 'flymake-create-temp-inplace)) - (local-file (file-relative-name - temp-file - (file-name-directory buffer-file-name)))) - (list "epylint" (list local-file)))) - - (add-to-list 'flymake-allowed-file-name-masks - '("\\.py\\'" flymake-pylint-init))) - -;; Set as a minor mode for python -(add-hook 'python-mode-hook '(lambda () (flymake-mode))) diff --git a/elisp/pylint.el b/elisp/pylint.el deleted file mode 100644 index 327da0fcb..000000000 --- a/elisp/pylint.el +++ /dev/null @@ -1,255 +0,0 @@ -;;; pylint.el --- minor mode for running `pylint' - -;; Copyright (c) 2009, 2010 Ian Eure <ian.eure@gmail.com> -;; Author: Ian Eure <ian.eure@gmail.com> -;; Maintainer: Jonathan Kotta <jpkotta@gmail.com> - -;; Keywords: languages python -;; Version: 1.02 - -;; pylint.el is free software; you can redistribute it and/or modify it -;; under the terms of the GNU General Public License as published by the Free -;; Software Foundation; either version 2, or (at your option) any later -;; version. -;; -;; It is distributed in the hope that it will be useful, but WITHOUT ANY -;; WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -;; FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -;; details. -;; -;; You should have received a copy of the GNU General Public License along -;; with your copy of Emacs; see the file COPYING. If not, write to the Free -;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -;; MA 02110-1301, USA - -;;; Commentary: -;; -;; Specialized compile mode for pylint. You may want to add the -;; following to your init.el: -;; -;; (autoload 'pylint "pylint") -;; (add-hook 'python-mode-hook 'pylint-add-menu-items) -;; (add-hook 'python-mode-hook 'pylint-add-key-bindings) -;; -;; There is also a handy command `pylint-insert-ignore-comment' that -;; makes it easy to insert comments of the form `# pylint: -;; ignore=msg1,msg2,...'. - -;;; Code: - -(require 'compile) -(require 'tramp) - -(defgroup pylint nil - "Minor mode for running the Pylint Python checker" - :prefix "pylint-" - :group 'tools - :group 'languages) - -(defvar pylint-last-buffer nil - "The most recent PYLINT buffer. -A PYLINT buffer becomes most recent when you select PYLINT mode in it. -Notice that using \\[next-error] or \\[compile-goto-error] modifies -`completion-last-buffer' rather than `pylint-last-buffer'.") - -(defconst pylint-regexp-alist - (let ((base "^\\(.*\\):\\([0-9]+\\):\s+\\(\\[%s.*\\)$")) - (list - (list (format base "[FE]") 1 2) - (list (format base "[RWC]") 1 2 nil 1))) - "Regexp used to match PYLINT hits. See `compilation-error-regexp-alist'.") - -(defcustom pylint-options '("--reports=n" "--output-format=parseable") - "Options to pass to pylint.py" - :type '(repeat string) - :group 'pylint) - -(defcustom pylint-use-python-indent-offset nil - "If non-nil, use `python-indent-offset' to set indent-string." - :type 'boolean - :group 'pylint) - -(defcustom pylint-command "pylint" - "PYLINT command." - :type '(file) - :group 'pylint) - -(defcustom pylint-alternate-pylint-command "pylint2" - "Command for pylint when invoked with C-u." - :type '(file) - :group 'pylint) - -(defcustom pylint-ask-about-save nil - "Non-nil means \\[pylint] asks which buffers to save before compiling. -Otherwise, it saves all modified buffers without asking." - :type 'boolean - :group 'pylint) - -(defvar pylint--messages-list () - "A list of strings of all pylint messages.") - -(defvar pylint--messages-list-hist () - "Completion history for `pylint--messages-list'.") - -(defun pylint--sort-messages (a b) - "Compare function for sorting `pylint--messages-list'. - -Sorts most recently used elements first using `pylint--messages-list-hist'." - (let ((idx 0) - (a-idx most-positive-fixnum) - (b-idx most-positive-fixnum)) - (dolist (e pylint--messages-list-hist) - (when (string= e a) - (setq a-idx idx)) - (when (string= e b) - (setq b-idx idx)) - (setq idx (1+ idx))) - (< a-idx b-idx))) - -(defun pylint--create-messages-list () - "Use `pylint-command' to populate `pylint--messages-list'." - ;; example output: - ;; |--we want this--| - ;; v v - ;; :using-cmp-argument (W1640): *Using the cmp argument for list.sort / sorted* - ;; Using the cmp argument for list.sort or the sorted builtin should be avoided, - ;; since it was removed in Python 3. Using either `key` or `functools.cmp_to_key` - ;; should be preferred. This message can't be emitted when using Python >= 3.0. - (setq pylint--messages-list - (split-string - (with-temp-buffer - (shell-command (concat pylint-command " --list-msgs") (current-buffer)) - (flush-lines "^[^:]") - (goto-char (point-min)) - (while (not (eobp)) - (delete-char 1) ;; delete ";" - (re-search-forward " ") - (delete-region (point) (line-end-position)) - (forward-line 1)) - (buffer-substring-no-properties (point-min) (point-max)))))) - -;;;###autoload -(defun pylint-insert-ignore-comment (&optional arg) - "Insert a comment like \"# pylint: disable=msg1,msg2,...\". - -This command repeatedly uses `completing-read' to match known -messages, and ultimately inserts a comma-separated list of all -the selected messages. - -With prefix argument, only insert a comma-separated list (for -appending to an existing list)." - (interactive "*P") - (unless pylint--messages-list - (pylint--create-messages-list)) - (setq pylint--messages-list - (sort pylint--messages-list #'pylint--sort-messages)) - (let ((msgs ()) - (msg "") - (prefix (if arg - "," - "# pylint: disable=")) - (sentinel "[DONE]")) - (while (progn - (setq msg (completing-read - "Message: " - pylint--messages-list - nil t nil 'pylint--messages-list-hist sentinel)) - (unless (string= sentinel msg) - (add-to-list 'msgs msg 'append)))) - (setq pylint--messages-list-hist - (delete sentinel pylint--messages-list-hist)) - (insert prefix (mapconcat 'identity msgs ",")))) - -(define-compilation-mode pylint-mode "PYLINT" - (setq pylint-last-buffer (current-buffer)) - (set (make-local-variable 'compilation-error-regexp-alist) - pylint-regexp-alist) - (set (make-local-variable 'compilation-disable-input) t)) - -(defvar pylint-mode-map - (let ((map (make-sparse-keymap))) - (set-keymap-parent map compilation-minor-mode-map) - (define-key map " " 'scroll-up) - (define-key map "\^?" 'scroll-down) - (define-key map "\C-c\C-f" 'next-error-follow-minor-mode) - - (define-key map "\r" 'compile-goto-error) ;; ? - (define-key map "n" 'next-error-no-select) - (define-key map "p" 'previous-error-no-select) - (define-key map "{" 'compilation-previous-file) - (define-key map "}" 'compilation-next-file) - (define-key map "\t" 'compilation-next-error) - (define-key map [backtab] 'compilation-previous-error) - map) - "Keymap for PYLINT buffers. -`compilation-minor-mode-map' is a cdr of this.") - -(defun pylint--make-indent-string () - "Make a string for the `--indent-string' option." - (format "--indent-string='%s'" - (make-string python-indent-offset ?\ ))) - -;;;###autoload -(defun pylint (&optional arg) - "Run PYLINT, and collect output in a buffer, much like `compile'. - -While pylint runs asynchronously, you can use \\[next-error] (M-x next-error), -or \\<pylint-mode-map>\\[compile-goto-error] in the grep \ -output buffer, to go to the lines where pylint found matches. - -\\{pylint-mode-map}" - (interactive "P") - - (save-some-buffers (not pylint-ask-about-save) nil) - (let* ((filename (buffer-file-name)) - (localname-offset (cl-struct-slot-offset 'tramp-file-name 'localname)) - (filename (or (and (tramp-tramp-file-p filename) - (elt (tramp-dissect-file-name filename) localname-offset)) - filename)) - (filename (shell-quote-argument filename)) - (pylint-command (if arg - pylint-alternate-pylint-command - pylint-command)) - (pylint-options (if (not pylint-use-python-indent-offset) - pylint-options - (append pylint-options - (list (pylint--make-indent-string))))) - (command (mapconcat - 'identity - (append `(,pylint-command) pylint-options `(,filename)) - " "))) - - (compilation-start command 'pylint-mode))) - -;;;###autoload -(defun pylint-add-key-bindings () - (let ((map (cond - ((boundp 'py-mode-map) py-mode-map) - ((boundp 'python-mode-map) python-mode-map)))) - - ;; shortcuts in the tradition of python-mode and ropemacs - (define-key map (kbd "C-c m l") 'pylint) - (define-key map (kbd "C-c m p") 'previous-error) - (define-key map (kbd "C-c m n") 'next-error) - (define-key map (kbd "C-c m i") 'pylint-insert-ignore-comment) - nil)) - -;;;###autoload -(defun pylint-add-menu-items () - (let ((map (cond - ((boundp 'py-mode-map) py-mode-map) - ((boundp 'python-mode-map) python-mode-map)))) - - (define-key map [menu-bar Python pylint-separator] - '("--" . pylint-separator)) - (define-key map [menu-bar Python next-error] - '("Next error" . next-error)) - (define-key map [menu-bar Python prev-error] - '("Previous error" . previous-error)) - (define-key map [menu-bar Python lint] - '("Pylint" . pylint)) - nil)) - -(provide 'pylint) - -;;; pylint.el ends here diff --git a/elisp/startup b/elisp/startup deleted file mode 100644 index 2f8fed1d4..000000000 --- a/elisp/startup +++ /dev/null @@ -1,17 +0,0 @@ -;; -*-emacs-lisp-*- -;; -;; Emacs startup file for the Debian GNU/Linux %PACKAGE% package -;; -;; Originally contributed by Nils Naumann <naumann@unileoben.ac.at> -;; Modified by Dirk Eddelbuettel <edd@debian.org> -;; Adapted for dh-make by Jim Van Zandt <jrv@vanzandt.mv.com> - -;; The %PACKAGE% package follows the Debian/GNU Linux 'emacsen' policy and -;; byte-compiles its elisp files for each 'Emacs flavor' (emacs19, -;; xemacs19, emacs20, xemacs20...). The compiled code is then -;; installed in a subdirectory of the respective site-lisp directory. -;; We have to add this to the load-path: -(setq load-path (cons (concat "/usr/share/" - (symbol-name debian-emacs-flavor) - "/site-lisp/%PACKAGE%") load-path)) -(load-library "pylint") diff --git a/examples/pylintrc b/examples/pylintrc index a461b24d5..61a9361d6 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -260,7 +260,7 @@ exclude-protected=_asdict, valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls +valid-metaclass-classmethod-first-arg=mcs [DESIGN] @@ -307,8 +307,8 @@ min-public-methods=2 [EXCEPTIONS] # Exceptions that will emit a warning when caught. -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception [FORMAT] diff --git a/examples/pyproject.toml b/examples/pyproject.toml index b419f83e7..98cb39bb9 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -222,7 +222,7 @@ exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make"] valid-classmethod-first-arg = ["cls"] # List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg = ["cls"] +valid-metaclass-classmethod-first-arg = ["mcs"] [tool.pylint.design] # List of regular expressions of class ancestor names to ignore when counting @@ -264,7 +264,7 @@ min-public-methods = 2 [tool.pylint.exceptions] # Exceptions that will emit a warning when caught. -overgeneral-exceptions = ["BaseException", "Exception"] +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] [tool.pylint.format] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. diff --git a/pylint/__init__.py b/pylint/__init__.py index 8f1eaebe0..2cc7edadb 100644 --- a/pylint/__init__.py +++ b/pylint/__init__.py @@ -16,6 +16,7 @@ __all__ = [ import os import sys +import warnings from collections.abc import Sequence from typing import NoReturn @@ -54,10 +55,16 @@ def run_epylint(argv: Sequence[str] | None = None) -> NoReturn: """ from pylint.epylint import Run as EpylintRun + warnings.warn( + "'run_epylint' will be removed in pylint 3.0, use " + "https://github.com/emacsorphanage/pylint instead.", + DeprecationWarning, + stacklevel=1, + ) EpylintRun(argv) -def run_pyreverse(argv: Sequence[str] | None = None) -> NoReturn: # type: ignore[misc] +def run_pyreverse(argv: Sequence[str] | None = None) -> NoReturn: """Run pyreverse. argv can be a sequence of strings normally supplied as arguments on the command line diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 3b7b44c37..7d767f3a1 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ It's updated via tbump, do not modify. from __future__ import annotations -__version__ = "2.15.7" +__version__ = "2.16.0-dev" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/pylint/checkers/__init__.py b/pylint/checkers/__init__.py index 7e39c6877..ed641d8e5 100644 --- a/pylint/checkers/__init__.py +++ b/pylint/checkers/__init__.py @@ -127,7 +127,7 @@ def table_lines_from_stats( ) new_str = f"{new_value:.3f}" if isinstance(new_value, float) else str(new_value) old_str = f"{old_value:.3f}" if isinstance(old_value, float) else str(old_value) - lines.extend((value[0].replace("_", " "), new_str, old_str, diff_str)) + lines.extend((value[0].replace("_", " "), new_str, old_str, diff_str)) # type: ignore[arg-type] return lines diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index 4594bbaa8..a91291c91 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -17,7 +17,7 @@ from astroid import nodes from pylint import utils as lint_utils from pylint.checkers import BaseChecker, utils -from pylint.interfaces import HIGH, INFERENCE +from pylint.interfaces import HIGH, INFERENCE, Confidence from pylint.reporters.ureports import nodes as reporter_nodes from pylint.utils import LinterStats @@ -188,7 +188,7 @@ class BasicChecker(_BasicChecker): "re-raised.", ), "W0199": ( - "Assert called on a 2-item-tuple. Did you mean 'assert x,y'?", + "Assert called on a populated tuple. Did you mean 'assert x,y'?", "assert-on-tuple", "A call of assert on a tuple will always evaluate to true if " "the tuple is not empty, and will always evaluate to false if " @@ -252,6 +252,12 @@ class BasicChecker(_BasicChecker): "duplicate-value", "This message is emitted when a set contains the same value two or more times.", ), + "W0131": ( + "Named expression used without context", + "named-expr-without-context", + "Emitted if named expression is used to do a regular assignment " + "outside a context like if, for, while, or a comprehension.", + ), } reports = (("RP0101", "Statistics by type", report_by_type_stats),) @@ -410,7 +416,10 @@ class BasicChecker(_BasicChecker): self.linter.stats.node_count["klass"] += 1 @utils.only_required_for_messages( - "pointless-statement", "pointless-string-statement", "expression-not-assigned" + "pointless-statement", + "pointless-string-statement", + "expression-not-assigned", + "named-expr-without-context", ) def visit_expr(self, node: nodes.Expr) -> None: """Check for various kind of statements without effect.""" @@ -448,7 +457,9 @@ class BasicChecker(_BasicChecker): or (isinstance(expr, nodes.Const) and expr.value is Ellipsis) ): return - if any(expr.nodes_of_class(nodes.Call)): + if isinstance(expr, nodes.NamedExpr): + self.add_message("named-expr-without-context", node=node, confidence=HIGH) + elif any(expr.nodes_of_class(nodes.Call)): self.add_message( "expression-not-assigned", node=node, args=expr.as_string() ) @@ -561,7 +572,7 @@ class BasicChecker(_BasicChecker): def is_iterable(internal_node: nodes.NodeNG) -> bool: return isinstance(internal_node, (nodes.List, nodes.Set, nodes.Dict)) - defaults = node.args.defaults or [] + node.args.kw_defaults or [] + defaults = (node.args.defaults or []) + (node.args.kw_defaults or []) for default in defaults: if not default: continue @@ -658,12 +669,16 @@ class BasicChecker(_BasicChecker): self.add_message("misplaced-format-function", node=call_node) @utils.only_required_for_messages( - "eval-used", "exec-used", "bad-reversed-sequence", "misplaced-format-function" + "eval-used", + "exec-used", + "bad-reversed-sequence", + "misplaced-format-function", + "unreachable", ) def visit_call(self, node: nodes.Call) -> None: - """Visit a Call node -> check if this is not a disallowed builtin - call and check for * or ** use. - """ + """Visit a Call node.""" + if utils.is_terminating_func(node): + self._check_unreachable(node, confidence=INFERENCE) self._check_misplaced_format_function(node) if isinstance(node.func, nodes.Name): name = node.func.name @@ -680,12 +695,8 @@ class BasicChecker(_BasicChecker): @utils.only_required_for_messages("assert-on-tuple", "assert-on-string-literal") def visit_assert(self, node: nodes.Assert) -> None: """Check whether assert is used on a tuple or string literal.""" - if ( - node.fail is None - and isinstance(node.test, nodes.Tuple) - and len(node.test.elts) == 2 - ): - self.add_message("assert-on-tuple", node=node) + if isinstance(node.test, nodes.Tuple) and len(node.test.elts) > 0: + self.add_message("assert-on-tuple", node=node, confidence=HIGH) if isinstance(node.test, nodes.Const) and isinstance(node.test.value, str): if node.test.value: @@ -735,7 +746,9 @@ class BasicChecker(_BasicChecker): self._tryfinallys.pop() def _check_unreachable( - self, node: nodes.Return | nodes.Continue | nodes.Break | nodes.Raise + self, + node: nodes.Return | nodes.Continue | nodes.Break | nodes.Raise | nodes.Call, + confidence: Confidence = HIGH, ) -> None: """Check unreachable code.""" unreachable_statement = node.next_sibling() @@ -750,7 +763,9 @@ class BasicChecker(_BasicChecker): unreachable_statement = unreachable_statement.next_sibling() if unreachable_statement is None: return - self.add_message("unreachable", node=unreachable_statement, confidence=HIGH) + self.add_message( + "unreachable", node=unreachable_statement, confidence=confidence + ) def _check_not_in_finally( self, diff --git a/pylint/checkers/base/basic_error_checker.py b/pylint/checkers/base/basic_error_checker.py index 3bd5e2ca6..f76c5aa8b 100644 --- a/pylint/checkers/base/basic_error_checker.py +++ b/pylint/checkers/base/basic_error_checker.py @@ -12,6 +12,7 @@ from typing import Any import astroid from astroid import nodes +from astroid.typing import InferenceResult from pylint.checkers import utils from pylint.checkers.base.basic_checker import _BasicChecker @@ -432,7 +433,9 @@ class BasicErrorChecker(_BasicChecker): for inferred in infer_all(node.func): self._check_inferred_class_is_abstract(inferred, node) - def _check_inferred_class_is_abstract(self, inferred, node: nodes.Call): + def _check_inferred_class_is_abstract( + self, inferred: InferenceResult, node: nodes.Call + ) -> None: if not isinstance(inferred, nodes.ClassDef): return diff --git a/pylint/checkers/base/comparison_checker.py b/pylint/checkers/base/comparison_checker.py index 98b0ec258..ffbd27374 100644 --- a/pylint/checkers/base/comparison_checker.py +++ b/pylint/checkers/base/comparison_checker.py @@ -89,16 +89,10 @@ class ComparisonChecker(_BasicChecker): checking_for_absence: bool = False, ) -> None: """Check if == or != is being used to compare a singleton value.""" - singleton_values = (True, False, None) - def _is_singleton_const(node: nodes.NodeNG) -> bool: - return isinstance(node, nodes.Const) and any( - node.value is value for value in singleton_values - ) - - if _is_singleton_const(left_value): + if utils.is_singleton_const(left_value): singleton, other_value = left_value.value, right_value - elif _is_singleton_const(right_value): + elif utils.is_singleton_const(right_value): singleton, other_value = right_value.value, left_value else: return @@ -201,17 +195,24 @@ class ComparisonChecker(_BasicChecker): is_const = isinstance(literal.value, (bytes, str, int, float)) if is_const or is_other_literal: - bad = node.as_string() - if "is not" in bad: + incorrect_node_str = node.as_string() + if "is not" in incorrect_node_str: equal_or_not_equal = "!=" is_or_is_not = "is not" else: equal_or_not_equal = "==" is_or_is_not = "is" - good = bad.replace(is_or_is_not, equal_or_not_equal) + fixed_node_str = incorrect_node_str.replace( + is_or_is_not, equal_or_not_equal + ) self.add_message( "literal-comparison", - args=(bad, equal_or_not_equal, is_or_is_not, good), + args=( + incorrect_node_str, + equal_or_not_equal, + is_or_is_not, + fixed_node_str, + ), node=node, confidence=HIGH, ) @@ -243,8 +244,8 @@ class ComparisonChecker(_BasicChecker): suggestion = f"{left_operand} {operator} {right_operand}" self.add_message("comparison-with-itself", node=node, args=(suggestion,)) - def _check_two_literals_being_compared(self, node: nodes.Compare) -> None: - """Check if two literals are being compared; this is always a logical tautology.""" + def _check_constants_comparison(self, node: nodes.Compare) -> None: + """When two constants are being compared it is always a logical tautology.""" left_operand = node.left if not isinstance(left_operand, nodes.Const): return @@ -297,7 +298,7 @@ class ComparisonChecker(_BasicChecker): self._check_callable_comparison(node) self._check_logical_tautology(node) self._check_unidiomatic_typecheck(node) - self._check_two_literals_being_compared(node) + self._check_constants_comparison(node) # NOTE: this checker only works with binary comparisons like 'x == 42' # but not 'x == y == 42' if len(node.ops) != 1: diff --git a/pylint/checkers/base/docstring_checker.py b/pylint/checkers/base/docstring_checker.py index d40f892de..791b085b5 100644 --- a/pylint/checkers/base/docstring_checker.py +++ b/pylint/checkers/base/docstring_checker.py @@ -108,16 +108,16 @@ class DocStringChecker(_BasicChecker): def open(self) -> None: self.linter.stats.reset_undocumented() - @utils.only_required_for_messages("missing-docstring", "empty-docstring") + @utils.only_required_for_messages("missing-module-docstring", "empty-docstring") def visit_module(self, node: nodes.Module) -> None: self._check_docstring("module", node) - @utils.only_required_for_messages("missing-docstring", "empty-docstring") + @utils.only_required_for_messages("missing-class-docstring", "empty-docstring") def visit_classdef(self, node: nodes.ClassDef) -> None: if self.linter.config.no_docstring_rgx.match(node.name) is None: self._check_docstring("class", node) - @utils.only_required_for_messages("missing-docstring", "empty-docstring") + @utils.only_required_for_messages("missing-function-docstring", "empty-docstring") def visit_functiondef(self, node: nodes.FunctionDef) -> None: if self.linter.config.no_docstring_rgx.match(node.name) is None: ftype = "method" if node.is_method() else "function" diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index e4b061a17..0eded5f9b 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -337,7 +337,7 @@ class NameChecker(_BasicChecker): if len(groups[min_warnings]) > 1: by_line = sorted( groups[min_warnings], - key=lambda group: min( + key=lambda group: min( # type: ignore[no-any-return] warning[0].lineno for warning in group if warning[0].lineno is not None diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py index 778345de8..0debfd3a2 100644 --- a/pylint/checkers/base_checker.py +++ b/pylint/checkers/base_checker.py @@ -7,7 +7,7 @@ from __future__ import annotations import abc import functools import warnings -from collections.abc import Iterator +from collections.abc import Iterable, Sequence from inspect import cleandoc from tokenize import TokenInfo from typing import TYPE_CHECKING, Any @@ -54,6 +54,7 @@ class BaseChecker(_ArgumentsProvider): "longer supported. Child classes should only inherit BaseChecker or any " "of the other checker types from pylint.checkers.", DeprecationWarning, + stacklevel=2, ) if self.name is not None: self.name = self.name.lower() @@ -105,8 +106,8 @@ class BaseChecker(_ArgumentsProvider): def get_full_documentation( self, msgs: dict[str, MessageDefinitionTuple], - options: Iterator[tuple[str, OptionDict, Any]], - reports: tuple[tuple[str, str, ReportsCallable], ...], + options: Iterable[tuple[str, OptionDict, Any]], + reports: Sequence[tuple[str, str, ReportsCallable]], doc: str | None = None, module: str | None = None, show_options: bool = True, diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index eb157187e..e2806ef43 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -9,10 +9,10 @@ from __future__ import annotations import collections import sys from collections import defaultdict -from collections.abc import Sequence +from collections.abc import Callable, Sequence from itertools import chain, zip_longest from re import Pattern -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Any, Union import astroid from astroid import bases, nodes @@ -200,7 +200,7 @@ def _positional_parameters(method: nodes.FunctionDef) -> list[nodes.AssignName]: positional = method.args.args if method.is_bound() and method.type in {"classmethod", "method"}: positional = positional[1:] - return positional + return positional # type: ignore[no-any-return] class _DefaultMissing: @@ -244,7 +244,9 @@ def _has_different_parameters_default_value( if not isinstance(overridden_default, original_type): # Two args with same name but different types return True - is_same_fn = ASTROID_TYPE_COMPARATORS.get(original_type) + is_same_fn: Callable[[Any, Any], bool] | None = ASTROID_TYPE_COMPARATORS.get( + original_type + ) if is_same_fn is None: # If the default value comparison is unhandled, assume the value is different return True @@ -257,7 +259,7 @@ def _has_different_parameters_default_value( def _has_different_parameters( original: list[nodes.AssignName], overridden: list[nodes.AssignName], - dummy_parameter_regex: Pattern, + dummy_parameter_regex: Pattern[str], ) -> list[str]: result: list[str] = [] zipped = zip_longest(original, overridden) @@ -311,7 +313,7 @@ def _has_different_keyword_only_parameters( def _different_parameters( original: nodes.FunctionDef, overridden: nodes.FunctionDef, - dummy_parameter_regex: Pattern, + dummy_parameter_regex: Pattern[str], ) -> list[str]: """Determine if the two methods have different parameters. @@ -581,7 +583,7 @@ MSGS: dict[str, MessageDefinitionTuple] = { "implemented interface or in an overridden method.", ), "W0223": ( - "Method %r is abstract in class %r but is not overridden", + "Method %r is abstract in class %r but is not overridden in child class %r", "abstract-method", "Used when an abstract method (i.e. raise NotImplementedError) is " "not overridden in concrete class.", @@ -682,7 +684,7 @@ MSGS: dict[str, MessageDefinitionTuple] = { "Used when a value in __slots__ conflicts with a class variable, property or method.", ), "E0243": ( - "Invalid __class__ object", + "Invalid assignment to '__class__'. Should be a class definition but got a '%s'", "invalid-class-object", "Used when an invalid object is assigned to a __class__ property. " "Only a class is permitted.", @@ -790,7 +792,7 @@ a class method.", ( "valid-metaclass-classmethod-first-arg", { - "default": ("cls",), + "default": ("mcs",), "type": "csv", "metavar": "<argument names>", "help": "List of valid names for the first argument in \ @@ -839,7 +841,7 @@ a metaclass class method.", @cached_property def _dummy_rgx(self) -> Pattern[str]: - return self.linter.config.dummy_variables_rgx + return self.linter.config.dummy_variables_rgx # type: ignore[no-any-return] @only_required_for_messages( "abstract-method", @@ -1180,7 +1182,7 @@ a metaclass class method.", continue if not isinstance(parent_function, nodes.FunctionDef): continue - self._check_signature(node, parent_function, "overridden", klass) + self._check_signature(node, parent_function, klass) self._check_invalid_overridden_method(node, parent_function) break @@ -1577,7 +1579,12 @@ a metaclass class method.", ): # If is uninferable, we allow it to prevent false positives return - self.add_message("invalid-class-object", node=node) + self.add_message( + "invalid-class-object", + node=node, + args=inferred.__class__.__name__, + confidence=INFERENCE, + ) def _check_in_slots(self, node: nodes.AssignAttr) -> None: """Check that the given AssignAttr node @@ -1814,7 +1821,7 @@ a metaclass class method.", ) @staticmethod - def _is_inferred_instance(expr, klass: nodes.ClassDef) -> bool: + def _is_inferred_instance(expr: nodes.NodeNG, klass: nodes.ClassDef) -> bool: """Check if the inferred value of the given *expr* is an instance of *klass*. """ @@ -2005,7 +2012,7 @@ a metaclass class method.", """ def is_abstract(method: nodes.FunctionDef) -> bool: - return method.is_abstract(pass_is_abstract=False) + return method.is_abstract(pass_is_abstract=False) # type: ignore[no-any-return] # check if this class abstract if class_is_abstract(node): @@ -2024,7 +2031,13 @@ a metaclass class method.", if name in node.locals: # it is redefined as an attribute or with a descriptor continue - self.add_message("abstract-method", node=node, args=(name, owner.name)) + + self.add_message( + "abstract-method", + node=node, + args=(name, owner.name, node.name), + confidence=INFERENCE, + ) def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> None: """Check that the __init__ method call super or ancestors'__init__ @@ -2111,7 +2124,6 @@ a metaclass class method.", self, method1: nodes.FunctionDef, refmethod: nodes.FunctionDef, - class_type: str, cls: nodes.ClassDef, ) -> None: """Check that the signature of the two given methods match.""" @@ -2143,6 +2155,9 @@ a metaclass class method.", arg_differ_output = _different_parameters( refmethod, method1, dummy_parameter_regex=self._dummy_rgx ) + + class_type = "overriding" + if len(arg_differ_output) > 0: for msg in arg_differ_output: if "Number" in msg: @@ -2187,6 +2202,7 @@ a metaclass class method.", len(method1.args.defaults) < len(refmethod.args.defaults) and not method1.args.vararg ): + class_type = "overridden" self.add_message( "signature-differs", args=(class_type, method1.name), node=method1 ) diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index c97393bcc..a8e7fb1da 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -9,7 +9,7 @@ from __future__ import annotations import re from collections import defaultdict from collections.abc import Iterator -from typing import TYPE_CHECKING, List, cast +from typing import TYPE_CHECKING import astroid from astroid import nodes @@ -245,7 +245,7 @@ def _get_parents_iter( ``{A, B, C, D}`` -- both ``E`` and its ancestors are excluded. """ parents: set[nodes.ClassDef] = set() - to_explore = cast(List[nodes.ClassDef], list(node.ancestors(recurs=False))) + to_explore = list(node.ancestors(recurs=False)) while to_explore: parent = to_explore.pop() if parent.qname() in ignored_parents: diff --git a/pylint/checkers/dunder_methods.py b/pylint/checkers/dunder_methods.py index 2e5e54a57..987e539aa 100644 --- a/pylint/checkers/dunder_methods.py +++ b/pylint/checkers/dunder_methods.py @@ -10,122 +10,19 @@ from astroid import Instance, Uninferable, nodes from pylint.checkers import BaseChecker from pylint.checkers.utils import safe_infer +from pylint.constants import DUNDER_METHODS from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter -DUNDER_METHODS: dict[tuple[int, int], dict[str, str]] = { - (0, 0): { - "__init__": "Instantiate class directly", - "__del__": "Use del keyword", - "__repr__": "Use repr built-in function", - "__str__": "Use str built-in function", - "__bytes__": "Use bytes built-in function", - "__format__": "Use format built-in function, format string method, or f-string", - "__lt__": "Use < operator", - "__le__": "Use <= operator", - "__eq__": "Use == operator", - "__ne__": "Use != operator", - "__gt__": "Use > operator", - "__ge__": "Use >= operator", - "__hash__": "Use hash built-in function", - "__bool__": "Use bool built-in function", - "__getattr__": "Access attribute directly or use getattr built-in function", - "__getattribute__": "Access attribute directly or use getattr built-in function", - "__setattr__": "Set attribute directly or use setattr built-in function", - "__delattr__": "Use del keyword", - "__dir__": "Use dir built-in function", - "__get__": "Use get method", - "__set__": "Use set method", - "__delete__": "Use del keyword", - "__instancecheck__": "Use isinstance built-in function", - "__subclasscheck__": "Use issubclass built-in function", - "__call__": "Invoke instance directly", - "__len__": "Use len built-in function", - "__length_hint__": "Use length_hint method", - "__getitem__": "Access item via subscript", - "__setitem__": "Set item via subscript", - "__delitem__": "Use del keyword", - "__iter__": "Use iter built-in function", - "__next__": "Use next built-in function", - "__reversed__": "Use reversed built-in function", - "__contains__": "Use in keyword", - "__add__": "Use + operator", - "__sub__": "Use - operator", - "__mul__": "Use * operator", - "__matmul__": "Use @ operator", - "__truediv__": "Use / operator", - "__floordiv__": "Use // operator", - "__mod__": "Use % operator", - "__divmod__": "Use divmod built-in function", - "__pow__": "Use ** operator or pow built-in function", - "__lshift__": "Use << operator", - "__rshift__": "Use >> operator", - "__and__": "Use & operator", - "__xor__": "Use ^ operator", - "__or__": "Use | operator", - "__radd__": "Use + operator", - "__rsub__": "Use - operator", - "__rmul__": "Use * operator", - "__rmatmul__": "Use @ operator", - "__rtruediv__": "Use / operator", - "__rfloordiv__": "Use // operator", - "__rmod__": "Use % operator", - "__rdivmod__": "Use divmod built-in function", - "__rpow__": "Use ** operator or pow built-in function", - "__rlshift__": "Use << operator", - "__rrshift__": "Use >> operator", - "__rand__": "Use & operator", - "__rxor__": "Use ^ operator", - "__ror__": "Use | operator", - "__iadd__": "Use += operator", - "__isub__": "Use -= operator", - "__imul__": "Use *= operator", - "__imatmul__": "Use @= operator", - "__itruediv__": "Use /= operator", - "__ifloordiv__": "Use //= operator", - "__imod__": "Use %= operator", - "__ipow__": "Use **= operator", - "__ilshift__": "Use <<= operator", - "__irshift__": "Use >>= operator", - "__iand__": "Use &= operator", - "__ixor__": "Use ^= operator", - "__ior__": "Use |= operator", - "__neg__": "Multiply by -1 instead", - "__pos__": "Multiply by +1 instead", - "__abs__": "Use abs built-in function", - "__invert__": "Use ~ operator", - "__complex__": "Use complex built-in function", - "__int__": "Use int built-in function", - "__float__": "Use float built-in function", - "__round__": "Use round built-in function", - "__trunc__": "Use math.trunc function", - "__floor__": "Use math.floor function", - "__ceil__": "Use math.ceil function", - "__enter__": "Invoke context manager directly", - "__aenter__": "Invoke context manager directly", - "__copy__": "Use copy.copy function", - "__deepcopy__": "Use copy.deepcopy function", - "__fspath__": "Use os.fspath function instead", - }, - (3, 10): { - "__aiter__": "Use aiter built-in function", - "__anext__": "Use anext built-in function", - }, -} - - class DunderCallChecker(BaseChecker): """Check for unnecessary dunder method calls. Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization - We exclude __new__, __subclasses__, __init_subclass__, __set_name__, - __class_getitem__, __missing__, __exit__, __await__, - __aexit__, __getnewargs_ex__, __getnewargs__, __getstate__, - __setstate__, __reduce__, __reduce_ex__, - and __index__ (see https://github.com/PyCQA/pylint/issues/6795) + We exclude names in list pylint.constants.EXTRA_DUNDER_METHODS such as + __index__ (see https://github.com/PyCQA/pylint/issues/6795) since these either have no alternative method of being called or have a genuine use case for being called manually. diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index 0d3f0706f..a563c6eb9 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -8,6 +8,7 @@ from __future__ import annotations import builtins import inspect +import warnings from collections.abc import Generator from typing import TYPE_CHECKING, Any @@ -59,8 +60,6 @@ def _is_raising(body: list[nodes.NodeNG]) -> bool: return any(isinstance(node, nodes.Raise) for node in body) -OVERGENERAL_EXCEPTIONS = ("BaseException", "Exception") - MSGS: dict[str, MessageDefinitionTuple] = { "E0701": ( "Bad except clauses order (%s)", @@ -115,11 +114,12 @@ MSGS: dict[str, MessageDefinitionTuple] = { "bare-except", "Used when an except clause doesn't specify exceptions type to catch.", ), - "W0703": ( + "W0718": ( "Catching too general exception %s", - "broad-except", + "broad-exception-caught", "Used when an except catches a too general exception, " "possibly burying unrelated errors.", + {"old_names": [("W0703", "broad-except")]}, ), "W0705": ( "Catching previously caught exception type %s", @@ -165,6 +165,11 @@ MSGS: dict[str, MessageDefinitionTuple] = { "is not valid for the exception in question. Usually emitted when having " "binary operations between exceptions in except handlers.", ), + "W0719": ( + "Raising too general exception: %s", + "broad-exception-raised", + "Used when an except raises a too general exception.", + ), } @@ -192,7 +197,26 @@ class ExceptionRaiseRefVisitor(BaseVisitor): def visit_name(self, node: nodes.Name) -> None: if node.name == "NotImplemented": - self._checker.add_message("notimplemented-raised", node=self._node) + self._checker.add_message( + "notimplemented-raised", node=self._node, confidence=HIGH + ) + return + + try: + exceptions = list(_annotated_unpack_infer(node)) + except astroid.InferenceError: + return + + for _, exception in exceptions: + if isinstance( + exception, nodes.ClassDef + ) and self._checker._is_overgeneral_exception(exception): + self._checker.add_message( + "broad-exception-raised", + args=exception.name, + node=self._node, + confidence=INFERENCE, + ) def visit_call(self, node: nodes.Call) -> None: if isinstance(node.func, nodes.Name): @@ -204,7 +228,9 @@ class ExceptionRaiseRefVisitor(BaseVisitor): ): msg = node.args[0].value if "%" in msg or ("{" in msg and "}" in msg): - self._checker.add_message("raising-format-tuple", node=self._node) + self._checker.add_message( + "raising-format-tuple", node=self._node, confidence=HIGH + ) class ExceptionRaiseLeafVisitor(BaseVisitor): @@ -212,7 +238,10 @@ class ExceptionRaiseLeafVisitor(BaseVisitor): def visit_const(self, node: nodes.Const) -> None: self._checker.add_message( - "raising-bad-type", node=self._node, args=node.value.__class__.__name__ + "raising-bad-type", + node=self._node, + args=node.value.__class__.__name__, + confidence=INFERENCE, ) def visit_instance(self, instance: objects.ExceptionInstance) -> None: @@ -225,14 +254,28 @@ class ExceptionRaiseLeafVisitor(BaseVisitor): def visit_classdef(self, node: nodes.ClassDef) -> None: if not utils.inherit_from_std_ex(node) and utils.has_known_bases(node): if node.newstyle: - self._checker.add_message("raising-non-exception", node=self._node) + self._checker.add_message( + "raising-non-exception", + node=self._node, + confidence=INFERENCE, + ) def visit_tuple(self, _: nodes.Tuple) -> None: - self._checker.add_message("raising-bad-type", node=self._node, args="tuple") + self._checker.add_message( + "raising-bad-type", + node=self._node, + args="tuple", + confidence=INFERENCE, + ) def visit_default(self, node: nodes.NodeNG) -> None: name = getattr(node, "name", node.__class__.__name__) - self._checker.add_message("raising-bad-type", node=self._node, args=name) + self._checker.add_message( + "raising-bad-type", + node=self._node, + args=name, + confidence=INFERENCE, + ) class ExceptionsChecker(checkers.BaseChecker): @@ -244,7 +287,7 @@ class ExceptionsChecker(checkers.BaseChecker): ( "overgeneral-exceptions", { - "default": OVERGENERAL_EXCEPTIONS, + "default": ("builtins.BaseException", "builtins.Exception"), "type": "csv", "metavar": "<comma-separated class names>", "help": "Exceptions that will emit a warning when caught.", @@ -254,6 +297,18 @@ class ExceptionsChecker(checkers.BaseChecker): def open(self) -> None: self._builtin_exceptions = _builtin_exceptions() + for exc_name in self.linter.config.overgeneral_exceptions: + if "." not in exc_name: + warnings.warn_explicit( + "Specifying exception names in the overgeneral-exceptions option" + " without module name is deprecated and support for it" + " will be removed in pylint 3.0." + f" Use fully qualified name (maybe 'builtins.{exc_name}' ?) instead.", + category=UserWarning, + filename="pylint: Command line or configuration file", + lineno=1, + module="pylint", + ) super().open() @utils.only_required_for_messages( @@ -264,6 +319,7 @@ class ExceptionsChecker(checkers.BaseChecker): "bad-exception-cause", "raising-format-tuple", "raise-missing-from", + "broad-exception-raised", ) def visit_raise(self, node: nodes.Raise) -> None: if node.exc is None: @@ -302,7 +358,7 @@ class ExceptionsChecker(checkers.BaseChecker): expected = (nodes.ExceptHandler,) if not current or not isinstance(current.parent, expected): - self.add_message("misplaced-bare-raise", node=node) + self.add_message("misplaced-bare-raise", node=node, confidence=HIGH) def _check_bad_exception_cause(self, node: nodes.Raise) -> None: """Verify that the exception cause is properly set. @@ -493,7 +549,7 @@ class ExceptionsChecker(checkers.BaseChecker): @utils.only_required_for_messages( "bare-except", - "broad-except", + "broad-exception-caught", "try-except-raise", "binary-op-exception", "bad-except-order", @@ -508,17 +564,22 @@ class ExceptionsChecker(checkers.BaseChecker): for index, handler in enumerate(node.handlers): if handler.type is None: if not _is_raising(handler.body): - self.add_message("bare-except", node=handler) + self.add_message("bare-except", node=handler, confidence=HIGH) # check if an "except:" is followed by some other # except if index < (nb_handlers - 1): msg = "empty except clause should always appear last" - self.add_message("bad-except-order", node=node, args=msg) + self.add_message( + "bad-except-order", node=node, args=msg, confidence=HIGH + ) elif isinstance(handler.type, nodes.BoolOp): self.add_message( - "binary-op-exception", node=handler, args=handler.type.op + "binary-op-exception", + node=handler, + args=handler.type.op, + confidence=HIGH, ) else: try: @@ -547,24 +608,40 @@ class ExceptionsChecker(checkers.BaseChecker): if previous_exc in exc_ancestors: msg = f"{previous_exc.name} is an ancestor class of {exception.name}" self.add_message( - "bad-except-order", node=handler.type, args=msg + "bad-except-order", + node=handler.type, + args=msg, + confidence=INFERENCE, ) - if ( - exception.name in self.linter.config.overgeneral_exceptions - and exception.root().name == utils.EXCEPTIONS_MODULE - and not _is_raising(handler.body) + if self._is_overgeneral_exception(exception) and not _is_raising( + handler.body ): self.add_message( - "broad-except", args=exception.name, node=handler.type + "broad-exception-caught", + args=exception.name, + node=handler.type, + confidence=INFERENCE, ) if exception in exceptions_classes: self.add_message( - "duplicate-except", args=exception.name, node=handler.type + "duplicate-except", + args=exception.name, + node=handler.type, + confidence=INFERENCE, ) exceptions_classes += [exc for _, exc in exceptions] + def _is_overgeneral_exception(self, exception: nodes.ClassDef) -> bool: + return ( + exception.qname() in self.linter.config.overgeneral_exceptions + # TODO: 3.0: not a qualified name, deprecated + or "." not in exception.name + and exception.name in self.linter.config.overgeneral_exceptions + and exception.root().name == utils.EXCEPTIONS_MODULE + ) + def register(linter: PyLinter) -> None: linter.register_checker(ExceptionsChecker(linter)) diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index 5d9a854b9..001330b2b 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -22,12 +22,7 @@ from typing import TYPE_CHECKING from astroid import nodes from pylint.checkers import BaseRawFileChecker, BaseTokenChecker -from pylint.checkers.utils import ( - is_overload_stub, - is_protocol_class, - node_frame_class, - only_required_for_messages, -) +from pylint.checkers.utils import only_required_for_messages from pylint.constants import WarningScope from pylint.interfaces import HIGH from pylint.typing import MessageDefinitionTuple @@ -55,6 +50,8 @@ _KEYWORD_TOKENS = { "while", "yield", "with", + "=", + ":=", } _JUNK_TOKENS = {tokenize.COMMENT, tokenize.NL} @@ -273,7 +270,7 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): line = tokens.line(line_start) if tokens.type(line_start) not in _JUNK_TOKENS: self._lines[line_num] = line.split("\n")[0] - self.check_lines(line, line_num) + self.check_lines(tokens, line_start, line, line_num) def process_module(self, node: nodes.Module) -> None: pass @@ -404,12 +401,6 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): # the full line; therefore we check the next token on the line. if tok_type == tokenize.INDENT: self.new_line(TokenWrapper(tokens), idx - 1, idx + 1) - # A tokenizer oddity: if a line contains a multi-line string, - # the NEWLINE also gets its own token which we already checked in - # the multi-line docstring case. - # See https://github.com/PyCQA/pylint/issues/6936 - elif tok_type == tokenize.NEWLINE: - pass else: self.new_line(TokenWrapper(tokens), idx - 1, idx) @@ -535,7 +526,7 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): tolineno = node.tolineno assert tolineno, node lines: list[str] = [] - for line in range(line, tolineno + 1): + for line in range(line, tolineno + 1): # noqa: B020 self._visited_lines[line] = 1 try: lines.append(self._lines[line].rstrip()) @@ -567,26 +558,20 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): ): return - # Function overloads that use ``Ellipsis`` are exempted. + # Functions stubs with ``Ellipsis`` as body are exempted. if ( - isinstance(node, nodes.Expr) + isinstance(node.parent, nodes.FunctionDef) + and isinstance(node, nodes.Expr) and isinstance(node.value, nodes.Const) and node.value.value is Ellipsis ): - frame = node.frame(future=True) - if is_overload_stub(frame) or is_protocol_class(node_frame_class(frame)): - return + return self.add_message("multiple-statements", node=node) self._visited_lines[line] = 2 - def check_line_ending(self, line: str, i: int) -> None: - """Check that the final newline is not missing and that there is no trailing - white-space. - """ - if not line.endswith("\n"): - self.add_message("missing-final-newline", line=i) - return + def check_trailing_whitespace_ending(self, line: str, i: int) -> None: + """Check that there is no trailing white-space.""" # exclude \f (formfeed) from the rstrip stripped_line = line.rstrip("\t\n\r\v ") if line[len(stripped_line) :] not in ("\n", "\r\n"): @@ -655,7 +640,9 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): buffer += atomic_line return res - def check_lines(self, lines: str, lineno: int) -> None: + def check_lines( + self, tokens: TokenWrapper, line_start: int, lines: str, lineno: int + ) -> None: """Check given lines for potential messages. Check if lines have: @@ -676,17 +663,20 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): split_lines = self.specific_splitlines(lines) for offset, line in enumerate(split_lines): - self.check_line_ending(line, lineno + offset) - - # hold onto the initial lineno for later - potential_line_length_warning = False - for offset, line in enumerate(split_lines): - # this check is purposefully simple and doesn't rstrip - # since this is running on every line you're checking it's - # advantageous to avoid doing a lot of work - if len(line) > max_chars: - potential_line_length_warning = True - break + if not line.endswith("\n"): + self.add_message("missing-final-newline", line=lineno + offset) + continue + # We don't test for trailing whitespaces in strings + # See https://github.com/PyCQA/pylint/issues/6936 + # and https://github.com/PyCQA/pylint/issues/3822 + if tokens.type(line_start) != tokenize.STRING: + self.check_trailing_whitespace_ending(line, lineno + offset) + + # This check is purposefully simple and doesn't rstrip since this is running + # on every line you're checking it's advantageous to avoid doing a lot of work + potential_line_length_warning = any( + len(line) > max_chars for line in split_lines + ) # if there were no lines passing the max_chars config, we don't bother # running the full line check (as we've met an even more strict condition) diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 7cab78586..61c18649b 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -11,8 +11,8 @@ import copy import os import sys from collections import defaultdict -from collections.abc import Sequence -from typing import TYPE_CHECKING, Any +from collections.abc import ItemsView, Sequence +from typing import TYPE_CHECKING, Any, Dict, List, Union import astroid from astroid import nodes @@ -37,6 +37,9 @@ from pylint.utils.linterstats import LinterStats if TYPE_CHECKING: from pylint.lint import PyLinter +# The dictionary with Any should actually be a _ImportTree again +# but mypy doesn't support recursive types yet +_ImportTree = Dict[str, Union[List[Dict[str, Any]], List[str]]] DEPRECATED_MODULES = { (0, 0, 0): {"tkinter.tix", "fpectl"}, @@ -47,7 +50,7 @@ DEPRECATED_MODULES = { (3, 6, 0): {"asynchat", "asyncore", "smtpd"}, (3, 7, 0): {"macpath"}, (3, 9, 0): {"lib2to3", "parser", "symbol", "binhex"}, - (3, 10, 0): {"distutils"}, + (3, 10, 0): {"distutils", "typing.io", "typing.re"}, (3, 11, 0): { "aifc", "audioop", @@ -57,6 +60,7 @@ DEPRECATED_MODULES = { "crypt", "imghdr", "msilib", + "mailcap", "nis", "nntplib", "ossaudiodev", @@ -148,35 +152,37 @@ def _ignore_import_failure( # utilities to represents import dependencies as tree and dot graph ########### -def _make_tree_defs(mod_files_list): +def _make_tree_defs(mod_files_list: ItemsView[str, set[str]]) -> _ImportTree: """Get a list of 2-uple (module, list_of_files_which_import_this_module), it will return a dictionary to represent this as a tree. """ - tree_defs = {} + tree_defs: _ImportTree = {} for mod, files in mod_files_list: - node = (tree_defs, ()) + node: list[_ImportTree | list[str]] = [tree_defs, []] for prefix in mod.split("."): - node = node[0].setdefault(prefix, [{}, []]) + assert isinstance(node[0], dict) + node = node[0].setdefault(prefix, ({}, [])) # type: ignore[arg-type,assignment] + assert isinstance(node[1], list) node[1] += files return tree_defs -def _repr_tree_defs(data, indent_str=None): +def _repr_tree_defs(data: _ImportTree, indent_str: str | None = None) -> str: """Return a string which represents imports as a tree.""" lines = [] nodes_items = data.items() for i, (mod, (sub, files)) in enumerate(sorted(nodes_items, key=lambda x: x[0])): - files = "" if not files else f"({','.join(sorted(files))})" + files_list = "" if not files else f"({','.join(sorted(files))})" if indent_str is None: - lines.append(f"{mod} {files}") + lines.append(f"{mod} {files_list}") sub_indent_str = " " else: - lines.append(rf"{indent_str}\-{mod} {files}") + lines.append(rf"{indent_str}\-{mod} {files_list}") if i == len(nodes_items) - 1: sub_indent_str = f"{indent_str} " else: sub_indent_str = f"{indent_str}| " - if sub: + if sub and isinstance(sub, dict): lines.append(_repr_tree_defs(sub, sub_indent_str)) return "\n".join(lines) @@ -420,7 +426,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): def __init__(self, linter: PyLinter) -> None: BaseChecker.__init__(self, linter) self.import_graph: defaultdict[str, set[str]] = defaultdict(set) - self._imports_stack: list[tuple[Any, Any]] = [] + self._imports_stack: list[tuple[ImportNode, str]] = [] self._first_non_import_node = None self._module_pkg: dict[ Any, Any @@ -692,24 +698,32 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): self._imports_stack.append((node, importedname)) @staticmethod - def _is_fallback_import(node, imports): + def _is_fallback_import( + node: ImportNode, imports: list[tuple[ImportNode, str]] + ) -> bool: imports = [import_node for (import_node, _) in imports] return any(astroid.are_exclusive(import_node, node) for import_node in imports) - def _check_imports_order(self, _module_node): + def _check_imports_order( + self, _module_node: nodes.Module + ) -> tuple[ + list[tuple[ImportNode, str]], + list[tuple[ImportNode, str]], + list[tuple[ImportNode, str]], + ]: """Checks imports of module `node` are grouped by category. Imports must follow this order: standard, 3rd party, local """ - std_imports = [] - third_party_imports = [] - first_party_imports = [] + std_imports: list[tuple[ImportNode, str]] = [] + third_party_imports: list[tuple[ImportNode, str]] = [] + first_party_imports: list[tuple[ImportNode, str]] = [] # need of a list that holds third or first party ordered import - external_imports = [] - local_imports = [] - third_party_not_ignored = [] - first_party_not_ignored = [] - local_not_ignored = [] + external_imports: list[tuple[ImportNode, str]] = [] + local_imports: list[tuple[ImportNode, str]] = [] + third_party_not_ignored: list[tuple[ImportNode, str]] = [] + first_party_not_ignored: list[tuple[ImportNode, str]] = [] + local_not_ignored: list[tuple[ImportNode, str]] = [] isort_driver = IsortDriver(self.linter.config) for node, modname in self._imports_stack: if modname.startswith("."): @@ -864,7 +878,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): ): self._excluded_edges[context_name].add(importedmodname) - def _check_preferred_module(self, node, mod_path): + def _check_preferred_module(self, node: ImportNode, mod_path: str) -> None: """Check if the module has a preferred replacement.""" if mod_path in self.preferred_modules: self.add_message( diff --git a/pylint/checkers/mapreduce_checker.py b/pylint/checkers/mapreduce_checker.py index 9d721aa49..96e86d7c0 100644 --- a/pylint/checkers/mapreduce_checker.py +++ b/pylint/checkers/mapreduce_checker.py @@ -20,6 +20,7 @@ class MapReduceMixin(metaclass=abc.ABCMeta): "MapReduceMixin has been deprecated and will be removed in pylint 3.0. " "To make a checker reduce map data simply implement get_map_data and reduce_map_data.", DeprecationWarning, + stacklevel=2, ) @abc.abstractmethod diff --git a/pylint/checkers/modified_iterating_checker.py b/pylint/checkers/modified_iterating_checker.py index c98e7ebc5..bdc8fff7f 100644 --- a/pylint/checkers/modified_iterating_checker.py +++ b/pylint/checkers/modified_iterating_checker.py @@ -121,7 +121,7 @@ class ModifiedIterationChecker(checkers.BaseChecker): if isinstance(iter_obj, nodes.Attribute) else iter_obj.name ) - return (infer_val == utils.safe_infer(iter_obj)) and ( + return (infer_val == utils.safe_infer(iter_obj)) and ( # type: ignore[no-any-return] node.value.func.expr.name == iter_obj_name ) @@ -168,7 +168,7 @@ class ModifiedIterationChecker(checkers.BaseChecker): iter_obj_name = iter_obj.attrname else: iter_obj_name = iter_obj.name - return node.targets[0].value.name == iter_obj_name + return node.targets[0].value.name == iter_obj_name # type: ignore[no-any-return] def _modified_iterating_set_cond( self, node: nodes.NodeNG, iter_obj: nodes.Name | nodes.Attribute diff --git a/pylint/checkers/nested_min_max.py b/pylint/checkers/nested_min_max.py new file mode 100644 index 000000000..39feb5f42 --- /dev/null +++ b/pylint/checkers/nested_min_max.py @@ -0,0 +1,95 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Check for use of nested min/max functions.""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import only_required_for_messages, safe_infer +from pylint.interfaces import INFERENCE + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class NestedMinMaxChecker(BaseChecker): + """Multiple nested min/max calls on the same line will raise multiple messages. + + This behaviour is intended as it would slow down the checker to check + for nested call with minimal benefits. + """ + + FUNC_NAMES = ("builtins.min", "builtins.max") + + name = "nested_min_max" + msgs = { + "W3301": ( + "Do not use nested call of '%s'; it's possible to do '%s' instead", + "nested-min-max", + "Nested calls ``min(1, min(2, 3))`` can be rewritten as ``min(1, 2, 3)``.", + ) + } + + @classmethod + def is_min_max_call(cls, node: nodes.NodeNG) -> bool: + if not isinstance(node, nodes.Call): + return False + + inferred = safe_infer(node.func) + return ( + isinstance(inferred, nodes.FunctionDef) + and inferred.qname() in cls.FUNC_NAMES + ) + + @classmethod + def get_redundant_calls(cls, node: nodes.Call) -> list[nodes.Call]: + return [ + arg + for arg in node.args + if cls.is_min_max_call(arg) and arg.func.name == node.func.name + ] + + @only_required_for_messages("nested-min-max") + def visit_call(self, node: nodes.Call) -> None: + if not self.is_min_max_call(node): + return + + redundant_calls = self.get_redundant_calls(node) + if not redundant_calls: + return + + fixed_node = copy.copy(node) + while len(redundant_calls) > 0: + for i, arg in enumerate(fixed_node.args): + # Exclude any calls with generator expressions as there is no + # clear better suggestion for them. + if isinstance(arg, nodes.Call) and any( + isinstance(a, nodes.GeneratorExp) for a in arg.args + ): + return + + if arg in redundant_calls: + fixed_node.args = ( + fixed_node.args[:i] + arg.args + fixed_node.args[i + 1 :] + ) + break + + redundant_calls = self.get_redundant_calls(fixed_node) + + self.add_message( + "nested-min-max", + node=node, + args=(node.func.name, fixed_node.as_string()), + confidence=INFERENCE, + ) + + +def register(linter: PyLinter) -> None: + linter.register_checker(NestedMinMaxChecker(linter)) diff --git a/pylint/checkers/refactoring/implicit_booleaness_checker.py b/pylint/checkers/refactoring/implicit_booleaness_checker.py index 13172a97e..2ad038619 100644 --- a/pylint/checkers/refactoring/implicit_booleaness_checker.py +++ b/pylint/checkers/refactoring/implicit_booleaness_checker.py @@ -9,6 +9,7 @@ from astroid import bases, nodes from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import HIGH, INFERENCE class ImplicitBooleanessChecker(checkers.BaseChecker): @@ -50,7 +51,6 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): * comparison such as variable != empty_literal: """ - # configuration section name name = "refactoring" msgs = { "C1802": ( @@ -64,7 +64,7 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): {"old_names": [("C1801", "len-as-condition")]}, ), "C1803": ( - "'%s' can be simplified to '%s' as an empty sequence is falsey", + "'%s' can be simplified to '%s' as an empty %s is falsey", "use-implicit-booleaness-not-comparison", "Used when Pylint detects that collection literal comparison is being " "used to check for emptiness; Use implicit booleaness instead " @@ -99,7 +99,11 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): ) if isinstance(len_arg, generator_or_comprehension): # The node is a generator or comprehension as in len([x for x in ...]) - self.add_message("use-implicit-booleaness-not-len", node=node) + self.add_message( + "use-implicit-booleaness-not-len", + node=node, + confidence=HIGH, + ) return try: instance = next(len_arg.infer()) @@ -113,7 +117,11 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): if "range" in mother_classes or ( affected_by_pep8 and not self.instance_has_bool(instance) ): - self.add_message("use-implicit-booleaness-not-len", node=node) + self.add_message( + "use-implicit-booleaness-not-len", + node=node, + confidence=INFERENCE, + ) @staticmethod def instance_has_bool(class_def: nodes.ClassDef) -> bool: @@ -134,7 +142,9 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): and node.op == "not" and utils.is_call_of_name(node.operand, "len") ): - self.add_message("use-implicit-booleaness-not-len", node=node) + self.add_message( + "use-implicit-booleaness-not-len", node=node, confidence=HIGH + ) @utils.only_required_for_messages("use-implicit-booleaness-not-comparison") def visit_compare(self, node: nodes.Compare) -> None: @@ -177,35 +187,42 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): # No need to check for operator when visiting compare node if operator in {"==", "!=", ">=", ">", "<=", "<"}: - collection_literal = "{}" - if isinstance(literal_node, nodes.List): - collection_literal = "[]" - if isinstance(literal_node, nodes.Tuple): - collection_literal = "()" - - instance_name = "x" - if isinstance(target_node, nodes.Call) and target_node.func: - instance_name = f"{target_node.func.as_string()}(...)" - elif isinstance(target_node, (nodes.Attribute, nodes.Name)): - instance_name = target_node.as_string() - - original_comparison = ( - f"{instance_name} {operator} {collection_literal}" - ) - suggestion = ( - f"{instance_name}" - if operator == "!=" - else f"not {instance_name}" - ) self.add_message( "use-implicit-booleaness-not-comparison", - args=( - original_comparison, - suggestion, + args=self._implicit_booleaness_message_args( + literal_node, operator, target_node ), node=node, + confidence=HIGH, ) + def _get_node_description(self, node: nodes.NodeNG) -> str: + return { + nodes.List: "list", + nodes.Tuple: "tuple", + nodes.Dict: "dict", + nodes.Const: "str", + }.get(type(node), "iterable") + + def _implicit_booleaness_message_args( + self, literal_node: nodes.NodeNG, operator: str, target_node: nodes.NodeNG + ) -> tuple[str, str, str]: + """Helper to get the right message for "use-implicit-booleaness-not-comparison".""" + description = self._get_node_description(literal_node) + collection_literal = { + "list": "[]", + "tuple": "()", + "dict": "{}", + }.get(description, "iterable") + instance_name = "x" + if isinstance(target_node, nodes.Call) and target_node.func: + instance_name = f"{target_node.func.as_string()}(...)" + elif isinstance(target_node, (nodes.Attribute, nodes.Name)): + instance_name = target_node.as_string() + original_comparison = f"{instance_name} {operator} {collection_literal}" + suggestion = f"{instance_name}" if operator == "!=" else f"not {instance_name}" + return original_comparison, suggestion, description + @staticmethod def base_names_of_instance(node: bases.Uninferable | bases.Instance) -> list[str]: """Return all names inherited by a class instance or those returned by a diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 7873dc25e..cda26e064 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -9,6 +9,7 @@ from astroid import nodes from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import INFERENCE class RecommendationChecker(checkers.BaseChecker): @@ -67,7 +68,7 @@ class RecommendationChecker(checkers.BaseChecker): self._py36_plus = py_version >= (3, 6) @staticmethod - def _is_builtin(node, function): + def _is_builtin(node: nodes.NodeNG, function: str) -> bool: inferred = utils.safe_infer(node) if not inferred: return False @@ -85,6 +86,10 @@ class RecommendationChecker(checkers.BaseChecker): return if node.func.attrname != "keys": return + + if isinstance(node.parent, nodes.BinOp) and node.parent.op in {"&", "|", "^"}: + return + comp_ancestor = utils.get_node_first_ancestor_of_type(node, nodes.Compare) if ( isinstance(node.parent, (nodes.For, nodes.Comprehension)) @@ -101,7 +106,9 @@ class RecommendationChecker(checkers.BaseChecker): inferred.bound, nodes.Dict ): return - self.add_message("consider-iterating-dictionary", node=node) + self.add_message( + "consider-iterating-dictionary", node=node, confidence=INFERENCE + ) def _check_use_maxsplit_arg(self, node: nodes.Call) -> None: """Add message when accessing first or last elements of a str.split() or diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 87b831609..6a79ce55a 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -12,7 +12,7 @@ import tokenize from collections.abc import Iterator from functools import reduce from re import Pattern -from typing import TYPE_CHECKING, Any, NamedTuple, Union +from typing import TYPE_CHECKING, Any, NamedTuple, Union, cast import astroid from astroid import bases, nodes @@ -147,7 +147,7 @@ def _is_part_of_with_items(node: nodes.Call) -> bool: if isinstance(current, nodes.With): items_start = current.items[0][0].lineno items_end = current.items[-1][0].tolineno - return items_start <= node.lineno <= items_end + return items_start <= node.lineno <= items_end # type: ignore[no-any-return] current = current.parent return False @@ -181,7 +181,7 @@ def _is_part_of_assignment_target(node: nodes.NodeNG) -> bool: return node in node.parent.targets if isinstance(node.parent, nodes.AugAssign): - return node == node.parent.target + return node == node.parent.target # type: ignore[no-any-return] if isinstance(node.parent, (nodes.Tuple, nodes.List)): return _is_part_of_assignment_target(node.parent) @@ -464,9 +464,9 @@ class RefactoringChecker(checkers.BaseTokenChecker): "The literal is faster as it avoids an additional function call.", ), "R1735": ( - "Consider using {} instead of dict()", + "Consider using '%s' instead of a call to 'dict'.", "use-dict-literal", - "Emitted when using dict() to create an empty dictionary instead of the literal {}. " + "Emitted when using dict() to create a dictionary instead of a literal '{ ... }'. " "The literal is faster as it avoids an additional function call.", ), "R1736": ( @@ -523,7 +523,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): @cached_property def _dummy_rgx(self) -> Pattern[str]: - return self.linter.config.dummy_variables_rgx + return self.linter.config.dummy_variables_rgx # type: ignore[no-any-return] @staticmethod def _is_bool_const(node: nodes.Return | nodes.Assign) -> bool: @@ -748,13 +748,13 @@ class RefactoringChecker(checkers.BaseTokenChecker): @staticmethod def _type_and_name_are_equal(node_a: Any, node_b: Any) -> bool: if isinstance(node_a, nodes.Name) and isinstance(node_b, nodes.Name): - return node_a.name == node_b.name + return node_a.name == node_b.name # type: ignore[no-any-return] if isinstance(node_a, nodes.AssignName) and isinstance( node_b, nodes.AssignName ): - return node_a.name == node_b.name + return node_a.name == node_b.name # type: ignore[no-any-return] if isinstance(node_a, nodes.Const) and isinstance(node_b, nodes.Const): - return node_a.value == node_b.value + return node_a.value == node_b.value # type: ignore[no-any-return] return False def _is_dict_get_block(self, node: nodes.If) -> bool: @@ -1074,7 +1074,8 @@ class RefactoringChecker(checkers.BaseTokenChecker): self._check_super_with_arguments(node) self._check_consider_using_generator(node) self._check_consider_using_with(node) - self._check_use_list_or_dict_literal(node) + self._check_use_list_literal(node) + self._check_use_dict_literal(node) @staticmethod def _has_exit_in_scope(scope: nodes.LocalsDictNodeNG) -> bool: @@ -1364,7 +1365,9 @@ class RefactoringChecker(checkers.BaseTokenChecker): break @staticmethod - def _apply_boolean_simplification_rules(operator: str, values): + def _apply_boolean_simplification_rules( + operator: str, values: list[nodes.NodeNG] + ) -> list[nodes.NodeNG]: """Removes irrelevant values or returns short-circuiting values. This function applies the following two rules: @@ -1374,7 +1377,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): 2) False values in OR expressions are only relevant if all values are false, and the reverse for AND """ - simplified_values = [] + simplified_values: list[nodes.NodeNG] = [] for subnode in values: inferred_bool = None @@ -1390,7 +1393,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): return simplified_values or [nodes.Const(operator == "and")] - def _simplify_boolean_operation(self, bool_op: nodes.BoolOp): + def _simplify_boolean_operation(self, bool_op: nodes.BoolOp) -> nodes.BoolOp: """Attempts to simplify a boolean operation. Recursively applies simplification on the operator terms, @@ -1593,15 +1596,61 @@ class RefactoringChecker(checkers.BaseTokenChecker): if could_be_used_in_with and not _will_be_released_automatically(node): self.add_message("consider-using-with", node=node) - def _check_use_list_or_dict_literal(self, node: nodes.Call) -> None: - """Check if empty list or dict is created by using the literal [] or {}.""" - if node.as_string() in {"list()", "dict()"}: + def _check_use_list_literal(self, node: nodes.Call) -> None: + """Check if empty list is created by using the literal [].""" + if node.as_string() == "list()": inferred = utils.safe_infer(node.func) if isinstance(inferred, nodes.ClassDef) and not node.args: if inferred.qname() == "builtins.list": self.add_message("use-list-literal", node=node) - elif inferred.qname() == "builtins.dict" and not node.keywords: - self.add_message("use-dict-literal", node=node) + + def _check_use_dict_literal(self, node: nodes.Call) -> None: + """Check if dict is created by using the literal {}.""" + if not isinstance(node.func, astroid.Name) or node.func.name != "dict": + return + inferred = utils.safe_infer(node.func) + if ( + isinstance(inferred, nodes.ClassDef) + and inferred.qname() == "builtins.dict" + and not node.args + ): + self.add_message( + "use-dict-literal", + args=(self._dict_literal_suggestion(node),), + node=node, + confidence=INFERENCE, + ) + + @staticmethod + def _dict_literal_suggestion(node: nodes.Call) -> str: + """Return a suggestion of reasonable length.""" + elements: list[str] = [] + for keyword in node.keywords: + if len(", ".join(elements)) >= 64: + break + if keyword not in node.kwargs: + elements.append(f'"{keyword.arg}": {keyword.value.as_string()}') + for keyword in node.kwargs: + if len(", ".join(elements)) >= 64: + break + elements.append(f"**{keyword.value.as_string()}") + suggestion = ", ".join(elements) + return f"{{{suggestion}{', ... ' if len(suggestion) > 64 else ''}}}" + + @staticmethod + def _name_to_concatenate(node: nodes.NodeNG) -> str | None: + """Try to extract the name used in a concatenation loop.""" + if isinstance(node, nodes.Name): + return cast("str | None", node.name) + if not isinstance(node, nodes.JoinedStr): + return None + + values = [ + value for value in node.values if isinstance(value, nodes.FormattedValue) + ] + if len(values) != 1 or not isinstance(values[0].value, nodes.Name): + return None + return cast("str | None", values[0].value.name) def _check_consider_using_join(self, aug_assign: nodes.AugAssign) -> None: """We start with the augmented assignment and work our way upwards. @@ -1630,8 +1679,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): and aug_assign.target.name in result_assign_names and isinstance(assign.value, nodes.Const) and isinstance(assign.value.value, str) - and isinstance(aug_assign.value, nodes.Name) - and aug_assign.value.name == for_loop.target.name + and self._name_to_concatenate(aug_assign.value) == for_loop.target.name ) if is_concat_loop: self.add_message("consider-using-join", node=aug_assign) @@ -1749,7 +1797,9 @@ class RefactoringChecker(checkers.BaseTokenChecker): ) @staticmethod - def _and_or_ternary_arguments(node: nodes.BoolOp): + def _and_or_ternary_arguments( + node: nodes.BoolOp, + ) -> tuple[nodes.NodeNG, nodes.NodeNG, nodes.NodeNG]: false_value = node.values[1] condition, true_value = node.values[0].values return condition, true_value, false_value @@ -2101,11 +2151,19 @@ class RefactoringChecker(checkers.BaseTokenChecker): not isinstance(node.iter, nodes.Call) or not isinstance(node.iter.func, nodes.Name) or not node.iter.func.name == "enumerate" - or not node.iter.args - or not isinstance(node.iter.args[0], nodes.Name) ): return + try: + iterable_arg = utils.get_argument_from_call( + node.iter, position=0, keyword="iterable" + ) + except utils.NoSuchArgumentError: + return + + if not isinstance(iterable_arg, nodes.Name): + return + if not isinstance(node.target, nodes.Tuple) or len(node.target.elts) < 2: # enumerate() result is being assigned without destructuring return @@ -2121,7 +2179,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): # is not redundant, hence we should not report an error. return - iterating_object_name = node.iter.args[0].name + iterating_object_name = iterable_arg.name value_variable = node.target.elts[1] # Store potential violations. These will only be reported if we don't @@ -2248,15 +2306,13 @@ class RefactoringChecker(checkers.BaseTokenChecker): return False, confidence def _get_start_value(self, node: nodes.NodeNG) -> tuple[int | None, Confidence]: - confidence = HIGH - - if isinstance(node, (nodes.Name, nodes.Call)): + if isinstance(node, (nodes.Name, nodes.Call, nodes.Attribute)): inferred = utils.safe_infer(node) start_val = inferred.value if inferred else None - confidence = INFERENCE - elif isinstance(node, nodes.UnaryOp): - start_val = node.operand.value - else: - start_val = node.value + return start_val, INFERENCE + if isinstance(node, nodes.UnaryOp): + return node.operand.value, HIGH + if isinstance(node, nodes.Const): + return node.value, HIGH - return start_val, confidence + return None, HIGH diff --git a/pylint/checkers/similar.py b/pylint/checkers/similar.py index 7938c27e8..3b18ddbfd 100644 --- a/pylint/checkers/similar.py +++ b/pylint/checkers/similar.py @@ -28,7 +28,7 @@ import re import sys import warnings from collections import defaultdict -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Generator, Iterable, Sequence from getopt import getopt from io import BufferedIOBase, BufferedReader, BytesIO from itertools import chain, groupby @@ -49,7 +49,7 @@ import astroid from astroid import nodes from pylint.checkers import BaseChecker, BaseRawFileChecker, table_lines_from_stats -from pylint.reporters.ureports.nodes import Table +from pylint.reporters.ureports.nodes import Section, Table from pylint.typing import MessageDefinitionTuple, Options from pylint.utils import LinterStats, decoding_stream @@ -185,7 +185,7 @@ class LineSetStartCouple(NamedTuple): f"<LineSetStartCouple <{self.fst_lineset_index};{self.snd_lineset_index}>>" ) - def __eq__(self, other) -> bool: + def __eq__(self, other: Any) -> bool: if not isinstance(other, LineSetStartCouple): return NotImplemented return ( @@ -382,7 +382,7 @@ class Similar: self.namespace.ignore_docstrings, self.namespace.ignore_imports, self.namespace.ignore_signatures, - line_enabled_callback=self.linter._is_one_message_enabled # type: ignore[attr-defined] + line_enabled_callback=self.linter._is_one_message_enabled if hasattr(self, "linter") else None, ) @@ -585,7 +585,7 @@ def stripped_lines( line_begins_import = { lineno: all(is_import for _, is_import in node_is_import_group) for lineno, node_is_import_group in groupby( - node_is_import_by_lineno, key=lambda x: x[0] + node_is_import_by_lineno, key=lambda x: x[0] # type: ignore[no-any-return] ) } current_line_is_import = False @@ -689,7 +689,7 @@ class LineSet: line_enabled_callback=line_enabled_callback, ) - def __str__(self): + def __str__(self) -> str: return f"<Lineset for {self.name}>" def __len__(self) -> int: @@ -730,7 +730,7 @@ MSGS: dict[str, MessageDefinitionTuple] = { def report_similarities( - sect, + sect: Section, stats: LinterStats, old_stats: LinterStats | None, ) -> None: @@ -886,7 +886,7 @@ def usage(status: int = 0) -> NoReturn: sys.exit(status) -def Run(argv=None) -> NoReturn: +def Run(argv: Sequence[str] | None = None) -> NoReturn: """Standalone command line access point.""" if argv is None: argv = sys.argv[1:] @@ -905,7 +905,7 @@ def Run(argv=None) -> NoReturn: ignore_docstrings = False ignore_imports = False ignore_signatures = False - opts, args = getopt(argv, s_opts, l_opts) + opts, args = getopt(list(argv), s_opts, l_opts) for opt, val in opts: if opt in {"-d", "--duplicates"}: min_lines = int(val) diff --git a/pylint/checkers/spelling.py b/pylint/checkers/spelling.py index e16377ab7..23209a2bc 100644 --- a/pylint/checkers/spelling.py +++ b/pylint/checkers/spelling.py @@ -55,10 +55,10 @@ except ImportError: pass def get_tokenizer( - tag: str | None = None, - chunkers: list[Chunker] | None = None, - filters: list[Filter] | None = None, - ): # pylint: disable=unused-argument + tag: str | None = None, # pylint: disable=unused-argument + chunkers: list[Chunker] | None = None, # pylint: disable=unused-argument + filters: list[Filter] | None = None, # pylint: disable=unused-argument + ) -> Filter: return Filter() @@ -75,14 +75,14 @@ else: instr = " To make it work, install the 'python-enchant' package." -class WordsWithDigitsFilter(Filter): +class WordsWithDigitsFilter(Filter): # type: ignore[misc] """Skips words with digits.""" def _skip(self, word: str) -> bool: return any(char.isdigit() for char in word) -class WordsWithUnderscores(Filter): +class WordsWithUnderscores(Filter): # type: ignore[misc] """Skips words with underscores. They are probably function parameter names. @@ -92,7 +92,7 @@ class WordsWithUnderscores(Filter): return "_" in word -class RegExFilter(Filter): +class RegExFilter(Filter): # type: ignore[misc] """Parent class for filters using regular expressions. This filter skips any words the match the expression @@ -128,12 +128,14 @@ class SphinxDirectives(RegExFilter): _pattern = re.compile(r"^(:([a-z]+)){1,2}:`([^`]+)(`)?") -class ForwardSlashChunker(Chunker): +class ForwardSlashChunker(Chunker): # type: ignore[misc] """This chunker allows splitting words like 'before/after' into 'before' and 'after'. """ - def next(self): + _text: str + + def next(self) -> tuple[str, int]: while True: if not self._text: raise StopIteration() diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 456815cff..6f40116f7 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -243,7 +243,12 @@ DEPRECATED_METHODS: dict[int, DeprecationDict] = { }, (3, 11, 0): { "locale.getdefaultlocale", - "unittest.TestLoader.findTestCases", + "locale.resetlocale", + "re.template", + "unittest.findTestCases", + "unittest.makeSuite", + "unittest.getTestCaseNames", + "unittest.TestLoader.loadTestsFromModule", "unittest.TestLoader.loadTestsFromTestCase", "unittest.TestLoader.getTestCaseNames", }, @@ -300,6 +305,9 @@ DEPRECATED_CLASSES: dict[tuple[int, int, int], dict[str, set[str]]] = { } }, (3, 11, 0): { + "typing": { + "Text", + }, "webbrowser": { "MacOSX", }, @@ -373,7 +381,7 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "threading.Thread needs the target function", "bad-thread-instantiation", "The warning is emitted when a threading.Thread class " - "is instantiated without the target function being passed. " + "is instantiated without the target function being passed as a kwarg or as a second argument. " "By default, the first parameter is the group param, not the target param.", ), "W1507": ( @@ -389,6 +397,20 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "Env manipulation functions support only string type arguments. " "See https://docs.python.org/3/library/os.html#os.getenv.", ), + "E1519": ( + "singledispatch decorator should not be used with methods, " + "use singledispatchmethod instead.", + "singledispatch-method", + "singledispatch should decorate functions and not class/instance methods. " + "Use singledispatchmethod for those cases.", + ), + "E1520": ( + "singledispatchmethod decorator should not be used with functions, " + "use singledispatch instead.", + "singledispatchmethod-function", + "singledispatchmethod should decorate class/instance methods and not functions. " + "Use singledispatch for those cases.", + ), "W1508": ( "%s default type is %s. Expected str or None.", "invalid-envvar-default", @@ -466,8 +488,14 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): # synced with the config argument deprecated-modules def _check_bad_thread_instantiation(self, node: nodes.Call) -> None: - if not node.kwargs and not node.keywords and len(node.args) <= 1: - self.add_message("bad-thread-instantiation", node=node) + func_kwargs = {key.arg for key in node.keywords} + if "target" in func_kwargs: + return + + if len(node.args) < 2 and (not node.kwargs or "target" not in func_kwargs): + self.add_message( + "bad-thread-instantiation", node=node, confidence=interfaces.HIGH + ) def _check_for_preexec_fn_in_popen(self, node: nodes.Call) -> None: if node.keywords: @@ -557,10 +585,15 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): for value in node.values: self._check_datetime(value) - @utils.only_required_for_messages("method-cache-max-size-none") + @utils.only_required_for_messages( + "method-cache-max-size-none", + "singledispatch-method", + "singledispatchmethod-function", + ) def visit_functiondef(self, node: nodes.FunctionDef) -> None: if node.decorators and isinstance(node.parent, nodes.ClassDef): self._check_lru_cache_decorators(node.decorators) + self._check_dispatch_decorators(node) def _check_lru_cache_decorators(self, decorators: nodes.Decorators) -> None: """Check if instance methods are decorated with functools.lru_cache.""" @@ -599,6 +632,36 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): confidence=interfaces.INFERENCE, ) + def _check_dispatch_decorators(self, node: nodes.FunctionDef) -> None: + decorators_map: dict[str, tuple[nodes.NodeNG, interfaces.Confidence]] = {} + + for decorator in node.decorators.nodes: + if isinstance(decorator, nodes.Name) and decorator.name: + decorators_map[decorator.name] = (decorator, interfaces.HIGH) + elif utils.is_registered_in_singledispatch_function(node): + decorators_map["singledispatch"] = (decorator, interfaces.INFERENCE) + elif utils.is_registered_in_singledispatchmethod_function(node): + decorators_map["singledispatchmethod"] = ( + decorator, + interfaces.INFERENCE, + ) + + if "singledispatch" in decorators_map and "classmethod" in decorators_map: + self.add_message( + "singledispatch-method", + node=decorators_map["singledispatch"][0], + confidence=decorators_map["singledispatch"][1], + ) + elif ( + "singledispatchmethod" in decorators_map + and "staticmethod" in decorators_map + ): + self.add_message( + "singledispatchmethod-function", + node=decorators_map["singledispatchmethod"][0], + confidence=decorators_map["singledispatchmethod"][1], + ) + def _check_redundant_assert(self, node: nodes.Call, infer: InferenceResult) -> None: if ( isinstance(infer, astroid.BoundMethod) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index e5aa70b56..4afe32a16 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -437,7 +437,7 @@ class StringFormatChecker(BaseChecker): self._check_new_format(node, func) def _detect_vacuous_formatting( - self, node: nodes.Call, positional_arguments + self, node: nodes.Call, positional_arguments: list[SuccessfulInferenceResult] ) -> None: counter = collections.Counter( arg.name for arg in positional_arguments if isinstance(arg, nodes.Name) @@ -494,7 +494,7 @@ class StringFormatChecker(BaseChecker): check_args = False # Consider "{[0]} {[1]}" as num_args. - num_args += sum(1 for field in named_fields if field == "") + num_args += sum(1 for field in named_fields if not field) if named_fields: for field in named_fields: if field and field not in named_arguments: @@ -509,7 +509,7 @@ class StringFormatChecker(BaseChecker): # num_args can be 0 if manual_pos is not. num_args = num_args or manual_pos if positional_arguments or num_args: - empty = any(field == "" for field in named_fields) + empty = not all(field for field in named_fields) if named_arguments or empty: # Verify the required number of positional arguments # only if the .format got at least one keyword argument. @@ -534,7 +534,10 @@ class StringFormatChecker(BaseChecker): self._check_new_format_specifiers(node, fields, named_arguments) def _check_new_format_specifiers( - self, node: nodes.Call, fields: list[tuple[str, list[tuple[bool, str]]]], named + self, + node: nodes.Call, + fields: list[tuple[str, list[tuple[bool, str]]]], + named: dict[str, SuccessfulInferenceResult], ) -> None: """Check attribute and index access in the format string ("{0.a}" and "{0[a]}"). @@ -543,7 +546,7 @@ class StringFormatChecker(BaseChecker): for key, specifiers in fields: # Obtain the argument. If it can't be obtained # or inferred, skip this check. - if key == "": + if not key: # {[0]} will have an unnamed argument, defaulting # to 0. It will not be present in `named`, so use the value # 0 for it. @@ -831,16 +834,16 @@ class StringConstantChecker(BaseTokenChecker, BaseRawFileChecker): def process_string_token(self, token: str, start_row: int, start_col: int) -> None: quote_char = None - index = None - for index, char in enumerate(token): + for _index, char in enumerate(token): if char in "'\"": quote_char = char break if quote_char is None: return - - prefix = token[:index].lower() # markers like u, b, r. - after_prefix = token[index:] + # pylint: disable=undefined-loop-variable + prefix = token[:_index].lower() # markers like u, b, r. + after_prefix = token[_index:] + # pylint: enable=undefined-loop-variable # Chop off quotes quote_length = ( 3 if after_prefix[:3] == after_prefix[-3:] == 3 * quote_char else 1 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index d97f352f6..192bccbbb 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -13,15 +13,16 @@ import re import shlex import sys import types -from collections.abc import Callable, Iterator, Sequence +from collections.abc import Callable, Iterable, Iterator, Sequence from functools import singledispatch from re import Pattern -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, TypeVar, Union import astroid import astroid.exceptions import astroid.helpers -from astroid import bases, nodes +from astroid import arguments, bases, nodes +from astroid.typing import InferenceResult, SuccessfulInferenceResult from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import ( @@ -47,7 +48,7 @@ from pylint.checkers.utils import ( supports_membership_test, supports_setitem, ) -from pylint.interfaces import INFERENCE +from pylint.interfaces import HIGH, INFERENCE from pylint.typing import MessageDefinitionTuple if sys.version_info >= (3, 8): @@ -68,6 +69,8 @@ CallableObjects = Union[ nodes.ClassDef, ] +_T = TypeVar("_T") + STR_FORMAT = {"builtins.str.format"} ASYNCIO_COROUTINE = "asyncio.coroutines.coroutine" BUILTIN_TUPLE = "builtins.tuple" @@ -85,16 +88,16 @@ class VERSION_COMPATIBLE_OVERLOAD: VERSION_COMPATIBLE_OVERLOAD_SENTINEL = VERSION_COMPATIBLE_OVERLOAD() -def _unflatten(iterable): +def _unflatten(iterable: Iterable[_T]) -> Iterator[_T]: for index, elem in enumerate(iterable): if isinstance(elem, Sequence) and not isinstance(elem, str): yield from _unflatten(elem) elif elem and not index: # We're interested only in the first element. - yield elem + yield elem # type: ignore[misc] -def _flatten_container(iterable): +def _flatten_container(iterable: Iterable[_T]) -> Iterator[_T]: # Flatten nested containers into a single iterable for item in iterable: if isinstance(item, (list, tuple, types.GeneratorType)): @@ -103,7 +106,12 @@ def _flatten_container(iterable): yield item -def _is_owner_ignored(owner, attrname, ignored_classes, ignored_modules): +def _is_owner_ignored( + owner: SuccessfulInferenceResult, + attrname: str | None, + ignored_classes: Iterable[str], + ignored_modules: Iterable[str], +) -> bool: """Check if the given owner should be ignored. This will verify if the owner's module is in *ignored_modules* @@ -125,15 +133,15 @@ def _is_owner_ignored(owner, attrname, ignored_classes, ignored_modules): @singledispatch -def _node_names(node): +def _node_names(node: SuccessfulInferenceResult) -> Iterable[str]: if not hasattr(node, "locals"): return [] - return node.locals.keys() + return node.locals.keys() # type: ignore[no-any-return] @_node_names.register(nodes.ClassDef) @_node_names.register(astroid.Instance) -def _(node): +def _(node: nodes.ClassDef | bases.Instance) -> Iterable[str]: values = itertools.chain(node.instance_attrs.keys(), node.locals.keys()) try: @@ -145,7 +153,7 @@ def _(node): return itertools.chain(values, other_values) -def _string_distance(seq1, seq2): +def _string_distance(seq1: str, seq2: str) -> int: seq2_length = len(seq2) row = list(range(1, seq2_length + 1)) + [0] @@ -163,20 +171,25 @@ def _string_distance(seq1, seq2): return row[seq2_length - 1] -def _similar_names(owner, attrname, distance_threshold, max_choices): +def _similar_names( + owner: SuccessfulInferenceResult, + attrname: str | None, + distance_threshold: int, + max_choices: int, +) -> list[str]: """Given an owner and a name, try to find similar names. The similar names are searched given a distance metric and only a given number of choices will be returned. """ - possible_names = [] + possible_names: list[tuple[str, int]] = [] names = _node_names(owner) for name in names: if name == attrname: continue - distance = _string_distance(attrname, name) + distance = _string_distance(attrname or "", name) if distance <= distance_threshold: possible_names.append((name, distance)) @@ -191,7 +204,12 @@ def _similar_names(owner, attrname, distance_threshold, max_choices): return sorted(picked) -def _missing_member_hint(owner, attrname, distance_threshold, max_choices): +def _missing_member_hint( + owner: SuccessfulInferenceResult, + attrname: str | None, + distance_threshold: int, + max_choices: int, +) -> str: names = _similar_names(owner, attrname, distance_threshold, max_choices) if not names: # No similar name. @@ -356,6 +374,12 @@ MSGS: dict[str, MessageDefinitionTuple] = { "(i.e. doesn't define __hash__ method).", {"old_names": [("E1140", "unhashable-dict-key")]}, ), + "E1144": ( + "Slice step cannot be 0", + "invalid-slice-step", + "Used when a slice step is 0 and the object doesn't implement " + "a custom __getitem__ method.", + ), "W1113": ( "Keyword argument before variable positional arguments list " "in the definition of %s function", @@ -397,13 +421,13 @@ SEQUENCE_TYPES = { def _emit_no_member( - node, - owner, - owner_name, + node: nodes.Attribute | nodes.AssignAttr | nodes.DelAttr, + owner: InferenceResult, + owner_name: str | None, mixin_class_rgx: Pattern[str], - ignored_mixins=True, - ignored_none=True, -): + ignored_mixins: bool = True, + ignored_none: bool = True, +) -> bool: """Try to see if no-member should be emitted for the given owner. The following cases are ignored: @@ -640,7 +664,11 @@ def _determine_callable( raise ValueError -def _has_parent_of_type(node, node_type, statement): +def _has_parent_of_type( + node: nodes.Call, + node_type: nodes.Keyword | nodes.Starred, + statement: nodes.Statement, +) -> bool: """Check if the given node has a parent of the given type.""" parent = node.parent while not isinstance(parent, node_type) and statement.parent_of(parent): @@ -648,9 +676,9 @@ def _has_parent_of_type(node, node_type, statement): return isinstance(parent, node_type) -def _no_context_variadic_keywords(node, scope): +def _no_context_variadic_keywords(node: nodes.Call, scope: nodes.Lambda) -> bool: statement = node.statement(future=True) - variadics = () + variadics = [] if isinstance(scope, nodes.Lambda) and not isinstance(scope, nodes.FunctionDef): variadics = list(node.keywords or []) + node.kwargs @@ -663,12 +691,17 @@ def _no_context_variadic_keywords(node, scope): return _no_context_variadic(node, scope.args.kwarg, nodes.Keyword, variadics) -def _no_context_variadic_positional(node, scope): +def _no_context_variadic_positional(node: nodes.Call, scope: nodes.Lambda) -> bool: variadics = node.starargs + node.kwargs return _no_context_variadic(node, scope.args.vararg, nodes.Starred, variadics) -def _no_context_variadic(node, variadic_name, variadic_type, variadics): +def _no_context_variadic( + node: nodes.Call, + variadic_name: str | None, + variadic_type: nodes.Keyword | nodes.Starred, + variadics: list[nodes.Keyword | nodes.Starred], +) -> bool: """Verify if the given call node has variadic nodes without context. This is a workaround for handling cases of nested call functions @@ -714,7 +747,7 @@ def _no_context_variadic(node, variadic_name, variadic_type, variadics): return False -def _is_invalid_metaclass(metaclass): +def _is_invalid_metaclass(metaclass: nodes.ClassDef) -> bool: try: mro = metaclass.mro() except NotImplementedError: @@ -726,7 +759,9 @@ def _is_invalid_metaclass(metaclass): return False -def _infer_from_metaclass_constructor(cls, func: nodes.FunctionDef): +def _infer_from_metaclass_constructor( + cls: nodes.ClassDef, func: nodes.FunctionDef +) -> InferenceResult | None: """Try to infer what the given *func* constructor is building. :param astroid.FunctionDef func: @@ -763,14 +798,15 @@ def _infer_from_metaclass_constructor(cls, func: nodes.FunctionDef): return inferred or None -def _is_c_extension(module_node): +def _is_c_extension(module_node: InferenceResult) -> bool: return ( - not astroid.modutils.is_standard_module(module_node.name) + isinstance(module_node, nodes.Module) + and not astroid.modutils.is_standard_module(module_node.name) and not module_node.fully_defined() ) -def _is_invalid_isinstance_type(arg): +def _is_invalid_isinstance_type(arg: nodes.NodeNG) -> bool: # Return True if we are sure that arg is not a type inferred = utils.safe_infer(arg) if not inferred: @@ -945,11 +981,11 @@ accessed. Python regular expressions are accepted.", self._mixin_class_rgx = self.linter.config.mixin_class_rgx @cached_property - def _suggestion_mode(self): - return self.linter.config.suggestion_mode + def _suggestion_mode(self) -> bool: + return self.linter.config.suggestion_mode # type: ignore[no-any-return] @cached_property - def _compiled_generated_members(self) -> tuple[Pattern, ...]: + def _compiled_generated_members(self) -> tuple[Pattern[str], ...]: # do this lazily since config not fully initialized in __init__ # generated_members may contain regular expressions # (surrounded by quote `"` and followed by a comma `,`) @@ -973,15 +1009,15 @@ accessed. Python regular expressions are accepted.", @only_required_for_messages("invalid-metaclass") def visit_classdef(self, node: nodes.ClassDef) -> None: - def _metaclass_name(metaclass): + def _metaclass_name(metaclass: InferenceResult) -> str | None: # pylint: disable=unidiomatic-typecheck if isinstance(metaclass, (nodes.ClassDef, nodes.FunctionDef)): - return metaclass.name + return metaclass.name # type: ignore[no-any-return] if type(metaclass) is bases.Instance: # Really do mean type, not isinstance, since subclasses of bases.Instance # like Const or Dict should use metaclass.as_string below. return str(metaclass) - return metaclass.as_string() + return metaclass.as_string() # type: ignore[no-any-return] metaclass = node.declared_metaclass() if not metaclass: @@ -1040,9 +1076,9 @@ accessed. Python regular expressions are accepted.", return # list of (node, nodename) which are missing the attribute - missingattr = set() + missingattr: set[tuple[SuccessfulInferenceResult, str | None]] = set() - non_opaque_inference_results = [ + non_opaque_inference_results: list[SuccessfulInferenceResult] = [ owner for owner in inferred if owner is not astroid.Uninferable and not isinstance(owner, nodes.Unknown) @@ -1105,6 +1141,9 @@ accessed. Python regular expressions are accepted.", try: if isinstance( attr_node.statement(future=True), nodes.AugAssign + ) or ( + isinstance(attr_parent, nodes.Assign) + and utils.is_augmented_assign(attr_parent)[0] ): continue except astroid.exceptions.StatementMissing: @@ -1139,7 +1178,11 @@ accessed. Python regular expressions are accepted.", confidence=INFERENCE, ) - def _get_nomember_msgid_hint(self, node, owner): + def _get_nomember_msgid_hint( + self, + node: nodes.Attribute | nodes.AssignAttr | nodes.DelAttr, + owner: SuccessfulInferenceResult, + ) -> tuple[Literal["c-extension-no-member", "no-member"], str]: suggestions_are_possible = self._suggestion_mode and isinstance( owner, nodes.Module ) @@ -1157,7 +1200,7 @@ accessed. Python regular expressions are accepted.", ) else: hint = "" - return msg, hint + return msg, hint # type: ignore[return-value] @only_required_for_messages( "assignment-from-no-return", @@ -1240,7 +1283,7 @@ accessed. Python regular expressions are accepted.", and isinstance(utils.safe_infer(node.func.expr), nodes.List) ) - def _check_dundername_is_string(self, node) -> None: + def _check_dundername_is_string(self, node: nodes.Assign) -> None: """Check a string is assigned to self.__name__.""" # Check the left-hand side of the assignment is <something>.__name__ @@ -1261,7 +1304,7 @@ accessed. Python regular expressions are accepted.", # Add the message self.add_message("non-str-assignment-to-dunder-name", node=node) - def _check_uninferable_call(self, node): + def _check_uninferable_call(self, node: nodes.Call) -> None: """Check that the given uninferable Call node does not call an actual function. """ @@ -1313,7 +1356,13 @@ accessed. Python regular expressions are accepted.", self.add_message("not-callable", node=node, args=node.func.as_string()) - def _check_argument_order(self, node, call_site, called, called_param_names): + def _check_argument_order( + self, + node: nodes.Call, + call_site: arguments.CallSite, + called: CallableObjects, + called_param_names: list[str | None], + ) -> None: """Match the supplied argument names against the function parameters. Warn if some argument names are not in the same order as they are in @@ -1353,7 +1402,7 @@ accessed. Python regular expressions are accepted.", if calling_parg_names != called_param_names[: len(calling_parg_names)]: self.add_message("arguments-out-of-order", node=node, args=()) - def _check_isinstance_args(self, node): + def _check_isinstance_args(self, node: nodes.Call) -> None: if len(node.args) != 2: # isinstance called with wrong number of args return @@ -1449,7 +1498,7 @@ accessed. Python regular expressions are accepted.", # Analyze the list of formal parameters. args = list(itertools.chain(called.args.posonlyargs or (), called.args.args)) num_mandatory_parameters = len(args) - len(called.args.defaults) - parameters: list[list[Any]] = [] + parameters: list[tuple[tuple[str | None, nodes.NodeNG | None], bool]] = [] parameter_name_to_index = {} for i, arg in enumerate(args): if isinstance(arg, nodes.Tuple): @@ -1466,7 +1515,7 @@ accessed. Python regular expressions are accepted.", defval = called.args.defaults[i - num_mandatory_parameters] else: defval = None - parameters.append([(name, defval), False]) + parameters.append(((name, defval), False)) kwparams = {} for i, arg in enumerate(called.args.kwonlyargs): @@ -1484,7 +1533,7 @@ accessed. Python regular expressions are accepted.", # 1. Match the positional arguments. for i in range(num_positional_args): if i < len(parameters): - parameters[i][1] = True + parameters[i] = (parameters[i][0], True) elif called.args.vararg is not None: # The remaining positional arguments get assigned to the *args # parameter. @@ -1515,7 +1564,7 @@ accessed. Python regular expressions are accepted.", args=(keyword, callable_name), ) else: - parameters[i][1] = True + parameters[i] = (parameters[i][0], True) elif keyword in kwparams: if kwparams[keyword][1]: # Duplicate definition of function parameter. @@ -1541,11 +1590,14 @@ accessed. Python regular expressions are accepted.", # 3. Match the **kwargs, if any. if node.kwargs: - for i, [(name, defval), assigned] in enumerate(parameters): + # TODO: It's possible to remove this disable by using dummy-variables-rgx + # see https://github.com/PyCQA/pylint/pull/7697#discussion_r1010832518 + # pylint: disable-next=unused-variable + for i, [(name, _defval), _assigned] in enumerate(parameters): # Assume that *kwargs provides values for all remaining # unassigned named parameters. if name is not None: - parameters[i][1] = True + parameters[i] = (parameters[i][0], True) else: # **kwargs can't assign to tuples. pass @@ -1612,7 +1664,7 @@ accessed. Python regular expressions are accepted.", return True - def _check_invalid_sequence_index(self, subscript: nodes.Subscript): + def _check_invalid_sequence_index(self, subscript: nodes.Subscript) -> None: # Look for index operations where the parent is a sequence type. # If the types can be determined, only allow indices to be int, # slice or instances with __index__. @@ -1654,13 +1706,7 @@ accessed. Python regular expressions are accepted.", ): return None - # For ExtSlice objects coming from visit_extslice, no further - # inference is necessary, since if we got this far the ExtSlice - # is an error. - if isinstance(subscript.value, nodes.ExtSlice): - index_type = subscript.value - else: - index_type = safe_infer(subscript.slice) + index_type = safe_infer(subscript.slice) if index_type is None or index_type is astroid.Uninferable: return None # Constants must be of type int @@ -1717,14 +1763,6 @@ accessed. Python regular expressions are accepted.", self.add_message("not-callable", node=node, args=node.func.as_string()) - @only_required_for_messages("invalid-sequence-index") - def visit_extslice(self, node: nodes.ExtSlice) -> None: - if not node.parent or not hasattr(node.parent, "value"): - return None - # Check extended slice objects as if they were used as a sequence - # index to check if the object being sliced can support them - return self._check_invalid_sequence_index(node.parent) - def _check_invalid_slice_index(self, node: nodes.Slice) -> None: # Check the type of each part of the slice invalid_slices_nodes: list[nodes.NodeNG] = [] @@ -1753,14 +1791,16 @@ accessed. Python regular expressions are accepted.", pass invalid_slices_nodes.append(index) - if not invalid_slices_nodes: + invalid_slice_step = ( + node.step and isinstance(node.step, nodes.Const) and node.step.value == 0 + ) + + if not (invalid_slices_nodes or invalid_slice_step): return # Anything else is an error, unless the object that is indexed # is a custom object, which knows how to handle this kind of slices parent = node.parent - if isinstance(parent, nodes.ExtSlice): - parent = parent.parent if isinstance(parent, nodes.Subscript): inferred = safe_infer(parent.value) if inferred is None or inferred is astroid.Uninferable: @@ -1773,11 +1813,19 @@ accessed. Python regular expressions are accepted.", astroid.objects.FrozenSet, nodes.Set, ) - if not isinstance(inferred, known_objects): + if not ( + isinstance(inferred, known_objects) + or isinstance(inferred, nodes.Const) + and inferred.pytype() in {"builtins.str", "builtins.bytes"} + or isinstance(inferred, astroid.bases.Instance) + and inferred.pytype() == "builtins.range" + ): # Might be an instance that knows how to handle this slice object return for snode in invalid_slices_nodes: self.add_message("invalid-slice-index", node=snode) + if invalid_slice_step: + self.add_message("invalid-slice-step", node=node.step, confidence=HIGH) @only_required_for_messages("not-context-manager") def visit_with(self, node: nodes.With) -> None: @@ -1903,7 +1951,7 @@ accessed. Python regular expressions are accepted.", if not allowed_nested_syntax: self._check_unsupported_alternative_union_syntax(node) - def _includes_version_compatible_overload(self, attrs: list): + def _includes_version_compatible_overload(self, attrs: list[nodes.NodeNG]) -> bool: """Check if a set of overloads of an operator includes one that can be relied upon for our configured Python version. @@ -1973,7 +2021,7 @@ accessed. Python regular expressions are accepted.", """Detect TypeErrors for augmented binary arithmetic operands.""" self._check_binop_errors(node) - def _check_binop_errors(self, node): + def _check_binop_errors(self, node: nodes.BinOp | nodes.AugAssign) -> None: for error in node.type_errors(): # Let the error customize its output. if any( @@ -1983,7 +2031,7 @@ accessed. Python regular expressions are accepted.", continue self.add_message("unsupported-binary-operation", args=str(error), node=node) - def _check_membership_test(self, node): + def _check_membership_test(self, node: nodes.NodeNG) -> None: if is_inside_abstract_class(node): return if is_comprehension(node): @@ -2034,6 +2082,7 @@ accessed. Python regular expressions are accepted.", "unhashable-member", "invalid-sequence-index", "invalid-slice-index", + "invalid-slice-step", ) def visit_subscript(self, node: nodes.Subscript) -> None: self._check_invalid_sequence_index(node) @@ -2161,7 +2210,7 @@ class IterableChecker(BaseChecker): } @staticmethod - def _is_asyncio_coroutine(node): + def _is_asyncio_coroutine(node: nodes.NodeNG) -> bool: if not isinstance(node, nodes.Call): return False @@ -2179,7 +2228,7 @@ class IterableChecker(BaseChecker): return True return False - def _check_iterable(self, node, check_async=False): + def _check_iterable(self, node: nodes.NodeNG, check_async: bool = False) -> None: if is_inside_abstract_class(node): return inferred = safe_infer(node) @@ -2188,7 +2237,7 @@ class IterableChecker(BaseChecker): if not is_iterable(inferred, check_async=check_async): self.add_message("not-an-iterable", args=node.as_string(), node=node) - def _check_mapping(self, node): + def _check_mapping(self, node: nodes.NodeNG) -> None: if is_inside_abstract_class(node): return if isinstance(node, nodes.DictComp): diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 0ccf1d883..a2a0c1b37 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -231,6 +231,12 @@ SUBSCRIPTABLE_CLASSES_PEP585 = frozenset( ) ) +SINGLETON_VALUES = {True, False, None} + +TERMINATING_FUNCS_QNAMES = frozenset( + {"_sitebuiltins.Quitter", "sys.exit", "posix._exit", "nt._exit"} +) + class NoSuchArgumentError(Exception): pass @@ -246,6 +252,7 @@ def is_inside_lambda(node: nodes.NodeNG) -> bool: "utils.is_inside_lambda will be removed in favour of calling " "utils.get_node_first_ancestor_of_type(x, nodes.Lambda) in pylint 3.0", DeprecationWarning, + stacklevel=2, ) return any(isinstance(parent, nodes.Lambda) for parent in node.node_ancestors()) @@ -279,12 +286,12 @@ SPECIAL_BUILTINS = ("__builtins__",) # '__path__', '__file__') def is_builtin_object(node: nodes.NodeNG) -> bool: """Returns True if the given node is an object from the __builtin__ module.""" - return node and node.root().name == "builtins" + return node and node.root().name == "builtins" # type: ignore[no-any-return] def is_builtin(name: str) -> bool: """Return true if <name> could be considered as a builtin defined by python.""" - return name in builtins or name in SPECIAL_BUILTINS # type: ignore[attr-defined] + return name in builtins or name in SPECIAL_BUILTINS # type: ignore[operator] def is_defined_in_scope( @@ -359,6 +366,14 @@ def is_defined_before(var_node: nodes.Name) -> bool: continue defnode_scope = defnode.scope() if isinstance(defnode_scope, COMP_NODE_TYPES + (nodes.Lambda,)): + # Avoid the case where var_node_scope is a nested function + # FunctionDef is a Lambda until https://github.com/PyCQA/astroid/issues/291 + if isinstance(defnode_scope, nodes.FunctionDef): + var_node_scope = var_node.scope() + if var_node_scope is not defnode_scope and isinstance( + var_node_scope, nodes.FunctionDef + ): + return False return True if defnode.lineno < var_node.lineno: return True @@ -480,7 +495,7 @@ def only_required_for_messages( def store_messages( func: AstCallbackMethod[_CheckerT, _NodeT] ) -> AstCallbackMethod[_CheckerT, _NodeT]: - setattr(func, "checks_msgs", messages) + func.checks_msgs = messages # type: ignore[attr-defined] return func return store_messages @@ -499,6 +514,7 @@ def check_messages( "utils.check_messages will be removed in favour of calling " "utils.only_required_for_messages in pylint 3.0", DeprecationWarning, + stacklevel=2, ) return only_required_for_messages(*messages) @@ -598,7 +614,7 @@ def split_format_field_names( format_string: str, ) -> tuple[str, Iterable[tuple[bool, str]]]: try: - return _string.formatter_field_name_split(format_string) + return _string.formatter_field_name_split(format_string) # type: ignore[no-any-return] except ValueError as e: raise IncompleteFormatString() from e @@ -782,7 +798,7 @@ def error_of_type( expected_errors = {stringify_error(error) for error in error_type} if not handler.type: return False - return handler.catch(expected_errors) + return handler.catch(expected_errors) # type: ignore[no-any-return] def decorated_with_property(node: nodes.FunctionDef) -> bool: @@ -798,7 +814,7 @@ def decorated_with_property(node: nodes.FunctionDef) -> bool: return False -def _is_property_kind(node, *kinds: str) -> bool: +def _is_property_kind(node: nodes.NodeNG, *kinds: str) -> bool: if not isinstance(node, (astroid.UnboundMethod, nodes.FunctionDef)): return False if node.decorators: @@ -808,17 +824,17 @@ def _is_property_kind(node, *kinds: str) -> bool: return False -def is_property_setter(node) -> bool: +def is_property_setter(node: nodes.NodeNG) -> bool: """Check if the given node is a property setter.""" return _is_property_kind(node, "setter") -def is_property_deleter(node) -> bool: +def is_property_deleter(node: nodes.NodeNG) -> bool: """Check if the given node is a property deleter.""" return _is_property_kind(node, "deleter") -def is_property_setter_or_deleter(node) -> bool: +def is_property_setter_or_deleter(node: nodes.NodeNG) -> bool: """Check if the given node is either a property setter or a deleter.""" return _is_property_kind(node, "setter", "deleter") @@ -1404,7 +1420,7 @@ def has_known_bases( ) -> bool: """Return true if all base classes of a class could be inferred.""" try: - return klass._all_bases_known + return klass._all_bases_known # type: ignore[no-any-return] except AttributeError: pass for base in klass.bases: @@ -1462,11 +1478,15 @@ def is_registered_in_singledispatch_function(node: nodes.FunctionDef) -> bool: decorators = node.decorators.nodes if node.decorators else [] for decorator in decorators: - # func.register are function calls - if not isinstance(decorator, nodes.Call): + # func.register are function calls or register attributes + # when the function is annotated with types + if isinstance(decorator, nodes.Call): + func = decorator.func + elif isinstance(decorator, nodes.Attribute): + func = decorator + else: continue - func = decorator.func if not isinstance(func, nodes.Attribute) or func.attrname != "register": continue @@ -1481,6 +1501,43 @@ def is_registered_in_singledispatch_function(node: nodes.FunctionDef) -> bool: return False +def find_inferred_fn_from_register(node: nodes.NodeNG) -> nodes.FunctionDef | None: + # func.register are function calls or register attributes + # when the function is annotated with types + if isinstance(node, nodes.Call): + func = node.func + elif isinstance(node, nodes.Attribute): + func = node + else: + return None + + if not isinstance(func, nodes.Attribute) or func.attrname != "register": + return None + + func_def = safe_infer(func.expr) + if not isinstance(func_def, nodes.FunctionDef): + return None + + return func_def + + +def is_registered_in_singledispatchmethod_function(node: nodes.FunctionDef) -> bool: + """Check if the given function node is a singledispatchmethod function.""" + + singledispatchmethod_qnames = ( + "functools.singledispatchmethod", + "singledispatch.singledispatchmethod", + ) + + decorators = node.decorators.nodes if node.decorators else [] + for decorator in decorators: + func_def = find_inferred_fn_from_register(decorator) + if func_def: + return decorated_with(func_def, singledispatchmethod_qnames) + + return False + + def get_node_last_lineno(node: nodes.NodeNG) -> int: """Get the last lineno of the given node. @@ -1502,7 +1559,7 @@ def get_node_last_lineno(node: nodes.NodeNG) -> int: if getattr(node, "body", False): return get_node_last_lineno(node.body[-1]) # Not a compound statement - return node.lineno + return node.lineno # type: ignore[no-any-return] def is_postponed_evaluation_enabled(node: nodes.NodeNG) -> bool: @@ -1523,6 +1580,7 @@ def is_class_subscriptable_pep585_with_postponed_evaluation_enabled( "Use 'is_postponed_evaluation_enabled(node) and " "is_node_in_type_annotation_context(node)' instead.", DeprecationWarning, + stacklevel=2, ) return ( is_postponed_evaluation_enabled(node) @@ -1695,14 +1753,14 @@ def get_iterating_dictionary_name(node: nodes.For | nodes.Comprehension) -> str inferred = safe_infer(node.iter.func) if not isinstance(inferred, astroid.BoundMethod): return None - return node.iter.as_string().rpartition(".keys")[0] + return node.iter.as_string().rpartition(".keys")[0] # type: ignore[no-any-return] # Is it a dictionary? if isinstance(node.iter, (nodes.Name, nodes.Attribute)): inferred = safe_infer(node.iter) if not isinstance(inferred, nodes.Dict): return None - return node.iter.as_string() + return node.iter.as_string() # type: ignore[no-any-return] return None @@ -1738,7 +1796,7 @@ def get_import_name(importnode: ImportNode, modname: str | None) -> str | None: root = importnode.root() if isinstance(root, nodes.Module): try: - return root.relative_to_absolute_name(modname, level=importnode.level) + return root.relative_to_absolute_name(modname, level=importnode.level) # type: ignore[no-any-return] except TooManyLevelsError: return modname return modname @@ -1841,11 +1899,20 @@ def is_empty_str_literal(node: nodes.NodeNG | None) -> bool: def returns_bool(node: nodes.NodeNG) -> bool: - """Returns true if a node is a return that returns a constant boolean.""" + """Returns true if a node is a nodes.Return that returns a constant boolean.""" return ( isinstance(node, nodes.Return) and isinstance(node.value, nodes.Const) - and node.value.value in {True, False} + and isinstance(node.value.value, bool) + ) + + +def assigned_bool(node: nodes.NodeNG) -> bool: + """Returns true if a node is a nodes.Assign that returns a constant boolean.""" + return ( + isinstance(node, nodes.Assign) + and isinstance(node.value, nodes.Const) + and isinstance(node.value.value, bool) ) @@ -1855,7 +1922,7 @@ def get_node_first_ancestor_of_type( """Return the first parent node that is any of the provided types (or None).""" for ancestor in node.node_ancestors(): if isinstance(ancestor, ancestor_type): - return ancestor + return ancestor # type: ignore[no-any-return] return None @@ -1906,24 +1973,27 @@ def in_type_checking_block(node: nodes.NodeNG) -> bool: return False -def is_typing_literal(node: nodes.NodeNG) -> bool: - """Check if a node refers to typing.Literal.""" +def is_typing_member(node: nodes.NodeNG, names_to_check: tuple[str, ...]) -> bool: + """Check if `node` is a member of the `typing` module and has one of the names from + `names_to_check`. + """ if isinstance(node, nodes.Name): try: import_from = node.lookup(node.name)[1][0] except IndexError: return False + if isinstance(import_from, nodes.ImportFrom): return ( import_from.modname == "typing" - and import_from.real_name(node.name) == "Literal" + and import_from.real_name(node.name) in names_to_check ) elif isinstance(node, nodes.Attribute): inferred_module = safe_infer(node.expr) return ( isinstance(inferred_module, nodes.Module) and inferred_module.name == "typing" - and node.attrname == "Literal" + and node.attrname in names_to_check ) return False @@ -1969,6 +2039,71 @@ def is_hashable(node: nodes.NodeNG) -> bool: return True +def get_full_name_of_attribute(node: nodes.Attribute | nodes.AssignAttr) -> str: + """Return the full name of an attribute and the classes it belongs to. + + For example: "Class1.Class2.attr" + """ + parent = node.parent + ret = node.attrname or "" + while isinstance(parent, (nodes.Attribute, nodes.Name)): + if isinstance(parent, nodes.Attribute): + ret = f"{parent.attrname}.{ret}" + else: + ret = f"{parent.name}.{ret}" + parent = parent.parent + return ret + + +def _is_target_name_in_binop_side( + target: nodes.AssignName | nodes.AssignAttr, side: nodes.NodeNG | None +) -> bool: + """Determine whether the target name-like node is referenced in the side node.""" + if isinstance(side, nodes.Name): + if isinstance(target, nodes.AssignName): + return target.name == side.name # type: ignore[no-any-return] + return False + if isinstance(side, nodes.Attribute) and isinstance(target, nodes.AssignAttr): + return get_full_name_of_attribute(target) == get_full_name_of_attribute(side) + return False + + +def is_augmented_assign(node: nodes.Assign) -> tuple[bool, str]: + """Determine if the node is assigning itself (with modifications) to itself. + + For example: x = 1 + x + """ + if not isinstance(node.value, nodes.BinOp): + return False, "" + + binop = node.value + target = node.targets[0] + + if not isinstance(target, (nodes.AssignName, nodes.AssignAttr)): + return False, "" + + # We don't want to catch x = "1" + x or x = "%s" % x + if isinstance(binop.left, nodes.Const) and isinstance( + binop.left.value, (str, bytes) + ): + return False, "" + + # This could probably be improved but for now we disregard all assignments from calls + if isinstance(binop.left, nodes.Call) or isinstance(binop.right, nodes.Call): + return False, "" + + if _is_target_name_in_binop_side(target, binop.left): + return True, binop.op + if _is_target_name_in_binop_side(target, binop.right): + inferred_left = safe_infer(binop.left) + if isinstance(inferred_left, nodes.Const) and isinstance( + inferred_left.value, int + ): + return True, binop.op + return False, "" + return False, "" + + def is_module_ignored( module: nodes.Module, ignored_modules: Iterable[str], @@ -2001,3 +2136,31 @@ def is_module_ignored( return True return False + + +def is_singleton_const(node: nodes.NodeNG) -> bool: + return isinstance(node, nodes.Const) and any( + node.value is value for value in SINGLETON_VALUES + ) + + +def is_terminating_func(node: nodes.Call) -> bool: + """Detect call to exit(), quit(), os._exit(), or sys.exit().""" + if ( + not isinstance(node.func, nodes.Attribute) + and not (isinstance(node.func, nodes.Name)) + or isinstance(node.parent, nodes.Lambda) + ): + return False + + try: + for inferred in node.func.infer(): + if ( + hasattr(inferred, "qname") + and inferred.qname() in TERMINATING_FUNCS_QNAMES + ): + return True + except (StopIteration, astroid.InferenceError): + pass + + return False diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index f1c81fc33..818d625d1 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple import astroid from astroid import bases, extract_node, nodes +from astroid.nodes import _base_nodes from astroid.typing import InferenceResult from pylint.checkers import BaseChecker, utils @@ -112,6 +113,13 @@ TYPING_NAMES = frozenset( } ) +DICT_TYPES = ( + astroid.objects.DictValues, + astroid.objects.DictKeys, + astroid.objects.DictItems, + astroid.nodes.node_classes.Dict, +) + class VariableVisitConsumerAction(Enum): """Reported by _check_consumer() and its sub-methods to determine the @@ -125,7 +133,7 @@ class VariableVisitConsumerAction(Enum): RETURN = 1 -def _is_from_future_import(stmt, name): +def _is_from_future_import(stmt: nodes.ImportFrom, name: str) -> bool | None: """Check if the name is a future import from another module.""" try: module = stmt.do_import_module(stmt.modname) @@ -139,7 +147,9 @@ def _is_from_future_import(stmt, name): @lru_cache(maxsize=1000) -def overridden_method(klass, name): +def overridden_method( + klass: nodes.LocalsDictNodeNG, name: str | None +) -> nodes.FunctionDef | None: """Get overridden method if any.""" try: parent = next(klass.local_attr_ancestors(name)) @@ -156,23 +166,32 @@ def overridden_method(klass, name): return None -def _get_unpacking_extra_info(node, inferred): +def _get_unpacking_extra_info(node: nodes.Assign, inferred: InferenceResult) -> str: """Return extra information to add to the message for unpacking-non-sequence - and unbalanced-tuple-unpacking errors. + and unbalanced-tuple/dict-unpacking errors. """ more = "" + if isinstance(inferred, DICT_TYPES): + if isinstance(node, nodes.Assign): + more = node.value.as_string() + elif isinstance(node, nodes.For): + more = node.iter.as_string() + return more + inferred_module = inferred.root().name if node.root().name == inferred_module: if node.lineno == inferred.lineno: - more = f" {inferred.as_string()}" + more = f"'{inferred.as_string()}'" elif inferred.lineno: - more = f" defined at line {inferred.lineno}" + more = f"defined at line {inferred.lineno}" elif inferred.lineno: - more = f" defined at line {inferred.lineno} of {inferred_module}" + more = f"defined at line {inferred.lineno} of {inferred_module}" return more -def _detect_global_scope(node, frame, defframe): +def _detect_global_scope( + node: nodes.Name, frame: nodes.LocalsDictNodeNG, defframe: nodes.LocalsDictNodeNG +) -> bool: """Detect that the given frames share a global scope. Two frames share a global scope when neither @@ -215,7 +234,7 @@ def _detect_global_scope(node, frame, defframe): # for annotations of function arguments, they'll have # their parent the Arguments node. if frame.parent_of(defframe): - return node.lineno < defframe.lineno + return node.lineno < defframe.lineno # type: ignore[no-any-return] if not isinstance(node.parent, (nodes.FunctionDef, nodes.Arguments)): return False elif any( @@ -249,7 +268,7 @@ def _detect_global_scope(node, frame, defframe): return False # At this point, we are certain that frame and defframe share a scope # and the definition of the first depends on the second. - return frame.lineno < defframe.lineno + return frame.lineno < defframe.lineno # type: ignore[no-any-return] def _infer_name_module( @@ -257,10 +276,12 @@ def _infer_name_module( ) -> Generator[InferenceResult, None, None]: context = astroid.context.InferenceContext() context.lookupname = name - return node.infer(context, asname=False) + return node.infer(context, asname=False) # type: ignore[no-any-return] -def _fix_dot_imports(not_consumed): +def _fix_dot_imports( + not_consumed: dict[str, list[nodes.NodeNG]] +) -> list[tuple[str, _base_nodes.ImportNode]]: """Try to fix imports with multiple dots, by returning a dictionary with the import names expanded. @@ -268,7 +289,7 @@ def _fix_dot_imports(not_consumed): like 'xml' (when we have both 'xml.etree' and 'xml.sax'), to 'xml.etree' and 'xml.sax' respectively. """ - names = {} + names: dict[str, _base_nodes.ImportNode] = {} for name, stmts in not_consumed.items(): if any( isinstance(stmt, nodes.AssignName) @@ -301,7 +322,7 @@ def _fix_dot_imports(not_consumed): second_name = import_module_name if second_name and second_name not in names: names[second_name] = stmt - return sorted(names.items(), key=lambda a: a[1].fromlineno) + return sorted(names.items(), key=lambda a: a[1].fromlineno) # type: ignore[no-any-return] def _find_frame_imports(name: str, frame: nodes.LocalsDictNodeNG) -> bool: @@ -326,7 +347,9 @@ def _find_frame_imports(name: str, frame: nodes.LocalsDictNodeNG) -> bool: return False -def _import_name_is_global(stmt, global_names) -> bool: +def _import_name_is_global( + stmt: nodes.Global | _base_nodes.ImportNode, global_names: set[str] +) -> bool: for import_name, import_alias in stmt.names: # If the import uses an alias, check only that. # Otherwise, check only the import name. @@ -345,7 +368,7 @@ def _flattened_scope_names( return set(itertools.chain.from_iterable(values)) -def _assigned_locally(name_node: nodes.Name): +def _assigned_locally(name_node: nodes.Name) -> bool: """Checks if name_node has corresponding assign statement in same scope.""" name_node_scope = name_node.scope() assign_stmts = name_node_scope.nodes_of_class(nodes.AssignName) @@ -354,7 +377,7 @@ def _assigned_locally(name_node: nodes.Name): ) -def _has_locals_call_after_node(stmt, scope): +def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) -> bool: skip_nodes = ( nodes.FunctionDef, nodes.ClassDef, @@ -471,9 +494,8 @@ MSGS: dict[str, MessageDefinitionTuple] = { "the loop.", ), "W0632": ( - "Possible unbalanced tuple unpacking with " - "sequence%s: " - "left side has %d label(s), right side has %d value(s)", + "Possible unbalanced tuple unpacking with sequence %s: left side has %d " + "label%s, right side has %d value%s", "unbalanced-tuple-unpacking", "Used when there is an unbalanced tuple unpacking in assignment", {"old_names": [("E0632", "old-unbalanced-tuple-unpacking")]}, @@ -481,8 +503,7 @@ MSGS: dict[str, MessageDefinitionTuple] = { "E0633": ( "Attempting to unpack a non-sequence%s", "unpacking-non-sequence", - "Used when something which is not " - "a sequence is used in an unpack assignment", + "Used when something which is not a sequence is used in an unpack assignment", {"old_names": [("W0633", "old-unpacking-non-sequence")]}, ), "W0640": ( @@ -511,6 +532,12 @@ MSGS: dict[str, MessageDefinitionTuple] = { "Emitted when an index used on an iterable goes beyond the length of that " "iterable.", ), + "W0644": ( + "Possible unbalanced dict unpacking with %s: " + "left side has %d label%s, right side has %d value%s", + "unbalanced-dict-unpacking", + "Used when there is an unbalanced dict unpacking in assignment or for loop", + ), } @@ -526,21 +553,22 @@ class ScopeConsumer(NamedTuple): class NamesConsumer: """A simple class to handle consumed, to consume and scope type info of node locals.""" - def __init__(self, node, scope_type): + def __init__(self, node: nodes.NodeNG, scope_type: str) -> None: self._atomic = ScopeConsumer( copy.copy(node.locals), {}, collections.defaultdict(list), scope_type ) self.node = node + self._if_nodes_deemed_uncertain: set[nodes.If] = set() - def __repr__(self): - to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()] - consumed = [f"{k}->{v}" for k, v in self._atomic.consumed.items()] - consumed_uncertain = [ + def __repr__(self) -> str: + _to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()] + _consumed = [f"{k}->{v}" for k, v in self._atomic.consumed.items()] + _consumed_uncertain = [ f"{k}->{v}" for k, v in self._atomic.consumed_uncertain.items() ] - to_consumes = ", ".join(to_consumes) - consumed = ", ".join(consumed) - consumed_uncertain = ", ".join(consumed_uncertain) + to_consumes = ", ".join(_to_consumes) + consumed = ", ".join(_consumed) + consumed_uncertain = ", ".join(_consumed_uncertain) return f""" to_consume : {to_consumes} consumed : {consumed} @@ -548,15 +576,15 @@ consumed_uncertain: {consumed_uncertain} scope_type : {self._atomic.scope_type} """ - def __iter__(self): + def __iter__(self) -> Iterator[Any]: return iter(self._atomic) @property - def to_consume(self): + def to_consume(self) -> dict[str, list[nodes.NodeNG]]: return self._atomic.to_consume @property - def consumed(self): + def consumed(self) -> dict[str, list[nodes.NodeNG]]: return self._atomic.consumed @property @@ -572,10 +600,10 @@ scope_type : {self._atomic.scope_type} return self._atomic.consumed_uncertain @property - def scope_type(self): + def scope_type(self) -> str: return self._atomic.scope_type - def mark_as_consumed(self, name, consumed_nodes): + def mark_as_consumed(self, name: str, consumed_nodes: list[nodes.NodeNG]) -> None: """Mark the given nodes as consumed for the name. If all of the nodes for the name were consumed, delete the name from @@ -628,6 +656,13 @@ scope_type : {self._atomic.scope_type} if VariablesChecker._comprehension_between_frame_and_node(node): return found_nodes + # Filter out assignments guarded by always false conditions + if found_nodes: + uncertain_nodes = self._uncertain_nodes_in_false_tests(found_nodes, node) + self.consumed_uncertain[node.name] += uncertain_nodes + uncertain_nodes_set = set(uncertain_nodes) + found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] + # Filter out assignments in ExceptHandlers that node is not contained in if found_nodes: found_nodes = [ @@ -674,6 +709,118 @@ scope_type : {self._atomic.scope_type} return found_nodes @staticmethod + def _exhaustively_define_name_raise_or_return( + name: str, node: nodes.NodeNG + ) -> bool: + """Return True if there is a collectively exhaustive set of paths under + this `if_node` that define `name`, raise, or return. + """ + # Handle try and with + if isinstance(node, (nodes.TryExcept, nodes.TryFinally)): + # Allow either a path through try/else/finally OR a path through ALL except handlers + return ( + NamesConsumer._defines_name_raises_or_returns_recursive(name, node) + or isinstance(node, nodes.TryExcept) + and all( + NamesConsumer._defines_name_raises_or_returns_recursive( + name, handler + ) + for handler in node.handlers + ) + ) + if isinstance(node, nodes.With): + return NamesConsumer._defines_name_raises_or_returns_recursive(name, node) + + if not isinstance(node, nodes.If): + return False + + # Be permissive if there is a break + if any(node.nodes_of_class(nodes.Break)): + return True + + # Is there an assignment in this node itself, e.g. in named expression? + if NamesConsumer._defines_name_raises_or_returns(name, node): + return True + + # If there is no else, then there is no collectively exhaustive set of paths + if not node.orelse: + return False + + return NamesConsumer._branch_handles_name( + name, node.body + ) and NamesConsumer._branch_handles_name(name, node.orelse) + + @staticmethod + def _branch_handles_name(name: str, body: Iterable[nodes.NodeNG]) -> bool: + return any( + NamesConsumer._defines_name_raises_or_returns(name, if_body_stmt) + or isinstance( + if_body_stmt, + (nodes.If, nodes.TryExcept, nodes.TryFinally, nodes.With), + ) + and NamesConsumer._exhaustively_define_name_raise_or_return( + name, if_body_stmt + ) + for if_body_stmt in body + ) + + def _uncertain_nodes_in_false_tests( + self, found_nodes: list[nodes.NodeNG], node: nodes.NodeNG + ) -> list[nodes.NodeNG]: + """Identify nodes of uncertain execution because they are defined under + tests that evaluate false. + + Don't identify a node if there is a collectively exhaustive set of paths + that define the name, raise, or return (e.g. every if/else branch). + """ + uncertain_nodes = [] + for other_node in found_nodes: + if in_type_checking_block(other_node): + continue + + if not isinstance(other_node, nodes.AssignName): + continue + + closest_if = utils.get_node_first_ancestor_of_type(other_node, nodes.If) + if closest_if is None: + continue + if node.frame() is not closest_if.frame(): + continue + if closest_if is not None and closest_if.parent_of(node): + continue + + # Name defined in every if/else branch + if NamesConsumer._exhaustively_define_name_raise_or_return( + other_node.name, closest_if + ): + continue + + # Higher-level if already determined to be always false + if any( + if_node.parent_of(closest_if) + for if_node in self._if_nodes_deemed_uncertain + ): + uncertain_nodes.append(other_node) + continue + + # All inferred values must test false + if isinstance(closest_if.test, nodes.NamedExpr): + test = closest_if.test.value + else: + test = closest_if.test + all_inferred = utils.infer_all(test) + if not all_inferred or not all( + isinstance(inferred, nodes.Const) and not inferred.value + for inferred in all_inferred + ): + continue + + uncertain_nodes.append(other_node) + self._if_nodes_deemed_uncertain.add(closest_if) + + return uncertain_nodes + + @staticmethod def _uncertain_nodes_in_except_blocks( found_nodes: list[nodes.NodeNG], node: nodes.NodeNG, @@ -704,7 +851,14 @@ scope_type : {self._atomic.scope_type} isinstance(else_statement, nodes.Return) for else_statement in closest_try_except.orelse ) - if try_block_returns or else_block_returns: + else_block_exits = any( + isinstance(else_statement, nodes.Expr) + and isinstance(else_statement.value, nodes.Call) + and utils.is_terminating_func(else_statement.value) + for else_statement in closest_try_except.orelse + ) + + if try_block_returns or else_block_returns or else_block_exits: # Exception: if this node is in the final block of the other_node_statement, # it will execute before returning. Assume the except statements are uncertain. if ( @@ -739,7 +893,7 @@ scope_type : {self._atomic.scope_type} @staticmethod def _defines_name_raises_or_returns(name: str, node: nodes.NodeNG) -> bool: - if isinstance(node, (nodes.Raise, nodes.Return)): + if isinstance(node, (nodes.Raise, nodes.Assert, nodes.Return)): return True if ( isinstance(node, nodes.AnnAssign) @@ -1084,17 +1238,51 @@ class VariablesChecker(BaseChecker): ), ) - def __init__(self, linter=None): + def __init__(self, linter: PyLinter) -> None: super().__init__(linter) self._to_consume: list[NamesConsumer] = [] - self._checking_mod_attr = None - self._type_annotation_names = [] + self._type_annotation_names: list[str] = [] self._except_handler_names_queue: list[ tuple[nodes.ExceptHandler, nodes.AssignName] ] = [] """This is a queue, last in first out.""" self._postponed_evaluation_enabled = False + @utils.only_required_for_messages( + "unbalanced-dict-unpacking", + ) + def visit_for(self, node: nodes.For) -> None: + if not isinstance(node.target, nodes.Tuple): + return + + targets = node.target.elts + + inferred = utils.safe_infer(node.iter) + if not isinstance(inferred, DICT_TYPES): + return + + values = self._nodes_to_unpack(inferred) + if not values: + # no dict items returned + return + + if isinstance(inferred, astroid.objects.DictItems): + # dict.items() is a bit special because values will be a tuple + # So as long as there are always 2 targets and values each are + # a tuple with two items, this will unpack correctly. + # Example: `for key, val in {1: 2, 3: 4}.items()` + if len(targets) == 2 and all(len(x.elts) == 2 for x in values): + return + + # Starred nodes indicate ambiguous unpacking + # if `dict.items()` is used so we won't flag them. + if any(isinstance(target, nodes.Starred) for target in targets): + return + + if len(targets) != len(values): + details = _get_unpacking_extra_info(node, inferred) + self._report_unbalanced_unpacking(node, inferred, targets, values, details) + def leave_for(self, node: nodes.For) -> None: self._store_type_annotation_names(node) @@ -1139,6 +1327,7 @@ class VariablesChecker(BaseChecker): return self._check_imports(not_consumed) + self._type_annotation_names = [] def visit_classdef(self, node: nodes.ClassDef) -> None: """Visit class: update consumption analysis variable.""" @@ -1489,7 +1678,7 @@ class VariablesChecker(BaseChecker): stmt: nodes.NodeNG, frame: nodes.LocalsDictNodeNG, current_consumer: NamesConsumer, - base_scope_type: Any, + base_scope_type: str, ) -> tuple[VariableVisitConsumerAction, list[nodes.NodeNG] | None]: """Checks a consumer for conditions that should trigger messages.""" # If the name has already been consumed, only check it's not a loop @@ -1531,7 +1720,7 @@ class VariablesChecker(BaseChecker): defframe = defstmt.frame(future=True) # The class reuses itself in the class scope. - is_recursive_klass = ( + is_recursive_klass: bool = ( frame is defframe and defframe.parent_of(node) and isinstance(defframe, nodes.ClassDef) @@ -1731,7 +1920,10 @@ class VariablesChecker(BaseChecker): self._check_module_attrs(node, module, name.split(".")) @utils.only_required_for_messages( - "unbalanced-tuple-unpacking", "unpacking-non-sequence", "self-cls-assignment" + "unbalanced-tuple-unpacking", + "unpacking-non-sequence", + "self-cls-assignment", + "unbalanced_dict_unpacking", ) def visit_assign(self, node: nodes.Assign) -> None: """Check unbalanced tuple unpacking for assignments and unpacking @@ -1742,6 +1934,11 @@ class VariablesChecker(BaseChecker): return targets = node.targets[0].itered() + + # Check if we have starred nodes. + if any(isinstance(target, nodes.Starred) for target in targets): + return + try: inferred = utils.safe_infer(node.value) if inferred is not None: @@ -1771,19 +1968,21 @@ class VariablesChecker(BaseChecker): # Relying on other checker's options, which might not have been initialized yet. @cached_property - def _analyse_fallback_blocks(self): - return self.linter.config.analyse_fallback_blocks + def _analyse_fallback_blocks(self) -> bool: + return bool(self.linter.config.analyse_fallback_blocks) @cached_property - def _ignored_modules(self): - return self.linter.config.ignored_modules + def _ignored_modules(self) -> Iterable[str]: + return self.linter.config.ignored_modules # type: ignore[no-any-return] @cached_property - def _allow_global_unused_variables(self): - return self.linter.config.allow_global_unused_variables + def _allow_global_unused_variables(self) -> bool: + return bool(self.linter.config.allow_global_unused_variables) @staticmethod - def _defined_in_function_definition(node, frame): + def _defined_in_function_definition( + node: nodes.NodeNG, frame: nodes.NodeNG + ) -> bool: in_annotation_or_default_or_decorator = False if ( isinstance(frame, nodes.FunctionDef) @@ -1837,13 +2036,13 @@ class VariablesChecker(BaseChecker): @staticmethod def _is_variable_violation( node: nodes.Name, - defnode, + defnode: nodes.NodeNG, stmt: nodes.Statement, defstmt: nodes.Statement, - frame, # scope of statement of node - defframe, - base_scope_type, - is_recursive_klass, + frame: nodes.LocalsDictNodeNG, # scope of statement of node + defframe: nodes.LocalsDictNodeNG, + base_scope_type: str, + is_recursive_klass: bool, ) -> tuple[bool, bool, bool]: maybe_before_assign = True annotation_return = False @@ -2347,10 +2546,10 @@ class VariablesChecker(BaseChecker): def _check_is_unused( self, - name, - node, - stmt, - global_names, + name: str, + node: nodes.FunctionDef, + stmt: nodes.NodeNG, + global_names: set[str], nonlocal_names: Iterable[str], comprehension_target_names: Iterable[str], ) -> None: @@ -2438,21 +2637,31 @@ class VariablesChecker(BaseChecker): self.add_message(message_name, args=name, node=stmt) - def _is_name_ignored(self, stmt, name): + def _is_name_ignored( + self, stmt: nodes.NodeNG, name: str + ) -> re.Pattern[str] | re.Match[str] | None: authorized_rgx = self.linter.config.dummy_variables_rgx if ( isinstance(stmt, nodes.AssignName) and isinstance(stmt.parent, nodes.Arguments) or isinstance(stmt, nodes.Arguments) ): - regex = self.linter.config.ignored_argument_names + regex: re.Pattern[str] = self.linter.config.ignored_argument_names else: regex = authorized_rgx + # See https://stackoverflow.com/a/47007761/2519059 to + # understand what this function return. Please do NOT use + # this elsewhere, this is confusing for no benefit return regex and regex.match(name) def _check_unused_arguments( - self, name, node, stmt, argnames, nonlocal_names: Iterable[str] - ): + self, + name: str, + node: nodes.FunctionDef, + stmt: nodes.NodeNG, + argnames: list[str], + nonlocal_names: Iterable[str], + ) -> None: is_method = node.is_method() klass = node.parent.frame(future=True) if is_method and isinstance(klass, nodes.ClassDef): @@ -2548,12 +2757,12 @@ class VariablesChecker(BaseChecker): ): self.add_message("cell-var-from-loop", node=node, args=node.name) - def _should_ignore_redefined_builtin(self, stmt): + def _should_ignore_redefined_builtin(self, stmt: nodes.NodeNG) -> bool: if not isinstance(stmt, nodes.ImportFrom): return False return stmt.modname in self.linter.config.redefining_builtins_modules - def _allowed_redefined_builtin(self, name): + def _allowed_redefined_builtin(self, name: str) -> bool: return name in self.linter.config.allowed_redefined_builtins @staticmethod @@ -2568,7 +2777,7 @@ class VariablesChecker(BaseChecker): future=True ).parent_of(closest_comprehension_scope) - def _store_type_annotation_node(self, type_annotation): + def _store_type_annotation_node(self, type_annotation: nodes.NodeNG) -> None: """Given a type annotation, store all the name nodes it refers to.""" if isinstance(type_annotation, nodes.Name): self._type_annotation_names.append(type_annotation.name) @@ -2593,7 +2802,9 @@ class VariablesChecker(BaseChecker): annotation.name for annotation in type_annotation.nodes_of_class(nodes.Name) ) - def _store_type_annotation_names(self, node): + def _store_type_annotation_names( + self, node: nodes.For | nodes.Assign | nodes.With + ) -> None: type_annotation = node.type_annotation if not type_annotation: return @@ -2629,7 +2840,9 @@ class VariablesChecker(BaseChecker): if self_cls_name in assign_names: self.add_message("self-cls-assignment", node=node, args=(self_cls_name,)) - def _check_unpacking(self, inferred, node, targets): + def _check_unpacking( + self, inferred: InferenceResult, node: nodes.Assign, targets: list[nodes.NodeNG] + ) -> None: """Check for unbalanced tuple unpacking and unpacking non sequences. """ @@ -2649,40 +2862,62 @@ class VariablesChecker(BaseChecker): # Attempt to check unpacking is properly balanced values = self._nodes_to_unpack(inferred) + details = _get_unpacking_extra_info(node, inferred) + if values is not None: if len(targets) != len(values): - # Check if we have starred nodes. - if any(isinstance(target, nodes.Starred) for target in targets): - return - self.add_message( - "unbalanced-tuple-unpacking", - node=node, - args=( - _get_unpacking_extra_info(node, inferred), - len(targets), - len(values), - ), + self._report_unbalanced_unpacking( + node, inferred, targets, values, details ) # attempt to check unpacking may be possible (i.e. RHS is iterable) elif not utils.is_iterable(inferred): - self.add_message( - "unpacking-non-sequence", - node=node, - args=(_get_unpacking_extra_info(node, inferred),), - ) + self._report_unpacking_non_sequence(node, details) @staticmethod def _nodes_to_unpack(node: nodes.NodeNG) -> list[nodes.NodeNG] | None: """Return the list of values of the `Assign` node.""" - if isinstance(node, (nodes.Tuple, nodes.List)): - return node.itered() + if isinstance(node, (nodes.Tuple, nodes.List) + DICT_TYPES): + return node.itered() # type: ignore[no-any-return] if isinstance(node, astroid.Instance) and any( ancestor.qname() == "typing.NamedTuple" for ancestor in node.ancestors() ): return [i for i in node.values() if isinstance(i, nodes.AssignName)] return None - def _check_module_attrs(self, node, module, module_names): + def _report_unbalanced_unpacking( + self, + node: nodes.NodeNG, + inferred: InferenceResult, + targets: list[nodes.NodeNG], + values: list[nodes.NodeNG], + details: str, + ) -> None: + args = ( + details, + len(targets), + "" if len(targets) == 1 else "s", + len(values), + "" if len(values) == 1 else "s", + ) + + symbol = ( + "unbalanced-dict-unpacking" + if isinstance(inferred, DICT_TYPES) + else "unbalanced-tuple-unpacking" + ) + self.add_message(symbol, node=node, args=args, confidence=INFERENCE) + + def _report_unpacking_non_sequence(self, node: nodes.NodeNG, details: str) -> None: + if details and not details.startswith(" "): + details = f" {details}" + self.add_message("unpacking-non-sequence", node=node, args=details) + + def _check_module_attrs( + self, + node: _base_nodes.ImportNode, + module: nodes.Module, + module_names: list[str], + ) -> nodes.Module | None: """Check that module_names (list of string) are accessible through the given module, if the latest access name corresponds to a module, return it. """ @@ -2714,7 +2949,9 @@ class VariablesChecker(BaseChecker): return module return None - def _check_all(self, node: nodes.Module, not_consumed): + def _check_all( + self, node: nodes.Module, not_consumed: dict[str, list[nodes.NodeNG]] + ) -> None: assigned = next(node.igetattr("__all__")) if assigned is astroid.Uninferable: return @@ -2765,14 +3002,14 @@ class VariablesChecker(BaseChecker): # when the file will be checked pass - def _check_globals(self, not_consumed): + def _check_globals(self, not_consumed: dict[str, nodes.NodeNG]) -> None: if self._allow_global_unused_variables: return for name, node_lst in not_consumed.items(): for node in node_lst: self.add_message("unused-variable", args=(name,), node=node) - def _check_imports(self, not_consumed): + def _check_imports(self, not_consumed: dict[str, list[nodes.NodeNG]]) -> None: local_names = _fix_dot_imports(not_consumed) checked = set() unused_wildcard_imports: defaultdict[ @@ -2854,9 +3091,9 @@ class VariablesChecker(BaseChecker): ) del self._to_consume - def _check_metaclasses(self, node): + def _check_metaclasses(self, node: nodes.Module | nodes.FunctionDef) -> None: """Update consumption analysis for metaclasses.""" - consumed = [] # [(scope_locals, consumed_key)] + consumed: list[tuple[dict[str, list[nodes.NodeNG]], str]] = [] for child_node in node.get_children(): if isinstance(child_node, nodes.ClassDef): @@ -2867,14 +3104,16 @@ class VariablesChecker(BaseChecker): for scope_locals, name in consumed: scope_locals.pop(name, None) - def _check_classdef_metaclasses(self, klass, parent_node): + def _check_classdef_metaclasses( + self, klass: nodes.ClassDef, parent_node: nodes.Module | nodes.FunctionDef + ) -> list[tuple[dict[str, list[nodes.NodeNG]], str]]: if not klass._metaclass: # Skip if this class doesn't use explicitly a metaclass, but inherits it from ancestors return [] - consumed = [] # [(scope_locals, consumed_key)] + consumed: list[tuple[dict[str, list[nodes.NodeNG]], str]] = [] metaclass = klass.metaclass() - name = None + name = "" if isinstance(klass._metaclass, nodes.Name): name = klass._metaclass.name elif isinstance(klass._metaclass, nodes.Attribute) and klass._metaclass.expr: @@ -2949,7 +3188,7 @@ class VariablesChecker(BaseChecker): ) def visit_const(self, node: nodes.Const) -> None: """Take note of names that appear inside string literal type annotations - unless the string is a parameter to typing.Literal. + unless the string is a parameter to `typing.Literal` or `typing.Annotation`. """ if node.pytype() != "builtins.str": return @@ -2962,7 +3201,9 @@ class VariablesChecker(BaseChecker): parent = parent.parent if isinstance(parent, nodes.Subscript): origin = next(parent.get_children(), None) - if origin is not None and utils.is_typing_literal(origin): + if origin is not None and utils.is_typing_member( + origin, ("Annotated", "Literal") + ): return try: diff --git a/pylint/config/__init__.py b/pylint/config/__init__.py index 7dc96f0cf..5f90bbae0 100644 --- a/pylint/config/__init__.py +++ b/pylint/config/__init__.py @@ -31,8 +31,10 @@ from pylint.config.find_default_config_files import ( ) from pylint.config.option import Option from pylint.config.option_manager_mixin import OptionsManagerMixIn -from pylint.config.option_parser import OptionParser -from pylint.config.options_provider_mixin import OptionsProviderMixIn +from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] +from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] + OptionsProviderMixIn, +) from pylint.constants import PYLINT_HOME, USER_HOME from pylint.utils import LinterStats @@ -46,6 +48,7 @@ def load_results(base: str) -> LinterStats | None: "'pylint.config.load_results' is deprecated, please use " "'pylint.lint.load_results' instead. This will be removed in 3.0.", DeprecationWarning, + stacklevel=2, ) return _real_load_results(base, PYLINT_HOME) @@ -59,5 +62,6 @@ def save_results(results: LinterStats, base: str) -> None: "'pylint.config.save_results' is deprecated, please use " "'pylint.lint.save_results' instead. This will be removed in 3.0.", DeprecationWarning, + stacklevel=2, ) return _real_save_results(results, base, PYLINT_HOME) diff --git a/pylint/config/_pylint_config/__init__.py b/pylint/config/_pylint_config/__init__.py index d62400a0e..622d0dfe3 100644 --- a/pylint/config/_pylint_config/__init__.py +++ b/pylint/config/_pylint_config/__init__.py @@ -7,5 +7,7 @@ Everything in this module is private. """ -from pylint.config._pylint_config.main import _handle_pylint_config_commands # noqa -from pylint.config._pylint_config.setup import _register_generate_config_options # noqa +from pylint.config._pylint_config.main import _handle_pylint_config_commands +from pylint.config._pylint_config.setup import _register_generate_config_options + +__all__ = ("_handle_pylint_config_commands", "_register_generate_config_options") diff --git a/pylint/config/_pylint_config/generate_command.py b/pylint/config/_pylint_config/generate_command.py index 325c71333..110069b90 100644 --- a/pylint/config/_pylint_config/generate_command.py +++ b/pylint/config/_pylint_config/generate_command.py @@ -22,10 +22,11 @@ def generate_interactive_config(linter: PyLinter) -> None: print("Starting interactive pylint configuration generation") format_type = utils.get_and_validate_format() + minimal = format_type == "toml" and utils.get_minimal_setting() to_file, output_file_name = utils.get_and_validate_output_file() if format_type == "toml": - config_string = linter._generate_config_file() + config_string = linter._generate_config_file(minimal=minimal) else: output_stream = StringIO() with warnings.catch_warnings(): diff --git a/pylint/config/_pylint_config/utils.py b/pylint/config/_pylint_config/utils.py index 0534340b1..cd5f8affe 100644 --- a/pylint/config/_pylint_config/utils.py +++ b/pylint/config/_pylint_config/utils.py @@ -9,6 +9,7 @@ from __future__ import annotations import sys from collections.abc import Callable from pathlib import Path +from typing import TypeVar if sys.version_info >= (3, 8): from typing import Literal @@ -21,6 +22,7 @@ else: from typing_extensions import ParamSpec _P = ParamSpec("_P") +_ReturnValueT = TypeVar("_ReturnValueT", bool, str) SUPPORTED_FORMATS = {"t", "toml", "i", "ini"} YES_NO_ANSWERS = {"y", "yes", "n", "no"} @@ -36,11 +38,11 @@ class InvalidUserInput(Exception): def should_retry_after_invalid_input( - func: Callable[_P, str | bool] -) -> Callable[_P, str | bool]: + func: Callable[_P, _ReturnValueT] +) -> Callable[_P, _ReturnValueT]: """Decorator that handles InvalidUserInput exceptions and retries.""" - def inner_function(*args: _P.args, **kwargs: _P.kwargs) -> str | bool: + def inner_function(*args: _P.args, **kwargs: _P.kwargs) -> _ReturnValueT: called_once = False while True: try: @@ -81,7 +83,7 @@ def validate_yes_no(question: str, default: Literal["yes", "no"] | None) -> bool # pylint: disable-next=bad-builtin answer = input(question).lower() - if answer == "" and default: + if not answer and default: answer = default if answer not in YES_NO_ANSWERS: @@ -90,6 +92,13 @@ def validate_yes_no(question: str, default: Literal["yes", "no"] | None) -> bool return answer.startswith("y") +def get_minimal_setting() -> bool: + """Ask the user if they want to use the minimal setting.""" + return validate_yes_no( + "Do you want a minimal configuration without comments or default values?", "no" + ) + + def get_and_validate_output_file() -> tuple[bool, Path]: """Make sure that the output file is correct.""" to_file = validate_yes_no("Do you want to write the output to a file?", "no") diff --git a/pylint/config/argument.py b/pylint/config/argument.py index 375801c55..7a03d82b2 100644 --- a/pylint/config/argument.py +++ b/pylint/config/argument.py @@ -272,7 +272,7 @@ class _StoreTrueArgument(_BaseStoreArgument): https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument """ - # pylint: disable-next=useless-super-delegation # We narrow down the type of action + # pylint: disable-next=useless-parent-delegation # We narrow down the type of action def __init__( self, *, @@ -365,9 +365,9 @@ class _ExtendArgument(_DeprecationArgument): ) -> None: # The extend action is included in the stdlib from 3.8+ if PY38_PLUS: - action_class = argparse._ExtendAction # type: ignore[attr-defined] + action_class = argparse._ExtendAction else: - action_class = _ExtendAction + action_class = _ExtendAction # type: ignore[assignment] self.dest = dest """The destination of the argument.""" diff --git a/pylint/config/arguments_manager.py b/pylint/config/arguments_manager.py index 40058071c..c771ad355 100644 --- a/pylint/config/arguments_manager.py +++ b/pylint/config/arguments_manager.py @@ -38,8 +38,10 @@ from pylint.config.exceptions import ( ) from pylint.config.help_formatter import _HelpFormatter from pylint.config.option import Option -from pylint.config.option_parser import OptionParser -from pylint.config.options_provider_mixin import OptionsProviderMixIn +from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] +from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] + OptionsProviderMixIn, +) from pylint.config.utils import _convert_option_to_argument, _parse_rich_type_value from pylint.constants import MAIN_CHECKER_NAME from pylint.typing import DirectoryNamespaceDict, OptionDict @@ -123,6 +125,7 @@ class _ArgumentsManager: warnings.warn( "options_providers has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) return self._options_providers @@ -131,6 +134,7 @@ class _ArgumentsManager: warnings.warn( "Setting options_providers has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) self._options_providers = value @@ -280,6 +284,7 @@ class _ArgumentsManager: "reset_parsers has been deprecated. Parsers should be instantiated " "once during initialization and do not need to be reset.", DeprecationWarning, + stacklevel=2, ) # configuration file parser self.cfgfile_parser = configparser.ConfigParser( @@ -287,7 +292,7 @@ class _ArgumentsManager: ) # command line parser self.cmdline_parser = OptionParser(Option, usage=usage) - self.cmdline_parser.options_manager = self # type: ignore[attr-defined] + self.cmdline_parser.options_manager = self self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) def register_options_provider( @@ -299,6 +304,7 @@ class _ArgumentsManager: "arguments providers should be registered by initializing ArgumentsProvider. " "This automatically registers the provider on the ArgumentsManager.", DeprecationWarning, + stacklevel=2, ) self.options_providers.append(provider) non_group_spec_options = [ @@ -343,6 +349,7 @@ class _ArgumentsManager: "registered by initializing ArgumentsProvider. " "This automatically registers the group on the ArgumentsManager.", DeprecationWarning, + stacklevel=2, ) # add option group to the command line parser if group_name in self._mygroups: @@ -379,6 +386,7 @@ class _ArgumentsManager: "add_optik_option has been deprecated. Options should be automatically " "added by initializing an ArgumentsProvider.", DeprecationWarning, + stacklevel=2, ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -397,6 +405,7 @@ class _ArgumentsManager: "optik_option has been deprecated. Parsing of option dictionaries should be done " "automatically by initializing an ArgumentsProvider.", DeprecationWarning, + stacklevel=2, ) optdict = copy.copy(optdict) if "action" in optdict: @@ -434,6 +443,7 @@ class _ArgumentsManager: warnings.warn( "generate_config has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) options_by_section = {} sections = [] @@ -496,6 +506,7 @@ class _ArgumentsManager: "load_provider_defaults has been deprecated. Parsing of option defaults should be done " "automatically by initializing an ArgumentsProvider.", DeprecationWarning, + stacklevel=2, ) for provider in self.options_providers: with warnings.catch_warnings(): @@ -513,6 +524,7 @@ class _ArgumentsManager: warnings.warn( "read_config_file has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) if not config_file: if verbose: @@ -584,6 +596,7 @@ class _ArgumentsManager: warnings.warn( "load_config_file has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) parser = self.cfgfile_parser for section in parser.sections(): @@ -598,6 +611,7 @@ class _ArgumentsManager: warnings.warn( "load_configuration has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -609,6 +623,7 @@ class _ArgumentsManager: warnings.warn( "DEPRECATED: load_configuration_from_config has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) for opt, opt_value in config.items(): opt = opt.replace("_", "-") @@ -625,6 +640,7 @@ class _ArgumentsManager: warnings.warn( "load_command_line_configuration has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) args = sys.argv[1:] if args is None else list(args) (options, args) = self.cmdline_parser.parse_args(args=args) @@ -635,7 +651,7 @@ class _ArgumentsManager: if value is None: continue setattr(config, attr, value) - return args + return args # type: ignore[return-value] def help(self, level: int | None = None) -> str: """Return the usage string based on the available options.""" @@ -644,15 +660,19 @@ class _ArgumentsManager: "Supplying a 'level' argument to help() has been deprecated." "You can call help() without any arguments.", DeprecationWarning, + stacklevel=2, ) return self._arg_parser.format_help() - def cb_set_provider_option(self, option, opt, value, parser): # pragma: no cover + def cb_set_provider_option( # pragma: no cover + self, option: Any, opt: Any, value: Any, parser: Any + ) -> None: """DEPRECATED: Optik callback for option setting.""" # TODO: 3.0: Remove deprecated method. warnings.warn( "cb_set_provider_option has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) if opt.startswith("--"): # remove -- on long option @@ -672,10 +692,11 @@ class _ArgumentsManager: "global_set_option has been deprecated. You can use _arguments_manager.set_option " "or linter.set_option to set options on the global configuration object.", DeprecationWarning, + stacklevel=2, ) self.set_option(opt, value) - def _generate_config_file(self) -> str: + def _generate_config_file(self, *, minimal: bool = False) -> str: """Write a configuration file according to the current configuration into stdout. """ @@ -714,19 +735,21 @@ class _ArgumentsManager: continue # Add help comment - help_msg = optdict.get("help", "") - assert isinstance(help_msg, str) - help_text = textwrap.wrap(help_msg, width=79) - for line in help_text: - group_table.add(tomlkit.comment(line)) + if not minimal: + help_msg = optdict.get("help", "") + assert isinstance(help_msg, str) + help_text = textwrap.wrap(help_msg, width=79) + for line in help_text: + group_table.add(tomlkit.comment(line)) # Get current value of option value = getattr(self.config, optname.replace("-", "_")) # Create a comment if the option has no value if not value: - group_table.add(tomlkit.comment(f"{optname} =")) - group_table.add(tomlkit.nl()) + if not minimal: + group_table.add(tomlkit.comment(f"{optname} =")) + group_table.add(tomlkit.nl()) continue # Skip deprecated options @@ -747,19 +770,24 @@ class _ArgumentsManager: if optdict.get("type") == "py_version": value = ".".join(str(i) for i in value) + # Check if it is default value if we are in minimal mode + if minimal and value == optdict.get("default"): + continue + # Add to table group_table.add(optname, value) group_table.add(tomlkit.nl()) assert group.title - pylint_tool_table.add(group.title.lower(), group_table) + if group_table: + pylint_tool_table.add(group.title.lower(), group_table) toml_string = tomlkit.dumps(toml_doc) # Make sure the string we produce is valid toml and can be parsed tomllib.loads(toml_string) - return toml_string + return str(toml_string) def set_option( self, @@ -775,12 +803,14 @@ class _ArgumentsManager: "The 'action' argument has been deprecated. You can use set_option " "without the 'action' or 'optdict' arguments.", DeprecationWarning, + stacklevel=2, ) if optdict != "default_value": warnings.warn( "The 'optdict' argument has been deprecated. You can use set_option " "without the 'action' or 'optdict' arguments.", DeprecationWarning, + stacklevel=2, ) self.config = self._arg_parser.parse_known_args( diff --git a/pylint/config/arguments_provider.py b/pylint/config/arguments_provider.py index 2ab44b161..ea229e1c3 100644 --- a/pylint/config/arguments_provider.py +++ b/pylint/config/arguments_provider.py @@ -24,6 +24,7 @@ class UnsupportedAction(Exception): warnings.warn( "UnsupportedAction has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) super().__init__(*args) @@ -55,6 +56,7 @@ class _ArgumentsProvider: "The level attribute has been deprecated. It was used to display the checker in the help or not," " and everything is displayed in the help now. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) return self._level @@ -65,6 +67,7 @@ class _ArgumentsProvider: "Setting the level attribute has been deprecated. It was used to display the checker in the help or not," " and everything is displayed in the help now. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) self._level = value @@ -75,6 +78,7 @@ class _ArgumentsProvider: "The checker-specific config attribute has been deprecated. Please use " "'linter.config' to access the global configuration object.", DeprecationWarning, + stacklevel=2, ) return self._arguments_manager.config @@ -85,6 +89,7 @@ class _ArgumentsProvider: "registered by initializing an ArgumentsProvider. " "This automatically registers the group on the ArgumentsManager.", DeprecationWarning, + stacklevel=2, ) for opt, optdict in self.options: action = optdict.get("action") @@ -105,6 +110,7 @@ class _ArgumentsProvider: "option_attrname has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) if optdict is None: with warnings.catch_warnings(): @@ -118,11 +124,17 @@ class _ArgumentsProvider: "option_value has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) return getattr(self._arguments_manager.config, opt.replace("-", "_"), None) - # pylint: disable-next=unused-argument - def set_option(self, optname, value, action=None, optdict=None): # pragma: no cover + def set_option( # pragma: no cover + self, + optname: Any, + value: Any, + action: Any = None, # pylint: disable=unused-argument + optdict: Any = None, # pylint: disable=unused-argument + ) -> None: """DEPRECATED: Method called to set an option (registered in the options list). """ @@ -131,6 +143,7 @@ class _ArgumentsProvider: "set_option has been deprecated. You can use _arguments_manager.set_option " "or linter.set_option to set options on the global configuration object.", DeprecationWarning, + stacklevel=2, ) self._arguments_manager.set_option(optname, value) @@ -143,6 +156,7 @@ class _ArgumentsProvider: "get_option_def has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) assert self.options for option in self.options: @@ -169,6 +183,7 @@ class _ArgumentsProvider: "options_by_section has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) sections: dict[str, list[tuple[str, OptionDict, Any]]] = {} for optname, optdict in self.options: @@ -190,6 +205,7 @@ class _ArgumentsProvider: "options_and_values has been deprecated. It will be removed " "in a future release.", DeprecationWarning, + stacklevel=2, ) if options is None: options = self.options diff --git a/pylint/config/callback_actions.py b/pylint/config/callback_actions.py index 242629a88..a4c633464 100644 --- a/pylint/config/callback_actions.py +++ b/pylint/config/callback_actions.py @@ -378,7 +378,7 @@ class _XableAction(_AccessLinterObjectAction): xabling_function: Callable[[str], None], values: str | Sequence[Any] | None, option_string: str | None, - ): + ) -> None: assert isinstance(values, (tuple, list)) for msgid in utils._check_csv(values[0]): try: diff --git a/pylint/config/configuration_mixin.py b/pylint/config/configuration_mixin.py index 7854ff733..55857224a 100644 --- a/pylint/config/configuration_mixin.py +++ b/pylint/config/configuration_mixin.py @@ -2,29 +2,35 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + import warnings +from typing import Any from pylint.config.option_manager_mixin import OptionsManagerMixIn -from pylint.config.options_provider_mixin import OptionsProviderMixIn +from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] + OptionsProviderMixIn, +) -class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): +class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): # type: ignore[misc] """Basic mixin for simple configurations which don't need the manager / providers model. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: # TODO: 3.0: Remove deprecated class warnings.warn( "ConfigurationMixIn has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) if not args: kwargs.setdefault("usage", "") OptionsManagerMixIn.__init__(self, *args, **kwargs) OptionsProviderMixIn.__init__(self) if not getattr(self, "option_groups", None): - self.option_groups = [] + self.option_groups: list[tuple[str, str]] = [] for _, optdict in self.options: try: gdef = (optdict["group"].upper(), "") diff --git a/pylint/config/deprecation_actions.py b/pylint/config/deprecation_actions.py index c7c3e9b17..ceef200a7 100644 --- a/pylint/config/deprecation_actions.py +++ b/pylint/config/deprecation_actions.py @@ -52,7 +52,7 @@ class _OldNamesAction(argparse._StoreAction): namespace: argparse.Namespace, values: str | Sequence[Any] | None, option_string: str | None = None, - ): + ) -> None: assert isinstance(values, list) setattr(namespace, self.dest, values[0]) for old_name in self.old_names: @@ -97,7 +97,7 @@ class _NewNamesAction(argparse._StoreAction): namespace: argparse.Namespace, values: str | Sequence[Any] | None, option_string: str | None = None, - ): + ) -> None: assert isinstance(values, list) setattr(namespace, self.dest, values[0]) warnings.warn( diff --git a/pylint/config/find_default_config_files.py b/pylint/config/find_default_config_files.py index 314e70e11..43e682a58 100644 --- a/pylint/config/find_default_config_files.py +++ b/pylint/config/find_default_config_files.py @@ -122,6 +122,7 @@ def find_pylintrc() -> str | None: "Use find_default_config_files if you want access to pylint's configuration file " "finding logic.", DeprecationWarning, + stacklevel=2, ) for config_file in find_default_config_files(): if str(config_file).endswith("pylintrc"): diff --git a/pylint/config/option.py b/pylint/config/option.py index 5043fe765..95248d6b1 100644 --- a/pylint/config/option.py +++ b/pylint/config/option.py @@ -9,30 +9,38 @@ import optparse # pylint: disable=deprecated-module import pathlib import re import warnings +from collections.abc import Callable, Sequence from re import Pattern +from typing import Any from pylint import utils # pylint: disable=unused-argument -def _csv_validator(_, name, value): +def _csv_validator( + _: Any, name: str, value: str | list[str] | tuple[str] +) -> Sequence[str]: return utils._check_csv(value) # pylint: disable=unused-argument -def _regexp_validator(_, name, value): +def _regexp_validator( + _: Any, name: str, value: str | re.Pattern[str] +) -> re.Pattern[str]: if hasattr(value, "pattern"): - return value + return value # type: ignore[return-value] return re.compile(value) # pylint: disable=unused-argument -def _regexp_csv_validator(_, name, value): +def _regexp_csv_validator( + _: Any, name: str, value: str | list[str] +) -> list[re.Pattern[str]]: return [_regexp_validator(_, name, val) for val in _csv_validator(_, name, value)] def _regexp_paths_csv_validator( - _, name: str, value: str | list[Pattern[str]] + _: Any, name: str, value: str | list[Pattern[str]] ) -> list[Pattern[str]]: if isinstance(value, list): return value @@ -48,14 +56,14 @@ def _regexp_paths_csv_validator( return patterns -def _choice_validator(choices, name, value): +def _choice_validator(choices: list[Any], name: str, value: Any) -> Any: if value not in choices: msg = "option %s: invalid value: %r, should be in %s" raise optparse.OptionValueError(msg % (name, value, choices)) return value -def _yn_validator(opt, _, value): +def _yn_validator(opt: str, _: str, value: Any) -> bool: if isinstance(value, int): return bool(value) if isinstance(value, str): @@ -68,7 +76,7 @@ def _yn_validator(opt, _, value): raise optparse.OptionValueError(msg % (opt, value)) -def _multiple_choice_validator(choices, name, value): +def _multiple_choice_validator(choices: list[Any], name: str, value: Any) -> Any: values = utils._check_csv(value) for csv_value in values: if csv_value not in choices: @@ -77,18 +85,24 @@ def _multiple_choice_validator(choices, name, value): return values -def _non_empty_string_validator(opt, _, value): # pragma: no cover # Unused +def _non_empty_string_validator( # pragma: no cover # Unused + opt: Any, _: str, value: str +) -> str: if not value: msg = "indent string can't be empty." raise optparse.OptionValueError(msg) return utils._unquote(value) -def _multiple_choices_validating_option(opt, name, value): # pragma: no cover # Unused - return _multiple_choice_validator(opt.choices, name, value) +def _multiple_choices_validating_option( # pragma: no cover # Unused + opt: optparse.Option, name: str, value: Any +) -> Any: + return _multiple_choice_validator( + opt.choices, name, value # type: ignore[attr-defined] + ) -def _py_version_validator(_, name, value): +def _py_version_validator(_: Any, name: str, value: Any) -> tuple[int, int, int]: if not isinstance(value, tuple): try: value = tuple(int(val) for val in value.split(".")) @@ -96,10 +110,10 @@ def _py_version_validator(_, name, value): raise optparse.OptionValueError( f"Invalid format for {name}, should be version string. E.g., '3.8'" ) from None - return value + return value # type: ignore[no-any-return] -VALIDATORS = { +VALIDATORS: dict[str, Callable[[Any, str, Any], Any] | Callable[[Any], Any]] = { "string": utils._unquote, "int": int, "float": float, @@ -120,21 +134,21 @@ VALIDATORS = { } -def _call_validator(opttype, optdict, option, value): +def _call_validator(opttype: str, optdict: Any, option: str, value: Any) -> Any: if opttype not in VALIDATORS: - raise Exception(f'Unsupported type "{opttype}"') + raise TypeError(f'Unsupported type "{opttype}"') try: - return VALIDATORS[opttype](optdict, option, value) + return VALIDATORS[opttype](optdict, option, value) # type: ignore[call-arg] except TypeError: try: - return VALIDATORS[opttype](value) + return VALIDATORS[opttype](value) # type: ignore[call-arg] except Exception as e: raise optparse.OptionValueError( f"{option} value ({value!r}) should be of type {opttype}" ) from e -def _validate(value, optdict, name=""): +def _validate(value: Any, optdict: Any, name: str = "") -> Any: """Return a validated value for an option according to its type. optional argument name is only used for error message formatting @@ -171,37 +185,41 @@ class Option(optparse.Option): TYPE_CHECKER["non_empty_string"] = _non_empty_string_validator TYPE_CHECKER["py_version"] = _py_version_validator - def __init__(self, *opts, **attrs): + def __init__(self, *opts: Any, **attrs: Any) -> None: # TODO: 3.0: Remove deprecated class warnings.warn( "Option has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) super().__init__(*opts, **attrs) if hasattr(self, "hide") and self.hide: self.help = optparse.SUPPRESS_HELP - def _check_choice(self): + def _check_choice(self) -> None: if self.type in {"choice", "multiple_choice", "confidence"}: - if self.choices is None: + if self.choices is None: # type: ignore[attr-defined] raise optparse.OptionError( "must supply a list of choices for type 'choice'", self ) - if not isinstance(self.choices, (tuple, list)): + if not isinstance(self.choices, (tuple, list)): # type: ignore[attr-defined] raise optparse.OptionError( # pylint: disable-next=consider-using-f-string "choices must be a list of strings ('%s' supplied)" - % str(type(self.choices)).split("'")[1], + % str(type(self.choices)).split("'")[1], # type: ignore[attr-defined] self, ) - elif self.choices is not None: + elif self.choices is not None: # type: ignore[attr-defined] raise optparse.OptionError( f"must not supply choices for type {self.type!r}", self ) optparse.Option.CHECK_METHODS[2] = _check_choice # type: ignore[index] - def process(self, opt, value, values, parser): # pragma: no cover # Argparse + def process( # pragma: no cover # Argparse + self, opt: Any, value: Any, values: Any, parser: Any + ) -> int: + assert isinstance(self.dest, str) if self.callback and self.callback.__module__ == "pylint.lint.run": return 1 # First, convert the value(s) to the right type. Howl if any diff --git a/pylint/config/option_manager_mixin.py b/pylint/config/option_manager_mixin.py index 2f0aac75f..c468f494f 100644 --- a/pylint/config/option_manager_mixin.py +++ b/pylint/config/option_manager_mixin.py @@ -2,6 +2,7 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + # pylint: disable=duplicate-code from __future__ import annotations @@ -14,31 +15,37 @@ import optparse # pylint: disable=deprecated-module import os import sys import warnings +from collections.abc import Iterator from pathlib import Path -from typing import Any, TextIO +from typing import TYPE_CHECKING, Any, TextIO from pylint import utils from pylint.config.option import Option -from pylint.config.option_parser import OptionParser +from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] from pylint.typing import OptionDict +if TYPE_CHECKING: + from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] + OptionsProviderMixin, + ) + if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib -def _expand_default(self, option): +def _expand_default(self: optparse.HelpFormatter, option: Option) -> str: """Patch OptionParser.expand_default with custom behaviour. This will handle defaults to avoid overriding values in the configuration file. """ if self.parser is None or not self.default_tag: - return option.help + return str(option.help) optname = option._long_opts[0][2:] try: - provider = self.parser.options_manager._all_options[optname] + provider = self.parser.options_manager._all_options[optname] # type: ignore[attr-defined] except KeyError: value = None else: @@ -48,41 +55,42 @@ def _expand_default(self, option): value = utils._format_option_value(optdict, value) if value is optparse.NO_DEFAULT or not value: value = self.NO_DEFAULT_VALUE - return option.help.replace(self.default_tag, str(value)) + return option.help.replace(self.default_tag, str(value)) # type: ignore[union-attr] @contextlib.contextmanager -def _patch_optparse(): +def _patch_optparse() -> Iterator[None]: # pylint: disable = redefined-variable-type orig_default = optparse.HelpFormatter try: - optparse.HelpFormatter.expand_default = _expand_default + optparse.HelpFormatter.expand_default = _expand_default # type: ignore[assignment] yield finally: - optparse.HelpFormatter.expand_default = orig_default + optparse.HelpFormatter.expand_default = orig_default # type: ignore[assignment] class OptionsManagerMixIn: """Handle configuration from both a configuration file and command line options.""" - def __init__(self, usage): + def __init__(self, usage: str) -> None: # TODO: 3.0: Remove deprecated class warnings.warn( "OptionsManagerMixIn has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) self.reset_parsers(usage) # list of registered options providers - self.options_providers = [] + self.options_providers: list[OptionsProviderMixin] = [] # dictionary associating option name to checker - self._all_options = collections.OrderedDict() - self._short_options = {} - self._nocallback_options = {} - self._mygroups = {} + self._all_options: collections.OrderedDict[Any, Any] = collections.OrderedDict() + self._short_options: dict[Any, Any] = {} + self._nocallback_options: dict[Any, Any] = {} + self._mygroups: dict[Any, Any] = {} # verbosity self._maxlevel = 0 - def reset_parsers(self, usage=""): + def reset_parsers(self, usage: str = "") -> None: # configuration file parser self.cfgfile_parser = configparser.ConfigParser( inline_comment_prefixes=("#", ";") @@ -92,7 +100,9 @@ class OptionsManagerMixIn: self.cmdline_parser.options_manager = self self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) - def register_options_provider(self, provider, own_group=True): + def register_options_provider( + self, provider: OptionsProviderMixin, own_group: bool = True + ) -> None: """Register an options provider.""" self.options_providers.append(provider) non_group_spec_options = [ @@ -118,7 +128,9 @@ class OptionsManagerMixIn: ] self.add_option_group(gname, gdoc, goptions, provider) - def add_option_group(self, group_name, _, options, provider): + def add_option_group( + self, group_name: str, _: Any, options: Any, provider: OptionsProviderMixin + ) -> None: # add option group to the command line parser if group_name in self._mygroups: group = self._mygroups[group_name] @@ -131,7 +143,7 @@ class OptionsManagerMixIn: # add section to the config file if ( group_name != "DEFAULT" - and group_name not in self.cfgfile_parser._sections + and group_name not in self.cfgfile_parser._sections # type: ignore[attr-defined] ): self.cfgfile_parser.add_section(group_name) # add provider's specific options @@ -140,13 +152,21 @@ class OptionsManagerMixIn: optdict["action"] = "callback" self.add_optik_option(provider, group, opt, optdict) - def add_optik_option(self, provider, optikcontainer, opt, optdict): + def add_optik_option( + self, + provider: OptionsProviderMixin, + optikcontainer: Any, + opt: str, + optdict: OptionDict, + ) -> None: args, optdict = self.optik_option(provider, opt, optdict) option = optikcontainer.add_option(*args, **optdict) self._all_options[opt] = provider self._maxlevel = max(self._maxlevel, option.level or 0) - def optik_option(self, provider, opt, optdict): + def optik_option( + self, provider: OptionsProviderMixin, opt: str, optdict: OptionDict + ) -> tuple[list[str], OptionDict]: """Get our personal option definition and return a suitable form for use with optik/optparse. """ @@ -164,12 +184,12 @@ class OptionsManagerMixIn: and optdict.get("default") is not None and optdict["action"] not in ("store_true", "store_false") ): - optdict["help"] += " [current: %default]" + optdict["help"] += " [current: %default]" # type: ignore[operator] del optdict["default"] args = ["--" + str(opt)] if "short" in optdict: self._short_options[optdict["short"]] = opt - args.append("-" + optdict["short"]) + args.append("-" + optdict["short"]) # type: ignore[operator] del optdict["short"] # cleanup option definition dict before giving it to optik for key in list(optdict.keys()): @@ -177,7 +197,9 @@ class OptionsManagerMixIn: optdict.pop(key) return args, optdict - def cb_set_provider_option(self, option, opt, value, parser): + def cb_set_provider_option( + self, option: Option, opt: str, value: Any, parser: Any + ) -> None: """Optik callback for option setting.""" if opt.startswith("--"): # remove -- on long option @@ -190,7 +212,7 @@ class OptionsManagerMixIn: value = 1 self.global_set_option(opt, value) - def global_set_option(self, opt, value): + def global_set_option(self, opt: str, value: Any) -> None: """Set option on the correct option provider.""" self._all_options[opt].set_option(opt, value) @@ -229,7 +251,7 @@ class OptionsManagerMixIn: ) printed = True - def load_provider_defaults(self): + def load_provider_defaults(self) -> None: """Initialize configuration using default values.""" for provider in self.options_providers: provider.load_defaults() @@ -256,11 +278,11 @@ class OptionsManagerMixIn: with open(config_file, encoding="utf_8_sig") as fp: parser.read_file(fp) # normalize each section's title - for sect, values in list(parser._sections.items()): + for sect, values in list(parser._sections.items()): # type: ignore[attr-defined] if sect.startswith("pylint."): sect = sect[len("pylint.") :] if not sect.isupper() and values: - parser._sections[sect.upper()] = values + parser._sections[sect.upper()] = values # type: ignore[attr-defined] if not verbose: return @@ -302,7 +324,7 @@ class OptionsManagerMixIn: parser.add_section(section_name) parser.set(section_name, option, value=value) - def load_config_file(self): + def load_config_file(self) -> None: """Dispatch values previously read from a configuration file to each option's provider. """ @@ -314,17 +336,19 @@ class OptionsManagerMixIn: except (KeyError, optparse.OptionError): continue - def load_configuration(self, **kwargs): + def load_configuration(self, **kwargs: Any) -> None: """Override configuration according to given parameters.""" return self.load_configuration_from_config(kwargs) - def load_configuration_from_config(self, config): + def load_configuration_from_config(self, config: dict[str, Any]) -> None: for opt, opt_value in config.items(): opt = opt.replace("_", "-") provider = self._all_options[opt] provider.set_option(opt, opt_value) - def load_command_line_configuration(self, args=None) -> list[str]: + def load_command_line_configuration( + self, args: list[str] | None = None + ) -> list[str]: """Override configuration according to command line parameters. return additional arguments @@ -339,10 +363,10 @@ class OptionsManagerMixIn: if value is None: continue setattr(config, attr, value) - return args + return args # type: ignore[return-value] - def help(self, level=0): + def help(self, level: int = 0) -> str: """Return the usage string for available options.""" self.cmdline_parser.formatter.output_level = level with _patch_optparse(): - return self.cmdline_parser.format_help() + return str(self.cmdline_parser.format_help()) diff --git a/pylint/config/option_parser.py b/pylint/config/option_parser.py index b58fad3a4..c527c4f60 100644 --- a/pylint/config/option_parser.py +++ b/pylint/config/option_parser.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# type: ignore # Deprecated module. + import optparse # pylint: disable=deprecated-module import warnings @@ -23,6 +25,7 @@ class OptionParser(optparse.OptionParser): warnings.warn( "OptionParser has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) super().__init__(option_class=Option, *args, **kwargs) diff --git a/pylint/config/options_provider_mixin.py b/pylint/config/options_provider_mixin.py index 5b20a290f..67f64ee0a 100644 --- a/pylint/config/options_provider_mixin.py +++ b/pylint/config/options_provider_mixin.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# type: ignore # Deprecated module. + import optparse # pylint: disable=deprecated-module import warnings @@ -27,6 +29,7 @@ class OptionsProviderMixIn: warnings.warn( "OptionsProviderMixIn has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) self.config = optparse.Values() self.load_defaults() diff --git a/pylint/config/utils.py b/pylint/config/utils.py index bd6146336..d7cbd7c07 100644 --- a/pylint/config/utils.py +++ b/pylint/config/utils.py @@ -42,7 +42,8 @@ def _convert_option_to_argument( if "level" in optdict and "hide" not in optdict: warnings.warn( "The 'level' key in optdicts has been deprecated. " - "Use 'hide' with a boolean to hide an option from the help message.", + "Use 'hide' with a boolean to hide an option from the help message. " + f"optdict={optdict}", DeprecationWarning, ) @@ -79,7 +80,8 @@ def _convert_option_to_argument( warnings.warn( "An option dictionary should have a 'default' key to specify " "the option's default value. This key will be required in pylint " - "3.0. It is not required for 'store_true' and callable actions.", + "3.0. It is not required for 'store_true' and callable actions. " + f"optdict={optdict}", DeprecationWarning, ) default = None @@ -151,7 +153,7 @@ def _parse_rich_type_value(value: Any) -> str: if isinstance(value, (list, tuple)): return ",".join(_parse_rich_type_value(i) for i in value) if isinstance(value, re.Pattern): - return value.pattern + return str(value.pattern) if isinstance(value, dict): return ",".join(f"{k}:{v}" for k, v in value.items()) return str(value) diff --git a/pylint/constants.py b/pylint/constants.py index 6ad5b82d3..d9ff20c46 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -168,3 +168,133 @@ TYPING_NEVER = frozenset( "typing_extensions.Never", ) ) + +DUNDER_METHODS: dict[tuple[int, int], dict[str, str]] = { + (0, 0): { + "__init__": "Instantiate class directly", + "__del__": "Use del keyword", + "__repr__": "Use repr built-in function", + "__str__": "Use str built-in function", + "__bytes__": "Use bytes built-in function", + "__format__": "Use format built-in function, format string method, or f-string", + "__lt__": "Use < operator", + "__le__": "Use <= operator", + "__eq__": "Use == operator", + "__ne__": "Use != operator", + "__gt__": "Use > operator", + "__ge__": "Use >= operator", + "__hash__": "Use hash built-in function", + "__bool__": "Use bool built-in function", + "__getattr__": "Access attribute directly or use getattr built-in function", + "__getattribute__": "Access attribute directly or use getattr built-in function", + "__setattr__": "Set attribute directly or use setattr built-in function", + "__delattr__": "Use del keyword", + "__dir__": "Use dir built-in function", + "__get__": "Use get method", + "__set__": "Use set method", + "__delete__": "Use del keyword", + "__instancecheck__": "Use isinstance built-in function", + "__subclasscheck__": "Use issubclass built-in function", + "__call__": "Invoke instance directly", + "__len__": "Use len built-in function", + "__length_hint__": "Use length_hint method", + "__getitem__": "Access item via subscript", + "__setitem__": "Set item via subscript", + "__delitem__": "Use del keyword", + "__iter__": "Use iter built-in function", + "__next__": "Use next built-in function", + "__reversed__": "Use reversed built-in function", + "__contains__": "Use in keyword", + "__add__": "Use + operator", + "__sub__": "Use - operator", + "__mul__": "Use * operator", + "__matmul__": "Use @ operator", + "__truediv__": "Use / operator", + "__floordiv__": "Use // operator", + "__mod__": "Use % operator", + "__divmod__": "Use divmod built-in function", + "__pow__": "Use ** operator or pow built-in function", + "__lshift__": "Use << operator", + "__rshift__": "Use >> operator", + "__and__": "Use & operator", + "__xor__": "Use ^ operator", + "__or__": "Use | operator", + "__radd__": "Use + operator", + "__rsub__": "Use - operator", + "__rmul__": "Use * operator", + "__rmatmul__": "Use @ operator", + "__rtruediv__": "Use / operator", + "__rfloordiv__": "Use // operator", + "__rmod__": "Use % operator", + "__rdivmod__": "Use divmod built-in function", + "__rpow__": "Use ** operator or pow built-in function", + "__rlshift__": "Use << operator", + "__rrshift__": "Use >> operator", + "__rand__": "Use & operator", + "__rxor__": "Use ^ operator", + "__ror__": "Use | operator", + "__iadd__": "Use += operator", + "__isub__": "Use -= operator", + "__imul__": "Use *= operator", + "__imatmul__": "Use @= operator", + "__itruediv__": "Use /= operator", + "__ifloordiv__": "Use //= operator", + "__imod__": "Use %= operator", + "__ipow__": "Use **= operator", + "__ilshift__": "Use <<= operator", + "__irshift__": "Use >>= operator", + "__iand__": "Use &= operator", + "__ixor__": "Use ^= operator", + "__ior__": "Use |= operator", + "__neg__": "Multiply by -1 instead", + "__pos__": "Multiply by +1 instead", + "__abs__": "Use abs built-in function", + "__invert__": "Use ~ operator", + "__complex__": "Use complex built-in function", + "__int__": "Use int built-in function", + "__float__": "Use float built-in function", + "__round__": "Use round built-in function", + "__trunc__": "Use math.trunc function", + "__floor__": "Use math.floor function", + "__ceil__": "Use math.ceil function", + "__enter__": "Invoke context manager directly", + "__aenter__": "Invoke context manager directly", + "__copy__": "Use copy.copy function", + "__deepcopy__": "Use copy.deepcopy function", + "__fspath__": "Use os.fspath function instead", + }, + (3, 10): { + "__aiter__": "Use aiter built-in function", + "__anext__": "Use anext built-in function", + }, +} + +EXTRA_DUNDER_METHODS = [ + "__new__", + "__subclasses__", + "__init_subclass__", + "__set_name__", + "__class_getitem__", + "__missing__", + "__exit__", + "__await__", + "__aexit__", + "__getnewargs_ex__", + "__getnewargs__", + "__getstate__", + "__setstate__", + "__reduce__", + "__reduce_ex__", + "__post_init__", # part of `dataclasses` module +] + +DUNDER_PROPERTIES = [ + "__class__", + "__dict__", + "__doc__", + "__format__", + "__module__", + "__sizeof__", + "__subclasshook__", + "__weakref__", +] diff --git a/pylint/epylint.py b/pylint/epylint.py index b6b6bf402..a69ce0d87 100755 --- a/pylint/epylint.py +++ b/pylint/epylint.py @@ -41,6 +41,7 @@ from __future__ import annotations import os import shlex import sys +import warnings from collections.abc import Sequence from io import StringIO from subprocess import PIPE, Popen @@ -168,6 +169,11 @@ def py_run( To silently run Pylint on a module, and get its standard output and error: >>> (pylint_stdout, pylint_stderr) = py_run( 'module_name.py', True) """ + warnings.warn( + "'epylint' will be removed in pylint 3.0, use https://github.com/emacsorphanage/pylint instead.", + DeprecationWarning, + stacklevel=2, + ) # Detect if we use Python as executable or not, else default to `python` executable = sys.executable if "python" in sys.executable else "python" @@ -198,6 +204,11 @@ def py_run( def Run(argv: Sequence[str] | None = None) -> NoReturn: + warnings.warn( + "'epylint' will be removed in pylint 3.0, use https://github.com/emacsorphanage/pylint instead.", + DeprecationWarning, + stacklevel=2, + ) if not argv and len(sys.argv) == 1: print(f"Usage: {sys.argv[0]} <filename> [options]") sys.exit(1) diff --git a/pylint/extensions/_check_docs_utils.py b/pylint/extensions/_check_docs_utils.py index d8f797b6c..46f0a2ac6 100644 --- a/pylint/extensions/_check_docs_utils.py +++ b/pylint/extensions/_check_docs_utils.py @@ -43,7 +43,7 @@ def get_setters_property_name(node: nodes.FunctionDef) -> str | None: and decorator.attrname == "setter" and isinstance(decorator.expr, nodes.Name) ): - return decorator.expr.name + return decorator.expr.name # type: ignore[no-any-return] return None diff --git a/pylint/extensions/bad_builtin.py b/pylint/extensions/bad_builtin.py index 7ffaf0f6c..dd6ab3841 100644 --- a/pylint/extensions/bad_builtin.py +++ b/pylint/extensions/bad_builtin.py @@ -18,8 +18,8 @@ if TYPE_CHECKING: BAD_FUNCTIONS = ["map", "filter"] # Some hints regarding the use of bad builtins. -BUILTIN_HINTS = {"map": "Using a list comprehension can be clearer."} -BUILTIN_HINTS["filter"] = BUILTIN_HINTS["map"] +LIST_COMP_MSG = "Using a list comprehension can be clearer." +BUILTIN_HINTS = {"map": LIST_COMP_MSG, "filter": LIST_COMP_MSG} class BadBuiltinChecker(BaseChecker): diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index ef8aaea78..24eb7f667 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -5,12 +5,13 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING, Tuple, Type, Union, cast +from typing import TYPE_CHECKING, Tuple, Type, cast from astroid import nodes from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import only_required_for_messages, safe_infer +from pylint.interfaces import INFERENCE if TYPE_CHECKING: from pylint.lint import PyLinter @@ -58,6 +59,16 @@ class CodeStyleChecker(BaseChecker): "both can be combined by using an assignment expression ``:=``. " "Requires Python 3.8 and ``py-version >= 3.8``.", ), + "R6104": ( + "Use '%s' to do an augmented assign directly", + "consider-using-augmented-assign", + "Emitted when an assignment is referring to the object that it is assigning " + "to. This can be changed to be an augmented assign.\n" + "Disabled by default!", + { + "default_enabled": False, + }, + ), } options = ( ( @@ -164,13 +175,11 @@ class CodeStyleChecker(BaseChecker): if list_length == 0: return for _, dict_value in node.items[1:]: - dict_value = cast(Union[nodes.List, nodes.Tuple], dict_value) if len(dict_value.elts) != list_length: return # Make sure at least one list entry isn't a dict for _, dict_value in node.items: - dict_value = cast(Union[nodes.List, nodes.Tuple], dict_value) if all(isinstance(entry, nodes.Dict) for entry in dict_value.elts): return @@ -303,6 +312,19 @@ class CodeStyleChecker(BaseChecker): return True return False + @only_required_for_messages("consider-using-augmented-assign") + def visit_assign(self, node: nodes.Assign) -> None: + is_aug, op = utils.is_augmented_assign(node) + if is_aug: + self.add_message( + "consider-using-augmented-assign", + args=f"{op}=", + node=node, + line=node.lineno, + col_offset=node.col_offset, + confidence=INFERENCE, + ) + def register(linter: PyLinter) -> None: linter.register_checker(CodeStyleChecker(linter)) diff --git a/pylint/extensions/comparetozero.py b/pylint/extensions/comparetozero.py index 8aca300d2..116bf229a 100644 --- a/pylint/extensions/comparetozero.py +++ b/pylint/extensions/comparetozero.py @@ -14,13 +14,18 @@ from astroid import nodes from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter def _is_constant_zero(node: str | nodes.NodeNG) -> bool: - return isinstance(node, astroid.Const) and node.value == 0 + # We have to check that node.value is not False because node.value == 0 is True + # when node.value is False + return ( + isinstance(node, astroid.Const) and node.value == 0 and node.value is not False + ) class CompareToZeroChecker(checkers.BaseChecker): @@ -35,7 +40,7 @@ class CompareToZeroChecker(checkers.BaseChecker): name = "compare-to-zero" msgs = { "C2001": ( - "Avoid comparisons to zero", + '"%s" can be simplified to "%s" as 0 is falsey', "compare-to-zero", "Used when Pylint detects comparison to a 0 constant.", ) @@ -65,12 +70,25 @@ class CompareToZeroChecker(checkers.BaseChecker): # 0 ?? X if _is_constant_zero(op_1) and op_2 in _operators: error_detected = True + op = op_3 # X ?? 0 elif op_2 in _operators and _is_constant_zero(op_3): error_detected = True + op = op_1 if error_detected: - self.add_message("compare-to-zero", node=node) + original = f"{op_1.as_string()} {op_2} {op_3.as_string()}" + suggestion = ( + op.as_string() + if op_2 in {"!=", "is not"} + else f"not {op.as_string()}" + ) + self.add_message( + "compare-to-zero", + args=(original, suggestion), + node=node, + confidence=HIGH, + ) def register(linter: PyLinter) -> None: diff --git a/pylint/extensions/dict_init_mutate.py b/pylint/extensions/dict_init_mutate.py new file mode 100644 index 000000000..fb4c83647 --- /dev/null +++ b/pylint/extensions/dict_init_mutate.py @@ -0,0 +1,66 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Check for use of dictionary mutation after initialization.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import only_required_for_messages +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint.pylinter import PyLinter + + +class DictInitMutateChecker(BaseChecker): + name = "dict-init-mutate" + msgs = { + "C3401": ( + "Declare all known key/values when initializing the dictionary.", + "dict-init-mutate", + "Dictionaries can be initialized with a single statement " + "using dictionary literal syntax.", + ) + } + + @only_required_for_messages("dict-init-mutate") + def visit_assign(self, node: nodes.Assign) -> None: + """ + Detect dictionary mutation immediately after initialization. + + At this time, detecting nested mutation is not supported. + """ + if not isinstance(node.value, nodes.Dict): + return + + dict_name = node.targets[0] + if len(node.targets) != 1 or not isinstance(dict_name, nodes.AssignName): + return + + first_sibling = node.next_sibling() + if ( + not first_sibling + or not isinstance(first_sibling, nodes.Assign) + or len(first_sibling.targets) != 1 + ): + return + + sibling_target = first_sibling.targets[0] + if not isinstance(sibling_target, nodes.Subscript): + return + + sibling_name = sibling_target.value + if not isinstance(sibling_name, nodes.Name): + return + + if sibling_name.name == dict_name.name: + self.add_message("dict-init-mutate", node=node, confidence=HIGH) + + +def register(linter: PyLinter) -> None: + linter.register_checker(DictInitMutateChecker(linter)) diff --git a/pylint/extensions/docparams.py b/pylint/extensions/docparams.py index 19d9cf890..62ada6824 100644 --- a/pylint/extensions/docparams.py +++ b/pylint/extensions/docparams.py @@ -298,10 +298,14 @@ class DocstringParameterChecker(BaseChecker): doc = utils.docstringify( func_node.doc_node, self.linter.config.default_docstring_type ) + + if self.linter.config.accept_no_raise_doc and not doc.exceptions(): + return + if not doc.matching_sections(): if doc.doc: missing = {exc.name for exc in expected_excs} - self._handle_no_raise_doc(missing, func_node) + self._add_raise_message(missing, func_node) return found_excs_full_names = doc.exceptions() @@ -652,12 +656,6 @@ class DocstringParameterChecker(BaseChecker): confidence=HIGH, ) - def _handle_no_raise_doc(self, excs: set[str], node: nodes.FunctionDef) -> None: - if self.linter.config.accept_no_raise_doc: - return - - self._add_raise_message(excs, node) - def _add_raise_message( self, missing_exceptions: set[str], node: nodes.FunctionDef ) -> None: diff --git a/pylint/extensions/dunder.py b/pylint/extensions/dunder.py new file mode 100644 index 000000000..e0e9af316 --- /dev/null +++ b/pylint/extensions/dunder.py @@ -0,0 +1,77 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.constants import DUNDER_METHODS, DUNDER_PROPERTIES, EXTRA_DUNDER_METHODS +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class DunderChecker(BaseChecker): + """Checks related to dunder methods.""" + + name = "dunder" + priority = -1 + msgs = { + "W3201": ( + "Bad or misspelled dunder method name %s.", + "bad-dunder-name", + "Used when a dunder method is misspelled or defined with a name " + "not within the predefined list of dunder names.", + ), + } + options = ( + ( + "good-dunder-names", + { + "default": [], + "type": "csv", + "metavar": "<comma-separated names>", + "help": "Good dunder names which should always be accepted.", + }, + ), + ) + + def open(self) -> None: + self._dunder_methods = ( + EXTRA_DUNDER_METHODS + + DUNDER_PROPERTIES + + self.linter.config.good_dunder_names + ) + for since_vers, dunder_methods in DUNDER_METHODS.items(): + if since_vers <= self.linter.config.py_version: + self._dunder_methods.extend(list(dunder_methods.keys())) + + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + """Check if known dunder method is misspelled or dunder name is not one + of the pre-defined names. + """ + # ignore module-level functions + if not node.is_method(): + return + + # Detect something that could be a bad dunder method + if ( + node.name.startswith("_") + and node.name.endswith("_") + and node.name not in self._dunder_methods + ): + self.add_message( + "bad-dunder-name", + node=node, + args=(node.name), + confidence=HIGH, + ) + + +def register(linter: PyLinter) -> None: + linter.register_checker(DunderChecker(linter)) diff --git a/pylint/extensions/emptystring.py b/pylint/extensions/emptystring.py index ec2839bdd..f96a980f5 100644 --- a/pylint/extensions/emptystring.py +++ b/pylint/extensions/emptystring.py @@ -7,31 +7,23 @@ from __future__ import annotations import itertools -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from astroid import nodes from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter class CompareToEmptyStringChecker(checkers.BaseChecker): - """Checks for comparisons to empty string. - - Most of the time you should use the fact that empty strings are false. - An exception to this rule is when an empty string value is allowed in the program - and has a different meaning than None! - """ - - # configuration section name name = "compare-to-empty-string" msgs = { "C1901": ( - "Avoid comparisons to empty string", + '"%s" can be simplified to "%s" as an empty string is falsey', "compare-to-empty-string", "Used when Pylint detects comparison to an empty string constant.", ) @@ -41,31 +33,45 @@ class CompareToEmptyStringChecker(checkers.BaseChecker): @utils.only_required_for_messages("compare-to-empty-string") def visit_compare(self, node: nodes.Compare) -> None: - _operators = ["!=", "==", "is not", "is"] - # note: astroid.Compare has the left most operand in node.left - # while the rest are a list of tuples in node.ops - # the format of the tuple is ('compare operator sign', node) - # here we squash everything into `ops` to make it easier for processing later - ops = [("", node.left)] + """Checks for comparisons to empty string. + + Most of the time you should use the fact that empty strings are false. + An exception to this rule is when an empty string value is allowed in the program + and has a different meaning than None! + """ + _operators = {"!=", "==", "is not", "is"} + # note: astroid.Compare has the left most operand in node.left while the rest + # are a list of tuples in node.ops the format of the tuple is + # ('compare operator sign', node) here we squash everything into `ops` + # to make it easier for processing later + ops: list[tuple[str, nodes.NodeNG | None]] = [("", node.left)] ops.extend(node.ops) - iter_ops: Iterable[Any] = iter(ops) - ops = list(itertools.chain(*iter_ops)) - + iter_ops = iter(ops) + ops = list(itertools.chain(*iter_ops)) # type: ignore[arg-type] for ops_idx in range(len(ops) - 2): - op_1 = ops[ops_idx] - op_2 = ops[ops_idx + 1] - op_3 = ops[ops_idx + 2] + op_1: nodes.NodeNG | None = ops[ops_idx] + op_2: str = ops[ops_idx + 1] # type: ignore[assignment] + op_3: nodes.NodeNG | None = ops[ops_idx + 2] error_detected = False - + if op_1 is None or op_3 is None or op_2 not in _operators: + continue + node_name = "" # x ?? "" - if utils.is_empty_str_literal(op_1) and op_2 in _operators: + if utils.is_empty_str_literal(op_1): error_detected = True + node_name = op_3.as_string() # '' ?? X - elif op_2 in _operators and utils.is_empty_str_literal(op_3): + elif utils.is_empty_str_literal(op_3): error_detected = True - + node_name = op_1.as_string() if error_detected: - self.add_message("compare-to-empty-string", node=node) + suggestion = f"not {node_name}" if op_2 in {"==", "is"} else node_name + self.add_message( + "compare-to-empty-string", + args=(node.as_string(), suggestion), + node=node, + confidence=HIGH, + ) def register(linter: PyLinter) -> None: diff --git a/pylint/extensions/for_any_all.py b/pylint/extensions/for_any_all.py index e6ab41c3f..257b5d37f 100644 --- a/pylint/extensions/for_any_all.py +++ b/pylint/extensions/for_any_all.py @@ -11,7 +11,12 @@ from typing import TYPE_CHECKING from astroid import nodes from pylint.checkers import BaseChecker -from pylint.checkers.utils import only_required_for_messages, returns_bool +from pylint.checkers.utils import ( + assigned_bool, + only_required_for_messages, + returns_bool, +) +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter @@ -36,19 +41,100 @@ class ConsiderUsingAnyOrAllChecker(BaseChecker): return if_children = list(node.body[0].get_children()) - if not len(if_children) == 2: # The If node has only a comparison and return - return - if not returns_bool(if_children[1]): + if any(isinstance(child, nodes.If) for child in if_children): + # an if node within the if-children indicates an elif clause, + # suggesting complex logic. return - # Check for terminating boolean return right after the loop node_after_loop = node.next_sibling() - if returns_bool(node_after_loop): + + if self._assigned_reassigned_returned(node, if_children, node_after_loop): + final_return_bool = node_after_loop.value.name + suggested_string = self._build_suggested_string(node, final_return_bool) + self.add_message( + "consider-using-any-or-all", + node=node, + args=suggested_string, + confidence=HIGH, + ) + return + + if self._if_statement_returns_bool(if_children, node_after_loop): final_return_bool = node_after_loop.value.value suggested_string = self._build_suggested_string(node, final_return_bool) self.add_message( - "consider-using-any-or-all", node=node, args=suggested_string + "consider-using-any-or-all", + node=node, + args=suggested_string, + confidence=HIGH, ) + return + + @staticmethod + def _if_statement_returns_bool( + if_children: list[nodes.NodeNG], node_after_loop: nodes.NodeNG + ) -> bool: + """Detect for-loop, if-statement, return pattern: + + Ex: + def any_uneven(items): + for item in items: + if not item % 2 == 0: + return True + return False + """ + if not len(if_children) == 2: + # The If node has only a comparison and return + return False + if not returns_bool(if_children[1]): + return False + + # Check for terminating boolean return right after the loop + return returns_bool(node_after_loop) + + @staticmethod + def _assigned_reassigned_returned( + node: nodes.For, if_children: list[nodes.NodeNG], node_after_loop: nodes.NodeNG + ) -> bool: + """Detect boolean-assign, for-loop, re-assign, return pattern: + + Ex: + def check_lines(lines, max_chars): + long_line = False + for line in lines: + if len(line) > max_chars: + long_line = True + # no elif / else statement + return long_line + """ + node_before_loop = node.previous_sibling() + + if not assigned_bool(node_before_loop): + # node before loop isn't assigning to boolean + return False + + assign_children = [x for x in if_children if isinstance(x, nodes.Assign)] + if not assign_children: + # if-nodes inside loop aren't assignments + return False + + # We only care for the first assign node of the if-children. Otherwise it breaks the pattern. + first_target = assign_children[0].targets[0] + target_before_loop = node_before_loop.targets[0] + + if not ( + isinstance(first_target, nodes.AssignName) + and isinstance(target_before_loop, nodes.AssignName) + ): + return False + + node_before_loop_name = node_before_loop.targets[0].name + return ( + first_target.name == node_before_loop_name + and isinstance(node_after_loop, nodes.Return) + and isinstance(node_after_loop.value, nodes.Name) + and node_after_loop.value.name == node_before_loop_name + ) @staticmethod def _build_suggested_string(node: nodes.For, final_return_bool: bool) -> str: diff --git a/pylint/extensions/magic_value.py b/pylint/extensions/magic_value.py new file mode 100644 index 000000000..161bb2c95 --- /dev/null +++ b/pylint/extensions/magic_value.py @@ -0,0 +1,88 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Checks for magic values instead of literals.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker, utils +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class MagicValueChecker(BaseChecker): + """Checks for constants in comparisons.""" + + name = "magic-value" + msgs = { + "R2004": ( + "Consider using a named constant or an enum instead of '%s'.", + "magic-value-comparison", + "Using named constants instead of magic values helps improve readability and maintainability of your" + " code, try to avoid them in comparisons.", + ) + } + + options = ( + ( + "valid-magic-values", + { + "default": (0, -1, 1, "", "__main__"), + "type": "csv", + "metavar": "<argument names>", + "help": " List of valid magic values that `magic-value-compare` will not detect.", + }, + ), + ) + + def _check_constants_comparison(self, node: nodes.Compare) -> None: + """ + Magic values in any side of the comparison should be avoided, + Detects comparisons that `comparison-of-constants` core checker cannot detect. + """ + const_operands = [] + LEFT_OPERAND = 0 + RIGHT_OPERAND = 1 + + left_operand = node.left + const_operands.append(isinstance(left_operand, nodes.Const)) + + right_operand = node.ops[0][1] + const_operands.append(isinstance(right_operand, nodes.Const)) + + if all(const_operands): + # `comparison-of-constants` avoided + return + + operand_value = None + if const_operands[LEFT_OPERAND] and self._is_magic_value(left_operand): + operand_value = left_operand.value + elif const_operands[RIGHT_OPERAND] and self._is_magic_value(right_operand): + operand_value = right_operand.value + if operand_value is not None: + self.add_message( + "magic-value-comparison", + node=node, + args=(operand_value), + confidence=HIGH, + ) + + def _is_magic_value(self, node: nodes.NodeNG) -> bool: + return (not utils.is_singleton_const(node)) and ( + node.value not in self.linter.config.valid_magic_values + ) + + @utils.only_required_for_messages("magic-comparison") + def visit_compare(self, node: nodes.Compare) -> None: + self._check_constants_comparison(node) + + +def register(linter: PyLinter) -> None: + linter.register_checker(MagicValueChecker(linter)) diff --git a/pylint/extensions/mccabe.py b/pylint/extensions/mccabe.py index e46bd5fbd..ea64d2ebf 100644 --- a/pylint/extensions/mccabe.py +++ b/pylint/extensions/mccabe.py @@ -45,13 +45,13 @@ _AppendableNodeT = TypeVar( ) -class PathGraph(Mccabe_PathGraph): +class PathGraph(Mccabe_PathGraph): # type: ignore[misc] def __init__(self, node: _SubGraphNodes | nodes.FunctionDef): super().__init__(name="", entity="", lineno=1) self.root = node -class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor): +class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor): # type: ignore[misc] def __init__(self) -> None: super().__init__() self._bottom_counter = 0 diff --git a/pylint/extensions/private_import.py b/pylint/extensions/private_import.py index 53e285ac3..61d37af37 100644 --- a/pylint/extensions/private_import.py +++ b/pylint/extensions/private_import.py @@ -195,7 +195,7 @@ class PrivateImportChecker(BaseChecker): """ if isinstance(node, nodes.Name) and node.name not in all_used_type_annotations: all_used_type_annotations[node.name] = True - return node.name + return node.name # type: ignore[no-any-return] if isinstance(node, nodes.Subscript): # e.g. Optional[List[str]] # slice is the next nested type self._populate_type_annotations_annotation( diff --git a/pylint/interfaces.py b/pylint/interfaces.py index a4d1288d8..221084fab 100644 --- a/pylint/interfaces.py +++ b/pylint/interfaces.py @@ -7,9 +7,8 @@ from __future__ import annotations import warnings -from collections import namedtuple from tokenize import TokenInfo -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NamedTuple from astroid import nodes @@ -33,7 +32,12 @@ __all__ = ( "CONFIDENCE_LEVEL_NAMES", ) -Confidence = namedtuple("Confidence", ["name", "description"]) + +class Confidence(NamedTuple): + name: str + description: str + + # Warning Certainties HIGH = Confidence("HIGH", "Warning that is not based on inference result.") CONTROL_FLOW = Confidence( @@ -57,6 +61,7 @@ class Interface: "Interface and all of its subclasses have been deprecated " "and will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) @classmethod @@ -78,6 +83,7 @@ def implements( "implements has been deprecated in favour of using basic " "inheritance patterns without using __implements__.", DeprecationWarning, + stacklevel=2, ) implements_ = getattr(obj, "__implements__", ()) if not isinstance(implements_, (list, tuple)): diff --git a/pylint/lint/base_options.py b/pylint/lint/base_options.py index 3ea95c745..6de88e44e 100644 --- a/pylint/lint/base_options.py +++ b/pylint/lint/base_options.py @@ -69,7 +69,7 @@ def _make_linter_options(linter: PyLinter) -> Options: "default": [], "help": "Add files or directories matching the regular expressions patterns to the " "ignore-list. The regex matches against paths and can be in " - "Posix or Windows format. Because '\\' represents the directory delimiter " + "Posix or Windows format. Because '\\\\' represents the directory delimiter " "on Windows systems, it can't be used as an escape character.", }, ), diff --git a/pylint/lint/caching.py b/pylint/lint/caching.py index 573b97628..8ea8a2236 100644 --- a/pylint/lint/caching.py +++ b/pylint/lint/caching.py @@ -12,12 +12,14 @@ from pathlib import Path from pylint.constants import PYLINT_HOME from pylint.utils import LinterStats +PYLINT_HOME_AS_PATH = Path(PYLINT_HOME) + def _get_pdata_path( - base_name: Path, recurs: int, pylint_home: Path = Path(PYLINT_HOME) + base_name: Path, recurs: int, pylint_home: Path = PYLINT_HOME_AS_PATH ) -> Path: - # We strip all characters that can't be used in a filename - # Also strip '/' and '\\' because we want to create a single file, not sub-directories + # We strip all characters that can't be used in a filename. Also strip '/' and + # '\\' because we want to create a single file, not sub-directories. underscored_name = "_".join( str(p.replace(":", "_").replace("/", "_").replace("\\", "_")) for p in base_name.parts diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index 2f2b3581b..e43208dea 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -18,7 +18,7 @@ def _modpath_from_file(filename: str, is_namespace: bool, path: list[str]) -> li def _is_package_cb(inner_path: str, parts: list[str]) -> bool: return modutils.check_modpath_has_init(inner_path, parts) or is_namespace - return modutils.modpath_from_file_with_callback( + return modutils.modpath_from_file_with_callback( # type: ignore[no-any-return] filename, path=path, is_package_cb=_is_package_cb ) diff --git a/pylint/lint/message_state_handler.py b/pylint/lint/message_state_handler.py index 4cd40e276..7c81c2c86 100644 --- a/pylint/lint/message_state_handler.py +++ b/pylint/lint/message_state_handler.py @@ -361,7 +361,7 @@ class _MessageStateHandler: match = OPTION_PO.search(content) if match is None: continue - try: + try: # pylint: disable = too-many-try-statements for pragma_repr in parse_pragma(match.group(2)): if pragma_repr.action in {"disable-all", "skip-file"}: if pragma_repr.action == "disable-all": diff --git a/pylint/lint/parallel.py b/pylint/lint/parallel.py index 646d26994..8d0053108 100644 --- a/pylint/lint/parallel.py +++ b/pylint/lint/parallel.py @@ -67,7 +67,7 @@ def _worker_check_single_file( defaultdict[str, list[Any]], ]: if not _worker_linter: - raise Exception("Worker linter not yet initialised") + raise RuntimeError("Worker linter not yet initialised") _worker_linter.open() _worker_linter.check_single_file_item(file_item) mapreduce_data = defaultdict(list) diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 5a600eb5b..70f27c7c0 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -95,7 +95,7 @@ def _load_reporter_by_class(reporter_class: str) -> type[BaseReporter]: class_name = qname.split(".")[-1] klass = getattr(module, class_name) assert issubclass(klass, BaseReporter), f"{klass} is not a BaseReporter" - return klass + return klass # type: ignore[no-any-return] # Python Linter class ######################################################### @@ -299,18 +299,6 @@ class PyLinter( self._dynamic_plugins: dict[str, ModuleType | ModuleNotFoundError | bool] = {} """Set of loaded plugin names.""" - # Attributes related to registering messages and their handling - self.msgs_store = MessageDefinitionStore() - self.msg_status = 0 - self._by_id_managed_msgs: list[ManagedMessage] = [] - - # Attributes related to visiting files - self.file_state = FileState("", self.msgs_store, is_base_filestate=True) - self.current_name: str | None = None - self.current_file: str | None = None - self._ignore_file = False - self._ignore_paths: list[Pattern[str]] = [] - # Attributes related to stats self.stats = LinterStats() @@ -338,6 +326,19 @@ class PyLinter( ), ("RP0003", "Messages", report_messages_stats), ) + + # Attributes related to registering messages and their handling + self.msgs_store = MessageDefinitionStore(self.config.py_version) + self.msg_status = 0 + self._by_id_managed_msgs: list[ManagedMessage] = [] + + # Attributes related to visiting files + self.file_state = FileState("", self.msgs_store, is_base_filestate=True) + self.current_name: str | None = None + self.current_file: str | None = None + self._ignore_file = False + self._ignore_paths: list[Pattern[str]] = [] + self.register_checker(self) @property @@ -346,6 +347,7 @@ class PyLinter( warnings.warn( "The option_groups attribute has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) return self._option_groups @@ -354,6 +356,7 @@ class PyLinter( warnings.warn( "The option_groups attribute has been deprecated and will be removed in pylint 3.0", DeprecationWarning, + stacklevel=2, ) self._option_groups = value @@ -400,7 +403,7 @@ class PyLinter( "bad-plugin-value", args=(modname, module_or_error), line=0 ) elif hasattr(module_or_error, "load_configuration"): - module_or_error.load_configuration(self) # type: ignore[union-attr] + module_or_error.load_configuration(self) # We re-set all the dictionary values to True here to make sure the dict # is pickle-able. This is only a problem in multiprocessing/parallel mode. @@ -486,6 +489,9 @@ class PyLinter( self.register_report(r_id, r_title, r_cb, checker) if hasattr(checker, "msgs"): self.msgs_store.register_messages_from_checker(checker) + for message in checker.messages: + if not message.default_enabled: + self.disable(message.msgid) # Register the checker, but disable all of its messages. if not getattr(checker, "enabled", True): self.disable(checker.name) @@ -605,7 +611,7 @@ class PyLinter( # initialize msgs_state now that all messages have been registered into # the store for msg in self.msgs_store.messages: - if not msg.may_be_emitted(): + if not msg.may_be_emitted(self.config.py_version): self._msgs_state[msg.msgid] = False def _discover_files(self, files_or_modules: Sequence[str]) -> Iterator[str]: @@ -658,6 +664,7 @@ class PyLinter( warnings.warn( "In pylint 3.0, the checkers check function will only accept sequence of string", DeprecationWarning, + stacklevel=2, ) files_or_modules = (files_or_modules,) # type: ignore[assignment] if self.config.recursive: @@ -729,6 +736,7 @@ class PyLinter( "In pylint 3.0, the checkers check_single_file function will be removed. " "Use check_single_file_item instead.", DeprecationWarning, + stacklevel=2, ) self.check_single_file_item(FileItem(name, filepath, modname)) @@ -911,6 +919,7 @@ class PyLinter( "If unknown it should be initialized as an empty string." ), DeprecationWarning, + stacklevel=2, ) self.current_name = modname self.current_file = filepath or modname @@ -947,9 +956,7 @@ class PyLinter( walker = ASTWalker(self) _checkers = self.prepare_checkers() tokencheckers = [ - c - for c in _checkers - if isinstance(c, checkers.BaseTokenChecker) and c is not self + c for c in _checkers if isinstance(c, checkers.BaseTokenChecker) ] # TODO: 3.0: Remove deprecated for-loop for c in _checkers: diff --git a/pylint/message/message.py b/pylint/message/message.py index 11961d9af..23dd6c082 100644 --- a/pylint/message/message.py +++ b/pylint/message/message.py @@ -43,6 +43,7 @@ class Message: # pylint: disable=too-many-instance-attributes warn( "In pylint 3.0, Messages will only accept a MessageLocationTuple as location parameter", DeprecationWarning, + stacklevel=2, ) location = MessageLocationTuple( location[0], diff --git a/pylint/message/message_definition.py b/pylint/message/message_definition.py index 3b403b008..25aa87d92 100644 --- a/pylint/message/message_definition.py +++ b/pylint/message/message_definition.py @@ -5,6 +5,7 @@ from __future__ import annotations import sys +import warnings from typing import TYPE_CHECKING, Any from astroid import nodes @@ -31,6 +32,7 @@ class MessageDefinition: maxversion: tuple[int, int] | None = None, old_names: list[tuple[str, str]] | None = None, shared: bool = False, + default_enabled: bool = True, ) -> None: self.checker_name = checker.name self.check_msgid(msgid) @@ -42,6 +44,7 @@ class MessageDefinition: self.minversion = minversion self.maxversion = maxversion self.shared = shared + self.default_enabled = default_enabled self.old_names: list[tuple[str, str]] = [] if old_names: for old_msgid, old_symbol in old_names: @@ -70,11 +73,24 @@ class MessageDefinition: def __str__(self) -> str: return f"{repr(self)}:\n{self.msg} {self.description}" - def may_be_emitted(self) -> bool: - """Return True if message may be emitted using the current interpreter.""" - if self.minversion is not None and self.minversion > sys.version_info: + def may_be_emitted( + self, + py_version: tuple[int, ...] | sys._version_info | None = None, + ) -> bool: + """Return True if message may be emitted using the configured py_version.""" + if py_version is None: + py_version = sys.version_info + warnings.warn( + "'py_version' will be a required parameter of " + "'MessageDefinition.may_be_emitted' in pylint 3.0. The most likely " + "solution is to use 'linter.config.py_version' if you need to keep " + "using this function, or to use 'MessageDefinition.is_message_enabled'" + " instead.", + DeprecationWarning, + ) + if self.minversion is not None and self.minversion > py_version: return False - if self.maxversion is not None and self.maxversion <= sys.version_info: + if self.maxversion is not None and self.maxversion <= py_version: return False return True diff --git a/pylint/message/message_definition_store.py b/pylint/message/message_definition_store.py index ef26d648d..7bbc70a51 100644 --- a/pylint/message/message_definition_store.py +++ b/pylint/message/message_definition_store.py @@ -6,6 +6,7 @@ from __future__ import annotations import collections import functools +import sys from collections.abc import Sequence, ValuesView from typing import TYPE_CHECKING @@ -23,7 +24,9 @@ class MessageDefinitionStore: has no particular state during analysis. """ - def __init__(self) -> None: + def __init__( + self, py_version: tuple[int, ...] | sys._version_info = sys.version_info + ) -> None: self.message_id_store: MessageIdStore = MessageIdStore() # Primary registry for all active messages definitions. # It contains the 1:1 mapping from msgid to MessageDefinition. @@ -31,6 +34,7 @@ class MessageDefinitionStore: self._messages_definitions: dict[str, MessageDefinition] = {} # MessageDefinition kept by category self._msgs_by_category: dict[str, list[str]] = collections.defaultdict(list) + self.py_version = py_version @property def messages(self) -> ValuesView[MessageDefinition]: @@ -52,10 +56,12 @@ class MessageDefinitionStore: self._msgs_by_category[message.msgid[0]].append(message.msgid) # Since MessageDefinitionStore is only initialized once - # and the arguments are relatively small in size we do not run the + # and the arguments are relatively small we do not run the # risk of creating a large memory leak. # See discussion in: https://github.com/PyCQA/pylint/pull/5673 - @functools.lru_cache(maxsize=None) # pylint: disable=method-cache-max-size-none + @functools.lru_cache( # pylint: disable=method-cache-max-size-none # noqa: B019 + maxsize=None + ) def get_message_definitions(self, msgid_or_symbol: str) -> list[MessageDefinition]: """Returns the Message definition for either a numeric or symbolic id. @@ -108,7 +114,7 @@ class MessageDefinitionStore: emittable = [] non_emittable = [] for message in messages: - if message.may_be_emitted(): + if message.may_be_emitted(self.py_version): emittable.append(message) else: non_emittable.append(message) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index c6939e2a1..85b23052e 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -36,7 +36,7 @@ class DiaDefGenerator: title = node.name if self.module_names: title = f"{node.root().name}.{title}" - return title + return title # type: ignore[no-any-return] def _set_option(self, option: bool | None) -> bool: """Activate some options if not explicitly deactivated.""" @@ -70,7 +70,7 @@ class DiaDefGenerator: """True if builtins and not show_builtins.""" if self.config.show_builtin: return True - return node.root().name != "builtins" + return node.root().name != "builtins" # type: ignore[no-any-return] def add_class(self, node: nodes.ClassDef) -> None: """Visit one class and add it to diagram.""" diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py index 7b15bf816..382d76bf7 100644 --- a/pylint/pyreverse/diagrams.py +++ b/pylint/pyreverse/diagrams.py @@ -143,7 +143,7 @@ class ClassDiagram(Figure, FilterMixIn): and not decorated_with_property(m) and self.show_attr(m.name) ] - return sorted(methods, key=lambda n: n.name) + return sorted(methods, key=lambda n: n.name) # type: ignore[no-any-return] def add_object(self, title: str, node: nodes.ClassDef) -> None: """Create a diagram object.""" @@ -214,20 +214,35 @@ class ClassDiagram(Figure, FilterMixIn): self.add_relationship(obj, impl_obj, "implements") except KeyError: continue - # associations link - for name, values in list(node.instance_attrs_type.items()) + list( + + # associations & aggregations links + for name, values in list(node.aggregations_type.items()): + for value in values: + self.assign_association_relationship( + value, obj, name, "aggregation" + ) + + for name, values in list(node.associations_type.items()) + list( node.locals_type.items() ): + for value in values: - if value is astroid.Uninferable: - continue - if isinstance(value, astroid.Instance): - value = value._proxied - try: - associated_obj = self.object_from_node(value) - self.add_relationship(associated_obj, obj, "association", name) - except KeyError: - continue + self.assign_association_relationship( + value, obj, name, "association" + ) + + def assign_association_relationship( + self, value: astroid.NodeNG, obj: ClassEntity, name: str, type_relationship: str + ) -> None: + if value is astroid.Uninferable: + return + if isinstance(value, astroid.Instance): + value = value._proxied + try: + associated_obj = self.object_from_node(value) + self.add_relationship(associated_obj, obj, type_relationship, name) + except KeyError: + return class PackageDiagram(ClassDiagram): diff --git a/pylint/pyreverse/dot_printer.py b/pylint/pyreverse/dot_printer.py index 883682704..1d5f2c32b 100644 --- a/pylint/pyreverse/dot_printer.py +++ b/pylint/pyreverse/dot_printer.py @@ -10,6 +10,7 @@ import os import subprocess import sys import tempfile +from enum import Enum from pathlib import Path from astroid import nodes @@ -17,19 +18,34 @@ from astroid import nodes from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer from pylint.pyreverse.utils import get_annotation_label + +class HTMLLabels(Enum): + LINEBREAK_LEFT = '<br ALIGN="LEFT"/>' + + ALLOWED_CHARSETS: frozenset[str] = frozenset(("utf-8", "iso-8859-1", "latin1")) SHAPES: dict[NodeType, str] = { NodeType.PACKAGE: "box", NodeType.INTERFACE: "record", NodeType.CLASS: "record", } +# pylint: disable-next=consider-using-namedtuple-or-dataclass ARROWS: dict[EdgeType, dict[str, str]] = { - EdgeType.INHERITS: dict(arrowtail="none", arrowhead="empty"), - EdgeType.IMPLEMENTS: dict(arrowtail="node", arrowhead="empty", style="dashed"), - EdgeType.ASSOCIATION: dict( - fontcolor="green", arrowtail="none", arrowhead="diamond", style="solid" - ), - EdgeType.USES: dict(arrowtail="none", arrowhead="open"), + EdgeType.INHERITS: {"arrowtail": "none", "arrowhead": "empty"}, + EdgeType.IMPLEMENTS: {"arrowtail": "node", "arrowhead": "empty", "style": "dashed"}, + EdgeType.ASSOCIATION: { + "fontcolor": "green", + "arrowtail": "none", + "arrowhead": "diamond", + "style": "solid", + }, + EdgeType.AGGREGATION: { + "fontcolor": "green", + "arrowtail": "none", + "arrowhead": "odiamond", + "style": "solid", + }, + EdgeType.USES: {"arrowtail": "none", "arrowhead": "open"}, } @@ -73,7 +89,7 @@ class DotPrinter(Printer): color = properties.color if properties.color is not None else self.DEFAULT_COLOR style = "filled" if color != self.DEFAULT_COLOR else "solid" label = self._build_label_for_node(properties) - label_part = f', label="{label}"' if label else "" + label_part = f", label=<{label}>" if label else "" fontcolor_part = ( f', fontcolor="{properties.fontcolor}"' if properties.fontcolor else "" ) @@ -92,17 +108,22 @@ class DotPrinter(Printer): # Add class attributes attrs: list[str] = properties.attrs or [] - attrs_string = r"\l".join(attr.replace("|", r"\|") for attr in attrs) - label = rf"{{{label}|{attrs_string}\l|" + attrs_string = rf"{HTMLLabels.LINEBREAK_LEFT.value}".join( + attr.replace("|", r"\|") for attr in attrs + ) + label = rf"{{{label}|{attrs_string}{HTMLLabels.LINEBREAK_LEFT.value}|" # Add class methods methods: list[nodes.FunctionDef] = properties.methods or [] for func in methods: args = self._get_method_arguments(func) - label += rf"{func.name}({', '.join(args)})" + method_name = ( + f"<I>{func.name}</I>" if func.is_abstract() else f"{func.name}" + ) + label += rf"{method_name}({', '.join(args)})" if func.returns: label += ": " + get_annotation_label(func.returns) - label += r"\l" + label += rf"{HTMLLabels.LINEBREAK_LEFT.value}" label += "}" return label diff --git a/pylint/pyreverse/inspector.py b/pylint/pyreverse/inspector.py index 042d3845e..8c403ffc6 100644 --- a/pylint/pyreverse/inspector.py +++ b/pylint/pyreverse/inspector.py @@ -13,6 +13,7 @@ import collections import os import traceback import warnings +from abc import ABC, abstractmethod from collections.abc import Generator from typing import Any, Callable, Optional @@ -123,6 +124,12 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor): * instance_attrs_type as locals_type but for klass member attributes (only on astroid.Class) + * associations_type + as instance_attrs_type but for association relationships + + * aggregations_type + as instance_attrs_type but for aggregations relationships + * implements, list of implemented interface _objects_ (only on astroid.Class nodes) """ @@ -134,6 +141,8 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor): self.tag = tag # visited project self.project = project + self.associations_handler = AggregationsHandler() + self.associations_handler.set_next(OtherAssociationsHandler()) def visit_project(self, node: Project) -> None: """Visit a pyreverse.utils.Project node. @@ -178,9 +187,12 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor): baseobj.specializations = specializations # resolve instance attributes node.instance_attrs_type = collections.defaultdict(list) + node.aggregations_type = collections.defaultdict(list) + node.associations_type = collections.defaultdict(list) for assignattrs in tuple(node.instance_attrs.values()): for assignattr in assignattrs: if not isinstance(assignattr, nodes.Unknown): + self.associations_handler.handle(assignattr, node) self.handle_assignattr_type(assignattr, node) # resolve implemented interface try: @@ -313,6 +325,61 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor): mod_paths.append(mod_path) +class AssociationHandlerInterface(ABC): + @abstractmethod + def set_next( + self, handler: AssociationHandlerInterface + ) -> AssociationHandlerInterface: + pass + + @abstractmethod + def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: + pass + + +class AbstractAssociationHandler(AssociationHandlerInterface): + """ + Chain of Responsibility for handling types of association, useful + to expand in the future if we want to add more distinct associations. + + Every link of the chain checks if it's a certain type of association. + If no association is found it's set as a generic association in `associations_type`. + + The default chaining behavior is implemented inside the base handler + class. + """ + + _next_handler: AssociationHandlerInterface + + def set_next( + self, handler: AssociationHandlerInterface + ) -> AssociationHandlerInterface: + self._next_handler = handler + return handler + + @abstractmethod + def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: + if self._next_handler: + self._next_handler.handle(node, parent) + + +class AggregationsHandler(AbstractAssociationHandler): + def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: + if isinstance(node.parent.value, astroid.node_classes.Name): + current = set(parent.aggregations_type[node.attrname]) + parent.aggregations_type[node.attrname] = list( + current | utils.infer_node(node) + ) + else: + super().handle(node, parent) + + +class OtherAssociationsHandler(AbstractAssociationHandler): + def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None: + current = set(parent.associations_type[node.attrname]) + parent.associations_type[node.attrname] = list(current | utils.infer_node(node)) + + def project_from_files( files: list[str], func_wrapper: _WrapperFuncT = _astroid_wrapper, diff --git a/pylint/pyreverse/main.py b/pylint/pyreverse/main.py index 043e2a3f3..72429b41a 100644 --- a/pylint/pyreverse/main.py +++ b/pylint/pyreverse/main.py @@ -36,14 +36,14 @@ DIRECTLY_SUPPORTED_FORMATS = ( OPTIONS: Options = ( ( "filter-mode", - dict( - short="f", - default="PUB_ONLY", - dest="mode", - type="string", - action="store", - metavar="<mode>", - help="""filter attributes and functions according to + { + "short": "f", + "default": "PUB_ONLY", + "dest": "mode", + "type": "string", + "action": "store", + "metavar": "<mode>", + "help": """filter attributes and functions according to <mode>. Correct modes are : 'PUB_ONLY' filter all non public attributes [DEFAULT], equivalent to PRIVATE+SPECIAL_A @@ -52,154 +52,154 @@ OPTIONS: Options = ( except constructor 'OTHER' filter protected and private attributes""", - ), + }, ), ( "class", - dict( - short="c", - action="extend", - metavar="<class>", - type="csv", - dest="classes", - default=None, - help="create a class diagram with all classes related to <class>;\ + { + "short": "c", + "action": "extend", + "metavar": "<class>", + "type": "csv", + "dest": "classes", + "default": None, + "help": "create a class diagram with all classes related to <class>;\ this uses by default the options -ASmy", - ), + }, ), ( "show-ancestors", - dict( - short="a", - action="store", - metavar="<ancestor>", - type="int", - default=None, - help="show <ancestor> generations of ancestor classes not in <projects>", - ), + { + "short": "a", + "action": "store", + "metavar": "<ancestor>", + "type": "int", + "default": None, + "help": "show <ancestor> generations of ancestor classes not in <projects>", + }, ), ( "all-ancestors", - dict( - short="A", - default=None, - action="store_true", - help="show all ancestors off all classes in <projects>", - ), + { + "short": "A", + "default": None, + "action": "store_true", + "help": "show all ancestors off all classes in <projects>", + }, ), ( "show-associated", - dict( - short="s", - action="store", - metavar="<association_level>", - type="int", - default=None, - help="show <association_level> levels of associated classes not in <projects>", - ), + { + "short": "s", + "action": "store", + "metavar": "<association_level>", + "type": "int", + "default": None, + "help": "show <association_level> levels of associated classes not in <projects>", + }, ), ( "all-associated", - dict( - short="S", - default=None, - action="store_true", - help="show recursively all associated off all associated classes", - ), + { + "short": "S", + "default": None, + "action": "store_true", + "help": "show recursively all associated off all associated classes", + }, ), ( "show-builtin", - dict( - short="b", - action="store_true", - default=False, - help="include builtin objects in representation of classes", - ), + { + "short": "b", + "action": "store_true", + "default": False, + "help": "include builtin objects in representation of classes", + }, ), ( "module-names", - dict( - short="m", - default=None, - type="yn", - metavar="<y or n>", - help="include module name in representation of classes", - ), + { + "short": "m", + "default": None, + "type": "yn", + "metavar": "<y or n>", + "help": "include module name in representation of classes", + }, ), ( "only-classnames", - dict( - short="k", - action="store_true", - default=False, - help="don't show attributes and methods in the class boxes; this disables -f values", - ), + { + "short": "k", + "action": "store_true", + "default": False, + "help": "don't show attributes and methods in the class boxes; this disables -f values", + }, ), ( "output", - dict( - short="o", - dest="output_format", - action="store", - default="dot", - metavar="<format>", - type="string", - help=( + { + "short": "o", + "dest": "output_format", + "action": "store", + "default": "dot", + "metavar": "<format>", + "type": "string", + "help": ( f"create a *.<format> output file if format is available. Available formats are: {', '.join(DIRECTLY_SUPPORTED_FORMATS)}. " f"Any other format will be tried to create by means of the 'dot' command line tool, which requires a graphviz installation." ), - ), + }, ), ( "colorized", - dict( - dest="colorized", - action="store_true", - default=False, - help="Use colored output. Classes/modules of the same package get the same color.", - ), + { + "dest": "colorized", + "action": "store_true", + "default": False, + "help": "Use colored output. Classes/modules of the same package get the same color.", + }, ), ( "max-color-depth", - dict( - dest="max_color_depth", - action="store", - default=2, - metavar="<depth>", - type="int", - help="Use separate colors up to package depth of <depth>", - ), + { + "dest": "max_color_depth", + "action": "store", + "default": 2, + "metavar": "<depth>", + "type": "int", + "help": "Use separate colors up to package depth of <depth>", + }, ), ( "ignore", - dict( - type="csv", - metavar="<file[,file...]>", - dest="ignore_list", - default=constants.DEFAULT_IGNORE_LIST, - help="Files or directories to be skipped. They should be base names, not paths.", - ), + { + "type": "csv", + "metavar": "<file[,file...]>", + "dest": "ignore_list", + "default": constants.DEFAULT_IGNORE_LIST, + "help": "Files or directories to be skipped. They should be base names, not paths.", + }, ), ( "project", - dict( - default="", - type="string", - short="p", - metavar="<project name>", - help="set the project name.", - ), + { + "default": "", + "type": "string", + "short": "p", + "metavar": "<project name>", + "help": "set the project name.", + }, ), ( "output-directory", - dict( - default="", - type="path", - short="d", - action="store", - metavar="<output_directory>", - help="set the output directory path.", - ), + { + "default": "", + "type": "path", + "short": "d", + "action": "store", + "metavar": "<output_directory>", + "help": "set the output directory path.", + }, ), ) @@ -210,8 +210,7 @@ class Run(_ArgumentsManager, _ArgumentsProvider): options = OPTIONS name = "pyreverse" - # For mypy issue, see https://github.com/python/mypy/issues/10342 - def __init__(self, args: Sequence[str]) -> NoReturn: # type: ignore[misc] + def __init__(self, args: Sequence[str]) -> NoReturn: _ArgumentsManager.__init__(self, prog="pyreverse", description=__doc__) _ArgumentsProvider.__init__(self, self) diff --git a/pylint/pyreverse/mermaidjs_printer.py b/pylint/pyreverse/mermaidjs_printer.py index 9a2309a74..a8f3c576b 100644 --- a/pylint/pyreverse/mermaidjs_printer.py +++ b/pylint/pyreverse/mermaidjs_printer.py @@ -24,6 +24,7 @@ class MermaidJSPrinter(Printer): EdgeType.INHERITS: "--|>", EdgeType.IMPLEMENTS: "..|>", EdgeType.ASSOCIATION: "--*", + EdgeType.AGGREGATION: "--o", EdgeType.USES: "-->", } @@ -54,6 +55,7 @@ class MermaidJSPrinter(Printer): for func in properties.methods: args = self._get_method_arguments(func) line = f"{func.name}({', '.join(args)})" + line += "*" if func.is_abstract() else "" if func.returns: line += f" {get_annotation_label(func.returns)}" body.append(line) diff --git a/pylint/pyreverse/plantuml_printer.py b/pylint/pyreverse/plantuml_printer.py index 45106152d..56463165d 100644 --- a/pylint/pyreverse/plantuml_printer.py +++ b/pylint/pyreverse/plantuml_printer.py @@ -24,6 +24,7 @@ class PlantUmlPrinter(Printer): EdgeType.INHERITS: "--|>", EdgeType.IMPLEMENTS: "..|>", EdgeType.ASSOCIATION: "--*", + EdgeType.AGGREGATION: "--o", EdgeType.USES: "-->", } @@ -66,7 +67,8 @@ class PlantUmlPrinter(Printer): if properties.methods: for func in properties.methods: args = self._get_method_arguments(func) - line = f"{func.name}({', '.join(args)})" + line = "{abstract}" if func.is_abstract() else "" + line += f"{func.name}({', '.join(args)})" if func.returns: line += " -> " + get_annotation_label(func.returns) body.append(line) diff --git a/pylint/pyreverse/printer.py b/pylint/pyreverse/printer.py index 55ce2c8b1..cdbf7e3c8 100644 --- a/pylint/pyreverse/printer.py +++ b/pylint/pyreverse/printer.py @@ -25,6 +25,7 @@ class EdgeType(Enum): INHERITS = "inherits" IMPLEMENTS = "implements" ASSOCIATION = "association" + AGGREGATION = "aggregation" USES = "uses" diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py index a1de38685..078bc1b7e 100644 --- a/pylint/pyreverse/utils.py +++ b/pylint/pyreverse/utils.py @@ -74,12 +74,12 @@ def get_visibility(name: str) -> str: def is_interface(node: nodes.ClassDef) -> bool: # bw compatibility - return node.type == "interface" + return node.type == "interface" # type: ignore[no-any-return] def is_exception(node: nodes.ClassDef) -> bool: # bw compatibility - return node.type == "exception" + return node.type == "exception" # type: ignore[no-any-return] # Helpers ##################################################################### @@ -170,9 +170,9 @@ class LocalsVisitor: def get_annotation_label(ann: nodes.Name | nodes.NodeNG) -> str: if isinstance(ann, nodes.Name) and ann.name is not None: - return ann.name + return ann.name # type: ignore[no-any-return] if isinstance(ann, nodes.NodeNG): - return ann.as_string() + return ann.as_string() # type: ignore[no-any-return] return "" diff --git a/pylint/pyreverse/vcg_printer.py b/pylint/pyreverse/vcg_printer.py index ec7152baa..b9e2e94f3 100644 --- a/pylint/pyreverse/vcg_printer.py +++ b/pylint/pyreverse/vcg_printer.py @@ -154,20 +154,34 @@ SHAPES: dict[NodeType, str] = { NodeType.CLASS: "box", NodeType.INTERFACE: "ellipse", } -ARROWS: dict[EdgeType, dict] = { - EdgeType.USES: dict(arrowstyle="solid", backarrowstyle="none", backarrowsize=0), - EdgeType.INHERITS: dict( - arrowstyle="solid", backarrowstyle="none", backarrowsize=10 - ), - EdgeType.IMPLEMENTS: dict( - arrowstyle="solid", - backarrowstyle="none", - linestyle="dotted", - backarrowsize=10, - ), - EdgeType.ASSOCIATION: dict( - arrowstyle="solid", backarrowstyle="none", textcolor="green" - ), +# pylint: disable-next=consider-using-namedtuple-or-dataclass +ARROWS: dict[EdgeType, dict[str, str | int]] = { + EdgeType.USES: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "backarrowsize": 0, + }, + EdgeType.INHERITS: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "backarrowsize": 10, + }, + EdgeType.IMPLEMENTS: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "linestyle": "dotted", + "backarrowsize": 10, + }, + EdgeType.ASSOCIATION: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "textcolor": "green", + }, + EdgeType.AGGREGATION: { + "arrowstyle": "solid", + "backarrowstyle": "none", + "textcolor": "green", + }, } ORIENTATION: dict[Layout, str] = { Layout.LEFT_TO_RIGHT: "left_to_right", @@ -265,13 +279,15 @@ class VCGPrinter(Printer): ) self.emit("}") - def _write_attributes(self, attributes_dict: Mapping[str, Any], **args) -> None: + def _write_attributes( + self, attributes_dict: Mapping[str, Any], **args: Any + ) -> None: """Write graph, node or edge attributes.""" for key, value in args.items(): try: _type = attributes_dict[key] except KeyError as e: - raise Exception( + raise AttributeError( f"no such attribute {key}\npossible attributes are {attributes_dict.keys()}" ) from e @@ -282,6 +298,6 @@ class VCGPrinter(Printer): elif value in _type: self.emit(f"{key}:{value}\n") else: - raise Exception( + raise ValueError( f"value {value} isn't correct for attribute {key} correct values are {type}" ) diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py index 12a76df9b..68a49eea1 100644 --- a/pylint/pyreverse/writer.py +++ b/pylint/pyreverse/writer.py @@ -92,7 +92,7 @@ class DiagramWriter: def write_classes(self, diagram: ClassDiagram) -> None: """Write a class diagram.""" # sorted to get predictable (hence testable) results - for obj in sorted(diagram.objects, key=lambda x: x.title): + for obj in sorted(diagram.objects, key=lambda x: x.title): # type: ignore[no-any-return] obj.fig_id = obj.node.qname() type_ = NodeType.INTERFACE if obj.shape == "interface" else NodeType.CLASS self.printer.emit_node( @@ -120,6 +120,14 @@ class DiagramWriter: label=rel.name, type_=EdgeType.ASSOCIATION, ) + # generate aggregations + for rel in diagram.get_relationships("aggregation"): + self.printer.emit_edge( + rel.from_object.fig_id, + rel.to_object.fig_id, + label=rel.name, + type_=EdgeType.AGGREGATION, + ) def set_printer(self, file_name: str, basename: str) -> None: """Set printer.""" diff --git a/pylint/reporters/base_reporter.py b/pylint/reporters/base_reporter.py index 0b4507e5c..3df970d80 100644 --- a/pylint/reporters/base_reporter.py +++ b/pylint/reporters/base_reporter.py @@ -36,6 +36,7 @@ class BaseReporter: "Using the __implements__ inheritance pattern for BaseReporter is no " "longer supported. Child classes should only inherit BaseReporter", DeprecationWarning, + stacklevel=2, ) self.linter: PyLinter self.section = 0 @@ -54,6 +55,7 @@ class BaseReporter: warn( "'set_output' will be removed in 3.0, please use 'reporter.out = stream' instead", DeprecationWarning, + stacklevel=2, ) self.out = output or sys.stdout diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index 29bd46798..546b33378 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -135,6 +135,7 @@ def colorize_ansi( warnings.warn( "In pylint 3.0, the colorize_ansi function of Text reporters will only accept a MessageStyle parameter", DeprecationWarning, + stacklevel=2, ) color = kwargs.get("color") style_attrs = tuple(_splitstrip(style)) @@ -225,6 +226,7 @@ class ParseableTextReporter(TextReporter): warnings.warn( f"{self.name} output format is deprecated. This is equivalent to --msg-template={self.line_format}", DeprecationWarning, + stacklevel=2, ) super().__init__(output) @@ -265,6 +267,7 @@ class ColorizedTextReporter(TextReporter): warnings.warn( "In pylint 3.0, the ColorizedTextReporter will only accept ColorMappingDict as color_mapping parameter", DeprecationWarning, + stacklevel=2, ) temp_color_mapping: ColorMappingDict = {} for key, value in color_mapping.items(): diff --git a/pylint/testutils/_primer/package_to_lint.py b/pylint/testutils/_primer/package_to_lint.py index b6ccc8b8b..09ecb4456 100644 --- a/pylint/testutils/_primer/package_to_lint.py +++ b/pylint/testutils/_primer/package_to_lint.py @@ -5,11 +5,17 @@ from __future__ import annotations import logging +import sys from pathlib import Path from git.cmd import Git from git.repo import Repo +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + PRIMER_DIRECTORY_PATH = Path("tests") / ".pylint_primer_tests" @@ -34,6 +40,9 @@ class PackageToLint: pylintrc_relpath: str | None """Path relative to project's main directory to the pylintrc if it exists.""" + minimum_python: str | None + """Minimum python version supported by the package.""" + def __init__( self, url: str, @@ -42,6 +51,7 @@ class PackageToLint: commit: str | None = None, pylint_additional_args: list[str] | None = None, pylintrc_relpath: str | None = None, + minimum_python: str | None = None, ) -> None: self.url = url self.branch = branch @@ -49,11 +59,13 @@ class PackageToLint: self.commit = commit self.pylint_additional_args = pylint_additional_args or [] self.pylintrc_relpath = pylintrc_relpath + self.minimum_python = minimum_python @property - def pylintrc(self) -> Path | None: + def pylintrc(self) -> Path | Literal[""]: if self.pylintrc_relpath is None: - return None + # Fall back to "" to ensure pylint's own pylintrc is not discovered + return "" return self.clone_directory / self.pylintrc_relpath @property @@ -70,9 +82,8 @@ class PackageToLint: @property def pylint_args(self) -> list[str]: options: list[str] = [] - if self.pylintrc is not None: - # There is an error if rcfile is given but does not exist - options += [f"--rcfile={self.pylintrc}"] + # There is an error if rcfile is given but does not exist + options += [f"--rcfile={self.pylintrc}"] return self.paths_to_lint + options + self.pylint_additional_args def lazy_clone(self) -> str: # pragma: no cover diff --git a/pylint/testutils/_primer/primer.py b/pylint/testutils/_primer/primer.py index 417bb1988..7d08f1df5 100644 --- a/pylint/testutils/_primer/primer.py +++ b/pylint/testutils/_primer/primer.py @@ -6,6 +6,7 @@ from __future__ import annotations import argparse import json +import sys from pathlib import Path from pylint.testutils._primer import PackageToLint @@ -92,9 +93,18 @@ class Primer: self.command.run() @staticmethod + def _minimum_python_supported(package_data: dict[str, str]) -> bool: + min_python_str = package_data.get("minimum_python", None) + if not min_python_str: + return True + min_python_tuple = tuple(int(n) for n in min_python_str.split(".")) + return min_python_tuple <= sys.version_info[:2] + + @staticmethod def _get_packages_to_lint_from_json(json_path: Path) -> dict[str, PackageToLint]: with open(json_path, encoding="utf8") as f: return { name: PackageToLint(**package_data) for name, package_data in json.load(f).items() + if Primer._minimum_python_supported(package_data) } diff --git a/pylint/testutils/_primer/primer_compare_command.py b/pylint/testutils/_primer/primer_compare_command.py index baf28d8b7..442ffa227 100644 --- a/pylint/testutils/_primer/primer_compare_command.py +++ b/pylint/testutils/_primer/primer_compare_command.py @@ -63,15 +63,14 @@ class CompareCommand(PrimerCommand): comment += self._create_comment_for_package( package, new_messages, missing_messages ) - if comment == "": - comment = ( + comment = ( + f"🤖 **Effect of this PR on checked open source code:** 🤖\n\n{comment}" + if comment + else ( "🤖 According to the primer, this change has **no effect** on the" " checked open source code. 🤖🎉\n\n" ) - else: - comment = ( - f"🤖 **Effect of this PR on checked open source code:** 🤖\n\n{comment}" - ) + ) return self._truncate_comment(comment) def _create_comment_for_package( diff --git a/pylint/testutils/_primer/primer_prepare_command.py b/pylint/testutils/_primer/primer_prepare_command.py index 83b3a2b96..e69e55b95 100644 --- a/pylint/testutils/_primer/primer_prepare_command.py +++ b/pylint/testutils/_primer/primer_prepare_command.py @@ -3,6 +3,8 @@ # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt from __future__ import annotations +import sys + from git.cmd import Git from git.repo import Repo @@ -12,6 +14,7 @@ from pylint.testutils._primer.primer_command import PrimerCommand class PrepareCommand(PrimerCommand): def run(self) -> None: commit_string = "" + version_string = ".".join(str(x) for x in sys.version_info[:2]) if self.config.clone: for package, data in self.packages.items(): local_commit = data.lazy_clone() @@ -31,12 +34,14 @@ class PrepareCommand(PrimerCommand): commit_string += remote_sha1_commit + "_" elif self.config.read_commit_string: with open( - self.primer_directory / "commit_string.txt", encoding="utf-8" + self.primer_directory / f"commit_string_{version_string}.txt", + encoding="utf-8", ) as f: print(f.read()) - if commit_string: with open( - self.primer_directory / "commit_string.txt", "w", encoding="utf-8" + self.primer_directory / f"commit_string_{version_string}.txt", + "w", + encoding="utf-8", ) as f: f.write(commit_string) diff --git a/pylint/testutils/_primer/primer_run_command.py b/pylint/testutils/_primer/primer_run_command.py index d2fce7793..cd17d6b1d 100644 --- a/pylint/testutils/_primer/primer_run_command.py +++ b/pylint/testutils/_primer/primer_run_command.py @@ -85,7 +85,7 @@ class RunCommand(PrimerCommand): try: Run(arguments, reporter=reporter) except SystemExit as e: - pylint_exit_code = int(e.code) + pylint_exit_code = int(e.code) # type: ignore[arg-type] readable_messages: str = output.getvalue() messages: list[OldJsonExport] = json.loads(readable_messages) fatal_msgs: list[Message] = [] diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index 0c24648b3..291f52002 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -85,6 +85,7 @@ class CheckerTestCase: f"the expected value in {expected_msg}. In pylint 3.0 correct end_line " "attributes will be required for MessageTest.", DeprecationWarning, + stacklevel=2, ) if not expected_msg.end_col_offset == gotten_msg.end_col_offset: warnings.warn( # pragma: no cover @@ -92,6 +93,7 @@ class CheckerTestCase: f"the expected value in {expected_msg}. In pylint 3.0 correct end_col_offset " "attributes will be required for MessageTest.", DeprecationWarning, + stacklevel=2, ) def walk(self, node: nodes.NodeNG) -> None: diff --git a/pylint/testutils/functional/find_functional_tests.py b/pylint/testutils/functional/find_functional_tests.py index 7b86ee642..200cee7ec 100644 --- a/pylint/testutils/functional/find_functional_tests.py +++ b/pylint/testutils/functional/find_functional_tests.py @@ -38,9 +38,11 @@ def get_functional_test_files_from_directory( _check_functional_tests_structure(Path(input_dir)) - for dirpath, _, filenames in os.walk(input_dir): + for dirpath, dirnames, filenames in os.walk(input_dir): if dirpath.endswith("__pycache__"): continue + dirnames.sort() + filenames.sort() for filename in filenames: if filename != "__init__.py" and filename.endswith(".py"): suite.append(FunctionalTestFile(dirpath, filename)) diff --git a/pylint/testutils/functional_test_file.py b/pylint/testutils/functional_test_file.py index fc1cdcbb1..e2bd7f59b 100644 --- a/pylint/testutils/functional_test_file.py +++ b/pylint/testutils/functional_test_file.py @@ -20,4 +20,5 @@ warnings.warn( "'pylint.testutils.functional_test_file' will be accessible from" " the 'pylint.testutils.functional' namespace in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) diff --git a/pylint/testutils/lint_module_test.py b/pylint/testutils/lint_module_test.py index 0d3dbb0bf..d05f7e481 100644 --- a/pylint/testutils/lint_module_test.py +++ b/pylint/testutils/lint_module_test.py @@ -145,7 +145,7 @@ class LintModuleTest: self._runTest() def _should_be_skipped_due_to_version(self) -> bool: - return ( + return ( # type: ignore[no-any-return] sys.version_info < self._linter.config.min_pyver or sys.version_info > self._linter.config.max_pyver ) diff --git a/pylint/testutils/output_line.py b/pylint/testutils/output_line.py index 6a2a2325f..a2c417621 100644 --- a/pylint/testutils/output_line.py +++ b/pylint/testutils/output_line.py @@ -98,6 +98,7 @@ class OutputLine(NamedTuple): "expected confidence level, expected end_line and expected end_column. " "An OutputLine should thus have a length of 8.", DeprecationWarning, + stacklevel=2, ) return cls( row[0], @@ -115,6 +116,7 @@ class OutputLine(NamedTuple): "expected end_line and expected end_column. An OutputLine should thus have " "a length of 8.", DeprecationWarning, + stacklevel=2, ) return cls( row[0], int(row[1]), column, None, None, row[3], row[4], row[5] diff --git a/pylint/testutils/utils.py b/pylint/testutils/utils.py index 4d5b82867..292e991c2 100644 --- a/pylint/testutils/utils.py +++ b/pylint/testutils/utils.py @@ -93,7 +93,7 @@ def create_files(paths: list[str], chroot: str = ".") -> None: path = os.path.join(chroot, path) filename = os.path.basename(path) # path is a directory path - if filename == "": + if not filename: dirs.add(path) # path is a filename path else: diff --git a/pylint/typing.py b/pylint/typing.py index 224e0bd6b..d62618605 100644 --- a/pylint/typing.py +++ b/pylint/typing.py @@ -24,12 +24,13 @@ from typing import ( ) if sys.version_info >= (3, 8): - from typing import Literal, TypedDict + from typing import Literal, Protocol, TypedDict else: - from typing_extensions import Literal, TypedDict + from typing_extensions import Literal, Protocol, TypedDict if TYPE_CHECKING: from pylint.config.callback_actions import _CallbackAction + from pylint.pyreverse.inspector import Project from pylint.reporters.ureports.nodes import Section from pylint.utils import LinterStats @@ -124,11 +125,16 @@ class ExtraMessageOptions(TypedDict, total=False): maxversion: tuple[int, int] minversion: tuple[int, int] shared: bool + default_enabled: bool MessageDefinitionTuple = Union[ Tuple[str, str, str], Tuple[str, str, str, ExtraMessageOptions], ] -# Mypy doesn't support recursive types (yet), see https://github.com/python/mypy/issues/731 -DirectoryNamespaceDict = Dict[Path, Tuple[argparse.Namespace, "DirectoryNamespaceDict"]] # type: ignore[misc] +DirectoryNamespaceDict = Dict[Path, Tuple[argparse.Namespace, "DirectoryNamespaceDict"]] + + +class GetProjectCallable(Protocol): + def __call__(self, module: str, name: str | None = "No Name") -> Project: + ... # pragma: no cover diff --git a/pylint/utils/ast_walker.py b/pylint/utils/ast_walker.py index cc387d860..4d552d995 100644 --- a/pylint/utils/ast_walker.py +++ b/pylint/utils/ast_walker.py @@ -37,7 +37,7 @@ class ASTWalker: def _is_method_enabled(self, method: AstCallback) -> bool: if not hasattr(method, "checks_msgs"): return True - return any(self.linter.is_message_enabled(m) for m in method.checks_msgs) # type: ignore[attr-defined] + return any(self.linter.is_message_enabled(m) for m in method.checks_msgs) def add_checker(self, checker: BaseChecker) -> None: """Walk to the checker's dir and collect visit and leave methods.""" diff --git a/pylint/utils/file_state.py b/pylint/utils/file_state.py index 9624174ad..19122b373 100644 --- a/pylint/utils/file_state.py +++ b/pylint/utils/file_state.py @@ -47,12 +47,14 @@ class FileState: "FileState needs a string as modname argument. " "This argument will be required in pylint 3.0", DeprecationWarning, + stacklevel=2, ) if msg_store is None: warnings.warn( "FileState needs a 'MessageDefinitionStore' as msg_store argument. " "This argument will be required in pylint 3.0", DeprecationWarning, + stacklevel=2, ) self.base_name = modname self._module_msgs_state: MessageStateDict = {} @@ -79,6 +81,7 @@ class FileState: warnings.warn( "'collect_block_lines' has been deprecated and will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) for msg, lines in self._module_msgs_state.items(): self._raw_module_msgs_state[msg] = lines.copy() @@ -292,4 +295,4 @@ class FileState: ) def get_effective_max_line_number(self) -> int | None: - return self._effective_max_line_number + return self._effective_max_line_number # type: ignore[no-any-return] diff --git a/pylint/utils/pragma_parser.py b/pylint/utils/pragma_parser.py index 8e34fa693..df3627380 100644 --- a/pylint/utils/pragma_parser.py +++ b/pylint/utils/pragma_parser.py @@ -5,8 +5,8 @@ from __future__ import annotations import re -from collections import namedtuple from collections.abc import Generator +from typing import NamedTuple # Allow stopping after the first semicolon/hash encountered, # so that an option can be continued with the reasons @@ -27,7 +27,9 @@ OPTION_RGX = r""" OPTION_PO = re.compile(OPTION_RGX, re.VERBOSE) -PragmaRepresenter = namedtuple("PragmaRepresenter", "action messages") +class PragmaRepresenter(NamedTuple): + action: str + messages: list[str] ATOMIC_KEYWORDS = frozenset(("disable-all", "skip-file")) diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 6a4277642..054d307bc 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -6,6 +6,7 @@ from __future__ import annotations try: import isort.api + import isort.settings HAS_ISORT_5 = True except ImportError: # isort < 5 @@ -280,6 +281,7 @@ def get_global_option( "get_global_option has been deprecated. You can use " "checker.linter.config to get all global options instead.", DeprecationWarning, + stacklevel=2, ) return getattr(checker.linter.config, option.replace("-", "_")) @@ -366,6 +368,7 @@ def format_section( warnings.warn( "format_section has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) if doc: print(_comment(doc), file=stream) @@ -380,6 +383,7 @@ def _ini_format(stream: TextIO, options: list[tuple[str, OptionDict, Any]]) -> N warnings.warn( "_ini_format has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, + stacklevel=2, ) for optname, optdict, value in options: # Skip deprecated option @@ -413,7 +417,7 @@ class IsortDriver: def __init__(self, config: argparse.Namespace) -> None: if HAS_ISORT_5: - self.isort5_config = isort.api.Config( + self.isort5_config = isort.settings.Config( # There is no typo here. EXTRA_standard_library is # what most users want. The option has been named # KNOWN_standard_library for ages in pylint, and we @@ -423,7 +427,7 @@ class IsortDriver: ) else: # pylint: disable-next=no-member - self.isort4_obj = isort.SortImports( + self.isort4_obj = isort.SortImports( # type: ignore[attr-defined] file_contents="", known_standard_library=config.known_standard_library, known_third_party=config.known_third_party, @@ -432,4 +436,4 @@ class IsortDriver: def place_module(self, package: str) -> str: if HAS_ISORT_5: return isort.api.place_module(package, self.isort5_config) - return self.isort4_obj.place_module(package) + return self.isort4_obj.place_module(package) # type: ignore[no-any-return] @@ -32,6 +32,8 @@ load-plugins= pylint.extensions.typing, pylint.extensions.redefined_variable_type, pylint.extensions.comparison_placement, + pylint.extensions.broad_try_clause, + pylint.extensions.dict_init_mutate, # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. @@ -320,7 +322,7 @@ method-naming-style=snake_case # Regular expression matching correct method names method-rgx=[a-z_][a-z0-9_]{2,}$ -# Regular expression which can overwrite the naming style set by typevar-naming-style. +# Regular expression matching correct type variable names #typevar-rgx= # Regular expression which should only match function or class names that do @@ -444,6 +446,9 @@ max-public-methods=25 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 +# Maximum number of statements in a try-block +max-try-statements = 14 + # List of regular expressions of class ancestor names to # ignore when counting public methods (see R0903). exclude-too-few-public-methods= @@ -510,7 +515,7 @@ preferred-modules= # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception [TYPING] diff --git a/pyproject.toml b/pyproject.toml index 90153e9de..0dadcf6c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Debuggers", @@ -78,3 +79,53 @@ pylint = ["testutils/testing_pylintrc"] [tool.setuptools.dynamic] version = {attr = "pylint.__pkginfo__.__version__"} + +[tool.aliases] +test = "pytest" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["*test_*.py"] +addopts = "--strict-markers" +filterwarnings = "error" +markers = [ + "primer_stdlib: Checks for crashes and errors when running pylint on stdlib", + "primer_external_batch_one: Checks for crashes and errors when running pylint on external libs (batch one)", + "benchmark: Baseline of pylint performance, if this regress something serious happened", + "timeout: Marks from pytest-timeout.", + "needs_two_cores: Checks that need 2 or more cores to be meaningful", +] + +[tool.isort] +profile = "black" +known_third_party = ["platformdirs", "astroid", "sphinx", "isort", "pytest", "mccabe", "six", "toml"] +skip_glob = ["tests/functional/**", "tests/input/**", "tests/extensions/data/**", "tests/regrtest_data/**", "tests/data/**", "astroid/**", "venv/**"] +src_paths = ["pylint"] + +[tool.mypy] +scripts_are_modules = true +warn_unused_ignores = true +show_error_codes = true +enable_error_code = "ignore-without-code" +strict = true +# TODO: Remove this once pytest has annotations +disallow_untyped_decorators = false + +[[tool.mypy.overrides]] +ignore_missing_imports = true +module = [ + "_pytest.*", + "_string", + "astroid.*", + # `colorama` ignore is needed for Windows environment + "colorama", + "contributors_txt", + "coverage", + "dill", + "enchant.*", + "git.*", + "mccabe", + "pytest_benchmark.*", + "pytest", + "sphinx.*", +] diff --git a/requirements_test.txt b/requirements_test.txt index 6ebb575cb..f5bfb9f45 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -r requirements_test_pre_commit.txt -r requirements_test_min.txt coveralls~=3.3 -coverage~=6.4 +coverage~=6.5 pre-commit~=2.20 tbump~=6.9.0 contributors-txt>=0.9.0 -pytest-cov~=3.0 +pytest-cov~=4.0 pytest-profiling~=1.7 -pytest-xdist~=2.5 +pytest-xdist~=3.0 # Type packages for mypy types-pkg_resources==0.1.3 tox>=3 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 54241ebbb..14daa70da 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,8 @@ # Everything in this file should reflect the pre-commit configuration # in .pre-commit-config.yaml -black==22.6.0 +black==22.10.0 flake8==5.0.4 -flake8-typing-imports==1.13.0 +flake8-bugbear==22.10.27 +flake8-typing-imports==1.14.0 isort==5.10.1 -mypy==0.971 +mypy==0.991 diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index 6563c71d4..44ca36e20 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -602,5 +602,9 @@ "yileiyang@google.com": { "mails": ["yileiyang@google.com"], "name": "Yilei \"Dolee\" Yang" + }, + "hofrob@protonmail.com": { + "mails": ["hofrob@protonmail.com"], + "name": "Robert Hofer" } } diff --git a/script/check_newsfragments.py b/script/check_newsfragments.py index 577b4ba88..3327d2d5d 100644 --- a/script/check_newsfragments.py +++ b/script/check_newsfragments.py @@ -51,14 +51,14 @@ def check_file(file: Path, verbose: bool) -> bool: if match: issue = match.group("issue") if file.stem != issue: - print( + echo( f"{file} must be named '{issue}.<fragmenttype>', after the issue it references." ) return False if verbose: - print(f"Checked '{file}': LGTM 🤖👍") + echo(f"Checked '{file}': LGTM 🤖👍") return True - print( + echo( f"""\ {file}: does not respect the standard format 🤖👎 @@ -82,5 +82,11 @@ Refs #1234 return False +def echo(msg: str) -> None: + # To support non-UTF-8 environments like Windows, we need + # to explicitly encode the message instead of using plain print() + sys.stdout.buffer.write(f"{msg}\n".encode()) + + if __name__ == "__main__": sys.exit(main()) diff --git a/script/create_contributor_list.py b/script/create_contributor_list.py index 7db3923f5..4502f824d 100644 --- a/script/create_contributor_list.py +++ b/script/create_contributor_list.py @@ -12,7 +12,7 @@ ALIASES_FILE = (BASE_DIRECTORY / "script/.contributors_aliases.json").relative_t DEFAULT_CONTRIBUTOR_PATH = (BASE_DIRECTORY / "CONTRIBUTORS.txt").relative_to(CWD) -def main(): +def main() -> None: create_contributors_txt( aliases_file=ALIASES_FILE, output=DEFAULT_CONTRIBUTOR_PATH, verbose=True ) diff --git a/script/fix_documentation.py b/script/fix_documentation.py index 1c97459c8..e8def2f73 100644 --- a/script/fix_documentation.py +++ b/script/fix_documentation.py @@ -36,12 +36,7 @@ def changelog_insert_empty_lines(file_content: str, subtitle_text: str) -> str: for i, line in enumerate(lines): if line.startswith(subtitle_text): subtitle_count += 1 - if ( - subtitle_count == 1 - or i < 2 - or lines[i - 1] == "" - and lines[i - 2] == "" - ): + if subtitle_count == 1 or i < 2 or not lines[i - 1] and not lines[i - 2]: continue lines.insert(i, "") return "\n".join(lines) @@ -8,26 +8,6 @@ license_files = LICENSE CONTRIBUTORS.txt -[aliases] -test = pytest - -[tool:pytest] -testpaths = tests -python_files = *test_*.py -addopts = --strict-markers -markers = - primer_stdlib: Checks for crashes and errors when running pylint on stdlib - primer_external_batch_one: Checks for crashes and errors when running pylint on external libs (batch one) - benchmark: Baseline of pylint performance, if this regress something serious happened - timeout: Marks from pytest-timeout. - needs_two_cores: Checks that need 2 or more cores to be meaningful - -[isort] -profile = black -known_third_party = platformdirs, astroid, sphinx, isort, pytest, mccabe, six, toml -skip_glob = tests/functional/**,tests/input/**,tests/extensions/data/**,tests/regrtest_data/**,tests/data/**,astroid/**,venv/** -src_paths = pylint - [flake8] ignore = E203, W503, # Incompatible with black see https://github.com/ambv/black/issues/315 @@ -37,58 +17,3 @@ max-complexity=39 # Required for flake8-typing-imports (v1.12.0) # The plugin doesn't yet read the value from pyproject.toml min_python_version = 3.7.2 - -[mypy] -no_implicit_optional = True -scripts_are_modules = True -warn_unused_ignores = True -show_error_codes = True -enable_error_code = ignore-without-code - -[mypy-astroid.*] -ignore_missing_imports = True - -[mypy-tests.*] -ignore_missing_imports = True - -[mypy-contributors_txt] -ignore_missing_imports = True - -[mypy-coverage] -ignore_missing_imports = True - -[mypy-enchant.*] -ignore_missing_imports = True - -[mypy-isort.*] -ignore_missing_imports = True - -[mypy-mccabe] -ignore_missing_imports = True - -[mypy-pytest] -ignore_missing_imports = True - -[mypy-_pytest.*] -ignore_missing_imports = True - -[mypy-setuptools] -ignore_missing_imports = True - -[mypy-_string] -ignore_missing_imports = True - -[mypy-git.*] -ignore_missing_imports = True - -[mypy-tomlkit] -ignore_missing_imports = True - -[mypy-sphinx.*] -ignore_missing_imports = True - -[mypy-dill] -ignore_missing_imports = True - -[mypy-colorama] -ignore_missing_imports = True diff --git a/tbump.toml b/tbump.toml index 166afddb6..3b61c031d 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/pylint" [version] -current = "2.15.7" +current = "2.16.0-dev" regex = ''' ^(?P<major>0|[1-9]\d*) \. diff --git a/tests/benchmark/test_baseline_benchmarks.py b/tests/benchmark/test_baseline_benchmarks.py index 6fb1cdf18..42521b593 100644 --- a/tests/benchmark/test_baseline_benchmarks.py +++ b/tests/benchmark/test_baseline_benchmarks.py @@ -13,6 +13,7 @@ from unittest.mock import patch import pytest from astroid import nodes +from pytest_benchmark.fixture import BenchmarkFixture from pylint.checkers import BaseRawFileChecker from pylint.lint import PyLinter, check_parallel @@ -22,7 +23,7 @@ from pylint.typing import FileItem from pylint.utils import register_plugins -def _empty_filepath(): +def _empty_filepath() -> str: return os.path.abspath( os.path.join( os.path.dirname(__file__), "..", "input", "benchmark_minimal_file.py" @@ -114,7 +115,7 @@ class TestEstablishBaselineBenchmarks: ) lot_of_files = 500 - def test_baseline_benchmark_j1(self, benchmark): + def test_baseline_benchmark_j1(self, benchmark: BenchmarkFixture) -> None: """Establish a baseline of pylint performance with no work. We will add extra Checkers in other benchmarks. @@ -131,7 +132,7 @@ class TestEstablishBaselineBenchmarks: ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_benchmark_j2(self, benchmark): + def test_baseline_benchmark_j2(self, benchmark: BenchmarkFixture) -> None: """Establish a baseline of pylint performance with no work across threads. Same as `test_baseline_benchmark_j1` but we use -j2 with 2 fake files to @@ -154,7 +155,9 @@ class TestEstablishBaselineBenchmarks: ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_benchmark_check_parallel_j2(self, benchmark): + def test_baseline_benchmark_check_parallel_j2( + self, benchmark: BenchmarkFixture + ) -> None: """Should demonstrate times very close to `test_baseline_benchmark_j2`.""" linter = PyLinter(reporter=Reporter()) @@ -167,7 +170,7 @@ class TestEstablishBaselineBenchmarks: linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" - def test_baseline_lots_of_files_j1(self, benchmark): + def test_baseline_lots_of_files_j1(self, benchmark: BenchmarkFixture) -> None: """Establish a baseline with only 'main' checker being run in -j1. We do not register any checkers except the default 'main', so the cost is just @@ -187,7 +190,7 @@ class TestEstablishBaselineBenchmarks: ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_lots_of_files_j2(self, benchmark): + def test_baseline_lots_of_files_j2(self, benchmark: BenchmarkFixture) -> None: """Establish a baseline with only 'main' checker being run in -j2. As with the -j1 variant above `test_baseline_lots_of_files_j1`, we do not @@ -207,7 +210,9 @@ class TestEstablishBaselineBenchmarks: linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" - def test_baseline_lots_of_files_j1_empty_checker(self, benchmark): + def test_baseline_lots_of_files_j1_empty_checker( + self, benchmark: BenchmarkFixture + ) -> None: """Baselines pylint for a single extra checker being run in -j1, for N-files. We use a checker that does no work, so the cost is just that of the system at @@ -228,7 +233,9 @@ class TestEstablishBaselineBenchmarks: ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_lots_of_files_j2_empty_checker(self, benchmark): + def test_baseline_lots_of_files_j2_empty_checker( + self, benchmark: BenchmarkFixture + ) -> None: """Baselines pylint for a single extra checker being run in -j2, for N-files. We use a checker that does no work, so the cost is just that of the system at @@ -248,7 +255,9 @@ class TestEstablishBaselineBenchmarks: linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" - def test_baseline_benchmark_j1_single_working_checker(self, benchmark): + def test_baseline_benchmark_j1_single_working_checker( + self, benchmark: BenchmarkFixture + ) -> None: """Establish a baseline of single-worker performance for PyLinter. Here we mimic a single Checker that does some work so that we can see the @@ -275,7 +284,9 @@ class TestEstablishBaselineBenchmarks: ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" @pytest.mark.needs_two_cores - def test_baseline_benchmark_j2_single_working_checker(self, benchmark): + def test_baseline_benchmark_j2_single_working_checker( + self, benchmark: BenchmarkFixture + ) -> None: """Establishes baseline of multi-worker performance for PyLinter/check_parallel. We expect this benchmark to take less time that test_baseline_benchmark_j1, @@ -302,7 +313,9 @@ class TestEstablishBaselineBenchmarks: linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" - def test_baseline_benchmark_j1_all_checks_single_file(self, benchmark): + def test_baseline_benchmark_j1_all_checks_single_file( + self, benchmark: BenchmarkFixture + ) -> None: """Runs a single file, with -j1, against all checkers/Extensions.""" args = [self.empty_filepath, "--enable=all", "--enable-all-extensions"] runner = benchmark(Run, args, reporter=Reporter(), exit=False) @@ -314,7 +327,9 @@ class TestEstablishBaselineBenchmarks: runner.linter.msg_status == 0 ), f"Expected no errors to be thrown: {pprint.pformat(runner.linter.reporter.messages)}" - def test_baseline_benchmark_j1_all_checks_lots_of_files(self, benchmark): + def test_baseline_benchmark_j1_all_checks_lots_of_files( + self, benchmark: BenchmarkFixture + ) -> None: """Runs lots of files, with -j1, against all plug-ins. ... that's the intent at least. diff --git a/tests/checkers/base/unittest_base.py b/tests/checkers/base/unittest_base.py index 05a7271cd..99a8f659e 100644 --- a/tests/checkers/base/unittest_base.py +++ b/tests/checkers/base/unittest_base.py @@ -9,7 +9,7 @@ import unittest class TestNoSix(unittest.TestCase): @unittest.skip("too many dependencies need six :(") - def test_no_six(self): + def test_no_six(self) -> None: try: has_six = True except ImportError: diff --git a/tests/checkers/unittest_deprecated.py b/tests/checkers/unittest_deprecated.py index 1c5fc2b16..a63ebfc20 100644 --- a/tests/checkers/unittest_deprecated.py +++ b/tests/checkers/unittest_deprecated.py @@ -129,7 +129,7 @@ class TestDeprecatedChecker(CheckerTestCase): line=9, col_offset=0, end_line=9, - end_col_offset=21, + end_col_offset=12, ) ): self.checker.visit_call(node) diff --git a/tests/checkers/unittest_non_ascii_name.py b/tests/checkers/unittest_non_ascii_name.py index 1830dd7ec..4f854dddc 100644 --- a/tests/checkers/unittest_non_ascii_name.py +++ b/tests/checkers/unittest_non_ascii_name.py @@ -23,7 +23,7 @@ class TestNonAsciiChecker(pylint.testutils.CheckerTestCase): @pytest.mark.skipif( sys.version_info < (3, 8), reason="requires python3.8 or higher" ) - def test_kwargs_and_position_only(self): + def test_kwargs_and_position_only(self) -> None: """Even the new position only and keyword only should be found.""" node = astroid.extract_node( """ @@ -136,7 +136,7 @@ class TestNonAsciiChecker(pylint.testutils.CheckerTestCase): self, code: str, assign_type: str, - ): + ) -> None: """Variables defined no matter where, should be checked for non ascii.""" assign_node = astroid.extract_node(code) @@ -261,7 +261,7 @@ class TestNonAsciiChecker(pylint.testutils.CheckerTestCase): ), ], ) - def test_check_import(self, import_statement: str, wrong_name: str | None): + def test_check_import(self, import_statement: str, wrong_name: str | None) -> None: """We expect that for everything that user can change there is a message.""" node = astroid.extract_node(f"{import_statement} #@") diff --git a/tests/checkers/unittest_spelling.py b/tests/checkers/unittest_spelling.py index b07212a19..abeb9dcf8 100644 --- a/tests/checkers/unittest_spelling.py +++ b/tests/checkers/unittest_spelling.py @@ -308,10 +308,10 @@ class TestSpellingChecker(CheckerTestCase): # pylint:disable=too-many-public-me # to show up in the pytest output as part of the test name # when running parameterized tests. self, - misspelled_portion_of_directive, - second_portion_of_directive, - description, - ): + misspelled_portion_of_directive: str, + second_portion_of_directive: str, + description: str, + ) -> None: full_comment = f"# {misspelled_portion_of_directive}{second_portion_of_directive} {misspelled_portion_of_directive}" with self.assertAddsMessages( MessageTest( @@ -386,7 +386,7 @@ class TestSpellingChecker(CheckerTestCase): # pylint:disable=too-many-public-me spelling_dict=spell_dict, spelling_ignore_comment_directives="newdirective:,noqa", ) - def test_skip_directives_specified_in_pylintrc(self): + def test_skip_directives_specified_in_pylintrc(self) -> None: full_comment = "# newdirective: do this newdirective" with self.assertAddsMessages( MessageTest( diff --git a/tests/checkers/unittest_stdlib.py b/tests/checkers/unittest_stdlib.py index 2f47a4075..66747deb9 100644 --- a/tests/checkers/unittest_stdlib.py +++ b/tests/checkers/unittest_stdlib.py @@ -6,23 +6,26 @@ from __future__ import annotations import contextlib from collections.abc import Callable, Iterator -from typing import Any +from typing import Any, Type import astroid from astroid import nodes +from astroid.context import InferenceContext from astroid.manager import AstroidManager from pylint.checkers import stdlib from pylint.testutils import CheckerTestCase +_NodeNGT = Type[nodes.NodeNG] + @contextlib.contextmanager def _add_transform( manager: AstroidManager, - node: type, - transform: Callable, + node: _NodeNGT, + transform: Callable[[_NodeNGT], _NodeNGT], predicate: Any | None = None, -) -> Iterator: +) -> Iterator[None]: manager.register_transform(node, transform, predicate) try: yield @@ -43,8 +46,8 @@ class TestStdlibChecker(CheckerTestCase): def infer_func( inner_node: nodes.Name, - context: Any | None = None, # pylint: disable=unused-argument - ) -> Iterator[Iterator | Iterator[nodes.AssignAttr]]: + context: InferenceContext | None = None, # pylint: disable=unused-argument + ) -> Iterator[nodes.AssignAttr]: new_node = nodes.AssignAttr(attrname="alpha", parent=inner_node) yield new_node diff --git a/tests/checkers/unittest_unicode/unittest_bad_chars.py b/tests/checkers/unittest_unicode/unittest_bad_chars.py index 41445e226..7746ce4ae 100644 --- a/tests/checkers/unittest_unicode/unittest_bad_chars.py +++ b/tests/checkers/unittest_unicode/unittest_bad_chars.py @@ -41,7 +41,9 @@ def bad_char_file_generator(tmp_path: Path) -> Callable[[str, bool, str], Path]: "# Invalid char esc: \x1B", ) - def _bad_char_file_generator(codec: str, add_invalid_bytes: bool, line_ending: str): + def _bad_char_file_generator( + codec: str, add_invalid_bytes: bool, line_ending: str + ) -> Path: byte_suffix = b"" if add_invalid_bytes: if codec == "utf-8": @@ -120,7 +122,7 @@ class TestBadCharsChecker(pylint.testutils.CheckerTestCase): codec_and_msg: tuple[str, tuple[pylint.testutils.MessageTest]], line_ending: str, add_invalid_bytes: bool, - ): + ) -> None: """All combinations of bad characters that are accepted by Python at the moment are tested in all possible combinations of - line ending @@ -215,7 +217,7 @@ class TestBadCharsChecker(pylint.testutils.CheckerTestCase): char: str, msg_id: str, codec_and_msg: tuple[str, tuple[pylint.testutils.MessageTest]], - ): + ) -> None: """Special test for a file containing chars that lead to Python or Astroid crashes (which causes Pylint to exit early) """ diff --git a/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py b/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py index c450db211..6b11dcfef 100644 --- a/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py +++ b/tests/checkers/unittest_unicode/unittest_bidirectional_unicode.py @@ -78,7 +78,7 @@ class TestBidirectionalUnicodeChecker(pylint.testutils.CheckerTestCase): ) ], ) - def test_find_bidi_string(self, bad_string: str, codec: str): + def test_find_bidi_string(self, bad_string: str, codec: str) -> None: """Ensure that all Bidirectional strings are detected. Tests also UTF-16 and UTF-32. diff --git a/tests/checkers/unittest_unicode/unittest_functions.py b/tests/checkers/unittest_unicode/unittest_functions.py index c2fef9357..0c809ccdc 100644 --- a/tests/checkers/unittest_unicode/unittest_functions.py +++ b/tests/checkers/unittest_unicode/unittest_functions.py @@ -105,8 +105,10 @@ SEARCH_DICT_BYTE_UTF8 = { def test_map_positions_to_result( line: pylint.checkers.unicode._StrLike, expected: dict[int, pylint.checkers.unicode._BadChar], - search_dict, -): + search_dict: dict[ + pylint.checkers.unicode._StrLike, pylint.checkers.unicode._BadChar + ], +) -> None: """Test all possible outcomes for map position function in UTF-8 and ASCII.""" if isinstance(line, bytes): newline = b"\n" @@ -133,7 +135,7 @@ def test_map_positions_to_result( pytest.param(b"12345678\n\r", id="wrong_order_byte"), ], ) -def test_line_length(line: pylint.checkers.unicode._StrLike): +def test_line_length(line: pylint.checkers.unicode._StrLike) -> None: assert pylint.checkers.unicode._line_length(line, "utf-8") == 10 @@ -146,7 +148,7 @@ def test_line_length(line: pylint.checkers.unicode._StrLike): pytest.param("12345678\n\r", id="wrong_order"), ], ) -def test_line_length_utf16(line: str): +def test_line_length_utf16(line: str) -> None: assert pylint.checkers.unicode._line_length(line.encode("utf-16"), "utf-16") == 10 @@ -159,7 +161,7 @@ def test_line_length_utf16(line: str): pytest.param("12345678\n\r", id="wrong_order"), ], ) -def test_line_length_utf32(line: str): +def test_line_length_utf32(line: str) -> None: assert pylint.checkers.unicode._line_length(line.encode("utf-32"), "utf-32") == 10 @@ -186,7 +188,7 @@ def test_line_length_utf32(line: str): ("ASCII", "ascii"), ], ) -def test__normalize_codec_name(codec: str, expected: str): +def test__normalize_codec_name(codec: str, expected: str) -> None: assert pylint.checkers.unicode._normalize_codec_name(codec) == expected @@ -216,7 +218,7 @@ def test__normalize_codec_name(codec: str, expected: str): ) def test___fix_utf16_32_line_stream( tmp_path: Path, codec: str, line_ending: str, final_new_line: bool -): +) -> None: """Content of stream should be the same as should be the length.""" def decode_line(line: bytes, codec: str) -> str: @@ -260,5 +262,5 @@ def test___fix_utf16_32_line_stream( ("ascii", 1), ], ) -def test__byte_to_str_length(codec: str, expected: int): +def test__byte_to_str_length(codec: str, expected: int) -> None: assert pylint.checkers.unicode._byte_to_str_length(codec) == expected diff --git a/tests/checkers/unittest_unicode/unittest_invalid_encoding.py b/tests/checkers/unittest_unicode/unittest_invalid_encoding.py index d2807301c..e8695a74f 100644 --- a/tests/checkers/unittest_unicode/unittest_invalid_encoding.py +++ b/tests/checkers/unittest_unicode/unittest_invalid_encoding.py @@ -52,7 +52,9 @@ class TestInvalidEncoding(pylint.testutils.CheckerTestCase): ("pep_bidirectional_utf_32_bom.txt", 1), ], ) - def test_invalid_unicode_files(self, tmp_path: Path, test_file: str, line_no: int): + def test_invalid_unicode_files( + self, tmp_path: Path, test_file: str, line_no: int + ) -> None: test_file_path = UNICODE_TESTS / test_file target = shutil.copy( test_file_path, tmp_path / test_file.replace(".txt", ".py") @@ -126,7 +128,7 @@ class TestInvalidEncoding(pylint.testutils.CheckerTestCase): ), ], ) - def test__determine_codec(self, content: bytes, codec: str, line: int): + def test__determine_codec(self, content: bytes, codec: str, line: int) -> None: """The codec determined should be exact no matter what we throw at it.""" assert self.checker._determine_codec(io.BytesIO(content)) == (codec, line) @@ -139,6 +141,8 @@ class TestInvalidEncoding(pylint.testutils.CheckerTestCase): "codec, msg", (pytest.param(codec, msg, id=codec) for codec, msg in CODEC_AND_MSG), ) - def test___check_codec(self, codec: str, msg: tuple[pylint.testutils.MessageTest]): + def test___check_codec( + self, codec: str, msg: tuple[pylint.testutils.MessageTest] + ) -> None: with self.assertAddsMessages(*msg): self.checker._check_codec(codec, 1) diff --git a/tests/checkers/unittest_utils.py b/tests/checkers/unittest_utils.py index f68a48dbb..08b9c188d 100644 --- a/tests/checkers/unittest_utils.py +++ b/tests/checkers/unittest_utils.py @@ -25,7 +25,7 @@ from pylint.checkers.base_checker import BaseChecker ("mybuiltin", False), ], ) -def testIsBuiltin(name, expected): +def testIsBuiltin(name: str, expected: bool) -> None: assert utils.is_builtin(name) == expected @@ -491,7 +491,7 @@ def test_deprecation_check_messages() -> None: ) -def test_is_typing_literal() -> None: +def test_is_typing_member() -> None: code = astroid.extract_node( """ from typing import Literal as Lit, Set as Literal @@ -503,9 +503,9 @@ def test_is_typing_literal() -> None: """ ) - assert not utils.is_typing_literal(code[0]) - assert utils.is_typing_literal(code[1]) - assert utils.is_typing_literal(code[2]) + assert not utils.is_typing_member(code[0], ("Literal",)) + assert utils.is_typing_member(code[1], ("Literal",)) + assert utils.is_typing_member(code[2], ("Literal",)) code = astroid.extract_node( """ @@ -513,5 +513,5 @@ def test_is_typing_literal() -> None: typing.Literal #@ """ ) - assert not utils.is_typing_literal(code[0]) - assert not utils.is_typing_literal(code[1]) + assert not utils.is_typing_member(code[0], ("Literal",)) + assert not utils.is_typing_member(code[1], ("Literal",)) diff --git a/tests/config/pylint_config/test_pylint_config_generate.py b/tests/config/pylint_config/test_pylint_config_generate.py index 4650ab1fb..65fc05557 100644 --- a/tests/config/pylint_config/test_pylint_config_generate.py +++ b/tests/config/pylint_config/test_pylint_config_generate.py @@ -23,6 +23,9 @@ def test_generate_interactive_exitcode(monkeypatch: MonkeyPatch) -> None: "pylint.config._pylint_config.utils.get_and_validate_format", lambda: "toml" ) monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_minimal_setting", lambda: False + ) + monkeypatch.setattr( "pylint.config._pylint_config.utils.get_and_validate_output_file", lambda: (False, Path()), ) @@ -42,6 +45,9 @@ def test_format_of_output( """Check that we output the correct format.""" # Monkeypatch everything we don't want to check in this test monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_minimal_setting", lambda: False + ) + monkeypatch.setattr( "pylint.config._pylint_config.utils.get_and_validate_output_file", lambda: (False, Path()), ) @@ -90,6 +96,9 @@ def test_writing_to_output_file( monkeypatch.setattr( "pylint.config._pylint_config.utils.get_and_validate_format", lambda: "toml" ) + monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_minimal_setting", lambda: False + ) # Set up a temporary file to write to tempfile_name = Path(tempfile.gettempdir()) / "CONFIG" @@ -150,3 +159,34 @@ def test_writing_to_output_file( Run(["generate", "--interactive"], exit=False) captured = capsys.readouterr() assert last_modified != tempfile_name.stat().st_mtime + + +def test_writing_minimal_file( + monkeypatch: MonkeyPatch, capsys: CaptureFixture[str] +) -> None: + """Check that we can write a minimal file.""" + # Monkeypatch everything we don't want to check in this test + monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_and_validate_format", lambda: "toml" + ) + monkeypatch.setattr( + "pylint.config._pylint_config.utils.get_and_validate_output_file", + lambda: (False, Path()), + ) + + # Set the answers needed for the input() calls + answers = iter(["no", "yes"]) + monkeypatch.setattr("builtins.input", lambda x: next(answers)) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="NOTE:.*", category=UserWarning) + # Check not minimal has comments + Run(["generate", "--interactive"], exit=False) + captured = capsys.readouterr() + assert any(line.startswith("#") for line in captured.out.splitlines()) + + # Check minimal doesn't have comments and no default values + Run(["--accept-no-return-doc=y", "generate", "--interactive"], exit=False) + captured = capsys.readouterr() + assert not any(i.startswith("#") for i in captured.out.split("\n")) + assert "accept-no-return-doc" not in captured.out diff --git a/tests/config/test_argparse_config.py b/tests/config/test_argparse_config.py index 3bad5e8fa..a9d7f70c2 100644 --- a/tests/config/test_argparse_config.py +++ b/tests/config/test_argparse_config.py @@ -43,7 +43,10 @@ class TestArgparseOptionsProviderMixin: def test_logger_rcfile() -> None: """Check that we parse the rcfile for the logging checker correctly.""" with pytest.raises(SystemExit) as ex: - Run([LOGGING_TEST, f"--rcfile={LOGGING_TEST.replace('.py', '.rc')}"]) + # replace only the last .py in the string with .rc + # we do so by inverting the string and replace the first occurrence (of the inverted tokens!) + _rcfile = LOGGING_TEST[::-1].replace("yp.", "cr.", 1)[::-1] + Run([LOGGING_TEST, f"--rcfile={_rcfile}"]) assert ex.value.code == 0 diff --git a/tests/config/test_find_default_config_files.py b/tests/config/test_find_default_config_files.py index 10484be1d..2fd66544d 100644 --- a/tests/config/test_find_default_config_files.py +++ b/tests/config/test_find_default_config_files.py @@ -233,7 +233,7 @@ disable = logging-not-lazy,logging-format-interpolation ], ], ) -def test_cfg_has_config(content: str, expected: str, tmp_path: Path) -> None: +def test_cfg_has_config(content: str, expected: bool, tmp_path: Path) -> None: """Test that a cfg file has a pylint config.""" fake_cfg = tmp_path / "fake.cfg" with open(fake_cfg, "w", encoding="utf8") as f: diff --git a/tests/config/test_functional_config_loading.py b/tests/config/test_functional_config_loading.py index 64227df79..432bdc1a1 100644 --- a/tests/config/test_functional_config_loading.py +++ b/tests/config/test_functional_config_loading.py @@ -64,9 +64,9 @@ def test_functional_config_loading( configuration_path: str, default_configuration: PylintConfiguration, file_to_lint_path: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], caplog: LogCaptureFixture, -): +) -> None: """Functional tests for configurations.""" # logging is helpful to see what's expected and why. The output of the # program is checked during the test so printing messes with the result. diff --git a/tests/config/test_per_directory_config.py b/tests/config/test_per_directory_config.py index 9bc0ef9bc..e0bf75e70 100644 --- a/tests/config/test_per_directory_config.py +++ b/tests/config/test_per_directory_config.py @@ -2,17 +2,16 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -from py._path.local import LocalPath # type: ignore[import] +from pathlib import Path from pylint.lint import Run -def test_fall_back_on_base_config(tmpdir: LocalPath) -> None: +def test_fall_back_on_base_config(tmp_path: Path) -> None: """Test that we correctly fall back on the base config.""" # A file under the current dir should fall back to the highest level # For pylint this is ./pylintrc - test_file = tmpdir / "test.py" + test_file = tmp_path / "test.py" runner = Run([__name__], exit=False) assert id(runner.linter.config) == id(runner.linter._base_config) diff --git a/tests/config/unittest_config.py b/tests/config/unittest_config.py index a75de41bb..343663602 100644 --- a/tests/config/unittest_config.py +++ b/tests/config/unittest_config.py @@ -17,19 +17,19 @@ from pylint.typing import MessageDefinitionTuple def test__regexp_validator_valid() -> None: - result = config.option._regexp_validator(None, None, "test_.*") + result = config.option._regexp_validator(None, "", "test_.*") assert isinstance(result, re.Pattern) assert result.pattern == "test_.*" def test__regexp_validator_invalid() -> None: with pytest.raises(re.error): - config.option._regexp_validator(None, None, "test_)") + config.option._regexp_validator(None, "", "test_)") def test__csv_validator_no_spaces() -> None: values = ["One", "Two", "Three"] - result = config.option._csv_validator(None, None, ",".join(values)) + result = config.option._csv_validator(None, "", ",".join(values)) assert isinstance(result, list) assert len(result) == 3 for i, value in enumerate(values): @@ -38,7 +38,7 @@ def test__csv_validator_no_spaces() -> None: def test__csv_validator_spaces() -> None: values = ["One", "Two", "Three"] - result = config.option._csv_validator(None, None, ", ".join(values)) + result = config.option._csv_validator(None, "", ", ".join(values)) assert isinstance(result, list) assert len(result) == 3 for i, value in enumerate(values): @@ -47,7 +47,7 @@ def test__csv_validator_spaces() -> None: def test__regexp_csv_validator_valid() -> None: pattern_strings = ["test_.*", "foo\\.bar", "^baz$"] - result = config.option._regexp_csv_validator(None, None, ",".join(pattern_strings)) + result = config.option._regexp_csv_validator(None, "", ",".join(pattern_strings)) for i, regex in enumerate(result): assert isinstance(regex, re.Pattern) assert regex.pattern == pattern_strings[i] @@ -56,7 +56,7 @@ def test__regexp_csv_validator_valid() -> None: def test__regexp_csv_validator_invalid() -> None: pattern_strings = ["test_.*", "foo\\.bar", "^baz)$"] with pytest.raises(re.error): - config.option._regexp_csv_validator(None, None, ",".join(pattern_strings)) + config.option._regexp_csv_validator(None, "", ",".join(pattern_strings)) class TestPyLinterOptionSetters(CheckerTestCase): diff --git a/tests/conftest.py b/tests/conftest.py index 1ebe62e97..a35e5cc14 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,13 +7,16 @@ from __future__ import annotations import os +from collections.abc import Callable from pathlib import Path import pytest from pylint import checkers +from pylint.checkers import BaseChecker from pylint.lint import PyLinter from pylint.lint.run import _cpu_count +from pylint.reporters import BaseReporter from pylint.testutils import MinimalTestReporter HERE = Path(__file__).parent @@ -25,7 +28,13 @@ def tests_directory() -> Path: @pytest.fixture -def linter(checker, register, enable, disable, reporter): +def linter( + checker: type[BaseChecker] | None, + register: Callable[[PyLinter], None] | None, + enable: str | None, + disable: str | None, + reporter: type[BaseReporter], +) -> PyLinter: _linter = PyLinter() _linter.set_reporter(reporter()) checkers.initialize(_linter) @@ -44,31 +53,31 @@ def linter(checker, register, enable, disable, reporter): @pytest.fixture(scope="module") -def checker(): +def checker() -> None: return None @pytest.fixture(scope="module") -def register(): +def register() -> None: return None @pytest.fixture(scope="module") -def enable(): +def enable() -> None: return None @pytest.fixture(scope="module") -def disable(): +def disable() -> None: return None @pytest.fixture(scope="module") -def reporter(): +def reporter() -> type[MinimalTestReporter]: return MinimalTestReporter -def pytest_addoption(parser) -> None: +def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--primer-stdlib", action="store_true", diff --git a/tests/extensions/test_check_docs_utils.py b/tests/extensions/test_check_docs_utils.py index 3e70ffbfd..692c14859 100644 --- a/tests/extensions/test_check_docs_utils.py +++ b/tests/extensions/test_check_docs_utils.py @@ -3,8 +3,12 @@ # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt """Unit tests for utils functions in :mod:`pylint.extensions._check_docs_utils`.""" + +from __future__ import annotations + import astroid import pytest +from astroid import nodes from pylint.extensions import _check_docs_utils as utils @@ -134,7 +138,7 @@ def test_space_indentation(string: str, count: int) -> None: ), ], ) -def test_exception(raise_node, expected): +def test_exception(raise_node: nodes.NodeNG, expected: set[str]) -> None: found_nodes = utils.possible_exc_types(raise_node) for node in found_nodes: assert isinstance(node, astroid.nodes.ClassDef) diff --git a/tests/extensions/test_private_import.py b/tests/extensions/test_private_import.py index d2d79947f..c82f51a42 100644 --- a/tests/extensions/test_private_import.py +++ b/tests/extensions/test_private_import.py @@ -4,7 +4,7 @@ """Tests the local module directory comparison logic which requires mocking file directories""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import astroid @@ -19,7 +19,7 @@ class TestPrivateImport(CheckerTestCase): CHECKER_CLASS = private_import.PrivateImportChecker @patch("pathlib.Path.parent") - def test_internal_module(self, parent) -> None: + def test_internal_module(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "module") import_from = astroid.extract_node("""from module import _file""") @@ -27,7 +27,7 @@ class TestPrivateImport(CheckerTestCase): self.checker.visit_importfrom(import_from) @patch("pathlib.Path.parent") - def test_external_module_nested(self, parent) -> None: + def test_external_module_nested(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "module", "module_files", "util") import_from = astroid.extract_node("""from module import _file""") @@ -36,7 +36,7 @@ class TestPrivateImport(CheckerTestCase): self.checker.visit_importfrom(import_from) @patch("pathlib.Path.parent") - def test_external_module_dot_import(self, parent) -> None: + def test_external_module_dot_import(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "outer", "inner", "module_files", "util") import_from = astroid.extract_node("""from outer.inner import _file""") @@ -45,7 +45,7 @@ class TestPrivateImport(CheckerTestCase): self.checker.visit_importfrom(import_from) @patch("pathlib.Path.parent") - def test_external_module_dot_import_outer_only(self, parent) -> None: + def test_external_module_dot_import_outer_only(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "outer", "extensions") import_from = astroid.extract_node("""from outer.inner import _file""") @@ -54,7 +54,7 @@ class TestPrivateImport(CheckerTestCase): self.checker.visit_importfrom(import_from) @patch("pathlib.Path.parent") - def test_external_module(self, parent) -> None: + def test_external_module(self, parent: MagicMock) -> None: parent.parts = ("", "dir", "other") import_from = astroid.extract_node("""from module import _file""") diff --git a/tests/functional/a/abstract/abstract_method.txt b/tests/functional/a/abstract/abstract_method.txt index 2b4ea9a2e..f2b2b6c74 100644 --- a/tests/functional/a/abstract/abstract_method.txt +++ b/tests/functional/a/abstract/abstract_method.txt @@ -1,16 +1,16 @@ -abstract-method:47:0:47:14:Concrete:Method 'bbbb' is abstract in class 'Abstract' but is not overridden:UNDEFINED -abstract-method:70:0:70:15:Container:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:70:0:70:15:Container:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:70:0:70:15:Container:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:76:0:76:13:Sizable:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:76:0:76:13:Sizable:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:76:0:76:13:Sizable:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:82:0:82:14:Hashable:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:82:0:82:14:Hashable:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:82:0:82:14:Hashable:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:87:0:87:14:Iterator:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:87:0:87:14:Iterator:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:87:0:87:14:Iterator:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:106:0:106:19:BadComplexMro:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED -abstract-method:106:0:106:19:BadComplexMro:Method '__len__' is abstract in class 'AbstractSizable' but is not overridden:UNDEFINED -abstract-method:106:0:106:19:BadComplexMro:Method 'length' is abstract in class 'AbstractSizable' but is not overridden:UNDEFINED +abstract-method:47:0:47:14:Concrete:Method 'bbbb' is abstract in class 'Abstract' but is not overridden in child class 'Concrete':INFERENCE +abstract-method:70:0:70:15:Container:Method '__hash__' is abstract in class 'Structure' but is not overridden in child class 'Container':INFERENCE +abstract-method:70:0:70:15:Container:Method '__iter__' is abstract in class 'Structure' but is not overridden in child class 'Container':INFERENCE +abstract-method:70:0:70:15:Container:Method '__len__' is abstract in class 'Structure' but is not overridden in child class 'Container':INFERENCE +abstract-method:76:0:76:13:Sizable:Method '__contains__' is abstract in class 'Structure' but is not overridden in child class 'Sizable':INFERENCE +abstract-method:76:0:76:13:Sizable:Method '__hash__' is abstract in class 'Structure' but is not overridden in child class 'Sizable':INFERENCE +abstract-method:76:0:76:13:Sizable:Method '__iter__' is abstract in class 'Structure' but is not overridden in child class 'Sizable':INFERENCE +abstract-method:82:0:82:14:Hashable:Method '__contains__' is abstract in class 'Structure' but is not overridden in child class 'Hashable':INFERENCE +abstract-method:82:0:82:14:Hashable:Method '__iter__' is abstract in class 'Structure' but is not overridden in child class 'Hashable':INFERENCE +abstract-method:82:0:82:14:Hashable:Method '__len__' is abstract in class 'Structure' but is not overridden in child class 'Hashable':INFERENCE +abstract-method:87:0:87:14:Iterator:Method '__contains__' is abstract in class 'Structure' but is not overridden in child class 'Iterator':INFERENCE +abstract-method:87:0:87:14:Iterator:Method '__hash__' is abstract in class 'Structure' but is not overridden in child class 'Iterator':INFERENCE +abstract-method:87:0:87:14:Iterator:Method '__len__' is abstract in class 'Structure' but is not overridden in child class 'Iterator':INFERENCE +abstract-method:106:0:106:19:BadComplexMro:Method '__hash__' is abstract in class 'Structure' but is not overridden in child class 'BadComplexMro':INFERENCE +abstract-method:106:0:106:19:BadComplexMro:Method '__len__' is abstract in class 'AbstractSizable' but is not overridden in child class 'BadComplexMro':INFERENCE +abstract-method:106:0:106:19:BadComplexMro:Method 'length' is abstract in class 'AbstractSizable' but is not overridden in child class 'BadComplexMro':INFERENCE diff --git a/tests/functional/a/alternative/alternative_union_syntax.py b/tests/functional/a/alternative/alternative_union_syntax.py index 4492323a2..d25d9e8f4 100644 --- a/tests/functional/a/alternative/alternative_union_syntax.py +++ b/tests/functional/a/alternative/alternative_union_syntax.py @@ -1,6 +1,6 @@ """Test PEP 604 - Alternative Union syntax""" # pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring -# pylint: disable=inherit-non-class,too-few-public-methods,unnecessary-direct-lambda-call +# pylint: disable=inherit-non-class,too-few-public-methods,unnecessary-direct-lambda-call,superfluous-parens import dataclasses import typing from dataclasses import dataclass diff --git a/tests/functional/a/arguments_differ.txt b/tests/functional/a/arguments_differ.txt index bb1abb2eb..54a75e84e 100644 --- a/tests/functional/a/arguments_differ.txt +++ b/tests/functional/a/arguments_differ.txt @@ -1,13 +1,13 @@ -arguments-differ:12:4:12:12:Child.test:Number of parameters was 1 in 'Parent.test' and is now 2 in overridden 'Child.test' method:UNDEFINED -arguments-differ:23:4:23:12:ChildDefaults.test:Number of parameters was 3 in 'ParentDefaults.test' and is now 2 in overridden 'ChildDefaults.test' method:UNDEFINED -arguments-differ:41:4:41:12:ClassmethodChild.func:Number of parameters was 2 in 'Classmethod.func' and is now 0 in overridden 'ClassmethodChild.func' method:UNDEFINED -arguments-differ:68:4:68:18:VarargsChild.has_kwargs:Variadics removed in overridden 'VarargsChild.has_kwargs' method:UNDEFINED -arguments-renamed:71:4:71:17:VarargsChild.no_kwargs:Parameter 'args' has been renamed to 'arg' in overridden 'VarargsChild.no_kwargs' method:UNDEFINED -arguments-differ:144:4:144:12:StaticmethodChild2.func:Number of parameters was 1 in 'Staticmethod.func' and is now 2 in overridden 'StaticmethodChild2.func' method:UNDEFINED -arguments-differ:180:4:180:12:SecondChangesArgs.test:Number of parameters was 2 in 'FirstHasArgs.test' and is now 4 in overridden 'SecondChangesArgs.test' method:UNDEFINED -arguments-differ:307:4:307:16:Foo.kwonly_1:Number of parameters was 4 in 'AbstractFoo.kwonly_1' and is now 3 in overridden 'Foo.kwonly_1' method:UNDEFINED -arguments-differ:310:4:310:16:Foo.kwonly_2:Number of parameters was 3 in 'AbstractFoo.kwonly_2' and is now 2 in overridden 'Foo.kwonly_2' method:UNDEFINED -arguments-differ:313:4:313:16:Foo.kwonly_3:Number of parameters was 3 in 'AbstractFoo.kwonly_3' and is now 3 in overridden 'Foo.kwonly_3' method:UNDEFINED -arguments-differ:316:4:316:16:Foo.kwonly_4:Number of parameters was 3 in 'AbstractFoo.kwonly_4' and is now 3 in overridden 'Foo.kwonly_4' method:UNDEFINED -arguments-differ:319:4:319:16:Foo.kwonly_5:Variadics removed in overridden 'Foo.kwonly_5' method:UNDEFINED -arguments-differ:359:4:359:14:ClassWithNewNonDefaultKeywordOnly.method:Number of parameters was 2 in 'AClass.method' and is now 3 in overridden 'ClassWithNewNonDefaultKeywordOnly.method' method:UNDEFINED +arguments-differ:12:4:12:12:Child.test:Number of parameters was 1 in 'Parent.test' and is now 2 in overriding 'Child.test' method:UNDEFINED +arguments-differ:23:4:23:12:ChildDefaults.test:Number of parameters was 3 in 'ParentDefaults.test' and is now 2 in overriding 'ChildDefaults.test' method:UNDEFINED +arguments-differ:41:4:41:12:ClassmethodChild.func:Number of parameters was 2 in 'Classmethod.func' and is now 0 in overriding 'ClassmethodChild.func' method:UNDEFINED +arguments-differ:68:4:68:18:VarargsChild.has_kwargs:Variadics removed in overriding 'VarargsChild.has_kwargs' method:UNDEFINED +arguments-renamed:71:4:71:17:VarargsChild.no_kwargs:Parameter 'args' has been renamed to 'arg' in overriding 'VarargsChild.no_kwargs' method:UNDEFINED +arguments-differ:144:4:144:12:StaticmethodChild2.func:Number of parameters was 1 in 'Staticmethod.func' and is now 2 in overriding 'StaticmethodChild2.func' method:UNDEFINED +arguments-differ:180:4:180:12:SecondChangesArgs.test:Number of parameters was 2 in 'FirstHasArgs.test' and is now 4 in overriding 'SecondChangesArgs.test' method:UNDEFINED +arguments-differ:307:4:307:16:Foo.kwonly_1:Number of parameters was 4 in 'AbstractFoo.kwonly_1' and is now 3 in overriding 'Foo.kwonly_1' method:UNDEFINED +arguments-differ:310:4:310:16:Foo.kwonly_2:Number of parameters was 3 in 'AbstractFoo.kwonly_2' and is now 2 in overriding 'Foo.kwonly_2' method:UNDEFINED +arguments-differ:313:4:313:16:Foo.kwonly_3:Number of parameters was 3 in 'AbstractFoo.kwonly_3' and is now 3 in overriding 'Foo.kwonly_3' method:UNDEFINED +arguments-differ:316:4:316:16:Foo.kwonly_4:Number of parameters was 3 in 'AbstractFoo.kwonly_4' and is now 3 in overriding 'Foo.kwonly_4' method:UNDEFINED +arguments-differ:319:4:319:16:Foo.kwonly_5:Variadics removed in overriding 'Foo.kwonly_5' method:UNDEFINED +arguments-differ:359:4:359:14:ClassWithNewNonDefaultKeywordOnly.method:Number of parameters was 2 in 'AClass.method' and is now 3 in overriding 'ClassWithNewNonDefaultKeywordOnly.method' method:UNDEFINED diff --git a/tests/functional/a/arguments_renamed.txt b/tests/functional/a/arguments_renamed.txt index 47d4188dd..10fe4c207 100644 --- a/tests/functional/a/arguments_renamed.txt +++ b/tests/functional/a/arguments_renamed.txt @@ -1,10 +1,10 @@ -arguments-renamed:17:4:17:12:Orange.brew:Parameter 'fruit_name' has been renamed to 'orange_name' in overridden 'Orange.brew' method:UNDEFINED -arguments-renamed:20:4:20:26:Orange.eat_with_condiment:Parameter 'fruit_name' has been renamed to 'orange_name' in overridden 'Orange.eat_with_condiment' method:UNDEFINED -arguments-differ:27:4:27:26:Banana.eat_with_condiment:Number of parameters was 3 in 'Fruit.eat_with_condiment' and is now 4 in overridden 'Banana.eat_with_condiment' method:UNDEFINED -arguments-renamed:40:4:40:12:Child.test:Parameter 'arg' has been renamed to 'arg1' in overridden 'Child.test' method:UNDEFINED -arguments-differ:43:4:43:19:Child.kwargs_test:Number of parameters was 4 in 'Parent.kwargs_test' and is now 4 in overridden 'Child.kwargs_test' method:UNDEFINED -arguments-renamed:48:4:48:12:Child2.test:Parameter 'arg' has been renamed to 'var' in overridden 'Child2.test' method:UNDEFINED -arguments-differ:51:4:51:19:Child2.kwargs_test:Number of parameters was 4 in 'Parent.kwargs_test' and is now 3 in overridden 'Child2.kwargs_test' method:UNDEFINED -arguments-renamed:67:4:67:13:ChildDefaults.test1:Parameter 'barg' has been renamed to 'param2' in overridden 'ChildDefaults.test1' method:UNDEFINED -arguments-renamed:95:8:95:16:FruitOverrideConditional.brew:Parameter 'fruit_name' has been renamed to 'orange_name' in overridden 'FruitOverrideConditional.brew' method:UNDEFINED -arguments-differ:99:12:99:34:FruitOverrideConditional.eat_with_condiment:Number of parameters was 3 in 'FruitConditional.eat_with_condiment' and is now 4 in overridden 'FruitOverrideConditional.eat_with_condiment' method:UNDEFINED +arguments-renamed:17:4:17:12:Orange.brew:Parameter 'fruit_name' has been renamed to 'orange_name' in overriding 'Orange.brew' method:UNDEFINED +arguments-renamed:20:4:20:26:Orange.eat_with_condiment:Parameter 'fruit_name' has been renamed to 'orange_name' in overriding 'Orange.eat_with_condiment' method:UNDEFINED +arguments-differ:27:4:27:26:Banana.eat_with_condiment:Number of parameters was 3 in 'Fruit.eat_with_condiment' and is now 4 in overriding 'Banana.eat_with_condiment' method:UNDEFINED +arguments-renamed:40:4:40:12:Child.test:Parameter 'arg' has been renamed to 'arg1' in overriding 'Child.test' method:UNDEFINED +arguments-differ:43:4:43:19:Child.kwargs_test:Number of parameters was 4 in 'Parent.kwargs_test' and is now 4 in overriding 'Child.kwargs_test' method:UNDEFINED +arguments-renamed:48:4:48:12:Child2.test:Parameter 'arg' has been renamed to 'var' in overriding 'Child2.test' method:UNDEFINED +arguments-differ:51:4:51:19:Child2.kwargs_test:Number of parameters was 4 in 'Parent.kwargs_test' and is now 3 in overriding 'Child2.kwargs_test' method:UNDEFINED +arguments-renamed:67:4:67:13:ChildDefaults.test1:Parameter 'barg' has been renamed to 'param2' in overriding 'ChildDefaults.test1' method:UNDEFINED +arguments-renamed:95:8:95:16:FruitOverrideConditional.brew:Parameter 'fruit_name' has been renamed to 'orange_name' in overriding 'FruitOverrideConditional.brew' method:UNDEFINED +arguments-differ:99:12:99:34:FruitOverrideConditional.eat_with_condiment:Number of parameters was 3 in 'FruitConditional.eat_with_condiment' and is now 4 in overriding 'FruitOverrideConditional.eat_with_condiment' method:UNDEFINED diff --git a/tests/functional/a/assert_on_tuple.py b/tests/functional/a/assert_on_tuple.py index 3ceb6e167..cf785d53a 100644 --- a/tests/functional/a/assert_on_tuple.py +++ b/tests/functional/a/assert_on_tuple.py @@ -1,11 +1,11 @@ '''Assert check example''' -# pylint: disable=comparison-with-itself, comparison-of-constants -assert (1 == 1, 2 == 2), "no error" +# pylint: disable=comparison-with-itself, comparison-of-constants, line-too-long +assert (1 == 1, 2 == 2), "message is raised even when there is an assert message" # [assert-on-tuple] assert (1 == 1, 2 == 2) # [assert-on-tuple] assert 1 == 1, "no error" -assert (1 == 1, ), "no error" -assert (1 == 1, ) -assert (1 == 1, 2 == 2, 3 == 5), "no error" +assert (1 == 1, ), "message is raised even when there is an assert message" # [assert-on-tuple] +assert (1 == 1, ) # [assert-on-tuple] +assert (1 == 1, 2 == 2, 3 == 5), "message is raised even when there is an assert message" # [assert-on-tuple] assert () assert (True, 'error msg') # [assert-on-tuple] diff --git a/tests/functional/a/assert_on_tuple.txt b/tests/functional/a/assert_on_tuple.txt index 85929cd42..d3e263f23 100644 --- a/tests/functional/a/assert_on_tuple.txt +++ b/tests/functional/a/assert_on_tuple.txt @@ -1,2 +1,6 @@ -assert-on-tuple:5:0:5:23::Assert called on a 2-item-tuple. Did you mean 'assert x,y'?:UNDEFINED -assert-on-tuple:11:0:11:26::Assert called on a 2-item-tuple. Did you mean 'assert x,y'?:UNDEFINED +assert-on-tuple:4:0:4:81::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:5:0:5:23::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:7:0:7:75::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:8:0:8:17::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:9:0:9:89::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH +assert-on-tuple:11:0:11:26::Assert called on a populated tuple. Did you mean 'assert x,y'?:HIGH diff --git a/tests/functional/b/bad_except_order.txt b/tests/functional/b/bad_except_order.txt index c6e6b4471..70443408f 100644 --- a/tests/functional/b/bad_except_order.txt +++ b/tests/functional/b/bad_except_order.txt @@ -1,5 +1,5 @@ -bad-except-order:9:7:9:16::Bad except clauses order (Exception is an ancestor class of TypeError):UNDEFINED -bad-except-order:16:7:16:17::Bad except clauses order (LookupError is an ancestor class of IndexError):UNDEFINED -bad-except-order:23:7:23:38::Bad except clauses order (LookupError is an ancestor class of IndexError):UNDEFINED -bad-except-order:23:7:23:38::Bad except clauses order (NameError is an ancestor class of UnboundLocalError):UNDEFINED -bad-except-order:26:0:31:8::Bad except clauses order (empty except clause should always appear last):UNDEFINED +bad-except-order:9:7:9:16::Bad except clauses order (Exception is an ancestor class of TypeError):INFERENCE +bad-except-order:16:7:16:17::Bad except clauses order (LookupError is an ancestor class of IndexError):INFERENCE +bad-except-order:23:7:23:38::Bad except clauses order (LookupError is an ancestor class of IndexError):INFERENCE +bad-except-order:23:7:23:38::Bad except clauses order (NameError is an ancestor class of UnboundLocalError):INFERENCE +bad-except-order:26:0:31:8::Bad except clauses order (empty except clause should always appear last):HIGH diff --git a/tests/functional/b/bad_exception_cause.py b/tests/functional/b/bad_exception_cause.py index fd9a9cca0..8d8db3677 100644 --- a/tests/functional/b/bad_exception_cause.py +++ b/tests/functional/b/bad_exception_cause.py @@ -28,4 +28,4 @@ def function(): try: pass except function as exc: # [catching-non-exception] - raise Exception from exc # [bad-exception-cause] + raise Exception from exc # [bad-exception-cause, broad-exception-raised] diff --git a/tests/functional/b/bad_exception_cause.txt b/tests/functional/b/bad_exception_cause.txt index ef8ce8831..3aa50fa37 100644 --- a/tests/functional/b/bad_exception_cause.txt +++ b/tests/functional/b/bad_exception_cause.txt @@ -3,3 +3,4 @@ bad-exception-cause:16:4:16:34:test:Exception cause set to something which is no bad-exception-cause:22:4:22:36:test:Exception cause set to something which is not an exception, nor None:INFERENCE catching-non-exception:30:7:30:15::"Catching an exception which doesn't inherit from Exception: function":UNDEFINED bad-exception-cause:31:4:31:28::Exception cause set to something which is not an exception, nor None:INFERENCE +broad-exception-raised:31:4:31:28::"Raising too general exception: Exception":INFERENCE diff --git a/tests/functional/b/bad_reversed_sequence_py37.txt b/tests/functional/b/bad_reversed_sequence_py37.txt index d87c84690..6fbbd2c59 100644 --- a/tests/functional/b/bad_reversed_sequence_py37.txt +++ b/tests/functional/b/bad_reversed_sequence_py37.txt @@ -1,2 +1,2 @@ -bad-reversed-sequence:5:::The first reversed() argument is not a sequence -bad-reversed-sequence:12:::The first reversed() argument is not a sequence +bad-reversed-sequence:5:0:5:26::The first reversed() argument is not a sequence:UNDEFINED +bad-reversed-sequence:12:0:12:39::The first reversed() argument is not a sequence:UNDEFINED diff --git a/tests/functional/b/bad_thread_instantiation.py b/tests/functional/b/bad_thread_instantiation.py index e7e02eaed..3c9aa5e55 100644 --- a/tests/functional/b/bad_thread_instantiation.py +++ b/tests/functional/b/bad_thread_instantiation.py @@ -1,8 +1,24 @@ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, redundant-keyword-arg, invalid-name, line-too-long import threading threading.Thread(lambda: None).run() # [bad-thread-instantiation] threading.Thread(None, lambda: None) +threading.Thread(lambda: None, group=None) # [bad-thread-instantiation] +threading.Thread() # [bad-thread-instantiation] + threading.Thread(group=None, target=lambda: None).run() -threading.Thread() # [bad-thread-instantiation] +threading.Thread(group=None, target=None, name=None, args=(), kwargs={}) +threading.Thread(None, None, "name") + +def thread_target(n): + print(n ** 2) + + +thread = threading.Thread(thread_target, args=(10,)) # [bad-thread-instantiation] + + +kw = {'target_typo': lambda x: x} +threading.Thread(None, **kw) # [unexpected-keyword-arg, bad-thread-instantiation] + +threading.Thread(None, target_typo=lambda x: x) # [unexpected-keyword-arg, bad-thread-instantiation] diff --git a/tests/functional/b/bad_thread_instantiation.txt b/tests/functional/b/bad_thread_instantiation.txt index e969a2473..91358d30a 100644 --- a/tests/functional/b/bad_thread_instantiation.txt +++ b/tests/functional/b/bad_thread_instantiation.txt @@ -1,2 +1,8 @@ -bad-thread-instantiation:5:0:5:30::threading.Thread needs the target function:UNDEFINED -bad-thread-instantiation:8:0:8:18::threading.Thread needs the target function:UNDEFINED +bad-thread-instantiation:5:0:5:30::threading.Thread needs the target function:HIGH +bad-thread-instantiation:7:0:7:42::threading.Thread needs the target function:HIGH +bad-thread-instantiation:8:0:8:18::threading.Thread needs the target function:HIGH +bad-thread-instantiation:18:9:18:52::threading.Thread needs the target function:HIGH +bad-thread-instantiation:22:0:22:28::threading.Thread needs the target function:HIGH +unexpected-keyword-arg:22:0:22:28::Unexpected keyword argument 'target_typo' in constructor call:UNDEFINED +bad-thread-instantiation:24:0:24:47::threading.Thread needs the target function:HIGH +unexpected-keyword-arg:24:0:24:47::Unexpected keyword argument 'target_typo' in constructor call:UNDEFINED diff --git a/tests/functional/b/bare_except.txt b/tests/functional/b/bare_except.txt index 584f1be6d..7957bc144 100644 --- a/tests/functional/b/bare_except.txt +++ b/tests/functional/b/bare_except.txt @@ -1 +1 @@ -bare-except:5:0:6:8::No exception type(s) specified:UNDEFINED +bare-except:5:0:6:8::No exception type(s) specified:HIGH diff --git a/tests/functional/b/boolean_datetime.py b/tests/functional/b/boolean_datetime.py new file mode 100644 index 000000000..cde355c01 --- /dev/null +++ b/tests/functional/b/boolean_datetime.py @@ -0,0 +1,15 @@ +"""Test boolean-datetime + +'py-version' needs to be set to <= '3.5'. +""" +import datetime + +if datetime.time(0, 0, 0): # [boolean-datetime] + print("datetime.time(0,0,0) is not a bug!") +else: + print("datetime.time(0,0,0) is a bug!") + +if datetime.time(0, 0, 1): # [boolean-datetime] + print("datetime.time(0,0,1) is not a bug!") +else: + print("datetime.time(0,0,1) is a bug!") diff --git a/tests/functional/b/boolean_datetime.rc b/tests/functional/b/boolean_datetime.rc new file mode 100644 index 000000000..068be2d4c --- /dev/null +++ b/tests/functional/b/boolean_datetime.rc @@ -0,0 +1,2 @@ +[MAIN] +py-version=3.4 diff --git a/tests/functional/b/boolean_datetime.txt b/tests/functional/b/boolean_datetime.txt new file mode 100644 index 000000000..316453a6e --- /dev/null +++ b/tests/functional/b/boolean_datetime.txt @@ -0,0 +1,2 @@ +boolean-datetime:7:3:7:25::Using datetime.time in a boolean context.:UNDEFINED +boolean-datetime:12:3:12:25::Using datetime.time in a boolean context.:UNDEFINED diff --git a/tests/functional/b/broad_except.py b/tests/functional/b/broad_except.py deleted file mode 100644 index b38b6f8dc..000000000 --- a/tests/functional/b/broad_except.py +++ /dev/null @@ -1,13 +0,0 @@ -# pylint: disable=missing-docstring -__revision__ = 0 - -try: - __revision__ += 1 -except Exception: # [broad-except] - print('error') - - -try: - __revision__ += 1 -except BaseException: # [broad-except] - print('error') diff --git a/tests/functional/b/broad_except.txt b/tests/functional/b/broad_except.txt deleted file mode 100644 index 9a795d3c6..000000000 --- a/tests/functional/b/broad_except.txt +++ /dev/null @@ -1,2 +0,0 @@ -broad-except:6:7:6:16::Catching too general exception Exception:UNDEFINED -broad-except:12:7:12:20::Catching too general exception BaseException:UNDEFINED diff --git a/tests/functional/b/broad_exception_caught.py b/tests/functional/b/broad_exception_caught.py new file mode 100644 index 000000000..0a69a7015 --- /dev/null +++ b/tests/functional/b/broad_exception_caught.py @@ -0,0 +1,39 @@ +# pylint: disable=missing-docstring +__revision__ = 0 + +class CustomBroadException(Exception): + pass + + +class CustomNarrowException(CustomBroadException): + pass + + +try: + __revision__ += 1 +except Exception: # [broad-exception-caught] + print('error') + + +try: + __revision__ += 1 +except BaseException: # [broad-exception-caught] + print('error') + + +try: + __revision__ += 1 +except ValueError: + print('error') + + +try: + __revision__ += 1 +except CustomBroadException: # [broad-exception-caught] + print('error') + + +try: + __revision__ += 1 +except CustomNarrowException: + print('error') diff --git a/tests/functional/b/broad_exception_caught.rc b/tests/functional/b/broad_exception_caught.rc new file mode 100644 index 000000000..e0e1a7b6c --- /dev/null +++ b/tests/functional/b/broad_exception_caught.rc @@ -0,0 +1,4 @@ +[EXCEPTIONS] +overgeneral-exceptions=builtins.BaseException, + builtins.Exception, + functional.b.broad_exception_caught.CustomBroadException diff --git a/tests/functional/b/broad_exception_caught.txt b/tests/functional/b/broad_exception_caught.txt new file mode 100644 index 000000000..386423b63 --- /dev/null +++ b/tests/functional/b/broad_exception_caught.txt @@ -0,0 +1,3 @@ +broad-exception-caught:14:7:14:16::Catching too general exception Exception:INFERENCE +broad-exception-caught:20:7:20:20::Catching too general exception BaseException:INFERENCE +broad-exception-caught:32:7:32:27::Catching too general exception CustomBroadException:INFERENCE diff --git a/tests/functional/b/broad_exception_raised.py b/tests/functional/b/broad_exception_raised.py new file mode 100644 index 000000000..c6ce64b46 --- /dev/null +++ b/tests/functional/b/broad_exception_raised.py @@ -0,0 +1,52 @@ +# pylint: disable=missing-docstring, unreachable + +ExceptionAlias = Exception + +class CustomBroadException(Exception): + pass + + +class CustomNarrowException(CustomBroadException): + pass + + +def exploding_apple(apple): + print(f"{apple} is about to explode") + raise Exception("{apple} exploded !") # [broad-exception-raised] + + +def raise_and_catch(): + try: + raise Exception("Oh No!!") # [broad-exception-raised] + except Exception as ex: # [broad-exception-caught] + print(ex) + + +def raise_catch_reraise(): + try: + exploding_apple("apple") + except Exception as ex: + print(ex) + raise ex + + +def raise_catch_raise(): + try: + exploding_apple("apple") + except Exception as ex: + print(ex) + raise Exception() from None # [broad-exception-raised] + + +def raise_catch_raise_using_alias(): + try: + exploding_apple("apple") + except Exception as ex: + print(ex) + raise ExceptionAlias() from None # [broad-exception-raised] + +raise Exception() # [broad-exception-raised] +raise BaseException() # [broad-exception-raised] +raise CustomBroadException() # [broad-exception-raised] +raise IndexError from None +raise CustomNarrowException() from None diff --git a/tests/functional/b/broad_exception_raised.rc b/tests/functional/b/broad_exception_raised.rc new file mode 100644 index 000000000..4f85d2933 --- /dev/null +++ b/tests/functional/b/broad_exception_raised.rc @@ -0,0 +1,4 @@ +[EXCEPTIONS] +overgeneral-exceptions=builtins.BaseException, + builtins.Exception, + functional.b.broad_exception_raised.CustomBroadException diff --git a/tests/functional/b/broad_exception_raised.txt b/tests/functional/b/broad_exception_raised.txt new file mode 100644 index 000000000..1e27b23f9 --- /dev/null +++ b/tests/functional/b/broad_exception_raised.txt @@ -0,0 +1,8 @@ +broad-exception-raised:15:4:15:41:exploding_apple:"Raising too general exception: Exception":INFERENCE +broad-exception-raised:20:8:20:34:raise_and_catch:"Raising too general exception: Exception":INFERENCE +broad-exception-caught:21:11:21:20:raise_and_catch:Catching too general exception Exception:INFERENCE +broad-exception-raised:38:8:38:35:raise_catch_raise:"Raising too general exception: Exception":INFERENCE +broad-exception-raised:46:8:46:40:raise_catch_raise_using_alias:"Raising too general exception: Exception":INFERENCE +broad-exception-raised:48:0:48:17::"Raising too general exception: Exception":INFERENCE +broad-exception-raised:49:0:49:21::"Raising too general exception: BaseException":INFERENCE +broad-exception-raised:50:0:50:28::"Raising too general exception: CustomBroadException":INFERENCE diff --git a/tests/functional/c/class_members_py30.py b/tests/functional/c/class_members_py30.py index 0d65331f9..4566ff44e 100644 --- a/tests/functional/c/class_members_py30.py +++ b/tests/functional/c/class_members_py30.py @@ -37,7 +37,7 @@ class TestMetaclass(metaclass=ABCMeta): class Metaclass(type):
""" metaclass """
@classmethod
- def test(cls):
+ def test(mcs):
""" classmethod """
class UsingMetaclass(metaclass=Metaclass):
diff --git a/tests/functional/c/comparison_with_callable.py b/tests/functional/c/comparison_with_callable.py index 1e8ea5d90..7006d4960 100644 --- a/tests/functional/c/comparison_with_callable.py +++ b/tests/functional/c/comparison_with_callable.py @@ -1,4 +1,4 @@ -# pylint: disable = disallowed-name, missing-docstring, useless-return, invalid-name, line-too-long, comparison-of-constants +# pylint: disable = disallowed-name, missing-docstring, useless-return, invalid-name, line-too-long, comparison-of-constants, broad-exception-raised def foo(): return None diff --git a/tests/functional/c/consider/consider_iterating_dictionary.py b/tests/functional/c/consider/consider_iterating_dictionary.py index fdb76e869..8c75b4e3e 100644 --- a/tests/functional/c/consider/consider_iterating_dictionary.py +++ b/tests/functional/c/consider/consider_iterating_dictionary.py @@ -92,3 +92,25 @@ class AClass: print("a" in another_metadata.keys()) # [consider-iterating-dictionary] return inner_function() return InnerClass().another_function() + +a_dict = {"a": 1, "b": 2, "c": 3} +a_set = {"c", "d"} + +# Test bitwise operations. These should not raise msg because removing `.keys()` +# either gives error or ends in a different result +print(a_dict.keys() | a_set) + +if "a" in a_dict.keys() | a_set: + pass + +if "a" in a_dict.keys() & a_set: + pass + +if 1 in a_dict.keys() ^ [1, 2]: + pass + +if "a" in a_dict.keys() or a_set: # [consider-iterating-dictionary] + pass + +if "a" in a_dict.keys() and a_set: # [consider-iterating-dictionary] + pass diff --git a/tests/functional/c/consider/consider_iterating_dictionary.txt b/tests/functional/c/consider/consider_iterating_dictionary.txt index f251fa286..5190e7789 100644 --- a/tests/functional/c/consider/consider_iterating_dictionary.txt +++ b/tests/functional/c/consider/consider_iterating_dictionary.txt @@ -1,26 +1,28 @@ -consider-iterating-dictionary:25:16:25:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:26:16:26:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:27:16:27:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:28:21:28:30::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:29:24:29:33::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:30:24:30:33::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:31:24:31:33::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:32:29:32:38::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:33:11:33:20::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:38:24:38:35::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:38:55:38:66::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:39:31:39:42::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:39:61:39:72::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:40:30:40:41::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:40:60:40:71::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:43:8:43:21::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:45:8:45:17::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:65:11:65:20::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:73:19:73:34::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:75:14:75:29::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:77:15:77:30::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:79:10:79:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:89:42:89:65:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:90:37:90:60:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:91:38:91:61:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED -consider-iterating-dictionary:92:33:92:56:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED +consider-iterating-dictionary:25:16:25:25::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:26:16:26:25::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:27:16:27:25::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:28:21:28:30::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:29:24:29:33::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:30:24:30:33::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:31:24:31:33::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:32:29:32:38::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:33:11:33:20::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:38:24:38:35::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:38:55:38:66::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:39:31:39:42::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:39:61:39:72::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:40:30:40:41::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:40:60:40:71::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:43:8:43:21::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:45:8:45:17::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:65:11:65:20::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:73:19:73:34::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:75:14:75:29::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:77:15:77:30::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:79:10:79:25::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:89:42:89:65:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:90:37:90:60:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:91:38:91:61:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:92:33:92:56:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:112:10:112:23::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +consider-iterating-dictionary:115:10:115:23::Consider iterating the dictionary directly instead of calling .keys():INFERENCE diff --git a/tests/functional/c/consider/consider_join.py b/tests/functional/c/consider/consider_join.py index 24cd6dd49..9f785b64d 100644 --- a/tests/functional/c/consider/consider_join.py +++ b/tests/functional/c/consider/consider_join.py @@ -17,6 +17,33 @@ another_result = result = '' for number in ['1', '2', '3']: result += number # [consider-using-join] +result = 'a' +for number in ['1', '2', '3']: + result += f'b{number}' # [consider-using-join] +assert result == 'ab1b2b3' +assert result == 'b'.join(['a', '1', '2', '3']) + +result = 'a' +for number in ['1', '2', '3']: + result += f'{number}c' # [consider-using-join] +assert result == 'a1c2c3c' +assert result == 'a' + 'c'.join(['1', '2', '3']) + 'c' + +result = 'a' +for number in ['1', '2', '3']: + result += f'b{number}c' # [consider-using-join] +assert result == 'ab1cb2cb3c' +assert result == 'ab' + 'cb'.join(['1', '2', '3']) + 'c' + +result = '' +for number in ['1', '2', '3']: + result += number # [consider-using-join] + +result = '' +for number in ['1', '2', '3']: + result += f"{number}, " # [consider-using-join] +result = result[:-2] + result = 0 # result is not a string for number in ['1', '2', '3']: result += number @@ -124,3 +151,11 @@ for number in ['1']: result['context'] = 0 for number in ['1']: result['context'] += 24 + +result = '' +for number in ['1', '2', '3']: + result += f' {result}' # f-string contains wrong name + +result = '' +for number in ['1', '2', '3']: + result += f' {number} {number} {number} ' # f-string contains several names diff --git a/tests/functional/c/consider/consider_join.txt b/tests/functional/c/consider/consider_join.txt index baea768be..fa9b427e3 100644 --- a/tests/functional/c/consider/consider_join.txt +++ b/tests/functional/c/consider/consider_join.txt @@ -2,10 +2,15 @@ consider-using-join:6:4:6:20::Consider using str.join(sequence) for concatenatin consider-using-join:10:4:10:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED consider-using-join:14:4:14:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED consider-using-join:18:4:18:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:58:4:58:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:62:4:62:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:66:4:66:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:71:4:71:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:75:4:75:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:79:4:79:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED -consider-using-join:110:31:110:47::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:22:4:22:26::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:28:4:28:26::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:34:4:34:27::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:40:4:40:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:44:4:44:27::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:85:4:85:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:89:4:89:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:93:4:93:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:98:4:98:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:102:4:102:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:106:4:106:20::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED +consider-using-join:137:31:137:47::Consider using str.join(sequence) for concatenating strings from an iterable:UNDEFINED diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index d43c0f0cf..280ffecf3 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -3,7 +3,7 @@ consider-using-dict-items:9:4:10:30:bad:Consider iterating with .items():UNDEFIN consider-using-dict-items:21:4:22:35:another_bad:Consider iterating with .items():UNDEFINED consider-using-dict-items:40:0:42:18::Consider iterating with .items():UNDEFINED consider-using-dict-items:44:0:45:20::Consider iterating with .items():UNDEFINED -consider-iterating-dictionary:47:10:47:23::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED +consider-iterating-dictionary:47:10:47:23::Consider iterating the dictionary directly instead of calling .keys():INFERENCE consider-using-dict-items:47:0:48:20::Consider iterating with .items():UNDEFINED consider-using-dict-items:54:0:55:24::Consider iterating with .items():UNDEFINED consider-using-dict-items:67:0:None:None::Consider iterating with .items():UNDEFINED @@ -11,6 +11,6 @@ consider-using-dict-items:68:0:None:None::Consider iterating with .items():UNDEF consider-using-dict-items:71:0:None:None::Consider iterating with .items():UNDEFINED consider-using-dict-items:72:0:None:None::Consider iterating with .items():UNDEFINED consider-using-dict-items:75:0:None:None::Consider iterating with .items():UNDEFINED -consider-iterating-dictionary:86:25:86:42::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED +consider-iterating-dictionary:86:25:86:42::Consider iterating the dictionary directly instead of calling .keys():INFERENCE consider-using-dict-items:86:0:None:None::Consider iterating with .items():UNDEFINED consider-using-dict-items:103:0:105:24::Consider iterating with .items():UNDEFINED diff --git a/tests/functional/c/ctor_arguments.py b/tests/functional/c/ctor_arguments.py index 954d9b8b2..d87732f1d 100644 --- a/tests/functional/c/ctor_arguments.py +++ b/tests/functional/c/ctor_arguments.py @@ -65,8 +65,8 @@ ClassNew(one=2) # [no-value-for-parameter,unexpected-keyword-arg] class Metaclass(type): - def __new__(cls, name, bases, namespace): - return type.__new__(cls, name, bases, namespace) + def __new__(mcs, name, bases, namespace): + return type.__new__(mcs, name, bases, namespace) def with_metaclass(meta, base=object): """Create a new type that can be used as a metaclass.""" diff --git a/tests/functional/d/dangerous_default_value.py b/tests/functional/d/dangerous_default_value.py index 161eaceed..a7ef4c389 100644 --- a/tests/functional/d/dangerous_default_value.py +++ b/tests/functional/d/dangerous_default_value.py @@ -109,3 +109,14 @@ def function23(value=collections.UserList()): # [dangerous-default-value] def function24(*, value=[]): # [dangerous-default-value] """dangerous default value in kwarg.""" return value + + +class Clazz: + # pylint: disable=too-few-public-methods + def __init__( # [dangerous-default-value] + self, + arg: str = None, + *, + kk: dict = {}, + ) -> None: + pass diff --git a/tests/functional/d/dangerous_default_value.txt b/tests/functional/d/dangerous_default_value.txt index 98d55c2b6..2376b8e29 100644 --- a/tests/functional/d/dangerous_default_value.txt +++ b/tests/functional/d/dangerous_default_value.txt @@ -20,3 +20,4 @@ dangerous-default-value:97:0:97:14:function21:Dangerous default value defaultdic dangerous-default-value:101:0:101:14:function22:Dangerous default value UserDict() (collections.UserDict) as argument:UNDEFINED dangerous-default-value:105:0:105:14:function23:Dangerous default value UserList() (collections.UserList) as argument:UNDEFINED dangerous-default-value:109:0:109:14:function24:Dangerous default value [] as argument:UNDEFINED +dangerous-default-value:116:4:116:16:Clazz.__init__:Dangerous default value {} as argument:UNDEFINED diff --git a/tests/functional/d/dataclass_kw_only.py b/tests/functional/d/dataclass_kw_only.py new file mode 100644 index 000000000..9cc5a23bb --- /dev/null +++ b/tests/functional/d/dataclass_kw_only.py @@ -0,0 +1,26 @@ +"""Test the behaviour of the kw_only keyword.""" + +# pylint: disable=invalid-name + +from dataclasses import dataclass + + +@dataclass(kw_only=True) +class FooBar: + """Simple dataclass with a kw_only parameter.""" + + a: int + b: str + + +@dataclass(kw_only=False) +class BarFoo(FooBar): + """Simple dataclass with a negated kw_only parameter.""" + + c: int + + +BarFoo(1, a=2, b="") +BarFoo( # [missing-kwoa,missing-kwoa,redundant-keyword-arg,too-many-function-args] + 1, 2, c=2 +) diff --git a/tests/functional/d/dataclass_kw_only.rc b/tests/functional/d/dataclass_kw_only.rc new file mode 100644 index 000000000..68a8c8ef1 --- /dev/null +++ b/tests/functional/d/dataclass_kw_only.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/d/dataclass_kw_only.txt b/tests/functional/d/dataclass_kw_only.txt new file mode 100644 index 000000000..9954596f6 --- /dev/null +++ b/tests/functional/d/dataclass_kw_only.txt @@ -0,0 +1,4 @@ +missing-kwoa:24:0:26:1::Missing mandatory keyword argument 'a' in constructor call:UNDEFINED +missing-kwoa:24:0:26:1::Missing mandatory keyword argument 'b' in constructor call:UNDEFINED +redundant-keyword-arg:24:0:26:1::Argument 'c' passed by position and keyword in constructor call:UNDEFINED +too-many-function-args:24:0:26:1::Too many positional arguments for constructor call:UNDEFINED diff --git a/tests/functional/d/duplicate_except.txt b/tests/functional/d/duplicate_except.txt index 8753f44b1..2bd56881a 100644 --- a/tests/functional/d/duplicate_except.txt +++ b/tests/functional/d/duplicate_except.txt @@ -1 +1 @@ -duplicate-except:9:11:9:21:main:Catching previously caught exception type ValueError:UNDEFINED +duplicate-except:9:11:9:21:main:Catching previously caught exception type ValueError:INFERENCE diff --git a/tests/functional/e/e1101_9588_base_attr_aug_assign.py b/tests/functional/e/e1101_9588_base_attr_aug_assign.py index 9fe95fd4b..131dfc2c9 100644 --- a/tests/functional/e/e1101_9588_base_attr_aug_assign.py +++ b/tests/functional/e/e1101_9588_base_attr_aug_assign.py @@ -31,7 +31,7 @@ class NegativeClass(BaseClass): def __init__(self): "Ordinary assignment is OK." BaseClass.__init__(self) - self.e1101 = self.e1101 + 1 + self.e1101 += self.e1101 def countup(self): "No problem." diff --git a/tests/functional/e/enum_subclasses.py b/tests/functional/e/enum_subclasses.py index c8493da78..6ad453a4b 100644 --- a/tests/functional/e/enum_subclasses.py +++ b/tests/functional/e/enum_subclasses.py @@ -1,5 +1,5 @@ # pylint: disable=missing-docstring, invalid-name -from enum import Enum, IntEnum, auto +from enum import Enum, Flag, IntEnum, auto class Issue1932(IntEnum): @@ -60,6 +60,7 @@ class MyEnum(BaseEnum): print(MyEnum.FOO.value) + class TestBase(Enum): """Adds a special method to enums.""" @@ -77,3 +78,18 @@ class TestEnum(TestBase): test_enum = TestEnum.a assert test_enum.hello_pylint() == test_enum.name + + +# Check combinations of Flag members using the bitwise operators (&, |, ^, ~) +# https://github.com/PyCQA/pylint/issues/7381 +class Colour(Flag): + NONE = 0 + RED = 2 + GREEN = 2 + BLUE = 4 + + +and_expr = Colour.RED & Colour.GREEN & Colour.BLUE +and_expr_with_complement = ~Colour.RED & ~Colour.GREEN & ~Colour.BLUE +or_expr = Colour.RED | Colour.GREEN | Colour.BLUE +xor_expr = Colour.RED ^ Colour.GREEN ^ Colour.BLUE diff --git a/tests/functional/e/exception_is_binary_op.txt b/tests/functional/e/exception_is_binary_op.txt index 4871a71d2..de371e42e 100644 --- a/tests/functional/e/exception_is_binary_op.txt +++ b/tests/functional/e/exception_is_binary_op.txt @@ -1,4 +1,4 @@ -binary-op-exception:5:0:6:20::"Exception to catch is the result of a binary ""or"" operation":UNDEFINED -binary-op-exception:7:0:8:20::"Exception to catch is the result of a binary ""and"" operation":UNDEFINED -binary-op-exception:9:0:10:20::"Exception to catch is the result of a binary ""or"" operation":UNDEFINED -binary-op-exception:11:0:12:20::"Exception to catch is the result of a binary ""or"" operation":UNDEFINED +binary-op-exception:5:0:6:20::"Exception to catch is the result of a binary ""or"" operation":HIGH +binary-op-exception:7:0:8:20::"Exception to catch is the result of a binary ""and"" operation":HIGH +binary-op-exception:9:0:10:20::"Exception to catch is the result of a binary ""or"" operation":HIGH +binary-op-exception:11:0:12:20::"Exception to catch is the result of a binary ""or"" operation":HIGH diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.py b/tests/functional/ext/bad_dunder/bad_dunder_name.py new file mode 100644 index 000000000..48247aba0 --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.py @@ -0,0 +1,54 @@ +# pylint: disable=missing-module-docstring, missing-class-docstring, +# pylint: disable=missing-function-docstring, unused-private-member + + +class Apples: + __slots__ = ("a", "b") + + def __hello__(self): # [bad-dunder-name] + # not one of the explicitly defined dunder name methods + print("hello") + + def hello(self): + print("hello") + + def __init__(self): + pass + + def init(self): + # valid name even though someone could accidentally mean __init__ + pass + + def __init_(self): # [bad-dunder-name] + # author likely unintentionally misspelled the correct init dunder. + pass + + def _init_(self): # [bad-dunder-name] + # author likely unintentionally misspelled the correct init dunder. + pass + + def ___neg__(self): # [bad-dunder-name] + # author likely accidentally added an additional `_` + pass + + def __inv__(self): # [bad-dunder-name] + # author likely meant to call the invert dunder method + pass + + def __allowed__(self): + # user-configured allowed dunder name + pass + + def _protected_method(self): + print("Protected") + + def __private_method(self): + print("Private") + + @property + def __doc__(self): + return "Docstring" + + +def __increase_me__(val): + return val + 1 diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.rc b/tests/functional/ext/bad_dunder/bad_dunder_name.rc new file mode 100644 index 000000000..0b449f3a3 --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.rc @@ -0,0 +1,4 @@ +[MAIN] +load-plugins=pylint.extensions.dunder + +good-dunder-names = __allowed__, diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.txt b/tests/functional/ext/bad_dunder/bad_dunder_name.txt new file mode 100644 index 000000000..bb1d1e692 --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.txt @@ -0,0 +1,5 @@ +bad-dunder-name:8:4:8:17:Apples.__hello__:Bad or misspelled dunder method name __hello__.:HIGH +bad-dunder-name:22:4:22:15:Apples.__init_:Bad or misspelled dunder method name __init_.:HIGH +bad-dunder-name:26:4:26:14:Apples._init_:Bad or misspelled dunder method name _init_.:HIGH +bad-dunder-name:30:4:30:16:Apples.___neg__:Bad or misspelled dunder method name ___neg__.:HIGH +bad-dunder-name:34:4:34:15:Apples.__inv__:Bad or misspelled dunder method name __inv__.:HIGH diff --git a/tests/functional/ext/code_style/cs_consider_using_augmented_assign.py b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.py new file mode 100644 index 000000000..e4af95686 --- /dev/null +++ b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.py @@ -0,0 +1,77 @@ +"""Tests for consider-using-augmented-assign.""" + +# pylint: disable=invalid-name,too-few-public-methods,import-error,consider-using-f-string + +from unknown import Unknown + +x = 1 +x = x + 1 # [consider-using-augmented-assign] +x = 1 + x # [consider-using-augmented-assign] +x, y = 1 + x, 2 + x +# We don't warn on intricate expressions as we lack knowledge +# of simplifying such expressions which is necessary to see +# if they can become augmented +x = 1 + x - 2 +x = 1 + x + 2 + +# For anything other than a float or an int we only want to warn on +# assignments where the 'itself' is on the left side of the assignment +my_list = [2, 3, 4] +my_list = [1] + my_list + + +class MyClass: + """Simple base class.""" + + def __init__(self) -> None: + self.x = 1 + self.x = self.x + 1 # [consider-using-augmented-assign] + self.x = 1 + self.x # [consider-using-augmented-assign] + + x = 1 # [redefined-outer-name] + self.x = x + + +instance = MyClass() + +x = instance.x + 1 + +my_str = "" +my_str = my_str + "foo" # [consider-using-augmented-assign] +my_str = "foo" + my_str + +my_bytes = b"" +my_bytes = my_bytes + b"foo" # [consider-using-augmented-assign] +my_bytes = b"foo" + my_bytes + + +def return_str() -> str: + """Return a string.""" + return "" + + +# Currently we disregard all calls +my_str = return_str() + my_str + +my_str = my_str % return_str() +my_str = my_str % 1 # [consider-using-augmented-assign] +my_str = my_str % (1, 2) # [consider-using-augmented-assign] +my_str = "%s" % my_str +my_str = return_str() % my_str +my_str = Unknown % my_str +my_str = my_str % Unknown # [consider-using-augmented-assign] + +x = x - 3 # [consider-using-augmented-assign] +x = x * 3 # [consider-using-augmented-assign] +x = x / 3 # [consider-using-augmented-assign] +x = x // 3 # [consider-using-augmented-assign] +x = x << 3 # [consider-using-augmented-assign] +x = x >> 3 # [consider-using-augmented-assign] +x = x % 3 # [consider-using-augmented-assign] +x = x**3 # [consider-using-augmented-assign] +x = x ^ 3 # [consider-using-augmented-assign] +x = x & 3 # [consider-using-augmented-assign] +x = x > 3 +x = x < 3 +x = x >= 3 +x = x <= 3 diff --git a/tests/functional/ext/code_style/cs_consider_using_augmented_assign.rc b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.rc new file mode 100644 index 000000000..584602294 --- /dev/null +++ b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.rc @@ -0,0 +1,3 @@ +[MAIN] +load-plugins=pylint.extensions.code_style +enable=consider-using-augmented-assign diff --git a/tests/functional/ext/code_style/cs_consider_using_augmented_assign.txt b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.txt new file mode 100644 index 000000000..1684953e9 --- /dev/null +++ b/tests/functional/ext/code_style/cs_consider_using_augmented_assign.txt @@ -0,0 +1,20 @@ +consider-using-augmented-assign:8:0:8:9::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:9:0:9:9::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:28:8:28:27:MyClass.__init__:Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:29:8:29:27:MyClass.__init__:Use '+=' to do an augmented assign directly:INFERENCE +redefined-outer-name:31:8:31:9:MyClass.__init__:Redefining name 'x' from outer scope (line 7):UNDEFINED +consider-using-augmented-assign:40:0:40:23::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:44:0:44:28::Use '+=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:57:0:57:19::Use '%=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:58:0:58:24::Use '%=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:62:0:62:25::Use '%=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:64:0:64:9::Use '-=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:65:0:65:9::Use '*=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:66:0:66:9::Use '/=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:67:0:67:10::Use '//=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:68:0:68:10::Use '<<=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:69:0:69:10::Use '>>=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:70:0:70:9::Use '%=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:71:0:71:8::Use '**=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:72:0:72:9::Use '^=' to do an augmented assign directly:INFERENCE +consider-using-augmented-assign:73:0:73:9::Use '&=' to do an augmented assign directly:INFERENCE diff --git a/tests/functional/ext/code_style/cs_default.py b/tests/functional/ext/code_style/cs_default.py new file mode 100644 index 000000000..bd4edab36 --- /dev/null +++ b/tests/functional/ext/code_style/cs_default.py @@ -0,0 +1,6 @@ +"""Test default configuration for code-style checker.""" +# pylint: disable=invalid-name + +# consider-using-augmented-assign is disabled by default +x = 1 +x = x + 1 diff --git a/tests/functional/ext/code_style/cs_default.rc b/tests/functional/ext/code_style/cs_default.rc new file mode 100644 index 000000000..8663ab085 --- /dev/null +++ b/tests/functional/ext/code_style/cs_default.rc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.code_style diff --git a/tests/functional/ext/comparetozero/comparetozero.py b/tests/functional/ext/comparetozero/compare_to_zero.py index 29fd13994..6a14b8bc9 100644 --- a/tests/functional/ext/comparetozero/comparetozero.py +++ b/tests/functional/ext/comparetozero/compare_to_zero.py @@ -1,4 +1,4 @@ -# pylint: disable=literal-comparison,missing-docstring +# pylint: disable=literal-comparison,missing-docstring, singleton-comparison X = 123 Y = len('test') @@ -6,15 +6,33 @@ Y = len('test') if X is 0: # [compare-to-zero] pass +if X is False: + pass + if Y is not 0: # [compare-to-zero] pass +if Y is not False: + pass + if X == 0: # [compare-to-zero] pass +if X == False: + pass + +if 0 == Y: # [compare-to-zero] + pass + if Y != 0: # [compare-to-zero] pass +if 0 != X: # [compare-to-zero] + pass + +if Y != False: + pass + if X > 0: pass diff --git a/tests/functional/ext/comparetozero/comparetozero.rc b/tests/functional/ext/comparetozero/compare_to_zero.rc index 70c6171b5..70c6171b5 100644 --- a/tests/functional/ext/comparetozero/comparetozero.rc +++ b/tests/functional/ext/comparetozero/compare_to_zero.rc diff --git a/tests/functional/ext/comparetozero/compare_to_zero.txt b/tests/functional/ext/comparetozero/compare_to_zero.txt new file mode 100644 index 000000000..a413a3268 --- /dev/null +++ b/tests/functional/ext/comparetozero/compare_to_zero.txt @@ -0,0 +1,6 @@ +compare-to-zero:6:3:6:9::"""X is 0"" can be simplified to ""not X"" as 0 is falsey":HIGH +compare-to-zero:12:3:12:13::"""Y is not 0"" can be simplified to ""Y"" as 0 is falsey":HIGH +compare-to-zero:18:3:18:9::"""X == 0"" can be simplified to ""not X"" as 0 is falsey":HIGH +compare-to-zero:24:3:24:9::"""0 == Y"" can be simplified to ""not Y"" as 0 is falsey":HIGH +compare-to-zero:27:3:27:9::"""Y != 0"" can be simplified to ""Y"" as 0 is falsey":HIGH +compare-to-zero:30:3:30:9::"""0 != X"" can be simplified to ""X"" as 0 is falsey":HIGH diff --git a/tests/functional/ext/comparetozero/comparetozero.txt b/tests/functional/ext/comparetozero/comparetozero.txt deleted file mode 100644 index 34f76c94e..000000000 --- a/tests/functional/ext/comparetozero/comparetozero.txt +++ /dev/null @@ -1,4 +0,0 @@ -compare-to-zero:6:3:6:9::Avoid comparisons to zero:UNDEFINED -compare-to-zero:9:3:9:13::Avoid comparisons to zero:UNDEFINED -compare-to-zero:12:3:12:9::Avoid comparisons to zero:UNDEFINED -compare-to-zero:15:3:15:9::Avoid comparisons to zero:UNDEFINED diff --git a/tests/functional/ext/dict_init_mutate.py b/tests/functional/ext/dict_init_mutate.py new file mode 100644 index 000000000..f3372bd7e --- /dev/null +++ b/tests/functional/ext/dict_init_mutate.py @@ -0,0 +1,38 @@ +"""Example cases for dict-init-mutate""" +# pylint: disable=use-dict-literal, invalid-name + +base = {} + +fruits = {} +for fruit in ["apple", "orange"]: + fruits[fruit] = 1 + fruits[fruit] += 1 + +count = 10 +fruits = {"apple": 1} +fruits["apple"] += count + +config = {} # [dict-init-mutate] +config['pwd'] = 'hello' + +config = {} # [dict-init-mutate] +config['dir'] = 'bin' +config['user'] = 'me' +config['workers'] = 5 +print(config) + +config = {} # Not flagging calls to update for now +config.update({"dir": "bin"}) + +config = {} # [dict-init-mutate] +config['options'] = {} # Identifying nested assignment not supporting this yet. +config['options']['debug'] = False +config['options']['verbose'] = True + + +config = {} +def update_dict(di): + """Update a dictionary""" + di["one"] = 1 + +update_dict(config) diff --git a/tests/functional/ext/dict_init_mutate.rc b/tests/functional/ext/dict_init_mutate.rc new file mode 100644 index 000000000..bbe6bd1f7 --- /dev/null +++ b/tests/functional/ext/dict_init_mutate.rc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.dict_init_mutate, diff --git a/tests/functional/ext/dict_init_mutate.txt b/tests/functional/ext/dict_init_mutate.txt new file mode 100644 index 000000000..c3702491f --- /dev/null +++ b/tests/functional/ext/dict_init_mutate.txt @@ -0,0 +1,3 @@ +dict-init-mutate:15:0:15:11::Declare all known key/values when initializing the dictionary.:HIGH +dict-init-mutate:18:0:18:11::Declare all known key/values when initializing the dictionary.:HIGH +dict-init-mutate:27:0:27:11::Declare all known key/values when initializing the dictionary.:HIGH diff --git a/tests/functional/ext/docparams/docparams.py b/tests/functional/ext/docparams/docparams.py index 3798ee3ac..051bdd24d 100644 --- a/tests/functional/ext/docparams/docparams.py +++ b/tests/functional/ext/docparams/docparams.py @@ -1,5 +1,5 @@ """Fixture for testing missing documentation in docparams.""" - +# pylint: disable=broad-exception-raised def _private_func1( # [missing-return-doc, missing-return-type-doc, missing-any-param-doc] param1, diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.rc b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.rc index 671820dbc..2900924f8 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.rc +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Sphinx.rc @@ -3,5 +3,6 @@ load-plugins = pylint.extensions.docparams [BASIC] accept-no-param-doc=no +accept-no-raise-doc=no no-docstring-rgx=^$ docstring-min-length: -1 diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc.txt b/tests/functional/ext/docparams/raise/missing_raises_doc.txt index 38b0437fc..d770776ef 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc.txt +++ b/tests/functional/ext/docparams/raise/missing_raises_doc.txt @@ -1,4 +1,4 @@ unreachable:25:4:25:25:test_ignores_raise_uninferable:Unreachable code:HIGH missing-raises-doc:28:0:28:45:test_ignores_returns_from_inner_functions:"""RuntimeError"" not documented as being raised":HIGH unreachable:42:4:42:25:test_ignores_returns_from_inner_functions:Unreachable code:HIGH -raising-bad-type:54:4:54:22:test_ignores_returns_use_only_names:Raising int while only classes or instances are allowed:UNDEFINED +raising-bad-type:54:4:54:22:test_ignores_returns_use_only_names:Raising int while only classes or instances are allowed:INFERENCE diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_options.py b/tests/functional/ext/docparams/raise/missing_raises_doc_options.py new file mode 100644 index 000000000..eb3cd3ac3 --- /dev/null +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_options.py @@ -0,0 +1,15 @@ +"""Minimal example where a W9006 message is displayed even if the +accept-no-raise-doc option is set to True. + +Requires at least one matching section (`Docstring.matching_sections`). + +Taken from https://github.com/PyCQA/pylint/issues/7208 +""" + + +def w9006issue(dummy: int): + """Sample function. + + :param dummy: Unused + """ + raise AssertionError() diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_options.rc b/tests/functional/ext/docparams/raise/missing_raises_doc_options.rc new file mode 100644 index 000000000..b36bb87a3 --- /dev/null +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_options.rc @@ -0,0 +1,5 @@ +[MAIN] +load-plugins = pylint.extensions.docparams + +[BASIC] +accept-no-raise-doc = yes diff --git a/tests/functional/ext/emptystring/empty_string_comparison.py b/tests/functional/ext/emptystring/empty_string_comparison.py index c6dcf8ea8..b61caeff6 100644 --- a/tests/functional/ext/emptystring/empty_string_comparison.py +++ b/tests/functional/ext/emptystring/empty_string_comparison.py @@ -14,3 +14,9 @@ if X == "": # [compare-to-empty-string] if Y != '': # [compare-to-empty-string] pass + +if "" == Y: # [compare-to-empty-string] + pass + +if '' != X: # [compare-to-empty-string] + pass diff --git a/tests/functional/ext/emptystring/empty_string_comparison.txt b/tests/functional/ext/emptystring/empty_string_comparison.txt index 5b259bc46..be9c91bc5 100644 --- a/tests/functional/ext/emptystring/empty_string_comparison.txt +++ b/tests/functional/ext/emptystring/empty_string_comparison.txt @@ -1,4 +1,6 @@ -compare-to-empty-string:6:3:6:10::Avoid comparisons to empty string:UNDEFINED -compare-to-empty-string:9:3:9:14::Avoid comparisons to empty string:UNDEFINED -compare-to-empty-string:12:3:12:10::Avoid comparisons to empty string:UNDEFINED -compare-to-empty-string:15:3:15:10::Avoid comparisons to empty string:UNDEFINED +compare-to-empty-string:6:3:6:10::"""X is ''"" can be simplified to ""not X"" as an empty string is falsey":HIGH +compare-to-empty-string:9:3:9:14::"""Y is not ''"" can be simplified to ""Y"" as an empty string is falsey":HIGH +compare-to-empty-string:12:3:12:10::"""X == ''"" can be simplified to ""not X"" as an empty string is falsey":HIGH +compare-to-empty-string:15:3:15:10::"""Y != ''"" can be simplified to ""Y"" as an empty string is falsey":HIGH +compare-to-empty-string:18:3:18:10::"""'' == Y"" can be simplified to ""not Y"" as an empty string is falsey":HIGH +compare-to-empty-string:21:3:21:10::"""'' != X"" can be simplified to ""X"" as an empty string is falsey":HIGH diff --git a/tests/functional/ext/for_any_all/for_any_all.py b/tests/functional/ext/for_any_all/for_any_all.py index 8b4c7275c..649739c37 100644 --- a/tests/functional/ext/for_any_all/for_any_all.py +++ b/tests/functional/ext/for_any_all/for_any_all.py @@ -1,4 +1,5 @@ """Functional test""" +# pylint: disable=missing-function-docstring, invalid-name def any_even(items): """Return True if the list contains any even numbers""" @@ -144,3 +145,94 @@ def is_from_decorator(node): if parent in parent.selected_annotations: return False return False + +def optimized_any_with_break(split_lines, max_chars): + """False negative found in https://github.com/PyCQA/pylint/pull/7697""" + potential_line_length_warning = False + for line in split_lines: # [consider-using-any-or-all] + if len(line) > max_chars: + potential_line_length_warning = True + break + return potential_line_length_warning + +def optimized_any_without_break(split_lines, max_chars): + potential_line_length_warning = False + for line in split_lines: # [consider-using-any-or-all] + if len(line) > max_chars: + potential_line_length_warning = True + return potential_line_length_warning + +def print_line_without_break(split_lines, max_chars): + potential_line_length_warning = False + for line in split_lines: + print(line) + if len(line) > max_chars: + potential_line_length_warning = True + return potential_line_length_warning + +def print_line_without_reassign(split_lines, max_chars): + potential_line_length_warning = False + for line in split_lines: + if len(line) > max_chars: + print(line) + return potential_line_length_warning + +def multiple_flags(split_lines, max_chars): + potential_line_length_warning = False + for line in split_lines: + if len(line) > max_chars: + num = 1 + print(num) + potential_line_length_warning = True + return potential_line_length_warning + +s = ["hi", "hello", "goodbye", None] + +flag = True +for i, elem in enumerate(s): + if elem is None: + continue + cnt_s = cnt_t = 0 + for j in range(i, len(s)): + if s[j] == elem: + cnt_s += 1 + s[j] = None + Flag = False + +def with_elif(split_lines, max_chars): + """ + Do not raise consider-using-any-or-all because the intent in this code + is to iterate over all the lines (not short-circuit) and see what + the last value would be. + """ + last_longest_line = False + for line in split_lines: + if len(line) > max_chars: + last_longest_line = True + elif len(line) == max_chars: + last_longest_line = False + return last_longest_line + +def first_even(items): + """Return first even number""" + for item in items: + if item % 2 == 0: + return item + return None + +def even(items): + for item in items: + if item % 2 == 0: + return True + return None + +def iterate_leaves(leaves, current_node): + results = [] + + current_node.was_checked = True + for leaf in leaves: + if isinstance(leaf, bool): + current_node.was_checked = False + else: + results.append(leaf) + return results diff --git a/tests/functional/ext/for_any_all/for_any_all.txt b/tests/functional/ext/for_any_all/for_any_all.txt index bc09876e4..dca0ad3d3 100644 --- a/tests/functional/ext/for_any_all/for_any_all.txt +++ b/tests/functional/ext/for_any_all/for_any_all.txt @@ -1,12 +1,14 @@ -consider-using-any-or-all:5:4:7:23:any_even:`for` loop could be `any(item % 2 == 0 for item in items)`:UNDEFINED -consider-using-any-or-all:12:4:14:24:all_even:`for` loop could be `all(item % 2 == 0 for item in items)`:UNDEFINED -consider-using-any-or-all:19:4:21:23:any_uneven:`for` loop could be `not all(item % 2 == 0 for item in items)`:UNDEFINED -consider-using-any-or-all:26:4:28:24:all_uneven:`for` loop could be `not any(item % 2 == 0 for item in items)`:UNDEFINED -consider-using-any-or-all:33:4:35:23:is_from_string:`for` loop could be `any(isinstance(parent, str) for parent in item.parents())`:UNDEFINED -consider-using-any-or-all:40:4:42:23:is_not_from_string:`for` loop could be `not all(isinstance(parent, str) for parent in item.parents())`:UNDEFINED -consider-using-any-or-all:49:8:51:28:nested_check:`for` loop could be `not any(item in (1, 2, 3) for item in items)`:UNDEFINED -consider-using-any-or-all:58:4:60:23:words_contains_word:`for` loop could be `any(word == 'word' for word in words)`:UNDEFINED -consider-using-any-or-all:65:4:67:24:complicated_condition_check:`for` loop could be `not any(item % 2 == 0 and (item % 3 == 0 or item > 15) for item in items)`:UNDEFINED -consider-using-any-or-all:72:4:77:23:is_from_decorator1:`for` loop could be `any(ancestor.name in ('Exception', 'BaseException') and ancestor.root().name == 'Exception' for ancestor in node)`:UNDEFINED -consider-using-any-or-all:82:4:84:24:is_from_decorator2:`for` loop could be `all(item % 2 == 0 and (item % 3 == 0 or item > 15) for item in items)`:UNDEFINED -consider-using-any-or-all:89:4:94:23:is_from_decorator3:`for` loop could be `not all(ancestor.name in ('Exception', 'BaseException') and ancestor.root().name == 'Exception' for ancestor in node)`:UNDEFINED +consider-using-any-or-all:6:4:8:23:any_even:`for` loop could be `any(item % 2 == 0 for item in items)`:HIGH +consider-using-any-or-all:13:4:15:24:all_even:`for` loop could be `all(item % 2 == 0 for item in items)`:HIGH +consider-using-any-or-all:20:4:22:23:any_uneven:`for` loop could be `not all(item % 2 == 0 for item in items)`:HIGH +consider-using-any-or-all:27:4:29:24:all_uneven:`for` loop could be `not any(item % 2 == 0 for item in items)`:HIGH +consider-using-any-or-all:34:4:36:23:is_from_string:`for` loop could be `any(isinstance(parent, str) for parent in item.parents())`:HIGH +consider-using-any-or-all:41:4:43:23:is_not_from_string:`for` loop could be `not all(isinstance(parent, str) for parent in item.parents())`:HIGH +consider-using-any-or-all:50:8:52:28:nested_check:`for` loop could be `not any(item in (1, 2, 3) for item in items)`:HIGH +consider-using-any-or-all:59:4:61:23:words_contains_word:`for` loop could be `any(word == 'word' for word in words)`:HIGH +consider-using-any-or-all:66:4:68:24:complicated_condition_check:`for` loop could be `not any(item % 2 == 0 and (item % 3 == 0 or item > 15) for item in items)`:HIGH +consider-using-any-or-all:73:4:78:23:is_from_decorator1:`for` loop could be `any(ancestor.name in ('Exception', 'BaseException') and ancestor.root().name == 'Exception' for ancestor in node)`:HIGH +consider-using-any-or-all:83:4:85:24:is_from_decorator2:`for` loop could be `all(item % 2 == 0 and (item % 3 == 0 or item > 15) for item in items)`:HIGH +consider-using-any-or-all:90:4:95:23:is_from_decorator3:`for` loop could be `not all(ancestor.name in ('Exception', 'BaseException') and ancestor.root().name == 'Exception' for ancestor in node)`:HIGH +consider-using-any-or-all:152:4:155:17:optimized_any_with_break:`for` loop could be `not any(len(line) > max_chars for line in split_lines)`:HIGH +consider-using-any-or-all:160:4:162:48:optimized_any_without_break:`for` loop could be `not any(len(line) > max_chars for line in split_lines)`:HIGH diff --git a/tests/functional/ext/magic_value_comparison/magic_value_comparison.py b/tests/functional/ext/magic_value_comparison/magic_value_comparison.py new file mode 100644 index 000000000..50c175af5 --- /dev/null +++ b/tests/functional/ext/magic_value_comparison/magic_value_comparison.py @@ -0,0 +1,33 @@ +""" +Checks that magic values are not used in comparisons +""" +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,wrong-import-position + +from enum import Enum + + +class Christmas(Enum): + EVE = 25 + DAY = 26 + MONTH = 12 + + +var = 7 +if var > 5: # [magic-value-comparison] + pass + +if (var + 5) > 10: # [magic-value-comparison] + pass + +is_big = 100 < var # [magic-value-comparison] + +shouldnt_raise = 5 > 7 # [comparison-of-constants] +shouldnt_raise = var == '__main__' +shouldnt_raise = var == 1 +shouldnt_raise = var == 0 +shouldnt_raise = var == -1 +shouldnt_raise = var == True # [singleton-comparison] +shouldnt_raise = var == False # [singleton-comparison] +shouldnt_raise = var == None # [singleton-comparison] +celebration_started = Christmas.EVE.value == Christmas.MONTH.value +shouldnt_raise = var == "" diff --git a/tests/functional/ext/magic_value_comparison/magic_value_comparison.rc b/tests/functional/ext/magic_value_comparison/magic_value_comparison.rc new file mode 100644 index 000000000..f69139825 --- /dev/null +++ b/tests/functional/ext/magic_value_comparison/magic_value_comparison.rc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.magic_value, diff --git a/tests/functional/ext/magic_value_comparison/magic_value_comparison.txt b/tests/functional/ext/magic_value_comparison/magic_value_comparison.txt new file mode 100644 index 000000000..63976ff68 --- /dev/null +++ b/tests/functional/ext/magic_value_comparison/magic_value_comparison.txt @@ -0,0 +1,7 @@ +magic-value-comparison:16:3:16:10::Consider using a named constant or an enum instead of '5'.:HIGH +magic-value-comparison:19:3:19:17::Consider using a named constant or an enum instead of '10'.:HIGH +magic-value-comparison:22:9:22:18::Consider using a named constant or an enum instead of '100'.:HIGH +comparison-of-constants:24:17:24:22::"Comparison between constants: '5 > 7' has a constant value":HIGH +singleton-comparison:29:17:29:28::Comparison 'var == True' should be 'var is True' if checking for the singleton value True, or 'bool(var)' if testing for truthiness:UNDEFINED +singleton-comparison:30:17:30:29::Comparison 'var == False' should be 'var is False' if checking for the singleton value False, or 'not var' if testing for falsiness:UNDEFINED +singleton-comparison:31:17:31:28::Comparison 'var == None' should be 'var is None':UNDEFINED diff --git a/tests/functional/ext/mccabe/mccabe.py b/tests/functional/ext/mccabe/mccabe.py index e3d11c5c8..92623cd1c 100644 --- a/tests/functional/ext/mccabe/mccabe.py +++ b/tests/functional/ext/mccabe/mccabe.py @@ -1,7 +1,7 @@ # pylint: disable=invalid-name,unnecessary-pass,no-else-return,useless-else-on-loop # pylint: disable=undefined-variable,consider-using-sys-exit,unused-variable,too-many-return-statements # pylint: disable=redefined-outer-name,using-constant-test,unused-argument -# pylint: disable=broad-except, not-context-manager, no-method-argument, unspecified-encoding +# pylint: disable=broad-except, not-context-manager, no-method-argument, unspecified-encoding, broad-exception-raised """Checks use of "too-complex" check""" diff --git a/tests/functional/ext/set_membership/use_set_membership.py b/tests/functional/ext/set_membership/use_set_membership.py index 50e07f4dd..7872d7f98 100644 --- a/tests/functional/ext/set_membership/use_set_membership.py +++ b/tests/functional/ext/set_membership/use_set_membership.py @@ -33,7 +33,7 @@ True == x in [1, 2, 3] # [use-set-for-membership] # noqa: E712 x in (1, "Hello World", False, None) # [use-set-for-membership] x in (1, []) # List is not hashable -if some_var: +if x: var2 = 2 else: var2 = [] diff --git a/tests/functional/ext/typing/typing_broken_noreturn.py b/tests/functional/ext/typing/typing_broken_noreturn.py index 4d10ed13a..e7b5643ae 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn.py +++ b/tests/functional/ext/typing/typing_broken_noreturn.py @@ -1,10 +1,10 @@ """ -'typing.NoReturn' is broken inside compond types for Python 3.7.0 +'typing.NoReturn' is broken inside compound types for Python 3.7.0 https://bugs.python.org/issue34921 If no runtime introspection is required, use string annotations instead. """ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised import typing from typing import TYPE_CHECKING, Callable, NoReturn, Union diff --git a/tests/functional/ext/typing/typing_broken_noreturn_future_import.py b/tests/functional/ext/typing/typing_broken_noreturn_future_import.py index 4743750bc..e0ea7761b 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn_future_import.py +++ b/tests/functional/ext/typing/typing_broken_noreturn_future_import.py @@ -7,7 +7,7 @@ If no runtime introspection is required, use string annotations instead. With 'from __future__ import annotations', only emit errors for nodes not in a type annotation context. """ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised from __future__ import annotations import typing diff --git a/tests/functional/ext/typing/typing_broken_noreturn_py372.py b/tests/functional/ext/typing/typing_broken_noreturn_py372.py index 4ff1a71b7..6bd31f069 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn_py372.py +++ b/tests/functional/ext/typing/typing_broken_noreturn_py372.py @@ -6,7 +6,7 @@ If no runtime introspection is required, use string annotations instead. Don't emit errors if py-version set to >= 3.7.2. """ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised import typing from typing import TYPE_CHECKING, Callable, NoReturn, Union diff --git a/tests/functional/f/first_arg.py b/tests/functional/f/first_arg.py index d8007e144..ac5c6bcf6 100644 --- a/tests/functional/f/first_arg.py +++ b/tests/functional/f/first_arg.py @@ -31,7 +31,7 @@ class Meta(type): pass # C0205, metaclass classmethod - def class1(cls): + def class1(mcs): pass class1 = classmethod(class1) # [no-classmethod-decorator] diff --git a/tests/functional/f/first_arg.txt b/tests/functional/f/first_arg.txt index 26aabd22e..3bfb8a0f5 100644 --- a/tests/functional/f/first_arg.txt +++ b/tests/functional/f/first_arg.txt @@ -2,8 +2,8 @@ bad-classmethod-argument:8:4:8:15:Obj.__new__:Class method __new__ should have ' no-classmethod-decorator:14:4:14:10:Obj:Consider using a decorator instead of calling classmethod:UNDEFINED bad-classmethod-argument:16:4:16:14:Obj.class2:Class method class2 should have 'cls' as first argument:UNDEFINED no-classmethod-decorator:18:4:18:10:Obj:Consider using a decorator instead of calling classmethod:UNDEFINED -bad-mcs-classmethod-argument:23:4:23:15:Meta.__new__:Metaclass class method __new__ should have 'cls' as first argument:UNDEFINED +bad-mcs-classmethod-argument:23:4:23:15:Meta.__new__:Metaclass class method __new__ should have 'mcs' as first argument:UNDEFINED bad-mcs-method-argument:30:4:30:15:Meta.method2:Metaclass method method2 should have 'cls' as first argument:UNDEFINED no-classmethod-decorator:36:4:36:10:Meta:Consider using a decorator instead of calling classmethod:UNDEFINED -bad-mcs-classmethod-argument:38:4:38:14:Meta.class2:Metaclass class method class2 should have 'cls' as first argument:UNDEFINED +bad-mcs-classmethod-argument:38:4:38:14:Meta.class2:Metaclass class method class2 should have 'mcs' as first argument:UNDEFINED no-classmethod-decorator:40:4:40:10:Meta:Consider using a decorator instead of calling classmethod:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_collections.txt b/tests/functional/g/generic_alias/generic_alias_collections.txt index 663b81abb..4abaa0338 100644 --- a/tests/functional/g/generic_alias/generic_alias_collections.txt +++ b/tests/functional/g/generic_alias/generic_alias_collections.txt @@ -1,16 +1,16 @@ unsubscriptable-object:66:0:66:24::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:67:0:67:21::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED -abstract-method:74:0:74:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:77:0:77:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:80:0:80:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden:UNDEFINED -abstract-method:80:0:80:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:80:0:80:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:99:0:99:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:99:0:99:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:104:0:104:24:CustomAbstractCls2:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:104:0:104:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:106:0:106:26:CustomImplementation:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:106:0:106:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:74:0:74:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE +abstract-method:77:0:77:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable':INFERENCE +abstract-method:80:0:80:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:80:0:80:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:80:0:80:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:99:0:99:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:99:0:99:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:104:0:104:24:CustomAbstractCls2:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'CustomAbstractCls2':INFERENCE +abstract-method:104:0:104:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE +abstract-method:106:0:106:26:CustomImplementation:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'CustomImplementation':INFERENCE +abstract-method:106:0:106:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE unsubscriptable-object:125:9:125:12::Value 'int' is unsubscriptable:UNDEFINED unsubscriptable-object:126:15:126:39::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:127:12:127:33::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37.txt b/tests/functional/g/generic_alias/generic_alias_collections_py37.txt index 84a217d2f..72104b4be 100644 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37.txt +++ b/tests/functional/g/generic_alias/generic_alias_collections_py37.txt @@ -39,7 +39,7 @@ unsubscriptable-object:63:0:63:8::Value 're.Match' is unsubscriptable:UNDEFINED unsubscriptable-object:69:0:69:24::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:70:0:70:21::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED unsubscriptable-object:73:0:73:26::Value 'collections.abc.ByteString' is unsubscriptable:UNDEFINED -abstract-method:77:0:77:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED +abstract-method:77:0:77:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE unsubscriptable-object:80:22:80:46:DerivedIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:83:24:83:50:DerivedCollection:Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED unsubscriptable-object:88:18:88:22:DerivedList:Value 'list' is unsubscriptable:UNDEFINED @@ -47,11 +47,11 @@ unsubscriptable-object:91:17:91:20:DerivedSet:Value 'set' is unsubscriptable:UND unsubscriptable-object:94:25:94:48:DerivedOrderedDict:Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED unsubscriptable-object:97:31:97:55:DerivedListIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:97:26:97:30:DerivedListIterable:Value 'list' is unsubscriptable:UNDEFINED -abstract-method:102:0:102:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:102:0:102:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:107:0:107:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:102:0:102:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:102:0:102:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:107:0:107:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE unsubscriptable-object:107:48:107:72:CustomAbstractCls2:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -abstract-method:109:0:109:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:109:0:109:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE unsubscriptable-object:114:11:114:16::Value 'tuple' is unsubscriptable:UNDEFINED unsubscriptable-object:115:10:115:14::Value 'dict' is unsubscriptable:UNDEFINED unsubscriptable-object:116:17:116:40::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt b/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt index ee1407bdb..0dd989f2e 100644 --- a/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt +++ b/tests/functional/g/generic_alias/generic_alias_collections_py37_with_typing.txt @@ -39,7 +39,7 @@ unsubscriptable-object:65:0:65:8::Value 're.Match' is unsubscriptable:UNDEFINED unsubscriptable-object:71:0:71:24::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:72:0:72:21::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED unsubscriptable-object:75:0:75:26::Value 'collections.abc.ByteString' is unsubscriptable:UNDEFINED -abstract-method:79:0:79:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED +abstract-method:79:0:79:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE unsubscriptable-object:82:22:82:46:DerivedIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:85:24:85:50:DerivedCollection:Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED unsubscriptable-object:90:18:90:22:DerivedList:Value 'list' is unsubscriptable:UNDEFINED @@ -47,11 +47,11 @@ unsubscriptable-object:93:17:93:20:DerivedSet:Value 'set' is unsubscriptable:UND unsubscriptable-object:96:25:96:48:DerivedOrderedDict:Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED unsubscriptable-object:99:31:99:55:DerivedListIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:99:26:99:30:DerivedListIterable:Value 'list' is unsubscriptable:UNDEFINED -abstract-method:104:0:104:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:104:0:104:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:109:0:109:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:104:0:104:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:104:0:104:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:109:0:109:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE unsubscriptable-object:109:48:109:72:CustomAbstractCls2:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -abstract-method:111:0:111:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:111:0:111:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE unsubscriptable-object:116:11:116:16::Value 'tuple' is unsubscriptable:UNDEFINED unsubscriptable-object:117:10:117:14::Value 'dict' is unsubscriptable:UNDEFINED unsubscriptable-object:118:17:118:40::Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt b/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt index 2bafe20ed..188039c60 100644 --- a/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt +++ b/tests/functional/g/generic_alias/generic_alias_mixed_py37.txt @@ -1,5 +1,5 @@ -abstract-method:34:0:34:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:37:0:37:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:40:0:40:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden:UNDEFINED -abstract-method:40:0:40:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:40:0:40:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:34:0:34:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE +abstract-method:37:0:37:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable':INFERENCE +abstract-method:40:0:40:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:40:0:40:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:40:0:40:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedCollection':INFERENCE diff --git a/tests/functional/g/generic_alias/generic_alias_mixed_py39.txt b/tests/functional/g/generic_alias/generic_alias_mixed_py39.txt index 06dbdc197..58b7e9860 100644 --- a/tests/functional/g/generic_alias/generic_alias_mixed_py39.txt +++ b/tests/functional/g/generic_alias/generic_alias_mixed_py39.txt @@ -1,5 +1,5 @@ -abstract-method:29:0:29:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:32:0:32:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:35:0:35:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden:UNDEFINED -abstract-method:35:0:35:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:35:0:35:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:29:0:29:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE +abstract-method:32:0:32:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable':INFERENCE +abstract-method:35:0:35:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:35:0:35:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:35:0:35:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedCollection':INFERENCE diff --git a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt b/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt index d481f7ac6..cbf46bfef 100644 --- a/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt +++ b/tests/functional/g/generic_alias/generic_alias_postponed_evaluation_py37.txt @@ -39,7 +39,7 @@ unsubscriptable-object:68:0:68:8::Value 're.Match' is unsubscriptable:UNDEFINED unsubscriptable-object:74:0:74:24::Value 'collections.abc.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:75:0:75:21::Value 'collections.abc.Sized' is unsubscriptable:UNDEFINED unsubscriptable-object:78:0:78:26::Value 'collections.abc.ByteString' is unsubscriptable:UNDEFINED -abstract-method:82:0:82:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED +abstract-method:82:0:82:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE unsubscriptable-object:85:22:85:46:DerivedIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:88:24:88:50:DerivedCollection:Value 'collections.abc.Collection' is unsubscriptable:UNDEFINED unsubscriptable-object:93:18:93:22:DerivedList:Value 'list' is unsubscriptable:UNDEFINED @@ -47,9 +47,9 @@ unsubscriptable-object:96:17:96:20:DerivedSet:Value 'set' is unsubscriptable:UND unsubscriptable-object:99:25:99:48:DerivedOrderedDict:Value 'collections.OrderedDict' is unsubscriptable:UNDEFINED unsubscriptable-object:102:31:102:55:DerivedListIterable:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED unsubscriptable-object:102:26:102:30:DerivedListIterable:Value 'list' is unsubscriptable:UNDEFINED -abstract-method:107:0:107:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:107:0:107:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:112:0:112:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:107:0:107:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:107:0:107:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:112:0:112:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE unsubscriptable-object:112:48:112:72:CustomAbstractCls2:Value 'collections.abc.Iterable' is unsubscriptable:UNDEFINED -abstract-method:114:0:114:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED +abstract-method:114:0:114:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE unsubscriptable-object:188:8:188:9:B:Value 'A' is unsubscriptable:UNDEFINED diff --git a/tests/functional/g/generic_alias/generic_alias_related.txt b/tests/functional/g/generic_alias/generic_alias_related.txt index d13f75fa7..b2123350c 100644 --- a/tests/functional/g/generic_alias/generic_alias_related.txt +++ b/tests/functional/g/generic_alias/generic_alias_related.txt @@ -2,4 +2,4 @@ unsubscriptable-object:34:0:34:20::Value 'ClsUnsubscriptable()' is unsubscriptab unsubscriptable-object:35:0:35:18::Value 'ClsUnsubscriptable' is unsubscriptable:UNDEFINED unsubscriptable-object:38:0:38:10::Value 'ClsGetItem' is unsubscriptable:UNDEFINED unsubscriptable-object:40:0:40:17::Value 'ClsClassGetItem()' is unsubscriptable:UNDEFINED -abstract-method:53:0:53:13:Derived:Method 'abstract_method' is abstract in class 'ClsAbstract' but is not overridden:UNDEFINED +abstract-method:53:0:53:13:Derived:Method 'abstract_method' is abstract in class 'ClsAbstract' but is not overridden in child class 'Derived':INFERENCE diff --git a/tests/functional/g/generic_alias/generic_alias_related_py39.txt b/tests/functional/g/generic_alias/generic_alias_related_py39.txt index 114376f5e..c24c0f98b 100644 --- a/tests/functional/g/generic_alias/generic_alias_related_py39.txt +++ b/tests/functional/g/generic_alias/generic_alias_related_py39.txt @@ -2,4 +2,4 @@ unsubscriptable-object:36:0:36:20::Value 'ClsUnsubscriptable()' is unsubscriptab unsubscriptable-object:37:0:37:18::Value 'ClsUnsubscriptable' is unsubscriptable:UNDEFINED unsubscriptable-object:40:0:40:10::Value 'ClsGetItem' is unsubscriptable:UNDEFINED unsubscriptable-object:42:0:42:17::Value 'ClsClassGetItem()' is unsubscriptable:UNDEFINED -abstract-method:55:0:55:13:Derived:Method 'abstract_method' is abstract in class 'ClsAbstract' but is not overridden:UNDEFINED +abstract-method:55:0:55:13:Derived:Method 'abstract_method' is abstract in class 'ClsAbstract' but is not overridden in child class 'Derived':INFERENCE diff --git a/tests/functional/g/generic_alias/generic_alias_typing.txt b/tests/functional/g/generic_alias/generic_alias_typing.txt index f33f49f91..2a433cd24 100644 --- a/tests/functional/g/generic_alias/generic_alias_typing.txt +++ b/tests/functional/g/generic_alias/generic_alias_typing.txt @@ -1,18 +1,18 @@ unsubscriptable-object:66:0:66:17::Value 'typing.ByteString' is unsubscriptable:UNDEFINED unsubscriptable-object:67:0:67:15::Value 'typing.Hashable' is unsubscriptable:UNDEFINED unsubscriptable-object:68:0:68:12::Value 'typing.Sized' is unsubscriptable:UNDEFINED -abstract-method:72:0:72:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:75:0:75:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:78:0:78:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden:UNDEFINED -abstract-method:78:0:78:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:78:0:78:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:100:0:100:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden:UNDEFINED -abstract-method:100:0:100:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:105:0:105:24:CustomAbstractCls2:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:105:0:105:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:107:0:107:26:CustomImplementation:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED -abstract-method:107:0:107:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden:UNDEFINED -abstract-method:118:0:118:22:DerivedIterable2:Method '__iter__' is abstract in class 'Iterable' but is not overridden:UNDEFINED +abstract-method:72:0:72:21:DerivedHashable:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedHashable':INFERENCE +abstract-method:75:0:75:21:DerivedIterable:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable':INFERENCE +abstract-method:78:0:78:23:DerivedCollection:Method '__contains__' is abstract in class 'Container' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:78:0:78:23:DerivedCollection:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:78:0:78:23:DerivedCollection:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedCollection':INFERENCE +abstract-method:100:0:100:21:DerivedMultiple:Method '__hash__' is abstract in class 'Hashable' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:100:0:100:21:DerivedMultiple:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'DerivedMultiple':INFERENCE +abstract-method:105:0:105:24:CustomAbstractCls2:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'CustomAbstractCls2':INFERENCE +abstract-method:105:0:105:24:CustomAbstractCls2:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomAbstractCls2':INFERENCE +abstract-method:107:0:107:26:CustomImplementation:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'CustomImplementation':INFERENCE +abstract-method:107:0:107:26:CustomImplementation:Method '__len__' is abstract in class 'Sized' but is not overridden in child class 'CustomImplementation':INFERENCE +abstract-method:118:0:118:22:DerivedIterable2:Method '__iter__' is abstract in class 'Iterable' but is not overridden in child class 'DerivedIterable2':INFERENCE unsubscriptable-object:138:9:138:12::Value 'int' is unsubscriptable:UNDEFINED unsubscriptable-object:139:17:139:34::Value 'typing.ByteString' is unsubscriptable:UNDEFINED unsubscriptable-object:140:15:140:30::Value 'typing.Hashable' is unsubscriptable:UNDEFINED diff --git a/tests/functional/i/implicit/implicit_str_concat.py b/tests/functional/i/implicit/implicit_str_concat.py index 920b29258..7e28b4cc2 100644 --- a/tests/functional/i/implicit/implicit_str_concat.py +++ b/tests/functional/i/implicit/implicit_str_concat.py @@ -1,4 +1,4 @@ -# pylint: disable=invalid-name, missing-docstring, redundant-u-string-prefix, line-too-long +# pylint: disable=invalid-name, missing-docstring, redundant-u-string-prefix, line-too-long, superfluous-parens # Basic test with a list TEST_LIST1 = ['a' 'b'] # [implicit-str-concat] diff --git a/tests/functional/i/inconsistent/inconsistent_returns.py b/tests/functional/i/inconsistent/inconsistent_returns.py index 08dde253e..c1183b288 100644 --- a/tests/functional/i/inconsistent/inconsistent_returns.py +++ b/tests/functional/i/inconsistent/inconsistent_returns.py @@ -91,13 +91,13 @@ def explicit_returns6(x, y, z): def explicit_returns7(arg): if arg < 0: - arg = 2 * arg + arg *= 2 return 'below 0' elif arg == 0: print("Null arg") return '0' else: - arg = 3 * arg + arg *= 3 return 'above 0' def bug_1772(): @@ -184,7 +184,7 @@ def explicit_implicit_returns3(arg): # [inconsistent-return-statements] def returns_missing_in_catched_exceptions(arg): # [inconsistent-return-statements] try: - arg = arg**2 + arg **= arg raise ValueError('test') except ValueError: print('ValueError') diff --git a/tests/functional/i/invalid/invalid_all_format.py b/tests/functional/i/invalid/invalid_all_format.py index 1d1c0b18f..10537c6fb 100644 --- a/tests/functional/i/invalid/invalid_all_format.py +++ b/tests/functional/i/invalid/invalid_all_format.py @@ -2,6 +2,6 @@ Tuples with one element MUST contain a comma! Otherwise it's a string. """ -__all__ = ("CONST") # [invalid-all-format] +__all__ = ("CONST") # [invalid-all-format, superfluous-parens] CONST = 42 diff --git a/tests/functional/i/invalid/invalid_all_format.txt b/tests/functional/i/invalid/invalid_all_format.txt index 2ba8dc17f..2f6ac363b 100644 --- a/tests/functional/i/invalid/invalid_all_format.txt +++ b/tests/functional/i/invalid/invalid_all_format.txt @@ -1 +1,2 @@ invalid-all-format:5:11:None:None::Invalid format for __all__, must be tuple or list:UNDEFINED +superfluous-parens:5:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED diff --git a/tests/functional/i/invalid/invalid_class_object.txt b/tests/functional/i/invalid/invalid_class_object.txt index 793a5de69..96e4d42c8 100644 --- a/tests/functional/i/invalid/invalid_class_object.txt +++ b/tests/functional/i/invalid/invalid_class_object.txt @@ -1,5 +1,5 @@ -invalid-class-object:20:0:20:11::Invalid __class__ object:UNDEFINED -invalid-class-object:21:0:21:11::Invalid __class__ object:UNDEFINED -invalid-class-object:50:8:50:22:Pylint7429Good.class_defining_function_bad:Invalid __class__ object:UNDEFINED -invalid-class-object:58:15:58:29:Pylint7429Good.class_defining_function_bad_inverted:Invalid __class__ object:UNDEFINED -invalid-class-object:62:15:62:29:Pylint7429Good.class_defining_function_complex_bad:Invalid __class__ object:UNDEFINED +invalid-class-object:20:0:20:11::Invalid assignment to '__class__'. Should be a class definition but got a 'Instance':INFERENCE +invalid-class-object:21:0:21:11::Invalid assignment to '__class__'. Should be a class definition but got a 'Const':INFERENCE +invalid-class-object:50:8:50:22:Pylint7429Good.class_defining_function_bad:Invalid assignment to '__class__'. Should be a class definition but got a 'Const':INFERENCE +invalid-class-object:58:15:58:29:Pylint7429Good.class_defining_function_bad_inverted:Invalid assignment to '__class__'. Should be a class definition but got a 'Const':INFERENCE +invalid-class-object:62:15:62:29:Pylint7429Good.class_defining_function_complex_bad:Invalid assignment to '__class__'. Should be a class definition but got a 'Const':INFERENCE diff --git a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.txt b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.txt index 9e8e7ae00..f2ccd8a05 100644 --- a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.txt +++ b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.txt @@ -1,11 +1,11 @@ -raising-non-exception:38:4:38:30:bad_case0:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:42:4:42:25:bad_case1:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:48:4:48:30:bad_case2:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:52:4:52:23:bad_case3:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -notimplemented-raised:56:4:56:31:bad_case4:NotImplemented raised - should raise NotImplementedError:UNDEFINED -raising-bad-type:60:4:60:11:bad_case5:Raising int while only classes or instances are allowed:UNDEFINED -raising-bad-type:64:4:64:14:bad_case6:Raising NoneType while only classes or instances are allowed:UNDEFINED -raising-non-exception:68:4:68:14:bad_case7:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:72:4:72:15:bad_case8:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-non-exception:76:4:76:14:bad_case9:Raising a new style class which doesn't inherit from BaseException:UNDEFINED -raising-bad-type:110:4:110:18:bad_case10:Raising str while only classes or instances are allowed:UNDEFINED +raising-non-exception:38:4:38:30:bad_case0:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:42:4:42:25:bad_case1:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:48:4:48:30:bad_case2:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:52:4:52:23:bad_case3:Raising a new style class which doesn't inherit from BaseException:INFERENCE +notimplemented-raised:56:4:56:31:bad_case4:NotImplemented raised - should raise NotImplementedError:HIGH +raising-bad-type:60:4:60:11:bad_case5:Raising int while only classes or instances are allowed:INFERENCE +raising-bad-type:64:4:64:14:bad_case6:Raising NoneType while only classes or instances are allowed:INFERENCE +raising-non-exception:68:4:68:14:bad_case7:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:72:4:72:15:bad_case8:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-non-exception:76:4:76:14:bad_case9:Raising a new style class which doesn't inherit from BaseException:INFERENCE +raising-bad-type:110:4:110:18:bad_case10:Raising str while only classes or instances are allowed:INFERENCE diff --git a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py index 5614674c3..efe6ba25f 100644 --- a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py +++ b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py @@ -1,6 +1,6 @@ """Check invalid value returned by __getnewargs_ex__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,use-dict-literal,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,use-dict-literal,unnecessary-lambda-assignment,use-dict-literal import six from missing import Missing diff --git a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_returned.py b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_returned.py index 49fe7b602..06cd81dd0 100644 --- a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_returned.py +++ b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_returned.py @@ -1,6 +1,6 @@ """Check invalid value returned by __getnewargs__ """ -# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment +# pylint: disable=too-few-public-methods,missing-docstring,import-error,unnecessary-lambda-assignment,use-dict-literal import six from missing import Missing diff --git a/tests/functional/i/invalid/invalid_name/invalid_name-module-disable.py b/tests/functional/i/invalid/invalid_name/invalid_name-module-disable.py new file mode 100644 index 000000000..f6074eebb --- /dev/null +++ b/tests/functional/i/invalid/invalid_name/invalid_name-module-disable.py @@ -0,0 +1,6 @@ +# pylint: disable=invalid-name + +"""Regression test for disabling of invalid-name for module names. + +See https://github.com/PyCQA/pylint/issues/3973. +""" diff --git a/tests/functional/i/invalid/invalid_sequence_index.py b/tests/functional/i/invalid/invalid_sequence_index.py index ec12b6e94..11108d4fe 100644 --- a/tests/functional/i/invalid/invalid_sequence_index.py +++ b/tests/functional/i/invalid/invalid_sequence_index.py @@ -202,7 +202,7 @@ def function24(): test[0] = 0 # setitem with int, no error del test[0] # delitem with int, no error -# Teest ExtSlice usage +# Test ExtSlice usage def function25(): """Extended slice used with a list""" return TESTLIST[..., 0] # [invalid-sequence-index] diff --git a/tests/functional/i/invalid/invalid_slice_index.py b/tests/functional/i/invalid/invalid_slice_index.py index 2e5d2cdb0..253d01ae1 100644 --- a/tests/functional/i/invalid/invalid_slice_index.py +++ b/tests/functional/i/invalid/invalid_slice_index.py @@ -1,6 +1,6 @@ """Errors for invalid slice indices""" # pylint: disable=too-few-public-methods,missing-docstring,expression-not-assigned,unnecessary-pass - +# pylint: disable=pointless-statement TESTLIST = [1, 2, 3] @@ -11,7 +11,10 @@ def function1(): def function2(): """strings used as indices""" - return TESTLIST['0':'1':] # [invalid-slice-index,invalid-slice-index] + TESTLIST['0':'1':] # [invalid-slice-index,invalid-slice-index] + ()['0':'1'] # [invalid-slice-index,invalid-slice-index] + ""["a":"z"] # [invalid-slice-index,invalid-slice-index] + b""["a":"z"] # [invalid-slice-index,invalid-slice-index] def function3(): """class without __index__ used as index""" @@ -22,10 +25,27 @@ def function3(): return TESTLIST[NoIndexTest()::] # [invalid-slice-index] +def invalid_step(): + """0 is an invalid value for slice step with most builtin sequences.""" + TESTLIST[::0] # [invalid-slice-step] + [][::0] # [invalid-slice-step] + ""[::0] # [invalid-slice-step] + b""[::0] # [invalid-slice-step] + + class Custom: + def __getitem__(self, indices): + ... + + Custom()[::0] # no error -> custom __getitem__ method + +def invalid_slice_range(): + range(5)['0':'1'] # [invalid-slice-index,invalid-slice-index] + + # Valid indices def function4(): """integers used as indices""" - return TESTLIST[0:0:0] # no error + return TESTLIST[0:1:1] def function5(): """None used as indices""" diff --git a/tests/functional/i/invalid/invalid_slice_index.txt b/tests/functional/i/invalid/invalid_slice_index.txt index 97754e840..3e7713ba7 100644 --- a/tests/functional/i/invalid/invalid_slice_index.txt +++ b/tests/functional/i/invalid/invalid_slice_index.txt @@ -1,5 +1,17 @@ invalid-slice-index:10:20:10:22:function1:Slice index is not an int, None, or instance with __index__:UNDEFINED invalid-slice-index:10:23:10:25:function1:Slice index is not an int, None, or instance with __index__:UNDEFINED -invalid-slice-index:14:20:14:23:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED -invalid-slice-index:14:24:14:27:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED -invalid-slice-index:23:20:23:33:function3:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:14:13:14:16:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:14:17:14:20:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:15:7:15:10:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:15:11:15:14:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:16:7:16:10:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:16:11:16:14:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:17:8:17:11:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:17:12:17:15:function2:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:26:20:26:33:function3:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-step:30:15:30:16:invalid_step:Slice step cannot be 0:HIGH +invalid-slice-step:31:9:31:10:invalid_step:Slice step cannot be 0:HIGH +invalid-slice-step:32:9:32:10:invalid_step:Slice step cannot be 0:HIGH +invalid-slice-step:33:10:33:11:invalid_step:Slice step cannot be 0:HIGH +invalid-slice-index:42:13:42:16:invalid_slice_range:Slice index is not an int, None, or instance with __index__:UNDEFINED +invalid-slice-index:42:17:42:20:invalid_slice_range:Slice index is not an int, None, or instance with __index__:UNDEFINED diff --git a/tests/functional/i/iterable_context.py b/tests/functional/i/iterable_context.py index bc77ade34..fb035c4df 100644 --- a/tests/functional/i/iterable_context.py +++ b/tests/functional/i/iterable_context.py @@ -4,6 +4,7 @@ iterating/mapping context. """ # pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,unused-argument,bad-mcs-method-argument, # pylint: disable=wrong-import-position,no-else-return, unnecessary-comprehension,redundant-u-string-prefix +# pylint: disable=use-dict-literal # primitives numbers = [1, 2, 3] diff --git a/tests/functional/i/iterable_context.txt b/tests/functional/i/iterable_context.txt index e0ca8c4fe..ef59b379c 100644 --- a/tests/functional/i/iterable_context.txt +++ b/tests/functional/i/iterable_context.txt @@ -1,10 +1,10 @@ -not-an-iterable:57:9:57:22::Non-iterable value powers_of_two is used in an iterating context:UNDEFINED -not-an-iterable:92:6:92:9::Non-iterable value A() is used in an iterating context:UNDEFINED -not-an-iterable:94:6:94:7::Non-iterable value B is used in an iterating context:UNDEFINED -not-an-iterable:95:9:95:12::Non-iterable value A() is used in an iterating context:UNDEFINED -not-an-iterable:99:9:99:10::Non-iterable value B is used in an iterating context:UNDEFINED -not-an-iterable:102:9:102:14::Non-iterable value range is used in an iterating context:UNDEFINED -not-an-iterable:106:9:106:13::Non-iterable value True is used in an iterating context:UNDEFINED -not-an-iterable:109:9:109:13::Non-iterable value None is used in an iterating context:UNDEFINED -not-an-iterable:112:9:112:12::Non-iterable value 8.5 is used in an iterating context:UNDEFINED -not-an-iterable:115:9:115:11::Non-iterable value 10 is used in an iterating context:UNDEFINED +not-an-iterable:58:9:58:22::Non-iterable value powers_of_two is used in an iterating context:UNDEFINED +not-an-iterable:93:6:93:9::Non-iterable value A() is used in an iterating context:UNDEFINED +not-an-iterable:95:6:95:7::Non-iterable value B is used in an iterating context:UNDEFINED +not-an-iterable:96:9:96:12::Non-iterable value A() is used in an iterating context:UNDEFINED +not-an-iterable:100:9:100:10::Non-iterable value B is used in an iterating context:UNDEFINED +not-an-iterable:103:9:103:14::Non-iterable value range is used in an iterating context:UNDEFINED +not-an-iterable:107:9:107:13::Non-iterable value True is used in an iterating context:UNDEFINED +not-an-iterable:110:9:110:13::Non-iterable value None is used in an iterating context:UNDEFINED +not-an-iterable:113:9:113:12::Non-iterable value 8.5 is used in an iterating context:UNDEFINED +not-an-iterable:116:9:116:11::Non-iterable value 10 is used in an iterating context:UNDEFINED diff --git a/tests/functional/m/mapping_context.py b/tests/functional/m/mapping_context.py index 1d8a46afc..8dc6b3b72 100644 --- a/tests/functional/m/mapping_context.py +++ b/tests/functional/m/mapping_context.py @@ -1,7 +1,7 @@ """ Checks that only valid values are used in a mapping context. """ -# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,wrong-import-position +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,import-error,wrong-import-position,use-dict-literal def test(**kwargs): diff --git a/tests/functional/m/misplaced_bare_raise.txt b/tests/functional/m/misplaced_bare_raise.txt index d26893e3f..dbb20c266 100644 --- a/tests/functional/m/misplaced_bare_raise.txt +++ b/tests/functional/m/misplaced_bare_raise.txt @@ -1,7 +1,7 @@ -misplaced-bare-raise:6:4:6:9::The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:36:16:36:21:test1.best:The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:39:4:39:9:test1:The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:40:0:40:5::The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:49:4:49:9::The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:57:4:57:9:A:The raise statement is not inside an except clause:UNDEFINED -misplaced-bare-raise:68:4:68:9::The raise statement is not inside an except clause:UNDEFINED +misplaced-bare-raise:6:4:6:9::The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:36:16:36:21:test1.best:The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:39:4:39:9:test1:The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:40:0:40:5::The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:49:4:49:9::The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:57:4:57:9:A:The raise statement is not inside an except clause:HIGH +misplaced-bare-raise:68:4:68:9::The raise statement is not inside an except clause:HIGH diff --git a/tests/functional/m/multiple_statements.py b/tests/functional/m/multiple_statements.py index 5b55eac42..c3252f797 100644 --- a/tests/functional/m/multiple_statements.py +++ b/tests/functional/m/multiple_statements.py @@ -27,4 +27,4 @@ finally: @overload def concat2(arg1: str) -> str: ... -def concat2(arg1: str) -> str: ... # [multiple-statements] +def concat2(arg1: str) -> str: ... diff --git a/tests/functional/m/multiple_statements.txt b/tests/functional/m/multiple_statements.txt index 34d80508e..661314268 100644 --- a/tests/functional/m/multiple_statements.txt +++ b/tests/functional/m/multiple_statements.txt @@ -3,4 +3,3 @@ multiple-statements:9:9:9:13::More than one statement on a single line:UNDEFINED multiple-statements:13:26:13:30:MyError:More than one statement on a single line:UNDEFINED multiple-statements:15:26:15:31:MyError:More than one statement on a single line:UNDEFINED multiple-statements:17:26:17:31:MyError:More than one statement on a single line:UNDEFINED -multiple-statements:30:31:30:34:concat2:More than one statement on a single line:UNDEFINED diff --git a/tests/functional/m/multiple_statements_single_line.py b/tests/functional/m/multiple_statements_single_line.py index 4a77d992e..93a470702 100644 --- a/tests/functional/m/multiple_statements_single_line.py +++ b/tests/functional/m/multiple_statements_single_line.py @@ -27,4 +27,4 @@ finally: @overload def concat2(arg1: str) -> str: ... -def concat2(arg1: str) -> str: ... # [multiple-statements] +def concat2(arg1: str) -> str: ... diff --git a/tests/functional/m/multiple_statements_single_line.txt b/tests/functional/m/multiple_statements_single_line.txt index a28fc96c4..cac2f7eb2 100644 --- a/tests/functional/m/multiple_statements_single_line.txt +++ b/tests/functional/m/multiple_statements_single_line.txt @@ -1,3 +1,2 @@ multiple-statements:9:9:9:13::More than one statement on a single line:UNDEFINED multiple-statements:17:26:17:31:MyError:More than one statement on a single line:UNDEFINED -multiple-statements:30:31:30:34:concat2:More than one statement on a single line:UNDEFINED diff --git a/tests/functional/n/named_expr_without_context_py38.py b/tests/functional/n/named_expr_without_context_py38.py new file mode 100644 index 000000000..ee45b84b3 --- /dev/null +++ b/tests/functional/n/named_expr_without_context_py38.py @@ -0,0 +1,6 @@ +# pylint: disable=missing-docstring + +if (a := 2): + pass + +(b := 1) # [named-expr-without-context] diff --git a/tests/functional/n/named_expr_without_context_py38.rc b/tests/functional/n/named_expr_without_context_py38.rc new file mode 100644 index 000000000..85fc502b3 --- /dev/null +++ b/tests/functional/n/named_expr_without_context_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/n/named_expr_without_context_py38.txt b/tests/functional/n/named_expr_without_context_py38.txt new file mode 100644 index 000000000..2ab9a2733 --- /dev/null +++ b/tests/functional/n/named_expr_without_context_py38.txt @@ -0,0 +1 @@ +named-expr-without-context:6:0:6:8::Named expression used without context:HIGH diff --git a/tests/functional/n/nested_min_max.py b/tests/functional/n/nested_min_max.py new file mode 100644 index 000000000..cef63dc2b --- /dev/null +++ b/tests/functional/n/nested_min_max.py @@ -0,0 +1,21 @@ +"""Test detection of redundant nested calls to min/max functions""" + +# pylint: disable=redefined-builtin,unnecessary-lambda-assignment + +min(1, min(2, 3)) # [nested-min-max] +max(1, max(2, 3)) # [nested-min-max] +min(min(1, 2), 3) # [nested-min-max] +min(min(min(1, 2), 3), 4) # [nested-min-max, nested-min-max] +min(1, max(2, 3)) +min(1, 2, 3) +min(min(1, 2), min(3, 4)) # [nested-min-max] +min(len([]), min(len([1]), len([1, 2]))) # [nested-min-max] + +orig_min = min +min = lambda *args: args[0] +min(1, min(2, 3)) +orig_min(1, orig_min(2, 3)) # [nested-min-max] + +# This is too complicated (for now) as there is no clear better way to write it +max(max(i for i in range(10)), 0) +max(max(max(i for i in range(10)), 0), 1) diff --git a/tests/functional/n/nested_min_max.txt b/tests/functional/n/nested_min_max.txt new file mode 100644 index 000000000..9bcb663e3 --- /dev/null +++ b/tests/functional/n/nested_min_max.txt @@ -0,0 +1,8 @@ +nested-min-max:5:0:5:17::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE +nested-min-max:6:0:6:17::Do not use nested call of 'max'; it's possible to do 'max(1, 2, 3)' instead:INFERENCE +nested-min-max:7:0:7:17::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE +nested-min-max:8:4:8:21::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE +nested-min-max:8:0:8:25::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3, 4)' instead:INFERENCE +nested-min-max:11:0:11:25::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3, 4)' instead:INFERENCE +nested-min-max:12:0:12:40::Do not use nested call of 'min'; it's possible to do 'min(len([]), len([1]), len([1, 2]))' instead:INFERENCE +nested-min-max:17:0:17:27::Do not use nested call of 'orig_min'; it's possible to do 'orig_min(1, 2, 3)' instead:INFERENCE diff --git a/tests/functional/n/no/no_else_raise.py b/tests/functional/n/no/no_else_raise.py index 9a54dfc9f..33a1fb561 100644 --- a/tests/functional/n/no/no_else_raise.py +++ b/tests/functional/n/no/no_else_raise.py @@ -1,6 +1,6 @@ """ Test that superfluous else raise are detected. """ -# pylint:disable=invalid-name,missing-docstring,unused-variable,raise-missing-from +# pylint:disable=invalid-name,missing-docstring,unused-variable,raise-missing-from,broad-exception-raised def foo1(x, y, z): if x: # [no-else-raise] diff --git a/tests/functional/n/no/no_member_augassign.py b/tests/functional/n/no/no_member_augassign.py new file mode 100644 index 000000000..1ffd9a168 --- /dev/null +++ b/tests/functional/n/no/no_member_augassign.py @@ -0,0 +1,25 @@ +"""Tests for no-member in relation to AugAssign operations.""" +# pylint: disable=missing-module-docstring, too-few-public-methods, missing-class-docstring, invalid-name + +# Test for: https://github.com/PyCQA/pylint/issues/4562 +class A: + value: int + +obj_a = A() +obj_a.value += 1 # [no-member] + + +class B: + value: int + +obj_b = B() +obj_b.value = 1 + obj_b.value # [no-member] + + +class C: + value: int + + +obj_c = C() +obj_c.value += 1 # [no-member] +obj_c.value = 1 + obj_c.value # [no-member] diff --git a/tests/functional/n/no/no_member_augassign.txt b/tests/functional/n/no/no_member_augassign.txt new file mode 100644 index 000000000..68abf0b93 --- /dev/null +++ b/tests/functional/n/no/no_member_augassign.txt @@ -0,0 +1,4 @@ +no-member:9:0:9:11::Instance of 'A' has no 'value' member:INFERENCE +no-member:16:18:16:29::Instance of 'B' has no 'value' member:INFERENCE +no-member:24:0:24:11::Instance of 'C' has no 'value' member:INFERENCE +no-member:25:18:25:29::Instance of 'C' has no 'value' member:INFERENCE diff --git a/tests/functional/n/non_ascii_name/non_ascii_name_for_loop.py b/tests/functional/n/non_ascii_name/non_ascii_name_for_loop.py index 59585645a..557741753 100644 --- a/tests/functional/n/non_ascii_name/non_ascii_name_for_loop.py +++ b/tests/functional/n/non_ascii_name/non_ascii_name_for_loop.py @@ -1,5 +1,5 @@ """invalid ascii char in a for loop""" - +# pylint: disable=consider-using-join import os diff --git a/tests/functional/n/not_callable.py b/tests/functional/n/not_callable.py index f781150a2..c5015e65f 100644 --- a/tests/functional/n/not_callable.py +++ b/tests/functional/n/not_callable.py @@ -232,3 +232,14 @@ instance_or_cls = MyClass() if not isinstance(instance_or_cls, MyClass): new = MyClass.__new__(instance_or_cls) new() + + +# Regression test for https://github.com/PyCQA/pylint/issues/5113. +# Do not emit `not-callable`. +ATTRIBUTES = { + 'DOMAIN': ("domain", str), + 'IMAGE': ("image", bytes), +} + +for key, (name, validate) in ATTRIBUTES.items(): + name = validate(1) diff --git a/tests/functional/r/raising/raising_bad_type.txt b/tests/functional/r/raising/raising_bad_type.txt index 28fcfadc6..04ea2d170 100644 --- a/tests/functional/r/raising/raising_bad_type.txt +++ b/tests/functional/r/raising/raising_bad_type.txt @@ -1 +1 @@ -raising-bad-type:3:0:3:31::Raising tuple while only classes or instances are allowed:UNDEFINED +raising-bad-type:3:0:3:31::Raising tuple while only classes or instances are allowed:INFERENCE diff --git a/tests/functional/r/raising/raising_format_tuple.txt b/tests/functional/r/raising/raising_format_tuple.txt index 5f9283bb1..a6456bc84 100644 --- a/tests/functional/r/raising/raising_format_tuple.txt +++ b/tests/functional/r/raising/raising_format_tuple.txt @@ -1,7 +1,7 @@ -raising-format-tuple:11:4:11:38:bad_percent:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:19:4:19:53:bad_multiarg:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:27:4:27:40:bad_braces:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:35:4:37:52:bad_multistring:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:41:4:43:53:bad_triplequote:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:47:4:47:36:bad_unicode:Exception arguments suggest string formatting might be intended:UNDEFINED -raising-format-tuple:52:4:52:56:raise_something_without_name:Exception arguments suggest string formatting might be intended:UNDEFINED +raising-format-tuple:11:4:11:38:bad_percent:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:19:4:19:53:bad_multiarg:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:27:4:27:40:bad_braces:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:35:4:37:52:bad_multistring:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:41:4:43:53:bad_triplequote:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:47:4:47:36:bad_unicode:Exception arguments suggest string formatting might be intended:HIGH +raising-format-tuple:52:4:52:56:raise_something_without_name:Exception arguments suggest string formatting might be intended:HIGH diff --git a/tests/functional/r/raising/raising_non_exception.txt b/tests/functional/r/raising/raising_non_exception.txt index efa816a5f..5cab16846 100644 --- a/tests/functional/r/raising/raising_non_exception.txt +++ b/tests/functional/r/raising/raising_non_exception.txt @@ -1 +1 @@ -raising-non-exception:13:0:13:22::Raising a new style class which doesn't inherit from BaseException:UNDEFINED +raising-non-exception:13:0:13:22::Raising a new style class which doesn't inherit from BaseException:INFERENCE diff --git a/tests/functional/r/redefined/redefined_except_handler.txt b/tests/functional/r/redefined/redefined_except_handler.txt index 1184bdd81..a0ccc6b9b 100644 --- a/tests/functional/r/redefined/redefined_except_handler.txt +++ b/tests/functional/r/redefined/redefined_except_handler.txt @@ -1,4 +1,4 @@ redefined-outer-name:11:4:12:12::Redefining name 'err' from outer scope (line 8):UNDEFINED redefined-outer-name:57:8:58:16::Redefining name 'err' from outer scope (line 51):UNDEFINED -used-before-assignment:69:14:69:29:func:Using variable 'CustomException' before assignment:HIGH +used-before-assignment:69:14:69:29:func:Using variable 'CustomException' before assignment:CONTROL_FLOW redefined-outer-name:71:4:72:12:func:Redefining name 'CustomException' from outer scope (line 62):UNDEFINED diff --git a/tests/functional/r/regression_02/regression_2964.py b/tests/functional/r/regression_02/regression_2964.py new file mode 100644 index 000000000..66235fc09 --- /dev/null +++ b/tests/functional/r/regression_02/regression_2964.py @@ -0,0 +1,24 @@ +""" +Regression test for `no-member`. +See: https://github.com/PyCQA/pylint/issues/2964 +""" + +# pylint: disable=missing-class-docstring,too-few-public-methods +# pylint: disable=unused-private-member,protected-access + + +class Node: + def __init__(self, name, path=()): + """ + Initialize self with "name" string and the tuple "path" of its parents. + "self" is added to the tuple as its last item. + """ + self.__name = name + self.__path = path + (self,) + + def get_full_name(self): + """ + A `no-member` message was emitted: + nodes.py:17:24: E1101: Instance of 'tuple' has no '__name' member (no-member) + """ + return ".".join(node.__name for node in self.__path) diff --git a/tests/functional/r/regression_02/regression_3976.py b/tests/functional/r/regression_02/regression_3976.py new file mode 100644 index 000000000..3610e9e30 --- /dev/null +++ b/tests/functional/r/regression_02/regression_3976.py @@ -0,0 +1,14 @@ +""" +Regression test for https://github.com/PyCQA/pylint/issues/3976 + +E1123: Unexpected keyword argument 'include_extras' in function call (unexpected-keyword-arg) +""" + +import typing_extensions + + +def function(): + """Simple function""" + + +typing_extensions.get_type_hints(function, include_extras=True) diff --git a/tests/functional/r/regression_02/regression_5048.py b/tests/functional/r/regression_02/regression_5048.py index 5656759af..08ff55fb2 100644 --- a/tests/functional/r/regression_02/regression_5048.py +++ b/tests/functional/r/regression_02/regression_5048.py @@ -1,6 +1,6 @@ """Crash regression in astroid on Compare node inference Fixed in https://github.com/PyCQA/astroid/pull/1185""" -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised # Reported at https://github.com/PyCQA/pylint/issues/5048 diff --git a/tests/functional/r/regression_02/regression_too_many_arguments_2335.py b/tests/functional/r/regression_02/regression_too_many_arguments_2335.py index d2759adfe..55aa87308 100644 --- a/tests/functional/r/regression_02/regression_too_many_arguments_2335.py +++ b/tests/functional/r/regression_02/regression_too_many_arguments_2335.py @@ -7,5 +7,5 @@ from abc import ABCMeta class NodeCheckMetaClass(ABCMeta): - def __new__(cls, name, bases, namespace, **kwargs): - return ABCMeta.__new__(cls, name, bases, namespace) + def __new__(mcs, name, bases, namespace, **kwargs): + return ABCMeta.__new__(mcs, name, bases, namespace) diff --git a/tests/functional/s/simplifiable/simplifiable_if_statement.py b/tests/functional/s/simplifiable/simplifiable_if_statement.py index 4d4c8b5d4..59251bd04 100644 --- a/tests/functional/s/simplifiable/simplifiable_if_statement.py +++ b/tests/functional/s/simplifiable/simplifiable_if_statement.py @@ -29,6 +29,7 @@ def test_simplifiable_3(arg, arg2): def test_simplifiable_4(arg):
+ var = False
if arg:
var = True
else:
@@ -89,6 +90,7 @@ def test_not_simplifiable_4(arg): def test_not_simplifiable_5(arg):
# Different actions in each branch
+ var = 43
if arg == "any":
return True
else:
diff --git a/tests/functional/s/simplifiable/simplifiable_if_statement.txt b/tests/functional/s/simplifiable/simplifiable_if_statement.txt index d36768ddd..e0a82ef6a 100644 --- a/tests/functional/s/simplifiable/simplifiable_if_statement.txt +++ b/tests/functional/s/simplifiable/simplifiable_if_statement.txt @@ -1,4 +1,4 @@ simplifiable-if-statement:8:4:11:20:test_simplifiable_1:The if statement can be replaced with 'return bool(test)':UNDEFINED simplifiable-if-statement:16:4:19:20:test_simplifiable_2:The if statement can be replaced with 'return bool(test)':UNDEFINED simplifiable-if-statement:24:4:27:19:test_simplifiable_3:The if statement can be replaced with 'var = bool(test)':UNDEFINED -simplifiable-if-statement:35:8:38:24:test_simplifiable_4:The if statement can be replaced with 'return bool(test)':UNDEFINED +simplifiable-if-statement:36:8:39:24:test_simplifiable_4:The if statement can be replaced with 'return bool(test)':UNDEFINED diff --git a/tests/functional/s/singledispatch_method.txt b/tests/functional/s/singledispatch_method.txt new file mode 100644 index 000000000..c747fb6a8 --- /dev/null +++ b/tests/functional/s/singledispatch_method.txt @@ -0,0 +1,3 @@ +singledispatch-method:26:5:26:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:31:5:31:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:37:5:37:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatch_method_py37.py b/tests/functional/s/singledispatch_method_py37.py new file mode 100644 index 000000000..c9269f7bf --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.py @@ -0,0 +1,23 @@ +"""Tests for singledispatch-method""" +# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods + + +from functools import singledispatch + + +class Board: + @singledispatch # [singledispatch-method] + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch_method_py37.rc b/tests/functional/s/singledispatch_method_py37.rc new file mode 100644 index 000000000..67a28a36a --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.rc @@ -0,0 +1,2 @@ +[testoptions] +max_pyver=3.8 diff --git a/tests/functional/s/singledispatch_method_py37.txt b/tests/functional/s/singledispatch_method_py37.txt new file mode 100644 index 000000000..111bc4722 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.txt @@ -0,0 +1,3 @@ +singledispatch-method:9:5:9:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:14:5:14:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:20:5:20:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatch_method_py38.py b/tests/functional/s/singledispatch_method_py38.py new file mode 100644 index 000000000..ad8eea1dd --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.py @@ -0,0 +1,40 @@ +"""Tests for singledispatch-method""" +# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods + + +from functools import singledispatch, singledispatchmethod + + +class BoardRight: + @singledispatchmethod + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + def _(self, position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +class Board: + @singledispatch # [singledispatch-method] + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch_method_py38.rc b/tests/functional/s/singledispatch_method_py38.rc new file mode 100644 index 000000000..85fc502b3 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/s/singledispatch_method_py38.txt b/tests/functional/s/singledispatch_method_py38.txt new file mode 100644 index 000000000..c747fb6a8 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.txt @@ -0,0 +1,3 @@ +singledispatch-method:26:5:26:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:31:5:31:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:37:5:37:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatchmethod_function_py38.py b/tests/functional/s/singledispatchmethod_function_py38.py new file mode 100644 index 000000000..ef44f71c1 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.py @@ -0,0 +1,41 @@ +"""Tests for singledispatchmethod-function""" +# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods + + +from functools import singledispatch, singledispatchmethod + + +class BoardRight: + @singledispatch + @staticmethod + def convert_position(position): + pass + + @convert_position.register + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +class Board: + @singledispatchmethod # [singledispatchmethod-function] + @staticmethod + def convert_position(position): + pass + + @convert_position.register # [singledispatchmethod-function] + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatchmethod-function] + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatchmethod_function_py38.rc b/tests/functional/s/singledispatchmethod_function_py38.rc new file mode 100644 index 000000000..85fc502b3 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/s/singledispatchmethod_function_py38.txt b/tests/functional/s/singledispatchmethod_function_py38.txt new file mode 100644 index 000000000..4c236b346 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.txt @@ -0,0 +1,3 @@ +singledispatchmethod-function:27:5:27:25:Board.convert_position:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:HIGH +singledispatchmethod-function:32:5:32:30:Board._:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:INFERENCE +singledispatchmethod-function:38:5:38:30:Board._:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:INFERENCE diff --git a/tests/functional/s/slots_checks.py b/tests/functional/s/slots_checks.py index e8d55d967..2c22e968e 100644 --- a/tests/functional/s/slots_checks.py +++ b/tests/functional/s/slots_checks.py @@ -64,7 +64,7 @@ class SixthBad: # [single-string-used-for-slots] __slots__ = "a" class SeventhBad: # [single-string-used-for-slots] - __slots__ = ('foo') + __slots__ = ('foo') # [superfluous-parens] class EighthBad: # [single-string-used-for-slots] __slots__ = deque.__name__ diff --git a/tests/functional/s/slots_checks.txt b/tests/functional/s/slots_checks.txt index 3abccff8f..d63ad2517 100644 --- a/tests/functional/s/slots_checks.txt +++ b/tests/functional/s/slots_checks.txt @@ -5,6 +5,7 @@ invalid-slots:57:0:57:15:FourthBad:Invalid __slots__ object:UNDEFINED invalid-slots-object:61:27:61:29:FifthBad:"Invalid object ""''"" in __slots__, must contain only non empty strings":INFERENCE single-string-used-for-slots:63:0:63:14:SixthBad:Class __slots__ should be a non-string iterable:UNDEFINED single-string-used-for-slots:66:0:66:16:SeventhBad:Class __slots__ should be a non-string iterable:UNDEFINED +superfluous-parens:67:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED single-string-used-for-slots:69:0:69:15:EighthBad:Class __slots__ should be a non-string iterable:UNDEFINED invalid-slots-object:73:17:73:20:NinthBad:Invalid object 'str' in __slots__, must contain only non empty strings:INFERENCE invalid-slots-object:76:17:76:26:TenthBad:Invalid object '1 + 2 + 3' in __slots__, must contain only non empty strings:INFERENCE diff --git a/tests/functional/s/star/star_needs_assignment_target_py37.txt b/tests/functional/s/star/star_needs_assignment_target_py37.txt index a4fa2caea..fb5a5faa6 100644 --- a/tests/functional/s/star/star_needs_assignment_target_py37.txt +++ b/tests/functional/s/star/star_needs_assignment_target_py37.txt @@ -1 +1 @@ -star-needs-assignment-target:15:37::Can use starred expression only in assignment target +star-needs-assignment-target:15:36:15:46::Can use starred expression only in assignment target:UNDEFINED diff --git a/tests/functional/s/stop_iteration_inside_generator.py b/tests/functional/s/stop_iteration_inside_generator.py index f543f40ee..efde61a77 100644 --- a/tests/functional/s/stop_iteration_inside_generator.py +++ b/tests/functional/s/stop_iteration_inside_generator.py @@ -1,10 +1,10 @@ """ Test that no StopIteration is raised inside a generator """ -# pylint: disable=missing-docstring,invalid-name,import-error, try-except-raise, wrong-import-position,not-callable,raise-missing-from +# pylint: disable=missing-docstring,invalid-name,import-error, try-except-raise, wrong-import-position +# pylint: disable=not-callable,raise-missing-from,broad-exception-raised import asyncio - class RebornStopIteration(StopIteration): """ A class inheriting from StopIteration exception diff --git a/tests/functional/s/superfluous_parens.py b/tests/functional/s/superfluous_parens.py index db9349cce..35acfd5d3 100644 --- a/tests/functional/s/superfluous_parens.py +++ b/tests/functional/s/superfluous_parens.py @@ -47,13 +47,6 @@ def function_B(var): def function_C(first, second): return (first or second) in (0, 1) -# TODO: Test string combinations, see https://github.com/PyCQA/pylint/issues/4792 -# The lines with "+" should raise the superfluous-parens message -J = "TestString" -K = ("Test " + "String") -L = ("Test " + "String") in I -assert "" + ("Version " + "String") in I - # Test numpy def function_numpy_A(var_1: int, var_2: int) -> np.ndarray: result = (((var_1 & var_2)) > 0) @@ -72,6 +65,19 @@ class ClassA: if (A == 2) is not (B == 2): pass +K = ("Test " + "String") # [superfluous-parens] M = A is not (A <= H) M = True is not (M == K) M = True is not (True is not False) # pylint: disable=comparison-of-constants + +Z = "TestString" +X = ("Test " + "String") # [superfluous-parens] +Y = ("Test " + "String") in Z # [superfluous-parens] +assert ("Test " + "String") in "hello" # [superfluous-parens] +assert ("Version " + "String") in ("Version " + "String") # [superfluous-parens] + +hi = ("CONST") # [superfluous-parens] +hi = ("CONST",) + +#TODO: maybe get this line to report [superfluous-parens] without causing other false positives. +assert "" + ("Version " + "String") in Z diff --git a/tests/functional/s/superfluous_parens.txt b/tests/functional/s/superfluous_parens.txt index f830922bc..08b2dd390 100644 --- a/tests/functional/s/superfluous_parens.txt +++ b/tests/functional/s/superfluous_parens.txt @@ -4,3 +4,9 @@ superfluous-parens:12:0:None:None::Unnecessary parens after 'for' keyword:UNDEFI superfluous-parens:14:0:None:None::Unnecessary parens after 'if' keyword:UNDEFINED superfluous-parens:19:0:None:None::Unnecessary parens after 'del' keyword:UNDEFINED superfluous-parens:31:0:None:None::Unnecessary parens after 'assert' keyword:UNDEFINED +superfluous-parens:68:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED +superfluous-parens:74:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED +superfluous-parens:75:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED +superfluous-parens:76:0:None:None::Unnecessary parens after 'assert' keyword:UNDEFINED +superfluous-parens:77:0:None:None::Unnecessary parens after 'assert' keyword:UNDEFINED +superfluous-parens:79:0:None:None::Unnecessary parens after '=' keyword:UNDEFINED diff --git a/tests/functional/s/superfluous_parens_walrus_py38.py b/tests/functional/s/superfluous_parens_walrus_py38.py index cf155954e..7922e4613 100644 --- a/tests/functional/s/superfluous_parens_walrus_py38.py +++ b/tests/functional/s/superfluous_parens_walrus_py38.py @@ -1,5 +1,5 @@ """Test the superfluous-parens warning with python 3.8 functionality (walrus operator)""" -# pylint: disable=missing-function-docstring, invalid-name, missing-class-docstring, import-error +# pylint: disable=missing-function-docstring, invalid-name, missing-class-docstring, import-error, pointless-statement,named-expr-without-context import numpy # Test parens in if statements @@ -49,3 +49,25 @@ class TestYieldClass: @classmethod def function_C(cls): yield (1 + 1) # [superfluous-parens] + + +if (x := "Test " + "String"): + print(x) + +if (x := ("Test " + "String")): # [superfluous-parens] + print(x) + +if not (foo := "Test " + "String" in "hello"): + print(foo) + +if not (foo := ("Test " + "String") in "hello"): # [superfluous-parens] + print(foo) + +assert (ret := "Test " + "String") +assert (ret := ("Test " + "String")) # [superfluous-parens] + +(walrus := False) +(walrus := (False)) # [superfluous-parens] + +(hi := ("CONST")) # [superfluous-parens] +(hi := ("CONST",)) diff --git a/tests/functional/s/superfluous_parens_walrus_py38.txt b/tests/functional/s/superfluous_parens_walrus_py38.txt index 58097f520..da8f1b999 100644 --- a/tests/functional/s/superfluous_parens_walrus_py38.txt +++ b/tests/functional/s/superfluous_parens_walrus_py38.txt @@ -3,3 +3,8 @@ superfluous-parens:19:0:None:None::Unnecessary parens after 'if' keyword:UNDEFIN superfluous-parens:22:0:None:None::Unnecessary parens after 'not' keyword:UNDEFINED superfluous-parens:25:0:None:None::Unnecessary parens after 'not' keyword:UNDEFINED superfluous-parens:51:0:None:None::Unnecessary parens after 'yield' keyword:UNDEFINED +superfluous-parens:57:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED +superfluous-parens:63:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED +superfluous-parens:67:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED +superfluous-parens:70:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED +superfluous-parens:72:0:None:None::"Unnecessary parens after ':=' keyword":UNDEFINED diff --git a/tests/functional/t/trailing_whitespaces.py b/tests/functional/t/trailing_whitespaces.py index cb9d642ee..c88b7ea62 100644 --- a/tests/functional/t/trailing_whitespaces.py +++ b/tests/functional/t/trailing_whitespaces.py @@ -18,3 +18,24 @@ print('but trailing whitespace on win is not') """ This module has the Board class. It's a very nice Board. """ + +# Regression test for https://github.com/PyCQA/pylint/issues/3822 +def example(*args): + """Example function.""" + print(*args) + + +example( + "bob", """ + foobar + more text +""", +) + +example( + "bob", + """ + foobar2 + more text +""", +) diff --git a/tests/functional/u/unbalanced_dict_unpacking.py b/tests/functional/u/unbalanced_dict_unpacking.py new file mode 100644 index 000000000..2c4d3b103 --- /dev/null +++ b/tests/functional/u/unbalanced_dict_unpacking.py @@ -0,0 +1,91 @@ +"""Check possible unbalanced dict unpacking """ +# pylint: disable=missing-function-docstring, invalid-name +# pylint: disable=unused-variable, redefined-outer-name, line-too-long + +def dict_vals(): + a, b, c, d, e, f, g = {1: 2}.values() # [unbalanced-dict-unpacking] + return a, b + +def dict_keys(): + a, b, c, d, e, f, g = {1: 2, "hi": 20}.keys() # [unbalanced-dict-unpacking] + return a, b + + +def dict_items(): + tupe_one, tuple_two = {1: 2, "boo": 3}.items() + tupe_one, tuple_two, tuple_three = {1: 2, "boo": 3}.items() # [unbalanced-dict-unpacking] + return tuple_three + +def all_dict(): + a, b, c, d, e, f, g = {1: 2, 3: 4} # [unbalanced-dict-unpacking] + return a + +for a, b, c, d, e, f, g in {1: 2}.items(): # [unbalanced-dict-unpacking] + pass + +for key, value in {1: 2}: # [unbalanced-dict-unpacking] + pass + +for key, value in {1: 2}.keys(): # [unbalanced-dict-unpacking, consider-iterating-dictionary] + pass + +for key, value in {1: 2}.values(): # [unbalanced-dict-unpacking] + pass + +empty = {} + +# this should not raise unbalanced-dict because it is valid code using `items()` +for key, value in empty.items(): + print(key) + print(value) + +for key, val in {1: 2}.items(): + print(key) + +populated = {2: 1} +for key, val in populated.items(): + print(key) + +key, val = populated.items() # [unbalanced-dict-unpacking] + +for key, val in {1: 2, 3: 4, 5: 6}.items(): + print(key) + +key, val = {1: 2, 3: 4, 5: 6}.items() # [unbalanced-dict-unpacking] + +a, b, c = {} # [unbalanced-dict-unpacking] + +for k in {'key': 'value', 1: 2}.items(): + print(k) + +for k, _ in {'key': 'value'}.items(): + print(k) + +for _, _ in {'key': 'value'}.items(): + print(_) + +for _, val in {'key': 'value'}.values(): # [unbalanced-dict-unpacking] + print(val) + +for key, *val in {'key': 'value', 1: 2}.items(): + print(key) + +for *key, val in {'key': 'value', 1: 2}.items(): + print(key) + + +for key, *val in {'key': 'value', 1: 2, 20: 21}.values(): # [unbalanced-dict-unpacking] + print(key) + +for *key, val in {'key': 'value', 1: 2, 20: 21}.values(): # [unbalanced-dict-unpacking] + print(key) + +one, *others = {1: 2, 3: 4, 5: 6}.items() +one, *others, last = {1: 2, 3: 4, 5: 6}.items() + +one, *others = {1: 2, 3: 4, 5: 6}.values() +one, *others, last = {1: 2, 3: 4, 5: 6}.values() + +_, *others = {1: 2, 3: 4, 5: 6}.items() +_, *others = {1: 2, 3: 4, 5: 6}.values() +_, others = {1: 2, 3: 4, 5: 6}.values() # [unbalanced-dict-unpacking] diff --git a/tests/functional/u/unbalanced_dict_unpacking.txt b/tests/functional/u/unbalanced_dict_unpacking.txt new file mode 100644 index 000000000..b31d89b40 --- /dev/null +++ b/tests/functional/u/unbalanced_dict_unpacking.txt @@ -0,0 +1,16 @@ +unbalanced-dict-unpacking:6:4:6:41:dict_vals:"Possible unbalanced dict unpacking with {1: 2}.values(): left side has 7 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:10:4:10:49:dict_keys:"Possible unbalanced dict unpacking with {1: 2, 'hi': 20}.keys(): left side has 7 labels, right side has 2 values":INFERENCE +unbalanced-dict-unpacking:16:4:16:63:dict_items:"Possible unbalanced dict unpacking with {1: 2, 'boo': 3}.items(): left side has 3 labels, right side has 2 values":INFERENCE +unbalanced-dict-unpacking:20:4:20:38:all_dict:"Possible unbalanced dict unpacking with {1: 2, 3: 4}: left side has 7 labels, right side has 2 values":INFERENCE +unbalanced-dict-unpacking:23:0:24:8::"Possible unbalanced dict unpacking with {1: 2}.items(): left side has 7 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:26:0:27:8::"Possible unbalanced dict unpacking with {1: 2}: left side has 2 labels, right side has 1 value":INFERENCE +consider-iterating-dictionary:29:18:29:31::Consider iterating the dictionary directly instead of calling .keys():INFERENCE +unbalanced-dict-unpacking:29:0:30:8::"Possible unbalanced dict unpacking with {1: 2}.keys(): left side has 2 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:32:0:33:8::"Possible unbalanced dict unpacking with {1: 2}.values(): left side has 2 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:49:0:49:28::"Possible unbalanced dict unpacking with populated.items(): left side has 2 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:54:0:54:37::"Possible unbalanced dict unpacking with {1: 2, 3: 4, 5: 6}.items(): left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-dict-unpacking:56:0:56:12::"Possible unbalanced dict unpacking with {}: left side has 3 labels, right side has 0 values":INFERENCE +unbalanced-dict-unpacking:67:0:68:14::"Possible unbalanced dict unpacking with {'key': 'value'}.values(): left side has 2 labels, right side has 1 value":INFERENCE +unbalanced-dict-unpacking:77:0:78:14::"Possible unbalanced dict unpacking with {'key': 'value', 1: 2, 20: 21}.values(): left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-dict-unpacking:80:0:81:14::"Possible unbalanced dict unpacking with {'key': 'value', 1: 2, 20: 21}.values(): left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-dict-unpacking:91:0:91:39::"Possible unbalanced dict unpacking with {1: 2, 3: 4, 5: 6}.values(): left side has 2 labels, right side has 3 values":INFERENCE diff --git a/tests/functional/u/unbalanced_tuple_unpacking.txt b/tests/functional/u/unbalanced_tuple_unpacking.txt index e32069847..651e09840 100644 --- a/tests/functional/u/unbalanced_tuple_unpacking.txt +++ b/tests/functional/u/unbalanced_tuple_unpacking.txt @@ -1,9 +1,9 @@ -unbalanced-tuple-unpacking:11:4:11:27:do_stuff:"Possible unbalanced tuple unpacking with sequence (1, 2, 3): left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:17:4:17:29:do_stuff1:"Possible unbalanced tuple unpacking with sequence [1, 2, 3]: left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:23:4:23:29:do_stuff2:"Possible unbalanced tuple unpacking with sequence (1, 2, 3): left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:82:4:82:28:do_stuff9:"Possible unbalanced tuple unpacking with sequence defined at line 7 of functional.u.unpacking.unpacking: left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:96:8:96:33:UnbalancedUnpacking.test:"Possible unbalanced tuple unpacking with sequence defined at line 7 of functional.u.unpacking.unpacking: left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:140:8:140:43:MyClass.sum_unpack_3_into_4:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 4 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:145:8:145:28:MyClass.sum_unpack_3_into_2:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 2 label(s), right side has 3 value(s)":UNDEFINED -unbalanced-tuple-unpacking:157:0:157:24::"Possible unbalanced tuple unpacking with sequence defined at line 151: left side has 2 label(s), right side has 0 value(s)":UNDEFINED -unbalanced-tuple-unpacking:162:0:162:16::"Possible unbalanced tuple unpacking with sequence (1, 2): left side has 3 label(s), right side has 2 value(s)":UNDEFINED +unbalanced-tuple-unpacking:11:4:11:27:do_stuff:"Possible unbalanced tuple unpacking with sequence '(1, 2, 3)': left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:17:4:17:29:do_stuff1:"Possible unbalanced tuple unpacking with sequence '[1, 2, 3]': left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:23:4:23:29:do_stuff2:"Possible unbalanced tuple unpacking with sequence '(1, 2, 3)': left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:82:4:82:28:do_stuff9:"Possible unbalanced tuple unpacking with sequence defined at line 7 of functional.u.unpacking.unpacking: left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:96:8:96:33:UnbalancedUnpacking.test:"Possible unbalanced tuple unpacking with sequence defined at line 7 of functional.u.unpacking.unpacking: left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:140:8:140:43:MyClass.sum_unpack_3_into_4:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 4 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:145:8:145:28:MyClass.sum_unpack_3_into_2:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 2 labels, right side has 3 values":INFERENCE +unbalanced-tuple-unpacking:157:0:157:24::"Possible unbalanced tuple unpacking with sequence defined at line 151: left side has 2 labels, right side has 0 values":INFERENCE +unbalanced-tuple-unpacking:162:0:162:16::"Possible unbalanced tuple unpacking with sequence '(1, 2)': left side has 3 labels, right side has 2 values":INFERENCE diff --git a/tests/functional/u/unbalanced_tuple_unpacking_py30.py b/tests/functional/u/unbalanced_tuple_unpacking_py30.py index dd3e4b6d3..c45cccdd1 100644 --- a/tests/functional/u/unbalanced_tuple_unpacking_py30.py +++ b/tests/functional/u/unbalanced_tuple_unpacking_py30.py @@ -1,10 +1,11 @@ """ Test that using starred nodes in unpacking does not trigger a false positive on Python 3. """ - +# pylint: disable=unused-variable def test(): """ Test that starred expressions don't give false positives. """ first, second, *last = (1, 2, 3, 4) + one, two, three, *four = (1, 2, 3, 4) *last, = (1, 2) return (first, second, last) diff --git a/tests/functional/u/undefined/undefined_loop_variable.py b/tests/functional/u/undefined/undefined_loop_variable.py index 9ab08d595..9d5cf4111 100644 --- a/tests/functional/u/undefined/undefined_loop_variable.py +++ b/tests/functional/u/undefined/undefined_loop_variable.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring,redefined-builtin, consider-using-f-string, unnecessary-direct-lambda-call +# pylint: disable=missing-docstring,redefined-builtin, consider-using-f-string, unnecessary-direct-lambda-call, broad-exception-raised import sys diff --git a/tests/functional/u/undefined/undefined_variable_py30.py b/tests/functional/u/undefined/undefined_variable_py30.py index 0b5aa0422..ff77aaf8e 100644 --- a/tests/functional/u/undefined/undefined_variable_py30.py +++ b/tests/functional/u/undefined/undefined_variable_py30.py @@ -89,9 +89,9 @@ def used_before_assignment(*, arg): return arg + 1 # Test for #4021 # https://github.com/PyCQA/pylint/issues/4021 class MetaClass(type): - def __new__(cls, *args, parameter=None, **kwargs): + def __new__(mcs, *args, parameter=None, **kwargs): print(parameter) - return super().__new__(cls, *args, **kwargs) + return super().__new__(mcs, *args, **kwargs) class InheritingClass(metaclass=MetaClass, parameter=variable): # [undefined-variable] diff --git a/tests/functional/u/undefined/undefined_variable_py38.py b/tests/functional/u/undefined/undefined_variable_py38.py index 61a70d472..8afb5eaf9 100644 --- a/tests/functional/u/undefined/undefined_variable_py38.py +++ b/tests/functional/u/undefined/undefined_variable_py38.py @@ -180,3 +180,9 @@ def expression_in_ternary_operator_inside_container_wrong_position(): # Self-referencing if (z := z): # [used-before-assignment] z = z + 1 + + +if (defined := False): + NEVER_DEFINED = 1 +print(defined) +print(NEVER_DEFINED) # [used-before-assignment] diff --git a/tests/functional/u/undefined/undefined_variable_py38.txt b/tests/functional/u/undefined/undefined_variable_py38.txt index 8460a989f..eb979fad2 100644 --- a/tests/functional/u/undefined/undefined_variable_py38.txt +++ b/tests/functional/u/undefined/undefined_variable_py38.txt @@ -8,3 +8,4 @@ used-before-assignment:140:10:140:16:type_annotation_used_improperly_after_compr used-before-assignment:147:10:147:16:type_annotation_used_improperly_after_comprehension_2:Using variable 'my_int' before assignment:HIGH used-before-assignment:177:12:177:16:expression_in_ternary_operator_inside_container_wrong_position:Using variable 'val3' before assignment:HIGH used-before-assignment:181:9:181:10::Using variable 'z' before assignment:HIGH +used-before-assignment:188:6:188:19::Using variable 'NEVER_DEFINED' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/unidiomatic_typecheck.py b/tests/functional/u/unidiomatic_typecheck.py index 1e7642046..2a1957d75 100644 --- a/tests/functional/u/unidiomatic_typecheck.py +++ b/tests/functional/u/unidiomatic_typecheck.py @@ -1,5 +1,5 @@ """Warnings for using type(x) == Y or type(x) is Y instead of isinstance(x, Y).""" -# pylint: disable=missing-docstring,expression-not-assigned,redefined-builtin,invalid-name,unnecessary-lambda-assignment +# pylint: disable=missing-docstring,expression-not-assigned,redefined-builtin,invalid-name,unnecessary-lambda-assignment,use-dict-literal def simple_positives(): type(42) is int # [unidiomatic-typecheck] diff --git a/tests/functional/u/unnecessary/unnecessary_lambda.py b/tests/functional/u/unnecessary/unnecessary_lambda.py index c12e30bc7..3e5ece2b1 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
+# pylint: disable=undefined-variable, use-list-literal, unnecessary-lambda-assignment, use-dict-literal
"""test suspicious lambda expressions
"""
diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py index e5cb13514..ec5ee22c2 100644 --- a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py @@ -130,3 +130,22 @@ def return_start(start): for i, k in enumerate(series, return_start(20)): print(series[idx]) + +for idx, val in enumerate(iterable=series, start=0): + print(series[idx]) # [unnecessary-list-index-lookup] + +result = [my_list[idx] for idx, val in enumerate(iterable=my_list)] # [unnecessary-list-index-lookup] + +for idx, val in enumerate(): + print(my_list[idx]) + +class Command: + def _get_extra_attrs(self, extra_columns): + self.extra_rows_start = 8 # pylint: disable=attribute-defined-outside-init + for index, column in enumerate(extra_columns, start=self.extra_rows_start): + pass + +Y_START = 2 +nums = list(range(20)) +for y, x in enumerate(nums, start=Y_START + 1): + pass diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt index a38788cc8..da658a20d 100644 --- a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt @@ -6,3 +6,5 @@ unnecessary-list-index-lookup:112:10:112:21::Unnecessary list index lookup, use unnecessary-list-index-lookup:115:10:115:21::Unnecessary list index lookup, use 'val' instead:HIGH unnecessary-list-index-lookup:119:10:119:21::Unnecessary list index lookup, use 'val' instead:INFERENCE unnecessary-list-index-lookup:122:10:122:21::Unnecessary list index lookup, use 'val' instead:INFERENCE +unnecessary-list-index-lookup:135:10:135:21::Unnecessary list index lookup, use 'val' instead:HIGH +unnecessary-list-index-lookup:137:10:137:22::Unnecessary list index lookup, use 'val' instead:HIGH diff --git a/tests/functional/u/unpacking/unpacking_non_sequence.txt b/tests/functional/u/unpacking/unpacking_non_sequence.txt index 3023bfc6b..473acde6f 100644 --- a/tests/functional/u/unpacking/unpacking_non_sequence.txt +++ b/tests/functional/u/unpacking/unpacking_non_sequence.txt @@ -1,7 +1,7 @@ unpacking-non-sequence:77:0:77:15::Attempting to unpack a non-sequence defined at line 74:UNDEFINED unpacking-non-sequence:78:0:78:17::Attempting to unpack a non-sequence:UNDEFINED -unpacking-non-sequence:79:0:79:11::Attempting to unpack a non-sequence None:UNDEFINED -unpacking-non-sequence:80:0:80:8::Attempting to unpack a non-sequence 1:UNDEFINED +unpacking-non-sequence:79:0:79:11::Attempting to unpack a non-sequence 'None':UNDEFINED +unpacking-non-sequence:80:0:80:8::Attempting to unpack a non-sequence '1':UNDEFINED unpacking-non-sequence:81:0:81:13::Attempting to unpack a non-sequence defined at line 9 of functional.u.unpacking.unpacking:UNDEFINED unpacking-non-sequence:82:0:82:15::Attempting to unpack a non-sequence defined at line 11 of functional.u.unpacking.unpacking:UNDEFINED unpacking-non-sequence:83:0:83:18::Attempting to unpack a non-sequence:UNDEFINED diff --git a/tests/functional/u/unreachable.py b/tests/functional/u/unreachable.py index fa40e88f6..0211a6136 100644 --- a/tests/functional/u/unreachable.py +++ b/tests/functional/u/unreachable.py @@ -1,5 +1,9 @@ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, broad-exception-raised, too-few-public-methods, redefined-outer-name +# pylint: disable=consider-using-sys-exit, protected-access +import os +import signal +import sys def func1(): return 1 @@ -32,3 +36,46 @@ def func6(): return yield print("unreachable") # [unreachable] + +def func7(): + sys.exit(1) + var = 2 + 2 # [unreachable] + print(var) + +def func8(): + signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) + try: + print(1) + except KeyboardInterrupt: + pass + +class FalseExit: + def exit(self, number): + print(f"False positive this is not sys.exit({number})") + +def func_false_exit(): + sys = FalseExit() + sys.exit(1) + var = 2 + 2 + print(var) + +def func9(): + os._exit() + var = 2 + 2 # [unreachable] + print(var) + +def func10(): + exit() + var = 2 + 2 # [unreachable] + print(var) + +def func11(): + quit() + var = 2 + 2 # [unreachable] + print(var) + +incognito_function = sys.exit +def func12(): + incognito_function() + var = 2 + 2 # [unreachable] + print(var) diff --git a/tests/functional/u/unreachable.txt b/tests/functional/u/unreachable.txt index 491912d99..82f9797aa 100644 --- a/tests/functional/u/unreachable.txt +++ b/tests/functional/u/unreachable.txt @@ -1,5 +1,10 @@ -unreachable:6:4:6:24:func1:Unreachable code:HIGH -unreachable:11:8:11:28:func2:Unreachable code:HIGH -unreachable:17:8:17:28:func3:Unreachable code:HIGH -unreachable:21:4:21:16:func4:Unreachable code:HIGH -unreachable:34:4:34:24:func6:Unreachable code:HIGH +unreachable:10:4:10:24:func1:Unreachable code:HIGH +unreachable:15:8:15:28:func2:Unreachable code:HIGH +unreachable:21:8:21:28:func3:Unreachable code:HIGH +unreachable:25:4:25:16:func4:Unreachable code:HIGH +unreachable:38:4:38:24:func6:Unreachable code:HIGH +unreachable:42:4:42:15:func7:Unreachable code:INFERENCE +unreachable:64:4:64:15:func9:Unreachable code:INFERENCE +unreachable:69:4:69:15:func10:Unreachable code:INFERENCE +unreachable:74:4:74:15:func11:Unreachable code:INFERENCE +unreachable:80:4:80:15:func12:Unreachable code:INFERENCE diff --git a/tests/functional/u/unsubscriptable_value.py b/tests/functional/u/unsubscriptable_value.py index 2a40d647f..79e17903b 100644 --- a/tests/functional/u/unsubscriptable_value.py +++ b/tests/functional/u/unsubscriptable_value.py @@ -4,6 +4,7 @@ Checks that value used in a subscript supports subscription """ # pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,wrong-import-position, unnecessary-comprehension # pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order, redundant-u-string-prefix +# pylint: disable=use-dict-literal # primitives numbers = [1, 2, 3] diff --git a/tests/functional/u/unsubscriptable_value.txt b/tests/functional/u/unsubscriptable_value.txt index 24209a64a..d0833b600 100644 --- a/tests/functional/u/unsubscriptable_value.txt +++ b/tests/functional/u/unsubscriptable_value.txt @@ -1,15 +1,15 @@ -unsubscriptable-object:31:0:31:18::Value 'NonSubscriptable()' is unsubscriptable:UNDEFINED -unsubscriptable-object:32:0:32:16::Value 'NonSubscriptable' is unsubscriptable:UNDEFINED -unsubscriptable-object:34:0:34:13::Value 'Subscriptable' is unsubscriptable:UNDEFINED -unsubscriptable-object:43:0:43:15::Value 'powers_of_two()' is unsubscriptable:UNDEFINED -unsubscriptable-object:44:0:44:13::Value 'powers_of_two' is unsubscriptable:UNDEFINED -unsubscriptable-object:48:0:48:4::Value 'True' is unsubscriptable:UNDEFINED -unsubscriptable-object:49:0:49:4::Value 'None' is unsubscriptable:UNDEFINED -unsubscriptable-object:50:0:50:3::Value '8.5' is unsubscriptable:UNDEFINED -unsubscriptable-object:51:0:51:2::Value '10' is unsubscriptable:UNDEFINED -unsubscriptable-object:54:0:54:27::Value '{x**2 for x in range(10)}' is unsubscriptable:UNDEFINED -unsubscriptable-object:55:0:55:12::Value 'set(numbers)' is unsubscriptable:UNDEFINED -unsubscriptable-object:56:0:56:18::Value 'frozenset(numbers)' is unsubscriptable:UNDEFINED -unsubscriptable-object:76:0:76:20::Value 'SubscriptableClass()' is unsubscriptable:UNDEFINED -unsubscriptable-object:83:0:83:4::Value 'test' is unsubscriptable:UNDEFINED -unsubscriptable-object:126:11:126:18:test_one:Value 'var_one' is unsubscriptable:UNDEFINED +unsubscriptable-object:32:0:32:18::Value 'NonSubscriptable()' is unsubscriptable:UNDEFINED +unsubscriptable-object:33:0:33:16::Value 'NonSubscriptable' is unsubscriptable:UNDEFINED +unsubscriptable-object:35:0:35:13::Value 'Subscriptable' is unsubscriptable:UNDEFINED +unsubscriptable-object:44:0:44:15::Value 'powers_of_two()' is unsubscriptable:UNDEFINED +unsubscriptable-object:45:0:45:13::Value 'powers_of_two' is unsubscriptable:UNDEFINED +unsubscriptable-object:49:0:49:4::Value 'True' is unsubscriptable:UNDEFINED +unsubscriptable-object:50:0:50:4::Value 'None' is unsubscriptable:UNDEFINED +unsubscriptable-object:51:0:51:3::Value '8.5' is unsubscriptable:UNDEFINED +unsubscriptable-object:52:0:52:2::Value '10' is unsubscriptable:UNDEFINED +unsubscriptable-object:55:0:55:27::Value '{x**2 for x in range(10)}' is unsubscriptable:UNDEFINED +unsubscriptable-object:56:0:56:12::Value 'set(numbers)' is unsubscriptable:UNDEFINED +unsubscriptable-object:57:0:57:18::Value 'frozenset(numbers)' is unsubscriptable:UNDEFINED +unsubscriptable-object:77:0:77:20::Value 'SubscriptableClass()' is unsubscriptable:UNDEFINED +unsubscriptable-object:84:0:84:4::Value 'test' is unsubscriptable:UNDEFINED +unsubscriptable-object:127:11:127:18:test_one:Value 'var_one' is unsubscriptable:UNDEFINED diff --git a/tests/functional/u/unsupported/unsupported_assignment_operation.py b/tests/functional/u/unsupported/unsupported_assignment_operation.py index 2cac693dd..93e84c020 100644 --- a/tests/functional/u/unsupported/unsupported_assignment_operation.py +++ b/tests/functional/u/unsupported/unsupported_assignment_operation.py @@ -3,7 +3,7 @@ Checks that value used in a subscript support assignments (i.e. defines __setitem__ method). """ # pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,wrong-import-position,unnecessary-comprehension -# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order +# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order,use-dict-literal # primitives numbers = [1, 2, 3] diff --git a/tests/functional/u/unsupported/unsupported_delete_operation.py b/tests/functional/u/unsupported/unsupported_delete_operation.py index 56a457324..c33a6eb89 100644 --- a/tests/functional/u/unsupported/unsupported_delete_operation.py +++ b/tests/functional/u/unsupported/unsupported_delete_operation.py @@ -3,7 +3,7 @@ Checks that value used in a subscript support deletion (i.e. defines __delitem__ method). """ # pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,wrong-import-position,unnecessary-comprehension -# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order +# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order,use-dict-literal # primitives numbers = [1, 2, 3] diff --git a/tests/functional/u/unused/unused_import.py b/tests/functional/u/unused/unused_import.py index 20a900fb5..24300587d 100644 --- a/tests/functional/u/unused/unused_import.py +++ b/tests/functional/u/unused/unused_import.py @@ -6,18 +6,24 @@ import os.path as test # [unused-import] from abc import ABCMeta from sys import argv as test2 # [unused-import] from sys import flags # [unused-import] + # +1:[unused-import,unused-import] from collections import deque, OrderedDict, Counter import re, html.parser # [unused-import] + DATA = Counter() # pylint: disable=self-assigning-variable from fake import SomeName, SomeOtherName # [unused-import] + + class SomeClass: - SomeName = SomeName # https://bitbucket.org/logilab/pylint/issue/475 + SomeName = SomeName # https://bitbucket.org/logilab/pylint/issue/475 SomeOtherName = 1 SomeOtherName = SomeOtherName + from never import __all__ + # pylint: disable=wrong-import-order,ungrouped-imports,reimported import typing from typing import TYPE_CHECKING @@ -32,24 +38,27 @@ if t.TYPE_CHECKING: import xml -def get_ordered_dict() -> 'collections.OrderedDict': +def get_ordered_dict() -> "collections.OrderedDict": return [] -def get_itertools_obj() -> 'itertools.count': +def get_itertools_obj() -> "itertools.count": return [] -def use_html_parser() -> 'html.parser.HTMLParser': + +def use_html_parser() -> "html.parser.HTMLParser": return html.parser.HTMLParser import os # [unused-import] import sys + class NonRegr: """???""" + def __init__(self): - print('initialized') + print("initialized") def sys(self): """should not get sys from there...""" @@ -61,7 +70,8 @@ class NonRegr: def blop(self): """yo""" - print(self, 'blip') + print(self, "blip") + if TYPE_CHECKING: if sys.version_info >= (3, 6, 2): @@ -71,6 +81,7 @@ if TYPE_CHECKING: from io import TYPE_CHECKING # pylint: disable=no-name-in-module import trace as t import astroid as typing + TYPE_CHECKING = "red herring" if TYPE_CHECKING: @@ -86,5 +97,16 @@ TYPE_CHECKING = False if TYPE_CHECKING: import zoneinfo + class WithMetaclass(metaclass=ABCMeta): pass + + +# Regression test for https://github.com/PyCQA/pylint/issues/3765 +# `unused-import` should not be emitted when a type annotation uses quotation marks +from typing import List + + +class Bee: + def get_all_classes(self) -> "List[Bee]": + pass diff --git a/tests/functional/u/unused/unused_import.txt b/tests/functional/u/unused/unused_import.txt index 857ba6a0d..f242bcb23 100644 --- a/tests/functional/u/unused/unused_import.txt +++ b/tests/functional/u/unused/unused_import.txt @@ -3,12 +3,12 @@ unused-import:4:0:4:14::Unused import xml.sax:UNDEFINED unused-import:5:0:5:22::Unused os.path imported as test:UNDEFINED unused-import:7:0:7:29::Unused argv imported from sys as test2:UNDEFINED unused-import:8:0:8:21::Unused flags imported from sys:UNDEFINED -unused-import:10:0:10:51::Unused OrderedDict imported from collections:UNDEFINED -unused-import:10:0:10:51::Unused deque imported from collections:UNDEFINED -unused-import:11:0:11:22::Unused import re:UNDEFINED -unused-import:14:0:14:40::Unused SomeOtherName imported from fake:UNDEFINED -unused-import:46:0:46:9::Unused import os:UNDEFINED -unused-import:77:4:77:19::Unused import unittest:UNDEFINED -unused-import:79:4:79:15::Unused import uuid:UNDEFINED -unused-import:81:4:81:19::Unused import warnings:UNDEFINED -unused-import:83:4:83:21::Unused import compileall:UNDEFINED +unused-import:11:0:11:51::Unused OrderedDict imported from collections:UNDEFINED +unused-import:11:0:11:51::Unused deque imported from collections:UNDEFINED +unused-import:12:0:12:22::Unused import re:UNDEFINED +unused-import:16:0:16:40::Unused SomeOtherName imported from fake:UNDEFINED +unused-import:53:0:53:9::Unused import os:UNDEFINED +unused-import:88:4:88:19::Unused import unittest:UNDEFINED +unused-import:90:4:90:15::Unused import uuid:UNDEFINED +unused-import:92:4:92:19::Unused import warnings:UNDEFINED +unused-import:94:4:94:21::Unused import compileall:UNDEFINED diff --git a/tests/functional/u/unused/unused_import_py39.py b/tests/functional/u/unused/unused_import_py39.py new file mode 100644 index 000000000..2a897b174 --- /dev/null +++ b/tests/functional/u/unused/unused_import_py39.py @@ -0,0 +1,10 @@ +""" +Test that a constant parameter of `typing.Annotated` does not emit `unused-import`. +`typing.Annotated` was introduced in Python version 3.9 +""" + +from pathlib import Path # [unused-import] +import typing as t + + +example: t.Annotated[str, "Path"] = "/foo/bar" diff --git a/tests/functional/u/unused/unused_import_py39.rc b/tests/functional/u/unused/unused_import_py39.rc new file mode 100644 index 000000000..16b75eea7 --- /dev/null +++ b/tests/functional/u/unused/unused_import_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.9 diff --git a/tests/functional/u/unused/unused_import_py39.txt b/tests/functional/u/unused/unused_import_py39.txt new file mode 100644 index 000000000..50e5ad5a9 --- /dev/null +++ b/tests/functional/u/unused/unused_import_py39.txt @@ -0,0 +1 @@ +unused-import:6:0:6:24::Unused Path imported from pathlib:UNDEFINED diff --git a/tests/functional/u/unused/unused_variable.py b/tests/functional/u/unused/unused_variable.py index 92a329f2f..0058516c9 100644 --- a/tests/functional/u/unused/unused_variable.py +++ b/tests/functional/u/unused/unused_variable.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring, invalid-name, too-few-public-methods, import-outside-toplevel, fixme, line-too-long +# pylint: disable=missing-docstring, invalid-name, too-few-public-methods, import-outside-toplevel, fixme, line-too-long, broad-exception-raised def test_regression_737(): import xml # [unused-import] diff --git a/tests/functional/u/use/use_implicit_booleaness_not_comparison.txt b/tests/functional/u/use/use_implicit_booleaness_not_comparison.txt index d316d5acd..2ace15d7e 100644 --- a/tests/functional/u/use/use_implicit_booleaness_not_comparison.txt +++ b/tests/functional/u/use/use_implicit_booleaness_not_comparison.txt @@ -1,32 +1,32 @@ -use-implicit-booleaness-not-comparison:14:7:14:21:github_issue_4774:'bad_list == []' can be simplified to 'not bad_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:22:3:22:20::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:25:3:25:19::'empty_list == []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:28:3:28:19::'empty_dict == {}' can be simplified to 'not empty_dict' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:31:3:31:20::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:34:3:34:19::'empty_list == []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:37:3:37:19::'empty_dict == {}' can be simplified to 'not empty_dict' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:42:11:42:18:bad_tuple_return:'t == ()' can be simplified to 'not t' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:46:11:46:18:bad_list_return:'b == []' can be simplified to 'not b' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:50:11:50:18:bad_dict_return:'c == {}' can be simplified to 'not c' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:52:7:52:24::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:53:7:53:23::'empty_list == []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:54:7:54:23::'empty_dict != {}' can be simplified to 'empty_dict' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:55:7:55:23::'empty_tuple < ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:56:7:56:23::'empty_list <= []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:57:7:57:23::'empty_tuple > ()' can be simplified to 'not empty_tuple' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:58:7:58:23::'empty_list >= []' can be simplified to 'not empty_list' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:83:3:83:10::'a == []' can be simplified to 'not a' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:95:3:95:10::'e == []' can be simplified to 'not e' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:95:15:95:22::'f == {}' can be simplified to 'not f' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:133:3:133:14::'A.lst == []' can be simplified to 'not A.lst' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:137:3:137:14::'A.lst == []' can be simplified to 'not A.lst' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:141:3:141:20::'A.test(...) == []' can be simplified to 'not A.test(...)' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:149:3:149:24::'test_function(...) == []' can be simplified to 'not test_function(...)' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:156:3:156:20::'numpy_array == []' can be simplified to 'not numpy_array' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:158:3:158:20::'numpy_array != []' can be simplified to 'numpy_array' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:160:3:160:20::'numpy_array >= ()' can be simplified to 'not numpy_array' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:185:3:185:13::'data == {}' can be simplified to 'not data' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:187:3:187:13::'data != {}' can be simplified to 'data' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:195:3:195:26::'long_test == {}' can be simplified to 'not long_test' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:233:11:233:41:test_func:'my_class.parent_function == {}' can be simplified to 'not my_class.parent_function' as an empty sequence is falsey:UNDEFINED -use-implicit-booleaness-not-comparison:234:11:234:37:test_func:'my_class.my_property == {}' can be simplified to 'not my_class.my_property' as an empty sequence is falsey:UNDEFINED +use-implicit-booleaness-not-comparison:14:7:14:21:github_issue_4774:'bad_list == []' can be simplified to 'not bad_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:22:3:22:20::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:25:3:25:19::'empty_list == []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:28:3:28:19::'empty_dict == {}' can be simplified to 'not empty_dict' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:31:3:31:20::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:34:3:34:19::'empty_list == []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:37:3:37:19::'empty_dict == {}' can be simplified to 'not empty_dict' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:42:11:42:18:bad_tuple_return:'t == ()' can be simplified to 'not t' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:46:11:46:18:bad_list_return:'b == []' can be simplified to 'not b' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:50:11:50:18:bad_dict_return:'c == {}' can be simplified to 'not c' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:52:7:52:24::'empty_tuple == ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:53:7:53:23::'empty_list == []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:54:7:54:23::'empty_dict != {}' can be simplified to 'empty_dict' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:55:7:55:23::'empty_tuple < ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:56:7:56:23::'empty_list <= []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:57:7:57:23::'empty_tuple > ()' can be simplified to 'not empty_tuple' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:58:7:58:23::'empty_list >= []' can be simplified to 'not empty_list' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:83:3:83:10::'a == []' can be simplified to 'not a' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:95:3:95:10::'e == []' can be simplified to 'not e' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:95:15:95:22::'f == {}' can be simplified to 'not f' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:133:3:133:14::'A.lst == []' can be simplified to 'not A.lst' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:137:3:137:14::'A.lst == []' can be simplified to 'not A.lst' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:141:3:141:20::'A.test(...) == []' can be simplified to 'not A.test(...)' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:149:3:149:24::'test_function(...) == []' can be simplified to 'not test_function(...)' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:156:3:156:20::'numpy_array == []' can be simplified to 'not numpy_array' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:158:3:158:20::'numpy_array != []' can be simplified to 'numpy_array' as an empty list is falsey:HIGH +use-implicit-booleaness-not-comparison:160:3:160:20::'numpy_array >= ()' can be simplified to 'not numpy_array' as an empty tuple is falsey:HIGH +use-implicit-booleaness-not-comparison:185:3:185:13::'data == {}' can be simplified to 'not data' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:187:3:187:13::'data != {}' can be simplified to 'data' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:195:3:195:26::'long_test == {}' can be simplified to 'not long_test' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:233:11:233:41:test_func:'my_class.parent_function == {}' can be simplified to 'not my_class.parent_function' as an empty dict is falsey:HIGH +use-implicit-booleaness-not-comparison:234:11:234:37:test_func:'my_class.my_property == {}' can be simplified to 'not my_class.my_property' as an empty dict is falsey:HIGH diff --git a/tests/functional/u/use/use_implicit_booleaness_not_len.txt b/tests/functional/u/use/use_implicit_booleaness_not_len.txt index 11412f5b2..85917de82 100644 --- a/tests/functional/u/use/use_implicit_booleaness_not_len.txt +++ b/tests/functional/u/use/use_implicit_booleaness_not_len.txt @@ -1,26 +1,26 @@ -use-implicit-booleaness-not-len:4:3:4:14::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:7:3:7:18::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:11:9:11:34::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:14:11:14:22::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED +use-implicit-booleaness-not-len:4:3:4:14::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:7:3:7:18::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:11:9:11:34::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:14:11:14:22::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE comparison-of-constants:39:3:39:28::"Comparison between constants: '0 < 1' has a constant value":HIGH -use-implicit-booleaness-not-len:56:5:56:16::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:61:5:61:20::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:64:6:64:17::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:67:6:67:21::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:70:12:70:23::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:73:6:73:21::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:96:11:96:20:github_issue_1331_v2:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:99:11:99:20:github_issue_1331_v3:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:102:17:102:26:github_issue_1331_v4:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:104:9:104:15::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:105:9:105:20::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:124:11:124:34:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:125:11:125:39:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:126:11:126:24:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:127:11:127:35:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:128:11:128:33:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:129:11:129:41:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:130:11:130:43:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED -use-implicit-booleaness-not-len:171:11:171:42:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:UNDEFINED +use-implicit-booleaness-not-len:56:5:56:16::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:61:5:61:20::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:64:6:64:17::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:67:6:67:21::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:70:12:70:23::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:73:6:73:21::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:96:11:96:20:github_issue_1331_v2:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:99:11:99:20:github_issue_1331_v3:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:102:17:102:26:github_issue_1331_v4:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:104:9:104:15::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:105:9:105:20::Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:124:11:124:34:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:125:11:125:39:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:126:11:126:24:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:127:11:127:35:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:128:11:128:33:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:129:11:129:41:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:HIGH +use-implicit-booleaness-not-len:130:11:130:43:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE +use-implicit-booleaness-not-len:171:11:171:42:github_issue_1879:Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty:INFERENCE undefined-variable:183:11:183:24:github_issue_4215:Undefined variable 'undefined_var':UNDEFINED undefined-variable:185:11:185:25:github_issue_4215:Undefined variable 'undefined_var2':UNDEFINED diff --git a/tests/functional/u/use/use_literal_dict.py b/tests/functional/u/use/use_literal_dict.py index 3377b4e63..598b382bd 100644 --- a/tests/functional/u/use/use_literal_dict.py +++ b/tests/functional/u/use/use_literal_dict.py @@ -1,7 +1,46 @@ -# pylint: disable=missing-docstring, invalid-name +# pylint: disable=missing-docstring, invalid-name, disallowed-name, unused-argument, too-few-public-methods x = dict() # [use-dict-literal] -x = dict(a="1", b=None, c=3) +x = dict(a="1", b=None, c=3) # [use-dict-literal] x = dict(zip(["a", "b", "c"], [1, 2, 3])) x = {} x = {"a": 1, "b": 2, "c": 3} +x = dict(**x) # [use-dict-literal] + +def bar(boo: bool = False): + return 1 + +x = dict(foo=bar()) # [use-dict-literal] + +baz = {"e": 9, "f": 1} + +dict( # [use-dict-literal] + **baz, + suggestions=list( + bar( + boo=True, + ) + ), +) + +class SomeClass: + prop: dict = {"a": 1} + +inst = SomeClass() + +dict( # [use-dict-literal] + url="/foo", + **inst.prop, +) + +dict( # [use-dict-literal] + Lorem="ipsum", + dolor="sit", + amet="consectetur", + adipiscing="elit", + sed="do", + eiusmod="tempor", + incididunt="ut", + labore="et", + dolore="magna", +) diff --git a/tests/functional/u/use/use_literal_dict.txt b/tests/functional/u/use/use_literal_dict.txt index cbcb83f24..145766479 100644 --- a/tests/functional/u/use/use_literal_dict.txt +++ b/tests/functional/u/use/use_literal_dict.txt @@ -1 +1,7 @@ -use-dict-literal:3:4:3:10::Consider using {} instead of dict():UNDEFINED +use-dict-literal:3:4:3:10::Consider using '{}' instead of a call to 'dict'.:INFERENCE +use-dict-literal:4:4:4:28::"Consider using '{""a"": '1', ""b"": None, ""c"": 3}' instead of a call to 'dict'.":INFERENCE +use-dict-literal:8:4:8:13::Consider using '{**x}' instead of a call to 'dict'.:INFERENCE +use-dict-literal:13:4:13:19::"Consider using '{""foo"": bar()}' instead of a call to 'dict'.":INFERENCE +use-dict-literal:17:0:24:1::"Consider using '{""suggestions"": list(bar(boo=True)), **baz}' instead of a call to 'dict'.":INFERENCE +use-dict-literal:31:0:34:1::"Consider using '{""url"": '/foo', **inst.prop}' instead of a call to 'dict'.":INFERENCE +use-dict-literal:36:0:46:1::"Consider using '{""Lorem"": 'ipsum', ""dolor"": 'sit', ""amet"": 'consectetur', ""adipiscing"": 'elit', ... }' instead of a call to 'dict'.":INFERENCE diff --git a/tests/functional/u/used/used_before_assignment.py b/tests/functional/u/used/used_before_assignment.py index 473af2b34..f8ed651b5 100644 --- a/tests/functional/u/used/used_before_assignment.py +++ b/tests/functional/u/used/used_before_assignment.py @@ -25,3 +25,84 @@ def redefine_time_import_with_global(): global time # pylint: disable=invalid-name print(time.time()) import time + + +# Control flow cases +FALSE = False +if FALSE: + VAR2 = True +if VAR2: # [used-before-assignment] + pass + +if FALSE: # pylint: disable=simplifiable-if-statement + VAR3 = True +elif VAR2: + VAR3 = True +else: + VAR3 = False +if VAR3: + pass + +if FALSE: + VAR4 = True +elif VAR2: + pass +else: + VAR4 = False +if VAR4: # [used-before-assignment] + pass + +if FALSE: + VAR5 = True +elif VAR2: + if FALSE: # pylint: disable=simplifiable-if-statement + VAR5 = True + else: + VAR5 = True +if VAR5: + pass + +if FALSE: + VAR6 = False +if VAR6: # [used-before-assignment] + pass + + +# Nested try +if FALSE: + try: + VAR7 = True + except ValueError: + pass +else: + VAR7 = False +if VAR7: + pass + +if FALSE: + try: + VAR8 = True + except ValueError as ve: + print(ve) + raise +else: + VAR8 = False +if VAR8: + pass + +if FALSE: + for i in range(5): + VAR9 = i + break +print(VAR9) + +if FALSE: + with open(__name__, encoding='utf-8') as f: + VAR10 = __name__ +print(VAR10) # [used-before-assignment] + +for num in [0, 1]: + VAR11 = num + if VAR11: + VAR12 = False +print(VAR12) diff --git a/tests/functional/u/used/used_before_assignment.txt b/tests/functional/u/used/used_before_assignment.txt index c48b3ed7c..70153f39a 100644 --- a/tests/functional/u/used/used_before_assignment.txt +++ b/tests/functional/u/used/used_before_assignment.txt @@ -2,3 +2,7 @@ used-before-assignment:5:19:5:22::Using variable 'MSG' before assignment:HIGH used-before-assignment:7:20:7:24::Using variable 'MSG2' before assignment:HIGH used-before-assignment:10:4:10:9:outer:Using variable 'inner' before assignment:HIGH used-before-assignment:20:10:20:14:redefine_time_import:Using variable 'time' before assignment:HIGH +used-before-assignment:34:3:34:7::Using variable 'VAR2' before assignment:CONTROL_FLOW +used-before-assignment:52:3:52:7::Using variable 'VAR4' before assignment:CONTROL_FLOW +used-before-assignment:67:3:67:7::Using variable 'VAR6' before assignment:CONTROL_FLOW +used-before-assignment:102:6:102:11::Using variable 'VAR10' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/used/used_before_assignment_comprehension_homonyms.py b/tests/functional/u/used/used_before_assignment_comprehension_homonyms.py index feae58dbe..2321afed7 100644 --- a/tests/functional/u/used/used_before_assignment_comprehension_homonyms.py +++ b/tests/functional/u/used/used_before_assignment_comprehension_homonyms.py @@ -1,4 +1,5 @@ """Homonym between filtered comprehension and assignment in except block.""" +# pylint: disable=broad-exception-raised def func(): """https://github.com/PyCQA/pylint/issues/5586""" diff --git a/tests/functional/u/used/used_before_assignment_conditional.py b/tests/functional/u/used/used_before_assignment_conditional.py index b5d16925e..b024d2898 100644 --- a/tests/functional/u/used/used_before_assignment_conditional.py +++ b/tests/functional/u/used/used_before_assignment_conditional.py @@ -1,4 +1,5 @@ """used-before-assignment cases involving IF conditions""" + if 1 + 1 == 2: x = x + 1 # [used-before-assignment] diff --git a/tests/functional/u/used/used_before_assignment_conditional.txt b/tests/functional/u/used/used_before_assignment_conditional.txt index 56626f0fe..a65f9f738 100644 --- a/tests/functional/u/used/used_before_assignment_conditional.txt +++ b/tests/functional/u/used/used_before_assignment_conditional.txt @@ -1,2 +1,2 @@ -used-before-assignment:3:8:3:9::Using variable 'x' before assignment:HIGH -used-before-assignment:5:3:5:4::Using variable 'y' before assignment:HIGH +used-before-assignment:4:8:4:9::Using variable 'x' before assignment:HIGH +used-before-assignment:6:3:6:4::Using variable 'y' before assignment:HIGH diff --git a/tests/functional/u/used/used_before_assignment_else_return.py b/tests/functional/u/used/used_before_assignment_else_return.py index a5dc5c23b..a7e58bb61 100644 --- a/tests/functional/u/used/used_before_assignment_else_return.py +++ b/tests/functional/u/used/used_before_assignment_else_return.py @@ -1,5 +1,6 @@ """If the else block returns, it is generally safe to rely on assignments in the except.""" - +# pylint: disable=missing-function-docstring, invalid-name +import sys def valid(): """https://github.com/PyCQA/pylint/issues/6790""" @@ -59,3 +60,15 @@ def invalid_4(): else: print(error) # [used-before-assignment] return + +def valid_exit(): + try: + pass + except SystemExit as e: + lint_result = e.code + else: + sys.exit("Bad") + if lint_result != 0: + sys.exit("Error is 0.") + + print(lint_result) diff --git a/tests/functional/u/used/used_before_assignment_else_return.txt b/tests/functional/u/used/used_before_assignment_else_return.txt index d7d1835f4..5ef28cfd6 100644 --- a/tests/functional/u/used/used_before_assignment_else_return.txt +++ b/tests/functional/u/used/used_before_assignment_else_return.txt @@ -1,4 +1,4 @@ -used-before-assignment:25:14:25:19:invalid:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:38:14:38:19:invalid_2:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:50:14:50:19:invalid_3:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:60:14:60:19:invalid_4:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:26:14:26:19:invalid:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:39:14:39:19:invalid_2:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:51:14:51:19:invalid_3:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:61:14:61:19:invalid_4:Using variable 'error' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.py b/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.py index 086ad0554..c83a48473 100644 --- a/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.py +++ b/tests/functional/u/used/used_before_assignment_except_handler_for_try_with_return.py @@ -2,7 +2,7 @@ try blocks with return statements. See: https://github.com/PyCQA/pylint/issues/5500. """ -# pylint: disable=inconsistent-return-statements +# pylint: disable=inconsistent-return-statements,broad-exception-raised def function(): @@ -77,7 +77,7 @@ def func_ok5(var): def func_ok6(var): """Define 'msg' in one handler nested under if block.""" - err_message = False + err_message = "Division by 0" try: return 1 / var.some_other_func() except ZeroDivisionError: diff --git a/tests/functional/u/used/used_before_assignment_issue626.txt b/tests/functional/u/used/used_before_assignment_issue626.txt index 1ee575ba3..3d0e57246 100644 --- a/tests/functional/u/used/used_before_assignment_issue626.txt +++ b/tests/functional/u/used/used_before_assignment_issue626.txt @@ -1,5 +1,5 @@ unused-variable:5:4:6:12:main1:Unused variable 'e':UNDEFINED -used-before-assignment:8:10:8:11:main1:Using variable 'e' before assignment:HIGH +used-before-assignment:8:10:8:11:main1:Using variable 'e' before assignment:CONTROL_FLOW unused-variable:21:4:22:12:main3:Unused variable 'e':UNDEFINED unused-variable:31:4:32:12:main4:Unused variable 'e':UNDEFINED -used-before-assignment:44:10:44:11:main4:Using variable 'e' before assignment:HIGH +used-before-assignment:44:10:44:11:main4:Using variable 'e' before assignment:CONTROL_FLOW diff --git a/tests/functional/u/used/used_before_assignment_nonlocal.py b/tests/functional/u/used/used_before_assignment_nonlocal.py index 270b72d22..18e16177d 100644 --- a/tests/functional/u/used/used_before_assignment_nonlocal.py +++ b/tests/functional/u/used/used_before_assignment_nonlocal.py @@ -88,3 +88,21 @@ def type_annotation_never_gets_value_despite_nonlocal(): nonlocal some_num
inner()
print(some_num) # [used-before-assignment]
+
+
+def inner_function_lacks_access_to_outer_args(args):
+ """Check homonym between inner function and outer function names"""
+ def inner():
+ print(args) # [used-before-assignment]
+ args = []
+ inner()
+ print(args)
+
+
+def inner_function_ok(args):
+ """Explicitly redefined homonym defined before is OK."""
+ def inner():
+ args = []
+ print(args)
+ inner()
+ print(args)
diff --git a/tests/functional/u/used/used_before_assignment_nonlocal.txt b/tests/functional/u/used/used_before_assignment_nonlocal.txt index 3e5045f27..2bdbf2fe1 100644 --- a/tests/functional/u/used/used_before_assignment_nonlocal.txt +++ b/tests/functional/u/used/used_before_assignment_nonlocal.txt @@ -5,3 +5,4 @@ used-before-assignment:33:22:33:32:test_fail4:Using variable 'test_fail5' before used-before-assignment:33:44:33:53:test_fail4:Using variable 'undefined' before assignment:HIGH used-before-assignment:39:18:39:28:test_fail5:Using variable 'undefined1' before assignment:HIGH used-before-assignment:90:10:90:18:type_annotation_never_gets_value_despite_nonlocal:Using variable 'some_num' before assignment:HIGH +used-before-assignment:96:14:96:18:inner_function_lacks_access_to_outer_args.inner:Using variable 'args' before assignment:HIGH diff --git a/tests/lint/test_pylinter.py b/tests/lint/test_pylinter.py index 731af5b0a..1d0f43819 100644 --- a/tests/lint/test_pylinter.py +++ b/tests/lint/test_pylinter.py @@ -2,12 +2,14 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +import os +from pathlib import Path from typing import Any, NoReturn from unittest import mock from unittest.mock import patch import pytest -from py._path.local import LocalPath # type: ignore[import] +from _pytest.recwarn import WarningsRecorder from pytest import CaptureFixture from pylint.lint.pylinter import PyLinter @@ -20,39 +22,39 @@ def raise_exception(*args: Any, **kwargs: Any) -> NoReturn: @patch.object(FileState, "iter_spurious_suppression_messages", raise_exception) def test_crash_in_file( - linter: PyLinter, capsys: CaptureFixture, tmpdir: LocalPath + linter: PyLinter, capsys: CaptureFixture[str], tmp_path: Path ) -> None: with pytest.warns(DeprecationWarning): args = linter.load_command_line_configuration([__file__]) - linter.crash_file_path = str(tmpdir / "pylint-crash-%Y") + linter.crash_file_path = str(tmp_path / "pylint-crash-%Y") linter.check(args) out, err = capsys.readouterr() assert not out assert not err - files = tmpdir.listdir() + files = os.listdir(tmp_path) assert len(files) == 1 assert "pylint-crash-20" in str(files[0]) assert any(m.symbol == "fatal" for m in linter.reporter.messages) -def test_check_deprecation(linter: PyLinter, recwarn): +def test_check_deprecation(linter: PyLinter, recwarn: WarningsRecorder) -> None: linter.check("myfile.py") msg = recwarn.pop() assert "check function will only accept sequence" in str(msg) def test_crash_during_linting( - linter: PyLinter, capsys: CaptureFixture[str], tmpdir: LocalPath + linter: PyLinter, capsys: CaptureFixture[str], tmp_path: Path ) -> None: with mock.patch( "pylint.lint.PyLinter.check_astroid_module", side_effect=RuntimeError ): - linter.crash_file_path = str(tmpdir / "pylint-crash-%Y") + linter.crash_file_path = str(tmp_path / "pylint-crash-%Y") linter.check([__file__]) out, err = capsys.readouterr() assert not out assert not err - files = tmpdir.listdir() + files = os.listdir(tmp_path) assert len(files) == 1 assert "pylint-crash-20" in str(files[0]) assert any(m.symbol == "astroid-error" for m in linter.reporter.messages) diff --git a/tests/lint/test_utils.py b/tests/lint/test_utils.py index 6cc79f18b..872919f72 100644 --- a/tests/lint/test_utils.py +++ b/tests/lint/test_utils.py @@ -18,7 +18,7 @@ def test_prepare_crash_report(tmp_path: PosixPath) -> None: with open(python_file, "w", encoding="utf8") as f: f.write(python_content) try: - raise Exception(exception_content) + raise Exception(exception_content) # pylint: disable=broad-exception-raised except Exception as ex: # pylint: disable=broad-except template_path = prepare_crash_report( ex, str(python_file), str(tmp_path / "pylint-crash-%Y.txt") diff --git a/tests/lint/unittest_expand_modules.py b/tests/lint/unittest_expand_modules.py index 3336c47bd..88f058b1e 100644 --- a/tests/lint/unittest_expand_modules.py +++ b/tests/lint/unittest_expand_modules.py @@ -12,7 +12,7 @@ import pytest from pylint.checkers import BaseChecker from pylint.lint.expand_modules import _is_in_ignore_list_re, expand_modules from pylint.testutils import CheckerTestCase, set_config -from pylint.typing import MessageDefinitionTuple +from pylint.typing import MessageDefinitionTuple, ModuleDescriptionDict def test__is_in_ignore_list_re_match() -> None: @@ -134,9 +134,12 @@ class TestExpandModules(CheckerTestCase): ], ) @set_config(ignore_paths="") - def test_expand_modules(self, files_or_modules, expected): + def test_expand_modules( + self, files_or_modules: list[str], expected: dict[str, ModuleDescriptionDict] + ) -> None: """Test expand_modules with the default value of ignore-paths.""" - ignore_list, ignore_list_re = [], [] + ignore_list: list[str] = [] + ignore_list_re: list[re.Pattern[str]] = [] modules, errors = expand_modules( files_or_modules, ignore_list, @@ -161,7 +164,7 @@ class TestExpandModules(CheckerTestCase): ) @set_config(ignore_paths="") def test_expand_modules_deduplication( - self, files_or_modules: list[str], expected + self, files_or_modules: list[str], expected: dict[str, ModuleDescriptionDict] ) -> None: """Test expand_modules deduplication.""" ignore_list: list[str] = [] @@ -189,9 +192,12 @@ class TestExpandModules(CheckerTestCase): ], ) @set_config(ignore_paths=".*/lint/.*") - def test_expand_modules_with_ignore(self, files_or_modules, expected): + def test_expand_modules_with_ignore( + self, files_or_modules: list[str], expected: dict[str, ModuleDescriptionDict] + ) -> None: """Test expand_modules with a non-default value of ignore-paths.""" - ignore_list, ignore_list_re = [], [] + ignore_list: list[str] = [] + ignore_list_re: list[re.Pattern[str]] = [] modules, errors = expand_modules( files_or_modules, ignore_list, diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py index 057b322d5..25df951df 100644 --- a/tests/lint/unittest_lint.py +++ b/tests/lint/unittest_lint.py @@ -12,17 +12,18 @@ import os import re import sys import tempfile -from collections.abc import Iterable, Iterator +from collections.abc import Iterator from contextlib import contextmanager from importlib import reload from io import StringIO from os import chdir, getcwd from os.path import abspath, dirname, join, sep from pathlib import Path -from shutil import copytree, rmtree +from shutil import copy, rmtree import platformdirs import pytest +from astroid import nodes from pytest import CaptureFixture from pylint import checkers, config, exceptions, interfaces, lint, testutils @@ -74,7 +75,7 @@ def fake_home() -> Iterator[str]: rmtree(folder, ignore_errors=True) -def remove(file): +def remove(file: str) -> None: try: os.remove(file) except OSError: @@ -108,9 +109,9 @@ def tempdir() -> Iterator[str]: @pytest.fixture -def fake_path() -> Iterator[Iterable[str]]: +def fake_path() -> Iterator[list[str]]: orig = list(sys.path) - fake: Iterable[str] = ["1", "2", "3"] + fake = ["1", "2", "3"] sys.path[:] = fake yield fake sys.path[:] = orig @@ -145,7 +146,7 @@ def test_one_arg(fake_path: list[str], case: list[str]) -> None: ["a", "a/c/__init__.py"], ], ) -def test_two_similar_args(fake_path, case): +def test_two_similar_args(fake_path: list[str], case: list[str]) -> None: with tempdir() as chroot: create_files(["a/b/__init__.py", "a/c/__init__.py"]) expected = [join(chroot, "a")] + fake_path @@ -164,7 +165,7 @@ def test_two_similar_args(fake_path, case): ["a/b/c", "a", "a/b/c", "a/e", "a"], ], ) -def test_more_args(fake_path, case): +def test_more_args(fake_path: list[str], case: list[str]) -> None: with tempdir() as chroot: create_files(["a/b/c/__init__.py", "a/d/__init__.py", "a/e/f.py"]) expected = [ @@ -179,12 +180,12 @@ def test_more_args(fake_path, case): @pytest.fixture(scope="module") -def disable(): +def disable() -> list[str]: return ["I"] @pytest.fixture(scope="module") -def reporter(): +def reporter() -> type[testutils.GenericTestReporter]: return testutils.GenericTestReporter @@ -208,7 +209,7 @@ def test_pylint_visit_method_taken_in_account(linter: PyLinter) -> None: msgs = {"W9999": ("", "custom", "")} @only_required_for_messages("custom") - def visit_class(self, _): + def visit_class(self, _: nodes.ClassDef) -> None: pass linter.register_checker(CustomChecker(linter)) @@ -533,7 +534,9 @@ def test_load_plugin_path_manipulation_case_6() -> None: the config file has run. This is not supported, and was previously a silent failure. This test ensures a ``bad-plugin-value`` message is emitted. """ - dummy_plugin_path = abspath(join(REGRTEST_DATA_DIR, "dummy_plugin")) + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") + ) with fake_home() as home_path: # construct a basic rc file that just modifies the path pylintrc_file = join(home_path, "pylintrc") @@ -546,7 +549,7 @@ def test_load_plugin_path_manipulation_case_6() -> None: ] ) - copytree(dummy_plugin_path, join(home_path, "copy_dummy")) + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) # To confirm we won't load this module _without_ the init hook running. assert home_path not in sys.path @@ -565,7 +568,7 @@ def test_load_plugin_path_manipulation_case_6() -> None: assert run._rcfile == pylintrc_file assert home_path in sys.path # The module should not be loaded - assert not any(ch.name == "copy_dummy" for ch in run.linter.get_checkers()) + assert not any(ch.name == "dummy_plugin" for ch in run.linter.get_checkers()) # There should be a bad-plugin-message for this module assert len(run.linter.reporter.messages) == 1 @@ -602,7 +605,9 @@ def test_load_plugin_path_manipulation_case_3() -> None: the config file has run. This is not supported, and was previously a silent failure. This test ensures a ``bad-plugin-value`` message is emitted. """ - dummy_plugin_path = abspath(join(REGRTEST_DATA_DIR, "dummy_plugin")) + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") + ) with fake_home() as home_path: # construct a basic rc file that just modifies the path pylintrc_file = join(home_path, "pylintrc") @@ -614,7 +619,7 @@ def test_load_plugin_path_manipulation_case_3() -> None: ] ) - copytree(dummy_plugin_path, join(home_path, "copy_dummy")) + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) # To confirm we won't load this module _without_ the init hook running. assert home_path not in sys.path @@ -633,7 +638,7 @@ def test_load_plugin_path_manipulation_case_3() -> None: assert run._rcfile == pylintrc_file assert home_path in sys.path # The module should not be loaded - assert not any(ch.name == "copy_dummy" for ch in run.linter.get_checkers()) + assert not any(ch.name == "dummy_plugin" for ch in run.linter.get_checkers()) # There should be a bad-plugin-message for this module assert len(run.linter.reporter.messages) == 1 @@ -661,49 +666,137 @@ def test_load_plugin_path_manipulation_case_3() -> None: sys.path.remove(home_path) -def test_load_plugin_command_line_before_init_hook() -> None: - """Check that the order of 'load-plugins' and 'init-hook' doesn't affect execution.""" - regrtest_data_dir_abs = abspath(REGRTEST_DATA_DIR) +@pytest.mark.usefixtures("pop_pylintrc") +def test_load_plugin_pylintrc_order_independent() -> None: + """Test that the init-hook is called independent of the order in a config file. - run = Run( - [ - "--load-plugins", - "dummy_plugin", - "--init-hook", - f'import sys; sys.path.append("{regrtest_data_dir_abs}")', - join(REGRTEST_DATA_DIR, "empty.py"), - ], - exit=False, - ) - assert ( - len([ch.name for ch in run.linter.get_checkers() if ch.name == "dummy_plugin"]) - == 2 + We want to ensure that any path manipulation in init hook + that means a plugin can load (as per GitHub Issue #7264 Cases 4+7) + runs before the load call, regardless of the order of lines in the + pylintrc file. + """ + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") ) - # Necessary as the executed init-hook modifies sys.path - sys.path.remove(regrtest_data_dir_abs) + with fake_home() as home_path: + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) + # construct a basic rc file that just modifies the path + pylintrc_file_before = join(home_path, "pylintrc_before") + with open(pylintrc_file_before, "w", encoding="utf8") as out: + out.writelines( + [ + "[MASTER]\n", + f"init-hook=\"import sys; sys.path.append(r'{home_path}')\"\n", + "load-plugins=copy_dummy\n", + ] + ) + pylintrc_file_after = join(home_path, "pylintrc_after") + with open(pylintrc_file_after, "w", encoding="utf8") as out: + out.writelines( + [ + "[MASTER]\n", + "load-plugins=copy_dummy\n" + f"init-hook=\"import sys; sys.path.append(r'{home_path}')\"\n", + ] + ) + for rcfile in (pylintrc_file_before, pylintrc_file_after): + # To confirm we won't load this module _without_ the init hook running. + assert home_path not in sys.path + run = Run( + [ + "--rcfile", + rcfile, + join(REGRTEST_DATA_DIR, "empty.py"), + ], + exit=False, + ) + assert ( + len( + [ + ch.name + for ch in run.linter.get_checkers() + if ch.name == "dummy_plugin" + ] + ) + == 2 + ) + assert run._rcfile == rcfile + assert home_path in sys.path + # Necessary as the executed init-hook modifies sys.path + sys.path.remove(home_path) -def test_load_plugin_command_line_with_init_hook_command_line() -> None: - regrtest_data_dir_abs = abspath(REGRTEST_DATA_DIR) - run = Run( - [ - "--init-hook", - f'import sys; sys.path.append("{regrtest_data_dir_abs}")', - "--load-plugins", - "dummy_plugin", - join(REGRTEST_DATA_DIR, "empty.py"), - ], - exit=False, +def test_load_plugin_command_line_before_init_hook() -> None: + """Check that the order of 'load-plugins' and 'init-hook' doesn't affect execution.""" + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") ) - assert ( - len([ch.name for ch in run.linter.get_checkers() if ch.name == "dummy_plugin"]) - == 2 + + with fake_home() as home_path: + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) + # construct a basic rc file that just modifies the path + assert home_path not in sys.path + run = Run( + [ + "--load-plugins", + "copy_dummy", + "--init-hook", + f'import sys; sys.path.append(r"{home_path}")', + join(REGRTEST_DATA_DIR, "empty.py"), + ], + exit=False, + ) + assert home_path in sys.path + assert ( + len( + [ + ch.name + for ch in run.linter.get_checkers() + if ch.name == "dummy_plugin" + ] + ) + == 2 + ) + + # Necessary as the executed init-hook modifies sys.path + sys.path.remove(home_path) + + +def test_load_plugin_command_line_with_init_hook_command_line() -> None: + dummy_plugin_path = abspath( + join(REGRTEST_DATA_DIR, "dummy_plugin", "dummy_plugin.py") ) - # Necessary as the executed init-hook modifies sys.path - sys.path.remove(regrtest_data_dir_abs) + with fake_home() as home_path: + copy(dummy_plugin_path, join(home_path, "copy_dummy.py")) + # construct a basic rc file that just modifies the path + assert home_path not in sys.path + run = Run( + [ + "--init-hook", + f'import sys; sys.path.append(r"{home_path}")', + "--load-plugins", + "copy_dummy", + join(REGRTEST_DATA_DIR, "empty.py"), + ], + exit=False, + ) + assert ( + len( + [ + ch.name + for ch in run.linter.get_checkers() + if ch.name == "dummy_plugin" + ] + ) + == 2 + ) + assert home_path in sys.path + + # Necessary as the executed init-hook modifies sys.path + sys.path.remove(home_path) def test_load_plugin_config_file() -> None: @@ -797,7 +890,7 @@ def test_full_documentation(linter: PyLinter) -> None: def test_list_msgs_enabled( - initialized_linter: PyLinter, capsys: CaptureFixture + initialized_linter: PyLinter, capsys: CaptureFixture[str] ) -> None: linter = initialized_linter linter.enable("W0101", scope="package") @@ -851,7 +944,7 @@ def test_pylint_home_from_environ() -> None: del os.environ["PYLINTHOME"] -def test_warn_about_old_home(capsys: CaptureFixture) -> None: +def test_warn_about_old_home(capsys: CaptureFixture[str]) -> None: """Test that we correctly warn about old_home.""" # Create old home old_home = Path(USER_HOME) / OLD_DEFAULT_PYLINT_HOME @@ -1064,7 +1157,7 @@ def test_by_module_statement_value(initialized_linter: PyLinter) -> None: ("--ignore-paths", ".*ignored.*/failing.*"), ], ) -def test_recursive_ignore(ignore_parameter, ignore_parameter_value) -> None: +def test_recursive_ignore(ignore_parameter: str, ignore_parameter_value: str) -> None: run = Run( [ "--recursive", diff --git a/tests/message/unittest_message_definition.py b/tests/message/unittest_message_definition.py index d42a249e3..aebd1bc6b 100644 --- a/tests/message/unittest_message_definition.py +++ b/tests/message/unittest_message_definition.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + import sys from unittest import mock @@ -21,7 +23,7 @@ from pylint.message import MessageDefinition ("W12345", "Invalid message id 'W12345'"), ], ) -def test_create_invalid_message_type(msgid, expected): +def test_create_invalid_message_type(msgid: str, expected: str) -> None: checker_mock = mock.Mock(name="Checker") checker_mock.name = "checker" @@ -51,16 +53,20 @@ class FalseChecker(BaseChecker): class TestMessagesDefinition: @staticmethod - def assert_with_fail_msg(msg: MessageDefinition, expected: bool = True) -> None: + def assert_with_fail_msg( + msg: MessageDefinition, + expected: bool = True, + py_version: tuple[int, ...] | sys._version_info = sys.version_info, + ) -> None: fail_msg = ( f"With minversion='{msg.minversion}' and maxversion='{msg.maxversion}'," - f" and the python interpreter being {sys.version_info} " + f" and the py-version option being {py_version} " "the message should{}be emitable" ) if expected: - assert msg.may_be_emitted(), fail_msg.format(" ") + assert msg.may_be_emitted(py_version), fail_msg.format(" ") else: - assert not msg.may_be_emitted(), fail_msg.format(" not ") + assert not msg.may_be_emitted(py_version), fail_msg.format(" not ") @staticmethod def get_message_definition() -> MessageDefinition: @@ -73,7 +79,7 @@ class TestMessagesDefinition: WarningScope.NODE, ) - def test_may_be_emitted(self) -> None: + def test_may_be_emitted_default(self) -> None: major = sys.version_info.major minor = sys.version_info.minor msg = self.get_message_definition() @@ -88,6 +94,21 @@ class TestMessagesDefinition: msg.maxversion = (major, minor - 1) self.assert_with_fail_msg(msg, expected=False) + def test_may_be_emitted_py_version(self) -> None: + msg = self.get_message_definition() + self.assert_with_fail_msg(msg, expected=True, py_version=(3, 2)) + + msg.maxversion = (3, 5) + self.assert_with_fail_msg(msg, expected=True, py_version=(3, 2)) + self.assert_with_fail_msg(msg, expected=False, py_version=(3, 5)) + self.assert_with_fail_msg(msg, expected=False, py_version=(3, 6)) + + msg.maxversion = None + msg.minversion = (3, 9) + self.assert_with_fail_msg(msg, expected=True, py_version=(3, 9)) + self.assert_with_fail_msg(msg, expected=True, py_version=(3, 10)) + self.assert_with_fail_msg(msg, expected=False, py_version=(3, 8)) + def test_repr(self) -> None: msg = self.get_message_definition() repr_str = str([msg, msg]) diff --git a/tests/message/unittest_message_definition_store.py b/tests/message/unittest_message_definition_store.py index 6a7914334..d36b1b42a 100644 --- a/tests/message/unittest_message_definition_store.py +++ b/tests/message/unittest_message_definition_store.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + from contextlib import redirect_stdout from io import StringIO @@ -13,6 +15,7 @@ from pylint.exceptions import InvalidMessageError, UnknownMessageError from pylint.lint.pylinter import PyLinter from pylint.message import MessageDefinition from pylint.message.message_definition_store import MessageDefinitionStore +from pylint.typing import MessageDefinitionTuple @pytest.mark.parametrize( @@ -120,7 +123,11 @@ from pylint.message.message_definition_store import MessageDefinitionStore ), ], ) -def test_register_error(empty_store, messages, expected): +def test_register_error( + empty_store: MessageDefinitionStore, + messages: dict[str, MessageDefinitionTuple], + expected: str, +) -> None: class Checker(BaseChecker): def __init__(self) -> None: super().__init__(PyLinter()) diff --git a/tests/message/unittest_message_id_store.py b/tests/message/unittest_message_id_store.py index e543fef55..9dcf774e5 100644 --- a/tests/message/unittest_message_id_store.py +++ b/tests/message/unittest_message_id_store.py @@ -127,7 +127,7 @@ def test_exclusivity_of_msgids() -> None: "07": ("exceptions", "broad_try_clause", "overlap-except"), "12": ("design", "logging"), "17": ("async", "refactoring"), - "20": ("compare-to-zero", "empty-comment"), + "20": ("compare-to-zero", "empty-comment", "magic-value"), } for msgid, definition in runner.linter.msgs_store._messages_definitions.items(): diff --git a/tests/primer/packages_to_lint_batch_one.json b/tests/primer/packages_to_lint_batch_one.json index 9b5eb7985..6520e2bd1 100644 --- a/tests/primer/packages_to_lint_batch_one.json +++ b/tests/primer/packages_to_lint_batch_one.json @@ -3,11 +3,5 @@ "branch": "master", "directories": ["keras"], "url": "https://github.com/keras-team/keras.git" - }, - "music21": { - "branch": "master", - "directories": ["music21"], - "pylintrc_relpath": ".pylintrc", - "url": "https://github.com/cuthbertLab/music21" } } diff --git a/tests/primer/packages_to_prime.json b/tests/primer/packages_to_prime.json index 0fb877dcc..a1fd74b4d 100644 --- a/tests/primer/packages_to_prime.json +++ b/tests/primer/packages_to_prime.json @@ -20,6 +20,13 @@ "directories": ["src/flask"], "url": "https://github.com/pallets/flask" }, + "music21": { + "branch": "master", + "directories": ["music21"], + "pylintrc_relpath": ".pylintrc", + "minimum_python": "3.10", + "url": "https://github.com/cuthbertLab/music21" + }, "pandas": { "branch": "main", "directories": ["pandas"], @@ -45,5 +52,11 @@ "branch": "master", "directories": ["src/sentry"], "url": "https://github.com/getsentry/sentry" + }, + "coverage": { + "branch": "master", + "directories": ["coverage"], + "url": "https://github.com/nedbat/coveragepy", + "pylintrc_relpath": "pylintrc" } } diff --git a/tests/primer/test_primer_stdlib.py b/tests/primer/test_primer_stdlib.py index 6cae6fd36..c2d879764 100644 --- a/tests/primer/test_primer_stdlib.py +++ b/tests/primer/test_primer_stdlib.py @@ -2,10 +2,14 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + import contextlib import io import os import sys +import warnings +from collections.abc import Iterator import pytest from pytest import CaptureFixture @@ -22,7 +26,7 @@ def is_package(filename: str, location: str) -> bool: @contextlib.contextmanager -def _patch_stdout(out): +def _patch_stdout(out: io.StringIO) -> Iterator[None]: sys.stdout = out try: yield @@ -57,11 +61,13 @@ def test_primer_stdlib_no_crash( # Duplicate code takes too long and is relatively safe # We don't want to lint the test directory which are repetitive disables = ["--disable=duplicate-code", "--ignore=test"] - Run([test_module_name] + enables + disables) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=UserWarning) + Run([test_module_name] + enables + disables) except SystemExit as ex: out, err = capsys.readouterr() assert not err, err assert not out msg = f"Encountered {{}} during primer stlib test for {test_module_name}" assert ex.code != 32, msg.format("a crash") - assert ex.code % 2 == 0, msg.format("a message of category 'fatal'") + assert ex.code % 2 == 0, msg.format("a message of category 'fatal'") # type: ignore[operator] diff --git a/tests/profile/test_profile_against_externals.py b/tests/profile/test_profile_against_externals.py index 579a5bc9c..7a429fad8 100644 --- a/tests/profile/test_profile_against_externals.py +++ b/tests/profile/test_profile_against_externals.py @@ -6,8 +6,11 @@ # pylint: disable=missing-function-docstring +from __future__ import annotations + import os import pprint +from pathlib import Path import pytest @@ -15,10 +18,10 @@ from pylint.testutils import GenericTestReporter as Reporter from pylint.testutils._run import _Run as Run -def _get_py_files(scanpath): +def _get_py_files(scanpath: str) -> list[str]: assert os.path.exists(scanpath), f"Dir not found {scanpath}" - filepaths = [] + filepaths: list[str] = [] for dirpath, dirnames, filenames in os.walk(scanpath): dirnames[:] = [dirname for dirname in dirnames if dirname != "__pycache__"] filepaths.extend( @@ -38,7 +41,7 @@ def _get_py_files(scanpath): @pytest.mark.parametrize( "name,git_repo", [("numpy", "https://github.com/numpy/numpy.git")] ) -def test_run(tmp_path, name, git_repo): +def test_run(tmp_path: Path, name: str, git_repo: str) -> None: """Runs pylint against external sources.""" checkoutdir = tmp_path / name checkoutdir.mkdir() diff --git a/tests/pyreverse/conftest.py b/tests/pyreverse/conftest.py index d8b6ea9f4..a37e4bde1 100644 --- a/tests/pyreverse/conftest.py +++ b/tests/pyreverse/conftest.py @@ -12,6 +12,7 @@ from astroid.nodes.scoped_nodes import Module from pylint.lint import fix_import_path from pylint.pyreverse.inspector import Project, project_from_files from pylint.testutils.pyreverse import PyreverseConfig +from pylint.typing import GetProjectCallable @pytest.fixture() @@ -66,11 +67,11 @@ def html_config() -> PyreverseConfig: @pytest.fixture(scope="session") -def get_project() -> Callable: +def get_project() -> GetProjectCallable: def _get_project(module: str, name: str | None = "No Name") -> Project: """Return an astroid project representation.""" - def _astroid_wrapper(func: Callable, modname: str) -> Module: + def _astroid_wrapper(func: Callable[[str], Module], modname: str) -> Module: return func(modname) with fix_import_path([module]): diff --git a/tests/pyreverse/data/classes_No_Name.dot b/tests/pyreverse/data/classes_No_Name.dot index 1f3f705e7..a598ab6d9 100644 --- a/tests/pyreverse/data/classes_No_Name.dot +++ b/tests/pyreverse/data/classes_No_Name.dot @@ -1,17 +1,17 @@ digraph "classes_No_Name" { rankdir=BT charset="utf-8" -"data.clientmodule_test.Ancestor" [color="black", fontcolor="black", label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value(value)\l}", shape="record", style="solid"]; -"data.suppliermodule_test.CustomException" [color="black", fontcolor="red", label="{CustomException|\l|}", shape="record", style="solid"]; -"data.suppliermodule_test.DoNothing" [color="black", fontcolor="black", label="{DoNothing|\l|}", shape="record", style="solid"]; -"data.suppliermodule_test.DoNothing2" [color="black", fontcolor="black", label="{DoNothing2|\l|}", shape="record", style="solid"]; -"data.suppliermodule_test.DoSomething" [color="black", fontcolor="black", label="{DoSomething|my_int : Optional[int]\lmy_int_2 : Optional[int]\lmy_string : str\l|do_it(new_int: int): int\l}", shape="record", style="solid"]; -"data.suppliermodule_test.Interface" [color="black", fontcolor="black", label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record", style="solid"]; -"data.property_pattern.PropertyPatterns" [color="black", fontcolor="black", label="{PropertyPatterns|prop1\lprop2\l|}", shape="record", style="solid"]; -"data.clientmodule_test.Specialization" [color="black", fontcolor="black", label="{Specialization|TYPE : str\lrelation\lrelation2\ltop : str\l|from_value(value: int)\lincrement_value(): None\ltransform_value(value: int): int\l}", shape="record", style="solid"]; +"data.clientmodule_test.Ancestor" [color="black", fontcolor="black", label=<{Ancestor|attr : str<br ALIGN="LEFT"/>cls_member<br ALIGN="LEFT"/>|get_value()<br ALIGN="LEFT"/>set_value(value)<br ALIGN="LEFT"/>}>, shape="record", style="solid"]; +"data.suppliermodule_test.CustomException" [color="black", fontcolor="red", label=<{CustomException|<br ALIGN="LEFT"/>|}>, shape="record", style="solid"]; +"data.suppliermodule_test.DoNothing" [color="black", fontcolor="black", label=<{DoNothing|<br ALIGN="LEFT"/>|}>, shape="record", style="solid"]; +"data.suppliermodule_test.DoNothing2" [color="black", fontcolor="black", label=<{DoNothing2|<br ALIGN="LEFT"/>|}>, shape="record", style="solid"]; +"data.suppliermodule_test.DoSomething" [color="black", fontcolor="black", label=<{DoSomething|my_int : Optional[int]<br ALIGN="LEFT"/>my_int_2 : Optional[int]<br ALIGN="LEFT"/>my_string : str<br ALIGN="LEFT"/>|do_it(new_int: int): int<br ALIGN="LEFT"/>}>, shape="record", style="solid"]; +"data.suppliermodule_test.Interface" [color="black", fontcolor="black", label=<{Interface|<br ALIGN="LEFT"/>|<I>get_value</I>()<br ALIGN="LEFT"/><I>set_value</I>(value)<br ALIGN="LEFT"/>}>, shape="record", style="solid"]; +"data.property_pattern.PropertyPatterns" [color="black", fontcolor="black", label=<{PropertyPatterns|prop1<br ALIGN="LEFT"/>prop2<br ALIGN="LEFT"/>|}>, shape="record", style="solid"]; +"data.clientmodule_test.Specialization" [color="black", fontcolor="black", label=<{Specialization|TYPE : str<br ALIGN="LEFT"/>relation<br ALIGN="LEFT"/>relation2<br ALIGN="LEFT"/>top : str<br ALIGN="LEFT"/>|from_value(value: int)<br ALIGN="LEFT"/>increment_value(): None<br ALIGN="LEFT"/>transform_value(value: int): int<br ALIGN="LEFT"/>}>, shape="record", style="solid"]; "data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"]; "data.clientmodule_test.Ancestor" -> "data.suppliermodule_test.Interface" [arrowhead="empty", arrowtail="node", style="dashed"]; "data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Ancestor" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"]; "data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"]; -"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"]; +"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="odiamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"]; } diff --git a/tests/pyreverse/data/classes_No_Name.html b/tests/pyreverse/data/classes_No_Name.html index 956758223..602f2e3b7 100644 --- a/tests/pyreverse/data/classes_No_Name.html +++ b/tests/pyreverse/data/classes_No_Name.html @@ -23,8 +23,8 @@ do_it(new_int: int) int } class Interface { - get_value() - set_value(value) + get_value()* + set_value(value)* } class PropertyPatterns { prop1 @@ -43,7 +43,7 @@ Ancestor ..|> Interface DoNothing --* Ancestor : cls_member DoNothing --* Specialization : relation - DoNothing2 --* Specialization : relation2 + DoNothing2 --o Specialization : relation2 </div> </body> diff --git a/tests/pyreverse/data/classes_No_Name.mmd b/tests/pyreverse/data/classes_No_Name.mmd index 4daa91c24..1db88b2ae 100644 --- a/tests/pyreverse/data/classes_No_Name.mmd +++ b/tests/pyreverse/data/classes_No_Name.mmd @@ -18,8 +18,8 @@ classDiagram do_it(new_int: int) int } class Interface { - get_value() - set_value(value) + get_value()* + set_value(value)* } class PropertyPatterns { prop1 @@ -38,4 +38,4 @@ classDiagram Ancestor ..|> Interface DoNothing --* Ancestor : cls_member DoNothing --* Specialization : relation - DoNothing2 --* Specialization : relation2 + DoNothing2 --o Specialization : relation2 diff --git a/tests/pyreverse/data/classes_No_Name.puml b/tests/pyreverse/data/classes_No_Name.puml index 37767b321..837e6865c 100644 --- a/tests/pyreverse/data/classes_No_Name.puml +++ b/tests/pyreverse/data/classes_No_Name.puml @@ -19,8 +19,8 @@ class "DoSomething" as data.suppliermodule_test.DoSomething { do_it(new_int: int) -> int } class "Interface" as data.suppliermodule_test.Interface { - get_value() - set_value(value) + {abstract}get_value() + {abstract}set_value(value) } class "PropertyPatterns" as data.property_pattern.PropertyPatterns { prop1 @@ -39,5 +39,5 @@ data.clientmodule_test.Specialization --|> data.clientmodule_test.Ancestor data.clientmodule_test.Ancestor ..|> data.suppliermodule_test.Interface data.suppliermodule_test.DoNothing --* data.clientmodule_test.Ancestor : cls_member data.suppliermodule_test.DoNothing --* data.clientmodule_test.Specialization : relation -data.suppliermodule_test.DoNothing2 --* data.clientmodule_test.Specialization : relation2 +data.suppliermodule_test.DoNothing2 --o data.clientmodule_test.Specialization : relation2 @enduml diff --git a/tests/pyreverse/data/classes_colorized.dot b/tests/pyreverse/data/classes_colorized.dot index 72f30658d..4ff12a819 100644 --- a/tests/pyreverse/data/classes_colorized.dot +++ b/tests/pyreverse/data/classes_colorized.dot @@ -1,17 +1,17 @@ digraph "classes_colorized" { rankdir=BT charset="utf-8" -"data.clientmodule_test.Ancestor" [color="aliceblue", fontcolor="black", label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value(value)\l}", shape="record", style="filled"]; -"data.suppliermodule_test.CustomException" [color="aliceblue", fontcolor="red", label="{CustomException|\l|}", shape="record", style="filled"]; -"data.suppliermodule_test.DoNothing" [color="aliceblue", fontcolor="black", label="{DoNothing|\l|}", shape="record", style="filled"]; -"data.suppliermodule_test.DoNothing2" [color="aliceblue", fontcolor="black", label="{DoNothing2|\l|}", shape="record", style="filled"]; -"data.suppliermodule_test.DoSomething" [color="aliceblue", fontcolor="black", label="{DoSomething|my_int : Optional[int]\lmy_int_2 : Optional[int]\lmy_string : str\l|do_it(new_int: int): int\l}", shape="record", style="filled"]; -"data.suppliermodule_test.Interface" [color="aliceblue", fontcolor="black", label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record", style="filled"]; -"data.property_pattern.PropertyPatterns" [color="aliceblue", fontcolor="black", label="{PropertyPatterns|prop1\lprop2\l|}", shape="record", style="filled"]; -"data.clientmodule_test.Specialization" [color="aliceblue", fontcolor="black", label="{Specialization|TYPE : str\lrelation\lrelation2\ltop : str\l|from_value(value: int)\lincrement_value(): None\ltransform_value(value: int): int\l}", shape="record", style="filled"]; +"data.clientmodule_test.Ancestor" [color="aliceblue", fontcolor="black", label=<{Ancestor|attr : str<br ALIGN="LEFT"/>cls_member<br ALIGN="LEFT"/>|get_value()<br ALIGN="LEFT"/>set_value(value)<br ALIGN="LEFT"/>}>, shape="record", style="filled"]; +"data.suppliermodule_test.CustomException" [color="aliceblue", fontcolor="red", label=<{CustomException|<br ALIGN="LEFT"/>|}>, shape="record", style="filled"]; +"data.suppliermodule_test.DoNothing" [color="aliceblue", fontcolor="black", label=<{DoNothing|<br ALIGN="LEFT"/>|}>, shape="record", style="filled"]; +"data.suppliermodule_test.DoNothing2" [color="aliceblue", fontcolor="black", label=<{DoNothing2|<br ALIGN="LEFT"/>|}>, shape="record", style="filled"]; +"data.suppliermodule_test.DoSomething" [color="aliceblue", fontcolor="black", label=<{DoSomething|my_int : Optional[int]<br ALIGN="LEFT"/>my_int_2 : Optional[int]<br ALIGN="LEFT"/>my_string : str<br ALIGN="LEFT"/>|do_it(new_int: int): int<br ALIGN="LEFT"/>}>, shape="record", style="filled"]; +"data.suppliermodule_test.Interface" [color="aliceblue", fontcolor="black", label=<{Interface|<br ALIGN="LEFT"/>|<I>get_value</I>()<br ALIGN="LEFT"/><I>set_value</I>(value)<br ALIGN="LEFT"/>}>, shape="record", style="filled"]; +"data.property_pattern.PropertyPatterns" [color="aliceblue", fontcolor="black", label=<{PropertyPatterns|prop1<br ALIGN="LEFT"/>prop2<br ALIGN="LEFT"/>|}>, shape="record", style="filled"]; +"data.clientmodule_test.Specialization" [color="aliceblue", fontcolor="black", label=<{Specialization|TYPE : str<br ALIGN="LEFT"/>relation<br ALIGN="LEFT"/>relation2<br ALIGN="LEFT"/>top : str<br ALIGN="LEFT"/>|from_value(value: int)<br ALIGN="LEFT"/>increment_value(): None<br ALIGN="LEFT"/>transform_value(value: int): int<br ALIGN="LEFT"/>}>, shape="record", style="filled"]; "data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"]; "data.clientmodule_test.Ancestor" -> "data.suppliermodule_test.Interface" [arrowhead="empty", arrowtail="node", style="dashed"]; "data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Ancestor" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"]; "data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"]; -"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"]; +"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="odiamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"]; } diff --git a/tests/pyreverse/data/classes_colorized.puml b/tests/pyreverse/data/classes_colorized.puml index 1226f7b4e..7398ee60f 100644 --- a/tests/pyreverse/data/classes_colorized.puml +++ b/tests/pyreverse/data/classes_colorized.puml @@ -19,8 +19,8 @@ class "DoSomething" as data.suppliermodule_test.DoSomething #aliceblue { do_it(new_int: int) -> int } class "Interface" as data.suppliermodule_test.Interface #aliceblue { - get_value() - set_value(value) + {abstract}get_value() + {abstract}set_value(value) } class "PropertyPatterns" as data.property_pattern.PropertyPatterns #aliceblue { prop1 @@ -39,5 +39,5 @@ data.clientmodule_test.Specialization --|> data.clientmodule_test.Ancestor data.clientmodule_test.Ancestor ..|> data.suppliermodule_test.Interface data.suppliermodule_test.DoNothing --* data.clientmodule_test.Ancestor : cls_member data.suppliermodule_test.DoNothing --* data.clientmodule_test.Specialization : relation -data.suppliermodule_test.DoNothing2 --* data.clientmodule_test.Specialization : relation2 +data.suppliermodule_test.DoNothing2 --o data.clientmodule_test.Specialization : relation2 @enduml diff --git a/tests/pyreverse/data/packages_No_Name.dot b/tests/pyreverse/data/packages_No_Name.dot index 461c8f9b4..5421c328c 100644 --- a/tests/pyreverse/data/packages_No_Name.dot +++ b/tests/pyreverse/data/packages_No_Name.dot @@ -1,9 +1,9 @@ digraph "packages_No_Name" { rankdir=BT charset="utf-8" -"data" [color="black", label="data", shape="box", style="solid"]; -"data.clientmodule_test" [color="black", label="data.clientmodule_test", shape="box", style="solid"]; -"data.property_pattern" [color="black", label="data.property_pattern", shape="box", style="solid"]; -"data.suppliermodule_test" [color="black", label="data.suppliermodule_test", shape="box", style="solid"]; +"data" [color="black", label=<data>, shape="box", style="solid"]; +"data.clientmodule_test" [color="black", label=<data.clientmodule_test>, shape="box", style="solid"]; +"data.property_pattern" [color="black", label=<data.property_pattern>, shape="box", style="solid"]; +"data.suppliermodule_test" [color="black", label=<data.suppliermodule_test>, shape="box", style="solid"]; "data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"]; } diff --git a/tests/pyreverse/data/packages_colorized.dot b/tests/pyreverse/data/packages_colorized.dot index 1a95d4c97..10005f26c 100644 --- a/tests/pyreverse/data/packages_colorized.dot +++ b/tests/pyreverse/data/packages_colorized.dot @@ -1,9 +1,9 @@ digraph "packages_colorized" { rankdir=BT charset="utf-8" -"data" [color="aliceblue", label="data", shape="box", style="filled"]; -"data.clientmodule_test" [color="aliceblue", label="data.clientmodule_test", shape="box", style="filled"]; -"data.property_pattern" [color="aliceblue", label="data.property_pattern", shape="box", style="filled"]; -"data.suppliermodule_test" [color="aliceblue", label="data.suppliermodule_test", shape="box", style="filled"]; +"data" [color="aliceblue", label=<data>, shape="box", style="filled"]; +"data.clientmodule_test" [color="aliceblue", label=<data.clientmodule_test>, shape="box", style="filled"]; +"data.property_pattern" [color="aliceblue", label=<data.property_pattern>, shape="box", style="filled"]; +"data.suppliermodule_test" [color="aliceblue", label=<data.suppliermodule_test>, shape="box", style="filled"]; "data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"]; } diff --git a/tests/pyreverse/functional/class_diagrams/annotations/attributes_annotation.dot b/tests/pyreverse/functional/class_diagrams/annotations/attributes_annotation.dot index e9b23699b..94c242ccf 100644 --- a/tests/pyreverse/functional/class_diagrams/annotations/attributes_annotation.dot +++ b/tests/pyreverse/functional/class_diagrams/annotations/attributes_annotation.dot @@ -1,6 +1,6 @@ digraph "classes" { rankdir=BT charset="utf-8" -"attributes_annotation.Dummy" [color="black", fontcolor="black", label="{Dummy|\l|}", shape="record", style="solid"]; -"attributes_annotation.Dummy2" [color="black", fontcolor="black", label="{Dummy2|alternative_union_syntax : str \| int\lclass_attr : list[Dummy]\loptional : Optional[Dummy]\lparam : str\lunion : Union[int, str]\l|}", shape="record", style="solid"]; +"attributes_annotation.Dummy" [color="black", fontcolor="black", label=<{Dummy|<br ALIGN="LEFT"/>|}>, shape="record", style="solid"]; +"attributes_annotation.Dummy2" [color="black", fontcolor="black", label=<{Dummy2|alternative_union_syntax : str \| int<br ALIGN="LEFT"/>class_attr : list[Dummy]<br ALIGN="LEFT"/>optional : Optional[Dummy]<br ALIGN="LEFT"/>param : str<br ALIGN="LEFT"/>union : Union[int, str]<br ALIGN="LEFT"/>|}>, shape="record", style="solid"]; } diff --git a/tests/pyreverse/test_diadefs.py b/tests/pyreverse/test_diadefs.py index 35dcd0e3a..da16eea33 100644 --- a/tests/pyreverse/test_diadefs.py +++ b/tests/pyreverse/test_diadefs.py @@ -9,7 +9,7 @@ from __future__ import annotations import sys -from collections.abc import Callable +from collections.abc import Iterator from pathlib import Path import pytest @@ -25,6 +25,7 @@ from pylint.pyreverse.diagrams import DiagramEntity, Relationship from pylint.pyreverse.inspector import Linker, Project from pylint.testutils.pyreverse import PyreverseConfig from pylint.testutils.utils import _test_cwd +from pylint.typing import GetProjectCallable HERE = Path(__file__) TESTS = HERE.parent.parent @@ -53,7 +54,7 @@ def HANDLER(default_config: PyreverseConfig) -> DiadefsHandler: @pytest.fixture(scope="module") -def PROJECT(get_project): +def PROJECT(get_project: GetProjectCallable) -> Iterator[Project]: with _test_cwd(TESTS): yield get_project("data") @@ -98,14 +99,13 @@ def test_default_values() -> None: class TestDefaultDiadefGenerator: _should_rels = [ + ("aggregation", "DoNothing2", "Specialization"), ("association", "DoNothing", "Ancestor"), ("association", "DoNothing", "Specialization"), - ("association", "DoNothing2", "Specialization"), ("implements", "Ancestor", "Interface"), ("specialization", "Specialization", "Ancestor"), ] - @pytest.mark.xfail def test_extract_relations(self, HANDLER: DiadefsHandler, PROJECT: Project) -> None: """Test extract_relations between classes.""" with pytest.warns(DeprecationWarning): @@ -114,9 +114,8 @@ class TestDefaultDiadefGenerator: relations = _process_relations(cd.relationships) assert relations == self._should_rels - @pytest.mark.xfail def test_functional_relation_extraction( - self, default_config: PyreverseConfig, get_project: Callable + self, default_config: PyreverseConfig, get_project: GetProjectCallable ) -> None: """Functional test of relations extraction; different classes possibly in different modules @@ -160,7 +159,9 @@ def test_known_values1(HANDLER: DiadefsHandler, PROJECT: Project) -> None: ] -def test_known_values2(HANDLER: DiadefsHandler, get_project: Callable) -> None: +def test_known_values2( + HANDLER: DiadefsHandler, get_project: GetProjectCallable +) -> None: project = get_project("data.clientmodule_test") dd = DefaultDiadefGenerator(Linker(project), HANDLER).visit(project) assert len(dd) == 1 @@ -205,7 +206,7 @@ def test_known_values4(HANDLER: DiadefsHandler, PROJECT: Project) -> None: @pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires dataclasses") def test_regression_dataclasses_inference( - HANDLER: DiadefsHandler, get_project: Callable + HANDLER: DiadefsHandler, get_project: GetProjectCallable ) -> None: project_path = Path("regrtest_data") / "dataclasses_pyreverse" path = get_project(str(project_path)) diff --git a/tests/pyreverse/test_diagrams.py b/tests/pyreverse/test_diagrams.py index b4a59a571..863bcecc9 100644 --- a/tests/pyreverse/test_diagrams.py +++ b/tests/pyreverse/test_diagrams.py @@ -6,15 +6,14 @@ from __future__ import annotations -from collections.abc import Callable - from pylint.pyreverse.diadefslib import DefaultDiadefGenerator, DiadefsHandler from pylint.pyreverse.inspector import Linker from pylint.testutils.pyreverse import PyreverseConfig +from pylint.typing import GetProjectCallable def test_property_handling( - default_config: PyreverseConfig, get_project: Callable + default_config: PyreverseConfig, get_project: GetProjectCallable ) -> None: project = get_project("data.property_pattern") class_diagram = DefaultDiadefGenerator( diff --git a/tests/pyreverse/test_inspector.py b/tests/pyreverse/test_inspector.py index 15f9d305a..00cad918f 100644 --- a/tests/pyreverse/test_inspector.py +++ b/tests/pyreverse/test_inspector.py @@ -9,7 +9,8 @@ from __future__ import annotations import os -from collections.abc import Callable, Generator +import warnings +from collections.abc import Generator from pathlib import Path import astroid @@ -19,17 +20,20 @@ from astroid import nodes from pylint.pyreverse import inspector from pylint.pyreverse.inspector import Project from pylint.testutils.utils import _test_cwd +from pylint.typing import GetProjectCallable HERE = Path(__file__) TESTS = HERE.parent.parent @pytest.fixture -def project(get_project: Callable) -> Generator[Project, None, None]: +def project(get_project: GetProjectCallable) -> Generator[Project, None, None]: with _test_cwd(TESTS): project = get_project("data", "data") linker = inspector.Linker(project) - linker.visit(project) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + linker.visit(project) yield project diff --git a/tests/pyreverse/test_main.py b/tests/pyreverse/test_main.py index 55c6ea1ec..4cc0573d6 100644 --- a/tests/pyreverse/test_main.py +++ b/tests/pyreverse/test_main.py @@ -14,6 +14,7 @@ from unittest import mock import pytest from _pytest.capture import CaptureFixture +from _pytest.fixtures import SubRequest from pylint.lint import fix_import_path from pylint.pyreverse import main @@ -23,13 +24,13 @@ PROJECT_ROOT_DIR = os.path.abspath(os.path.join(TEST_DATA_DIR, "..")) @pytest.fixture(name="mock_subprocess") -def mock_utils_subprocess(): +def mock_utils_subprocess() -> Iterator[mock.MagicMock]: with mock.patch("pylint.pyreverse.utils.subprocess") as mock_subprocess: yield mock_subprocess @pytest.fixture -def mock_graphviz(mock_subprocess): +def mock_graphviz(mock_subprocess: mock.MagicMock) -> Iterator[None]: mock_subprocess.run.return_value = mock.Mock( stderr=( 'Format: "XYZ" not recognized. Use one of: ' @@ -45,7 +46,7 @@ def mock_graphviz(mock_subprocess): @pytest.fixture(params=[PROJECT_ROOT_DIR, TEST_DATA_DIR]) -def setup_path(request) -> Iterator: +def setup_path(request: SubRequest) -> Iterator[None]: current_sys_path = list(sys.path) sys.path[:] = [] current_dir = os.getcwd() @@ -68,7 +69,9 @@ def test_project_root_in_sys_path() -> None: @mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock()) @mock.patch("pylint.pyreverse.main.writer") @pytest.mark.usefixtures("mock_graphviz") -def test_graphviz_supported_image_format(mock_writer, capsys: CaptureFixture) -> None: +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 @@ -88,7 +91,7 @@ def test_graphviz_supported_image_format(mock_writer, capsys: CaptureFixture) -> @mock.patch("pylint.pyreverse.main.writer") @pytest.mark.usefixtures("mock_graphviz") def test_graphviz_cant_determine_supported_formats( - mock_writer, mock_subprocess, capsys: CaptureFixture + mock_writer: mock.MagicMock, mock_subprocess: mock.MagicMock, capsys: CaptureFixture ) -> None: """Test that Graphviz is used if the image format is supported.""" mock_subprocess.run.return_value.stderr = "..." @@ -148,7 +151,7 @@ def test_graphviz_unsupported_image_format(capsys: CaptureFixture) -> 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]) + run = main.Run([TEST_DATA_DIR]) # type: ignore[var-annotated] assert getattr(run.config, arg) == expected_default @@ -175,7 +178,7 @@ def test_class_command( Make sure that we append multiple --class arguments to one option destination. """ - runner = main.Run( + runner = main.Run( # type: ignore[var-annotated] [ "--class", "data.clientmodule_test.Ancestor", diff --git a/tests/pyreverse/test_printer.py b/tests/pyreverse/test_printer.py index d6785940f..4248e8bae 100644 --- a/tests/pyreverse/test_printer.py +++ b/tests/pyreverse/test_printer.py @@ -39,7 +39,7 @@ def test_explicit_layout( "layout, printer_class", [(Layout.BOTTOM_TO_TOP, PlantUmlPrinter), (Layout.RIGHT_TO_LEFT, PlantUmlPrinter)], ) -def test_unsupported_layout(layout: Layout, printer_class: type[Printer]): +def test_unsupported_layout(layout: Layout, printer_class: type[Printer]) -> None: with pytest.raises(ValueError): printer_class(title="unittest", layout=layout) diff --git a/tests/pyreverse/test_printer_factory.py b/tests/pyreverse/test_printer_factory.py index 97ee1179c..76406f0a8 100644 --- a/tests/pyreverse/test_printer_factory.py +++ b/tests/pyreverse/test_printer_factory.py @@ -4,11 +4,14 @@ """Unit tests for pylint.pyreverse.printer_factory.""" +from __future__ import annotations + import pytest from pylint.pyreverse import printer_factory from pylint.pyreverse.dot_printer import DotPrinter from pylint.pyreverse.plantuml_printer import PlantUmlPrinter +from pylint.pyreverse.printer import Printer from pylint.pyreverse.vcg_printer import VCGPrinter @@ -22,5 +25,7 @@ from pylint.pyreverse.vcg_printer import VCGPrinter ("png", DotPrinter), ], ) -def test_get_printer_for_filetype(filetype, expected_printer_class): +def test_get_printer_for_filetype( + filetype: str, expected_printer_class: type[Printer] +) -> None: assert printer_factory.get_printer_for_filetype(filetype) == expected_printer_class diff --git a/tests/pyreverse/test_pyreverse_functional.py b/tests/pyreverse/test_pyreverse_functional.py index 2cc880d70..15fd1978b 100644 --- a/tests/pyreverse/test_pyreverse_functional.py +++ b/tests/pyreverse/test_pyreverse_functional.py @@ -5,7 +5,6 @@ from pathlib import Path import pytest -from py._path.local import LocalPath # type: ignore[import] from pylint.pyreverse.main import Run from pylint.testutils.pyreverse import ( @@ -23,17 +22,15 @@ CLASS_DIAGRAM_TEST_IDS = [testfile.source.stem for testfile in CLASS_DIAGRAM_TES CLASS_DIAGRAM_TESTS, ids=CLASS_DIAGRAM_TEST_IDS, ) -def test_class_diagrams( - testfile: FunctionalPyreverseTestfile, tmpdir: LocalPath -) -> None: +def test_class_diagrams(testfile: FunctionalPyreverseTestfile, tmp_path: Path) -> None: input_file = testfile.source for output_format in testfile.options["output_formats"]: with pytest.raises(SystemExit) as sys_exit: - args = ["-o", f"{output_format}", "-d", str(tmpdir)] + args = ["-o", f"{output_format}", "-d", str(tmp_path)] args.extend(testfile.options["command_line_args"]) args += [str(input_file)] Run(args) assert sys_exit.value.code == 0 assert testfile.source.with_suffix(f".{output_format}").read_text( encoding="utf8" - ) == Path(tmpdir / f"classes.{output_format}").read_text(encoding="utf8") + ) == (tmp_path / f"classes.{output_format}").read_text(encoding="utf8") diff --git a/tests/pyreverse/test_utils.py b/tests/pyreverse/test_utils.py index 70d95346f..d64bf4fa7 100644 --- a/tests/pyreverse/test_utils.py +++ b/tests/pyreverse/test_utils.py @@ -4,6 +4,8 @@ """Tests for pylint.pyreverse.utils.""" +from __future__ import annotations + from typing import Any from unittest.mock import patch @@ -31,7 +33,7 @@ from pylint.pyreverse.utils import ( ), ], ) -def test_get_visibility(names, expected): +def test_get_visibility(names: list[str], expected: str) -> None: for name in names: got = get_visibility(name) assert got == expected, f"got {got} instead of {expected} for value {name}" @@ -46,10 +48,12 @@ def test_get_visibility(names, expected): ("a: Optional[str] = None", "Optional[str]"), ], ) -def test_get_annotation_annassign(assign, label): +def test_get_annotation_annassign(assign: str, label: str) -> None: """AnnAssign.""" - node = astroid.extract_node(assign) - got = get_annotation(node.value).name + node: nodes.AnnAssign = astroid.extract_node(assign) + annotation = get_annotation(node.value) + assert annotation is not None + got = annotation.name assert isinstance(node, nodes.AnnAssign) assert got == label, f"got {got} instead of {label} for value {node}" @@ -65,7 +69,7 @@ def test_get_annotation_annassign(assign, label): ("def __init__(self, x: Optional[str] = 'str'): self.x = x", "Optional[str]"), ], ) -def test_get_annotation_assignattr(init_method, label): +def test_get_annotation_assignattr(init_method: str, label: str) -> None: """AssignAttr.""" assign = rf""" class A: @@ -75,7 +79,9 @@ def test_get_annotation_assignattr(init_method, label): instance_attrs = node.instance_attrs for assign_attrs in instance_attrs.values(): for assign_attr in assign_attrs: - got = get_annotation(assign_attr).name + annotation = get_annotation(assign_attr) + assert annotation is not None + got = annotation.name assert isinstance(assign_attr, nodes.AssignAttr) assert got == label, f"got {got} instead of {label} for value {node}" @@ -98,7 +104,7 @@ def test_get_annotation_label_of_return_type( @patch("pylint.pyreverse.utils.get_annotation") -@patch("astroid.node_classes.NodeNG.infer", side_effect=astroid.InferenceError) +@patch("astroid.nodes.NodeNG.infer", side_effect=astroid.InferenceError) def test_infer_node_1(mock_infer: Any, mock_get_annotation: Any) -> None: """Return set() when astroid.InferenceError is raised and an annotation has not been returned @@ -111,7 +117,7 @@ def test_infer_node_1(mock_infer: Any, mock_get_annotation: Any) -> None: @patch("pylint.pyreverse.utils.get_annotation") -@patch("astroid.node_classes.NodeNG.infer") +@patch("astroid.nodes.NodeNG.infer") def test_infer_node_2(mock_infer: Any, mock_get_annotation: Any) -> None: """Return set(node.infer()) when InferenceError is not raised and an annotation has not been returned diff --git a/tests/pyreverse/test_writer.py b/tests/pyreverse/test_writer.py index a03105092..805f8fab5 100644 --- a/tests/pyreverse/test_writer.py +++ b/tests/pyreverse/test_writer.py @@ -8,7 +8,7 @@ from __future__ import annotations import codecs import os -from collections.abc import Callable, Iterator +from collections.abc import Iterator from difflib import unified_diff from unittest.mock import Mock @@ -18,6 +18,7 @@ from pylint.pyreverse.diadefslib import DefaultDiadefGenerator, DiadefsHandler from pylint.pyreverse.inspector import Linker, Project from pylint.pyreverse.writer import DiagramWriter from pylint.testutils.pyreverse import PyreverseConfig +from pylint.typing import GetProjectCallable _DEFAULTS = { "all_ancestors": None, @@ -49,7 +50,7 @@ HTML_FILES = ["packages_No_Name.html", "classes_No_Name.html"] class Config: """Config object for tests.""" - def __init__(self): + def __init__(self) -> None: for attr, value in _DEFAULTS.items(): setattr(self, attr, value) @@ -69,7 +70,9 @@ def _file_lines(path: str) -> list[str]: @pytest.fixture() -def setup_dot(default_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_dot( + default_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(default_config) project = get_project(TEST_DATA_DIR) yield from _setup(project, default_config, writer) @@ -77,22 +80,26 @@ def setup_dot(default_config: PyreverseConfig, get_project: Callable) -> Iterato @pytest.fixture() def setup_colorized_dot( - colorized_dot_config: PyreverseConfig, get_project: Callable -) -> Iterator: + colorized_dot_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(colorized_dot_config) project = get_project(TEST_DATA_DIR, name="colorized") yield from _setup(project, colorized_dot_config, writer) @pytest.fixture() -def setup_vcg(vcg_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_vcg( + vcg_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(vcg_config) project = get_project(TEST_DATA_DIR) yield from _setup(project, vcg_config, writer) @pytest.fixture() -def setup_puml(puml_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_puml( + puml_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(puml_config) project = get_project(TEST_DATA_DIR) yield from _setup(project, puml_config, writer) @@ -100,15 +107,17 @@ def setup_puml(puml_config: PyreverseConfig, get_project: Callable) -> Iterator: @pytest.fixture() def setup_colorized_puml( - colorized_puml_config: PyreverseConfig, get_project: Callable -) -> Iterator: + colorized_puml_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(colorized_puml_config) project = get_project(TEST_DATA_DIR, name="colorized") yield from _setup(project, colorized_puml_config, writer) @pytest.fixture() -def setup_mmd(mmd_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_mmd( + mmd_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(mmd_config) project = get_project(TEST_DATA_DIR) @@ -116,7 +125,9 @@ def setup_mmd(mmd_config: PyreverseConfig, get_project: Callable) -> Iterator: @pytest.fixture() -def setup_html(html_config: PyreverseConfig, get_project: Callable) -> Iterator: +def setup_html( + html_config: PyreverseConfig, get_project: GetProjectCallable +) -> Iterator[None]: writer = DiagramWriter(html_config) project = get_project(TEST_DATA_DIR) @@ -125,7 +136,7 @@ def setup_html(html_config: PyreverseConfig, get_project: Callable) -> Iterator: def _setup( project: Project, config: PyreverseConfig, writer: DiagramWriter -) -> Iterator: +) -> Iterator[None]: linker = Linker(project) handler = DiadefsHandler(config) dd = DefaultDiadefGenerator(linker, handler).visit(project) diff --git a/tests/regrtest_data/imported_module_in_typehint/module_a.py b/tests/regrtest_data/imported_module_in_typehint/module_a.py new file mode 100644 index 000000000..d9754eca4 --- /dev/null +++ b/tests/regrtest_data/imported_module_in_typehint/module_a.py @@ -0,0 +1,5 @@ +import uuid +from typing import Optional + + +ID = None # type: Optional[uuid.UUID] diff --git a/tests/regrtest_data/imported_module_in_typehint/module_b.py b/tests/regrtest_data/imported_module_in_typehint/module_b.py new file mode 100644 index 000000000..4ab5fc595 --- /dev/null +++ b/tests/regrtest_data/imported_module_in_typehint/module_b.py @@ -0,0 +1 @@ +import uuid diff --git a/tests/reporters/unittest_json_reporter.py b/tests/reporters/unittest_json_reporter.py index 90a67fceb..9104016ea 100644 --- a/tests/reporters/unittest_json_reporter.py +++ b/tests/reporters/unittest_json_reporter.py @@ -102,7 +102,7 @@ def get_linter_result(score: bool, message: dict[str, Any]) -> list[dict[str, An reporter.display_reports(EvaluationSection(expected_score_message)) reporter.display_messages(None) report_result = json.loads(output.getvalue()) - return report_result + return report_result # type: ignore[no-any-return] @pytest.mark.parametrize( @@ -131,7 +131,7 @@ def get_linter_result(score: bool, message: dict[str, Any]) -> list[dict[str, An ) ], ) -def test_serialize_deserialize(message): +def test_serialize_deserialize(message: Message) -> None: # TODO: 3.0: Add confidence handling, add path and abs path handling or a new JSONReporter json_message = JSONReporter.serialize(message) assert message == JSONReporter.deserialize(json_message) diff --git a/tests/reporters/unittest_reporting.py b/tests/reporters/unittest_reporting.py index 37f3e5fd9..7b8139119 100644 --- a/tests/reporters/unittest_reporting.py +++ b/tests/reporters/unittest_reporting.py @@ -11,9 +11,11 @@ import warnings from contextlib import redirect_stdout from io import StringIO from json import dumps -from typing import TYPE_CHECKING +from pathlib import Path +from typing import TYPE_CHECKING, TextIO import pytest +from _pytest.recwarn import WarningsRecorder from pylint import checkers from pylint.interfaces import HIGH @@ -28,16 +30,16 @@ if TYPE_CHECKING: @pytest.fixture(scope="module") -def reporter(): +def reporter() -> type[TextReporter]: return TextReporter @pytest.fixture(scope="module") -def disable(): +def disable() -> list[str]: return ["I"] -def test_template_option(linter): +def test_template_option(linter: PyLinter) -> None: output = StringIO() linter.reporter.out = output linter.config.msg_template = "{msg_id}:{line:03d}" @@ -48,7 +50,7 @@ def test_template_option(linter): assert output.getvalue() == "************* Module 0123\nC0301:001\nC0301:002\n" -def test_template_option_default(linter) -> None: +def test_template_option_default(linter: PyLinter) -> None: """Test the default msg-template setting.""" output = StringIO() linter.reporter.out = output @@ -62,7 +64,7 @@ def test_template_option_default(linter) -> None: assert out_lines[2] == "my_module:2:0: C0301: Line too long (3/4) (line-too-long)" -def test_template_option_end_line(linter) -> None: +def test_template_option_end_line(linter: PyLinter) -> None: """Test the msg-template option with end_line and end_column.""" output = StringIO() linter.reporter.out = output @@ -81,7 +83,7 @@ def test_template_option_end_line(linter) -> None: assert out_lines[2] == "my_mod:2:0:2:4: C0301: Line too long (3/4) (line-too-long)" -def test_template_option_non_existing(linter) -> None: +def test_template_option_non_existing(linter: PyLinter) -> None: """Test the msg-template option with non-existent options. This makes sure that this option remains backwards compatible as new parameters do not break on previous versions @@ -113,7 +115,7 @@ def test_template_option_non_existing(linter) -> None: assert out_lines[2] == "my_mod:2::()" -def test_deprecation_set_output(recwarn): +def test_deprecation_set_output(recwarn: WarningsRecorder) -> None: """TODO remove in 3.0.""" reporter = BaseReporter() # noinspection PyDeprecation @@ -123,7 +125,7 @@ def test_deprecation_set_output(recwarn): assert reporter.out == sys.stdout -def test_parseable_output_deprecated(): +def test_parseable_output_deprecated() -> None: with warnings.catch_warnings(record=True) as cm: warnings.simplefilter("always") ParseableTextReporter() @@ -132,9 +134,10 @@ def test_parseable_output_deprecated(): assert isinstance(cm[0].message, DeprecationWarning) -def test_parseable_output_regression(): +def test_parseable_output_regression() -> None: output = StringIO() with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", category=DeprecationWarning) linter = PyLinter(reporter=ParseableTextReporter()) checkers.initialize(linter) @@ -155,18 +158,18 @@ class NopReporter(BaseReporter): name = "nop-reporter" extension = "" - def __init__(self, output=None): + def __init__(self, output: TextIO | None = None) -> None: super().__init__(output) print("A NopReporter was initialized.", file=self.out) - def writeln(self, string=""): + def writeln(self, string: str = "") -> None: pass def _display(self, layout: Section) -> None: pass -def test_multi_format_output(tmp_path): +def test_multi_format_output(tmp_path: Path) -> None: text = StringIO(newline=None) json = tmp_path / "somefile.json" @@ -191,12 +194,15 @@ def test_multi_format_output(tmp_path): linter.reporter.out = text linter.open() - linter.check_single_file_item(FileItem("somemodule", source_file, "somemodule")) + linter.check_single_file_item( + FileItem("somemodule", str(source_file), "somemodule") + ) linter.add_message("line-too-long", line=1, args=(1, 2)) linter.generate_reports() linter.reporter.writeln("direct output") # Ensure the output files are flushed and closed + assert isinstance(linter.reporter, MultiReporter) linter.reporter.close_output_files() del linter.reporter diff --git a/tests/test_check_parallel.py b/tests/test_check_parallel.py index a7fb5c158..15b59304c 100644 --- a/tests/test_check_parallel.py +++ b/tests/test_check_parallel.py @@ -110,7 +110,7 @@ class ParallelTestChecker(BaseRawFileChecker): for _ in self.data[1::2]: # Work on pairs of files, see class docstring. self.add_message("R9999", args=("From process_module, two files seen.",)) - def get_map_data(self): + def get_map_data(self) -> list[str]: return self.data def reduce_map_data(self, linter: PyLinter, data: list[list[str]]) -> None: @@ -161,10 +161,10 @@ class ThirdParallelTestChecker(ParallelTestChecker): class TestCheckParallelFramework: """Tests the check_parallel() function's framework.""" - def setup_class(self): + def setup_class(self) -> None: self._prev_global_linter = pylint.lint.parallel._worker_linter - def teardown_class(self): + def teardown_class(self) -> None: pylint.lint.parallel._worker_linter = self._prev_global_linter def test_worker_initialize(self) -> None: @@ -190,7 +190,7 @@ class TestCheckParallelFramework: def test_worker_check_single_file_uninitialised(self) -> None: pylint.lint.parallel._worker_linter = None with pytest.raises( # Objects that do not match the linter interface will fail - Exception, match="Worker linter not yet initialised" + RuntimeError, match="Worker linter not yet initialised" ): worker_check_single_file(_gen_file_data()) @@ -239,7 +239,9 @@ class TestCheckParallelFramework: linter.load_plugin_modules(["pylint.extensions.overlapping_exceptions"]) try: dill.dumps(linter) - assert False, "Plugins loaded were pickle-safe! This test needs altering" + raise AssertionError( + "Plugins loaded were pickle-safe! This test needs altering" + ) except (KeyError, TypeError, PickleError, NotImplementedError): pass @@ -247,8 +249,10 @@ class TestCheckParallelFramework: linter.load_plugin_configuration() try: dill.dumps(linter) - except KeyError: - assert False, "Cannot pickle linter when using non-pickleable plugin" + except KeyError as exc: + raise AssertionError( + "Cannot pickle linter when using non-pickleable plugin" + ) from exc def test_worker_check_sequential_checker(self) -> None: """Same as test_worker_check_single_file_no_checkers with SequentialTestChecker.""" @@ -430,7 +434,9 @@ class TestCheckParallel: (10, 2, 3), ], ) - def test_compare_workers_to_single_proc(self, num_files, num_jobs, num_checkers): + def test_compare_workers_to_single_proc( + self, num_files: int, num_jobs: int, num_checkers: int + ) -> None: """Compares the 3 key parameters for check_parallel() produces the same results. The intent here is to ensure that the check_parallel() operates on each file, @@ -527,7 +533,7 @@ class TestCheckParallel: (10, 2, 3), ], ) - def test_map_reduce(self, num_files, num_jobs, num_checkers): + def test_map_reduce(self, num_files: int, num_jobs: int, num_checkers: int) -> None: """Compares the 3 key parameters for check_parallel() produces the same results. The intent here is to validate the reduce step: no stats should be lost. diff --git a/tests/test_epylint.py b/tests/test_epylint.py index e1b090395..7e9116e99 100644 --- a/tests/test_epylint.py +++ b/tests/test_epylint.py @@ -25,12 +25,12 @@ def example_path(tmp_path: PosixPath) -> PosixPath: def test_epylint_good_command(example_path: PosixPath) -> None: - out, err = lint.py_run( - # pylint: disable-next=consider-using-f-string - "%s -E --disable=E1111 --msg-template '{category} {module} {obj} {line} {column} {msg}'" - % example_path, - return_std=True, - ) + with pytest.warns(DeprecationWarning): + out, _ = lint.py_run( + f"{example_path} -E --disable=E1111 --msg-template " + "'{category} {module} {obj} {line} {column} {msg}'", + return_std=True, + ) msg = out.read() assert ( msg @@ -39,16 +39,16 @@ def test_epylint_good_command(example_path: PosixPath) -> None: error my_app IvrAudioApp.run 4 8 Instance of 'IvrAudioApp' has no 'hassan' member """ ) - assert err.read() == "" def test_epylint_strange_command(example_path: PosixPath) -> None: - out, err = lint.py_run( - # pylint: disable-next=consider-using-f-string - "%s -E --disable=E1111 --msg-template={category} {module} {obj} {line} {column} {msg}" - % example_path, - return_std=True, - ) + with pytest.warns(DeprecationWarning): + out, _ = lint.py_run( + # pylint: disable-next=consider-using-f-string + "%s -E --disable=E1111 --msg-template={category} {module} {obj} {line} {column} {msg}" + % example_path, + return_std=True, + ) assert ( out.read() == """\ @@ -66,4 +66,3 @@ def test_epylint_strange_command(example_path: PosixPath) -> None: error """ ) - assert err.read() == "" diff --git a/tests/test_func.py b/tests/test_func.py index 23f5ff102..493489aee 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -25,11 +25,13 @@ FILTER_RGX = None INFO_TEST_RGX = re.compile(r"^func_i\d\d\d\d$") -def exception_str(self, ex) -> str: # pylint: disable=unused-argument +def exception_str( + self: Exception, ex: Exception # pylint: disable=unused-argument +) -> str: """Function used to replace default __str__ method of exception instances This function is not typed because it is legacy code """ - return f"in {ex.file}\n:: {', '.join(ex.args)}" + return f"in {ex.file}\n:: {', '.join(ex.args)}" # type: ignore[attr-defined] # Defined in the caller class LintTestUsingModule: @@ -92,7 +94,7 @@ class LintTestUsingModule: class LintTestUpdate(LintTestUsingModule): - def _check_result(self, got): + def _check_result(self, got: str) -> None: if not self._has_output(): return try: @@ -100,18 +102,20 @@ class LintTestUpdate(LintTestUsingModule): except OSError: expected = "" if got != expected: - with open(self.output, "w", encoding="utf-8") as f: + with open(self.output or "", "w", encoding="utf-8") as f: f.write(got) -def gen_tests(filter_rgx): +def gen_tests( + filter_rgx: str | re.Pattern[str] | None, +) -> list[tuple[str, str, list[tuple[str, str]]]]: if filter_rgx: is_to_run = re.compile(filter_rgx).search else: is_to_run = ( - lambda x: 1 # pylint: disable=unnecessary-lambda-assignment + lambda x: 1 # type: ignore[assignment,misc] # pylint: disable=unnecessary-lambda-assignment ) # noqa: E731 We're going to throw all this anyway - tests = [] + tests: list[tuple[str, str, list[tuple[str, str]]]] = [] for module_file, messages_file in _get_tests_info(INPUT_DIR, MSG_DIR, "func_", ""): if not is_to_run(module_file) or module_file.endswith((".pyc", "$py.class")): continue @@ -135,7 +139,10 @@ TEST_WITH_EXPECTED_DEPRECATION = ["func_excess_escapes.py"] ids=[o[0] for o in gen_tests(FILTER_RGX)], ) def test_functionality( - module_file, messages_file, dependencies, recwarn: pytest.WarningsRecorder + module_file: str, + messages_file: str, + dependencies: list[tuple[str, str]], + recwarn: pytest.WarningsRecorder, ) -> None: __test_functionality(module_file, messages_file, dependencies) if recwarn.list: diff --git a/tests/test_functional.py b/tests/test_functional.py index 74b541bcf..77cdbc58f 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -11,7 +11,6 @@ from pathlib import Path import pytest from _pytest.config import Config -from _pytest.recwarn import WarningsRecorder from pylint import testutils from pylint.testutils import UPDATE_FILE, UPDATE_OPTION @@ -33,34 +32,28 @@ TESTS = [ ] TESTS_NAMES = [t.base for t in TESTS] TEST_WITH_EXPECTED_DEPRECATION = [ + "anomalous_backslash_escape", + "anomalous_unicode_escape", + "excess_escapes", "future_unicode_literals", - "anomalous_unicode_escape_py3", ] @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES) -def test_functional( - test_file: FunctionalTestFile, recwarn: WarningsRecorder, pytestconfig: Config -) -> None: +def test_functional(test_file: FunctionalTestFile, pytestconfig: Config) -> None: __tracebackhide__ = True # pylint: disable=unused-variable + lint_test: LintModuleOutputUpdate | testutils.LintModuleTest if UPDATE_FILE.exists(): - lint_test: ( - LintModuleOutputUpdate | testutils.LintModuleTest - ) = LintModuleOutputUpdate(test_file, pytestconfig) + lint_test = LintModuleOutputUpdate(test_file, pytestconfig) else: lint_test = testutils.LintModuleTest(test_file, pytestconfig) lint_test.setUp() - lint_test.runTest() - if recwarn.list: - if ( - test_file.base in TEST_WITH_EXPECTED_DEPRECATION - and sys.version_info.minor > 5 - ): - assert any( - "invalid escape sequence" in str(i.message) - for i in recwarn.list - if issubclass(i.category, DeprecationWarning) - ) + + if test_file.base in TEST_WITH_EXPECTED_DEPRECATION: + with pytest.warns(DeprecationWarning, match="invalid escape sequence"): + lint_test.runTest() + else: + lint_test.runTest() if __name__ == "__main__": diff --git a/tests/test_import_graph.py b/tests/test_import_graph.py index a05ebbd71..2ad51f889 100644 --- a/tests/test_import_graph.py +++ b/tests/test_import_graph.py @@ -20,7 +20,7 @@ from pylint.lint import PyLinter @pytest.fixture -def dest(request: SubRequest) -> Iterator[Iterator | Iterator[str]]: +def dest(request: SubRequest) -> Iterator[str]: dest = request.param yield dest try: @@ -74,7 +74,7 @@ def linter() -> PyLinter: @pytest.fixture -def remove_files() -> Iterator: +def remove_files() -> Iterator[None]: yield for fname in ("import.dot", "ext_import.dot", "int_import.dot"): try: diff --git a/tests/test_numversion.py b/tests/test_numversion.py index 1bfb451da..2c34c1aa3 100644 --- a/tests/test_numversion.py +++ b/tests/test_numversion.py @@ -2,6 +2,8 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +from __future__ import annotations + import pytest from pylint.__pkginfo__ import get_numversion_from_version @@ -23,5 +25,5 @@ from pylint.__pkginfo__ import get_numversion_from_version ["2.8.3.dev3+g28c093c2.d20210428", (2, 8, 3)], ], ) -def test_numversion(version, expected_numversion): +def test_numversion(version: str, expected_numversion: tuple[int, int, int]) -> None: assert get_numversion_from_version(version) == expected_numversion diff --git a/tests/test_pylint_runners.py b/tests/test_pylint_runners.py index c83f3e627..06a16c3a5 100644 --- a/tests/test_pylint_runners.py +++ b/tests/test_pylint_runners.py @@ -10,47 +10,75 @@ import os import pathlib import shlex import sys -from collections.abc import Callable +from collections.abc import Sequence from io import BufferedReader -from typing import Any +from typing import Any, NoReturn from unittest.mock import MagicMock, mock_open, patch import pytest -from py._path.local import LocalPath # type: ignore[import] from pylint import run_epylint, run_pylint, run_pyreverse, run_symilar from pylint.lint import Run from pylint.testutils import GenericTestReporter as Reporter +from pylint.testutils.utils import _test_cwd +if sys.version_info >= (3, 8): + from typing import Protocol +else: + from typing_extensions import Protocol -@pytest.mark.parametrize( - "runner", [run_epylint, run_pylint, run_pyreverse, run_symilar] -) -def test_runner(runner: Callable, tmpdir: LocalPath) -> None: + +class _RunCallable(Protocol): # pylint: disable=too-few-public-methods + def __call__(self, argv: Sequence[str] | None = None) -> NoReturn | None: + ... + + +@pytest.mark.parametrize("runner", [run_pylint, run_pyreverse, run_symilar]) +def test_runner(runner: _RunCallable, tmp_path: pathlib.Path) -> None: filepath = os.path.abspath(__file__) testargs = ["", filepath] - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): with patch.object(sys, "argv", testargs): with pytest.raises(SystemExit) as err: runner() assert err.value.code == 0 -@pytest.mark.parametrize( - "runner", [run_epylint, run_pylint, run_pyreverse, run_symilar] -) -def test_runner_with_arguments(runner: Callable, tmpdir: LocalPath) -> None: +def test_epylint(tmp_path: pathlib.Path) -> None: + """TODO: 3.0 delete with epylint.""" + filepath = os.path.abspath(__file__) + with _test_cwd(tmp_path): + with patch.object(sys, "argv", ["", filepath]): + with pytest.raises(SystemExit) as err: + with pytest.warns(DeprecationWarning): + run_epylint() + assert err.value.code == 0 + + +@pytest.mark.parametrize("runner", [run_pylint, run_pyreverse, run_symilar]) +def test_runner_with_arguments(runner: _RunCallable, tmp_path: pathlib.Path) -> None: """Check the runners with arguments as parameter instead of sys.argv.""" filepath = os.path.abspath(__file__) testargs = [filepath] - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): with pytest.raises(SystemExit) as err: runner(testargs) assert err.value.code == 0 +def test_epylint_with_arguments(tmp_path: pathlib.Path) -> None: + """TODO: 3.0 delete with epylint.""" + filepath = os.path.abspath(__file__) + testargs = [filepath] + with _test_cwd(tmp_path): + with pytest.raises(SystemExit) as err: + with pytest.warns(DeprecationWarning): + run_epylint(testargs) + assert err.value.code == 0 + + def test_pylint_argument_deduplication( - tmpdir: LocalPath, tests_directory: pathlib.Path + tmp_path: pathlib.Path, tests_directory: pathlib.Path ) -> None: """Check that the Pylint runner does not over-report on duplicate arguments. @@ -62,7 +90,7 @@ def test_pylint_argument_deduplication( testargs = shlex.split("--report n --score n --max-branches 13") testargs.extend([filepath] * 4) exit_stack = contextlib.ExitStack() - exit_stack.enter_context(tmpdir.as_cwd()) + exit_stack.enter_context(_test_cwd(tmp_path)) exit_stack.enter_context(patch.object(sys, "argv", testargs)) err = exit_stack.enter_context(pytest.raises(SystemExit)) with exit_stack: @@ -71,7 +99,7 @@ def test_pylint_argument_deduplication( def test_pylint_run_jobs_equal_zero_dont_crash_with_cpu_fraction( - tmpdir: LocalPath, + tmp_path: pathlib.Path, ) -> None: """Check that the pylint runner does not crash if `pylint.lint.run._query_cpu` determines only a fraction of a CPU core to be available. @@ -80,21 +108,21 @@ def test_pylint_run_jobs_equal_zero_dont_crash_with_cpu_fraction( def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader: if args[0] == "/sys/fs/cgroup/cpu/cpu.cfs_quota_us": - return mock_open(read_data=b"-1")(*args, **kwargs) + return mock_open(read_data=b"-1")(*args, **kwargs) # type: ignore[no-any-return] if args[0] == "/sys/fs/cgroup/cpu/cpu.shares": - return mock_open(read_data=b"2")(*args, **kwargs) - return builtin_open(*args, **kwargs) + return mock_open(read_data=b"2")(*args, **kwargs) # type: ignore[no-any-return] + return builtin_open(*args, **kwargs) # type: ignore[no-any-return] pathlib_path = pathlib.Path - def _mock_path(*args: str, **kwargs) -> pathlib.Path: + def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path: if args[0] == "/sys/fs/cgroup/cpu/cpu.shares": return MagicMock(is_file=lambda: True) return pathlib_path(*args, **kwargs) filepath = os.path.abspath(__file__) testargs = [filepath, "--jobs=0"] - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): with pytest.raises(SystemExit) as err: with patch("builtins.open", _mock_open): with patch("pylint.lint.run.Path", _mock_path): diff --git a/tests/test_regr.py b/tests/test_regr.py index 80492ae78..eb8ad6c5d 100644 --- a/tests/test_regr.py +++ b/tests/test_regr.py @@ -27,12 +27,12 @@ sys.path.insert(1, REGR_DATA) @pytest.fixture(scope="module") -def reporter(): +def reporter() -> type[testutils.GenericTestReporter]: return testutils.GenericTestReporter @pytest.fixture(scope="module") -def disable(): +def disable() -> list[str]: return ["I"] @@ -48,7 +48,7 @@ def finalize_linter(linter: PyLinter) -> Iterator[PyLinter]: linter.reporter.finalize() -def Equals(expected): +def Equals(expected: str) -> Callable[[str], bool]: return lambda got: got == expected @@ -67,7 +67,7 @@ def Equals(expected): ], ) def test_package( - finalize_linter: PyLinter, file_names: list[str], check: Callable + finalize_linter: PyLinter, file_names: list[str], check: Callable[[str], bool] ) -> None: finalize_linter.check(file_names) finalize_linter.reporter = cast( # Due to fixture @@ -101,7 +101,7 @@ def test_descriptor_crash(fname: str, finalize_linter: PyLinter) -> None: @pytest.fixture -def modify_path() -> Iterator: +def modify_path() -> Iterator[None]: cwd = os.getcwd() sys.path.insert(0, "") yield diff --git a/tests/test_self.py b/tests/test_self.py index 010e60682..587b8bb58 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -27,14 +27,13 @@ from unittest import mock from unittest.mock import patch import pytest -from py._path.local import LocalPath # type: ignore[import] from pylint import extensions, modify_sys_path from pylint.constants import MAIN_CHECKER_NAME, MSG_TYPES_STATUS from pylint.lint.pylinter import PyLinter from pylint.message import Message -from pylint.reporters import JSONReporter -from pylint.reporters.text import BaseReporter, ColorizedTextReporter, TextReporter +from pylint.reporters import BaseReporter, JSONReporter +from pylint.reporters.text import ColorizedTextReporter, TextReporter from pylint.testutils._run import _add_rcfile_default_pylintrc from pylint.testutils._run import _Run as Run from pylint.testutils.utils import ( @@ -62,7 +61,7 @@ UNNECESSARY_LAMBDA = join( @contextlib.contextmanager -def _configure_lc_ctype(lc_ctype: str) -> Iterator: +def _configure_lc_ctype(lc_ctype: str) -> Iterator[None]: lc_ctype_env = "LC_CTYPE" original_lctype = os.environ.get(lc_ctype_env) os.environ[lc_ctype_env] = lc_ctype @@ -98,8 +97,8 @@ class MultiReporter(BaseReporter): def out(self) -> TextIO: # type: ignore[override] return self._reporters[0].out - @property # type: ignore[override] - def linter(self) -> PyLinter: # type: ignore[override] + @property + def linter(self) -> PyLinter: return self._linter @linter.setter @@ -140,7 +139,7 @@ class TestRunTC: with warnings.catch_warnings(): warnings.simplefilter("ignore") Run(args, reporter=reporter) - return cm.value.code + return int(cm.value.code) @staticmethod def _clean_paths(output: str) -> str: @@ -162,7 +161,7 @@ class TestRunTC: assert unexpected_output.strip() not in actual_output.strip() def _test_output_file( - self, args: list[str], filename: LocalPath, expected_output: str + self, args: list[str], filename: Path, expected_output: str ) -> None: """Run Pylint with the ``output`` option set (must be included in the ``args`` passed to this method!) and check the file content afterwards. @@ -297,6 +296,39 @@ class TestRunTC: actual_output = actual_output[actual_output.find("\n") :] assert self._clean_paths(expected_output.strip()) == actual_output.strip() + def test_type_annotation_names(self) -> None: + """Test resetting the `_type_annotation_names` list to `[]` when leaving a module. + + An import inside `module_a`, which is used as a type annotation in `module_a`, should not prevent + emitting the `unused-import` message when the same import occurs in `module_b` & is unused. + See: https://github.com/PyCQA/pylint/issues/4150 + """ + module1 = join( + HERE, "regrtest_data", "imported_module_in_typehint", "module_a.py" + ) + + module2 = join( + HERE, "regrtest_data", "imported_module_in_typehint", "module_b.py" + ) + expected_output = textwrap.dedent( + f""" + ************* Module module_b + {module2}:1:0: W0611: Unused import uuid (unused-import) + """ + ) + args = [ + module1, + module2, + "--disable=all", + "--enable=unused-import", + "-rn", + "-sn", + ] + out = StringIO() + self._run_pylint(args, out=out) + actual_output = self._clean_paths(out.getvalue().strip()) + assert self._clean_paths(expected_output.strip()) in actual_output.strip() + def test_import_itself_not_accounted_for_relative_imports(self) -> None: expected = "Your code has been rated at 10.00/10" package = join(HERE, "regrtest_data", "dummy") @@ -477,7 +509,7 @@ class TestRunTC: self._test_output([module, "-E"], expected_output=expected_output) @pytest.mark.skipif(sys.platform == "win32", reason="only occurs on *nix") - def test_parseable_file_path(self): + def test_parseable_file_path(self) -> None: file_name = "test_target.py" fake_path = HERE + os.getcwd() module = join(fake_path, file_name) @@ -503,7 +535,7 @@ class TestRunTC: ("mymodule.py", "mymodule", "mymodule.py"), ], ) - def test_stdin(self, input_path, module, expected_path): + def test_stdin(self, input_path: str, module: str, expected_path: str) -> None: expected_output = f"""************* Module {module} {expected_path}:1:0: W0611: Unused import os (unused-import) @@ -522,8 +554,8 @@ class TestRunTC: self._runtest(["--from-stdin"], code=32) @pytest.mark.parametrize("write_bpy_to_disk", [False, True]) - def test_relative_imports(self, write_bpy_to_disk, tmpdir): - a = tmpdir.join("a") + def test_relative_imports(self, write_bpy_to_disk: bool, tmp_path: Path) -> None: + a = tmp_path / "a" b_code = textwrap.dedent( """ @@ -543,12 +575,12 @@ class TestRunTC: ) a.mkdir() - a.join("__init__.py").write("") + (a / "__init__.py").write_text("") if write_bpy_to_disk: - a.join("b.py").write(b_code) - a.join("c.py").write(c_code) + (a / "b.py").write_text(b_code) + (a / "c.py").write_text(c_code) - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): # why don't we start pylint in a sub-process? expected = ( "************* Module a.b\n" @@ -696,14 +728,14 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er (-9, "missing-function-docstring", "fail_under_minus10.py", 22), (-5, "missing-function-docstring", "fail_under_minus10.py", 22), # --fail-under should guide whether error code as missing-function-docstring is not hit - (-10, "broad-except", "fail_under_plus7_5.py", 0), - (6, "broad-except", "fail_under_plus7_5.py", 0), - (7.5, "broad-except", "fail_under_plus7_5.py", 0), - (7.6, "broad-except", "fail_under_plus7_5.py", 16), - (-11, "broad-except", "fail_under_minus10.py", 0), - (-10, "broad-except", "fail_under_minus10.py", 0), - (-9, "broad-except", "fail_under_minus10.py", 22), - (-5, "broad-except", "fail_under_minus10.py", 22), + (-10, "broad-exception-caught", "fail_under_plus7_5.py", 0), + (6, "broad-exception-caught", "fail_under_plus7_5.py", 0), + (7.5, "broad-exception-caught", "fail_under_plus7_5.py", 0), + (7.6, "broad-exception-caught", "fail_under_plus7_5.py", 16), + (-11, "broad-exception-caught", "fail_under_minus10.py", 0), + (-10, "broad-exception-caught", "fail_under_minus10.py", 0), + (-9, "broad-exception-caught", "fail_under_minus10.py", 22), + (-5, "broad-exception-caught", "fail_under_minus10.py", 22), # Enable by message id (-10, "C0116", "fail_under_plus7_5.py", 16), # Enable by category @@ -713,7 +745,7 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er (-10, "C0115", "fail_under_plus7_5.py", 0), ], ) - def test_fail_on(self, fu_score, fo_msgs, fname, out): + def test_fail_on(self, fu_score: int, fo_msgs: str, fname: str, out: int) -> None: self._runtest( [ "--fail-under", @@ -741,7 +773,7 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er (["--fail-on=C0116", "--disable=C0116"], 16), ], ) - def test_fail_on_edge_case(self, opts, out): + def test_fail_on_edge_case(self, opts: list[str], out: int) -> None: self._runtest( opts + [join(HERE, "regrtest_data", "fail_under_plus7_5.py")], code=out, @@ -847,34 +879,34 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er ], ) def test_do_not_import_files_from_local_directory( - self, tmpdir: LocalPath, args: list[str] + self, tmp_path: Path, args: list[str] ) -> None: for path in ("astroid.py", "hmac.py"): - file_path = tmpdir / path - file_path.write("'Docstring'\nimport completely_unknown\n") + file_path = tmp_path / path + file_path.write_text("'Docstring'\nimport completely_unknown\n") pylint_call = [sys.executable, "-m", "pylint"] + args + [path] - with tmpdir.as_cwd(): - subprocess.check_output(pylint_call, cwd=str(tmpdir)) + with _test_cwd(tmp_path): + subprocess.check_output(pylint_call, cwd=str(tmp_path)) new_python_path = os.environ.get("PYTHONPATH", "").strip(":") - with tmpdir.as_cwd(), _test_environ_pythonpath(f"{new_python_path}:"): + with _test_cwd(tmp_path), _test_environ_pythonpath(f"{new_python_path}:"): # Appending a colon to PYTHONPATH should not break path stripping # https://github.com/PyCQA/pylint/issues/3636 - subprocess.check_output(pylint_call, cwd=str(tmpdir)) + subprocess.check_output(pylint_call, cwd=str(tmp_path)) @staticmethod def test_import_plugin_from_local_directory_if_pythonpath_cwd( - tmpdir: LocalPath, + tmp_path: Path, ) -> None: - p_plugin = tmpdir / "plugin.py" - p_plugin.write("# Some plugin content") + p_plugin = tmp_path / "plugin.py" + p_plugin.write_text("# Some plugin content") if sys.platform == "win32": python_path = "." else: python_path = f"{os.environ.get('PYTHONPATH', '').strip(':')}:." - with tmpdir.as_cwd(), _test_environ_pythonpath(python_path): + with _test_cwd(tmp_path), _test_environ_pythonpath(python_path): args = [sys.executable, "-m", "pylint", "--load-plugins", "plugin"] process = subprocess.run( - args, cwd=str(tmpdir), stderr=subprocess.PIPE, check=False + args, cwd=str(tmp_path), stderr=subprocess.PIPE, check=False ) assert ( "AttributeError: module 'plugin' has no attribute 'register'" @@ -882,18 +914,18 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er ) def test_allow_import_of_files_found_in_modules_during_parallel_check( - self, tmpdir: LocalPath + self, tmp_path: Path ) -> None: - test_directory = tmpdir / "test_directory" + test_directory = tmp_path / "test_directory" test_directory.mkdir() spam_module = test_directory / "spam.py" - spam_module.write("'Empty'") + spam_module.write_text("'Empty'") init_module = test_directory / "__init__.py" - init_module.write("'Empty'") + init_module.write_text("'Empty'") # For multiple jobs we could not find the `spam.py` file. - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): args = [ "-j2", "--disable=missing-docstring, missing-final-newline", @@ -902,7 +934,7 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er self._runtest(args, code=0) # A single job should be fine as well - with tmpdir.as_cwd(): + with _test_cwd(tmp_path): args = [ "-j1", "--disable=missing-docstring, missing-final-newline", @@ -911,11 +943,11 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er self._runtest(args, code=0) @staticmethod - def test_can_list_directories_without_dunder_init(tmpdir: LocalPath) -> None: - test_directory = tmpdir / "test_directory" + def test_can_list_directories_without_dunder_init(tmp_path: Path) -> None: + test_directory = tmp_path / "test_directory" test_directory.mkdir() spam_module = test_directory / "spam.py" - spam_module.write("'Empty'") + spam_module.write_text("'Empty'") subprocess.check_output( [ @@ -925,7 +957,7 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er "--disable=missing-docstring, missing-final-newline", "test_directory", ], - cwd=str(tmpdir), + cwd=str(tmp_path), stderr=subprocess.PIPE, ) @@ -943,9 +975,9 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er ) self._test_output([path, "-j2"], expected_output="") - def test_output_file_valid_path(self, tmpdir: LocalPath) -> None: + def test_output_file_valid_path(self, tmp_path: Path) -> None: path = join(HERE, "regrtest_data", "unused_variable.py") - output_file = tmpdir / "output.txt" + output_file = tmp_path / "output.txt" expected = "Your code has been rated at 7.50/10" self._test_output_file( [path, f"--output={output_file}"], @@ -972,7 +1004,7 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er (["--fail-on=useless-suppression", "--enable=C"], 22), ], ) - def test_fail_on_exit_code(self, args, expected): + def test_fail_on_exit_code(self, args: list[str], expected: int) -> None: path = join(HERE, "regrtest_data", "fail_on.py") # We set fail-under to be something very low so that even with the warnings # and errors that are generated they don't affect the exit code. @@ -998,7 +1030,7 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er (["--fail-on=useless-suppression", "--enable=C"], 1), ], ) - def test_fail_on_info_only_exit_code(self, args, expected): + def test_fail_on_info_only_exit_code(self, args: list[str], expected: int) -> None: path = join(HERE, "regrtest_data", "fail_on_info_only.py") self._runtest([path] + args, code=expected) @@ -1025,10 +1057,10 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er ], ) def test_output_file_can_be_combined_with_output_format_option( - self, tmpdir, output_format, expected_output - ): + self, tmp_path: Path, output_format: str, expected_output: str + ) -> None: path = join(HERE, "regrtest_data", "unused_variable.py") - output_file = tmpdir / "output.txt" + output_file = tmp_path / "output.txt" self._test_output_file( [path, f"--output={output_file}", f"--output-format={output_format}"], output_file, @@ -1036,10 +1068,10 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er ) def test_output_file_can_be_combined_with_custom_reporter( - self, tmpdir: LocalPath + self, tmp_path: Path ) -> None: path = join(HERE, "regrtest_data", "unused_variable.py") - output_file = tmpdir / "output.txt" + output_file = tmp_path / "output.txt" # It does not really have to be a truly custom reporter. # It is only important that it is being passed explicitly to ``Run``. myreporter = TextReporter() @@ -1050,9 +1082,9 @@ a.py:1:4: E0001: Parsing failed: 'invalid syntax (<unknown>, line 1)' (syntax-er ) assert output_file.exists() - def test_output_file_specified_in_rcfile(self, tmpdir: LocalPath) -> None: - output_file = tmpdir / "output.txt" - rcfile = tmpdir / "pylintrc" + def test_output_file_specified_in_rcfile(self, tmp_path: Path) -> None: + output_file = tmp_path / "output.txt" + rcfile = tmp_path / "pylintrc" rcfile_contents = textwrap.dedent( f""" [MAIN] diff --git a/tests/test_similar.py b/tests/test_similar.py index d59602ff6..5558b70e7 100644 --- a/tests/test_similar.py +++ b/tests/test_similar.py @@ -36,7 +36,7 @@ class TestSimilarCodeChecker: @staticmethod def _run_pylint(args: list[str], out: TextIO) -> int: """Runs pylint with a patched output.""" - args = args + [ + args += [ "--persistent=no", "--enable=astroid-error", # Enable functionality that will build another ast @@ -48,7 +48,7 @@ class TestSimilarCodeChecker: with warnings.catch_warnings(): warnings.simplefilter("ignore") Run(args) - return cm.value.code + return int(cm.value.code) @staticmethod def _clean_paths(output: str) -> str: diff --git a/tests/testutils/_primer/test_package_to_lint.py b/tests/testutils/_primer/test_package_to_lint.py index 2ee9f3dec..220e2c0b2 100644 --- a/tests/testutils/_primer/test_package_to_lint.py +++ b/tests/testutils/_primer/test_package_to_lint.py @@ -42,8 +42,8 @@ def test_package_to_lint_default_value() -> None: branch="main", directories=["src/flask"], # Must work on Windows (src\\flask) ) - assert package_to_lint.pylintrc is None + assert package_to_lint.pylintrc == "" expected_path_to_lint = ( PRIMER_DIRECTORY_PATH / "pallets" / "flask" / "src" / "flask" ) - assert package_to_lint.pylint_args == [str(expected_path_to_lint)] + assert package_to_lint.pylint_args == [str(expected_path_to_lint), "--rcfile="] diff --git a/tests/testutils/test_functional_testutils.py b/tests/testutils/test_functional_testutils.py index 9090661be..68dad697d 100644 --- a/tests/testutils/test_functional_testutils.py +++ b/tests/testutils/test_functional_testutils.py @@ -45,7 +45,7 @@ def test_get_functional_test_files_from_directory() -> None: get_functional_test_files_from_directory(DATA_DIRECTORY) -def test_minimal_messages_config_enabled(pytest_config) -> None: +def test_minimal_messages_config_enabled(pytest_config: MagicMock) -> None: """Test that all messages not targeted in the functional test are disabled when running with --minimal-messages-config. """ @@ -68,7 +68,7 @@ def test_minimal_messages_config_enabled(pytest_config) -> None: assert not mod_test._linter.is_message_enabled("unused-import") -def test_minimal_messages_config_excluded_file(pytest_config) -> None: +def test_minimal_messages_config_excluded_file(pytest_config: MagicMock) -> None: """Test that functional test files can be excluded from the run with --minimal-messages-config if they set the exclude_from_minimal_messages_config option in their rcfile. diff --git a/tests/testutils/test_output_line.py b/tests/testutils/test_output_line.py index 2a21ce1fd..5b2bf1a1b 100644 --- a/tests/testutils/test_output_line.py +++ b/tests/testutils/test_output_line.py @@ -6,7 +6,7 @@ from __future__ import annotations -from collections.abc import Callable +import sys import pytest @@ -16,9 +16,19 @@ from pylint.message import Message from pylint.testutils.output_line import OutputLine from pylint.typing import MessageLocationTuple +if sys.version_info >= (3, 8): + from typing import Protocol +else: + from typing_extensions import Protocol + + +class _MessageCallable(Protocol): + def __call__(self, confidence: Confidence = HIGH) -> Message: + ... + @pytest.fixture() -def message() -> Callable: +def message() -> _MessageCallable: def inner(confidence: Confidence = HIGH) -> Message: return Message( symbol="missing-docstring", @@ -55,7 +65,7 @@ def test_output_line() -> None: assert output_line.confidence == "HIGH" -def test_output_line_from_message(message: Callable) -> None: +def test_output_line_from_message(message: _MessageCallable) -> None: """Test that the OutputLine NamedTuple is instantiated correctly with from_msg.""" expected_column = 2 if PY38_PLUS else 0 @@ -91,7 +101,7 @@ def test_output_line_from_message(message: Callable) -> None: @pytest.mark.parametrize("confidence", [HIGH, INFERENCE]) -def test_output_line_to_csv(confidence: Confidence, message: Callable) -> None: +def test_output_line_to_csv(confidence: Confidence, message: _MessageCallable) -> None: """Test that the OutputLine NamedTuple is instantiated correctly with from_msg and then converted to csv. """ diff --git a/tests/testutils/test_testutils_utils.py b/tests/testutils/test_testutils_utils.py index 79f4e2a81..b521e25c4 100644 --- a/tests/testutils/test_testutils_utils.py +++ b/tests/testutils/test_testutils_utils.py @@ -6,6 +6,8 @@ import os import sys from pathlib import Path +import pytest + from pylint.testutils.utils import _test_cwd, _test_environ_pythonpath, _test_sys_path @@ -50,22 +52,28 @@ def test__test_cwd(tmp_path: Path) -> None: assert os.getcwd() == cwd -def test__test_environ_pythonpath_no_arg() -> None: - python_path = os.environ.get("PYTHONPATH") - with _test_environ_pythonpath(): - assert os.environ.get("PYTHONPATH") == python_path - new_pythonpath = "./whatever/:" - os.environ["PYTHONPATH"] = new_pythonpath - assert os.environ.get("PYTHONPATH") == new_pythonpath - assert os.environ.get("PYTHONPATH") == python_path +@pytest.mark.parametrize("old_pythonpath", ["./oldpath/:", None]) +def test__test_environ_pythonpath_no_arg(old_pythonpath: str) -> None: + real_pythonpath = os.environ.get("PYTHONPATH") + with _test_environ_pythonpath(old_pythonpath): + with _test_environ_pythonpath(): + assert os.environ.get("PYTHONPATH") is None + new_pythonpath = "./whatever/:" + os.environ["PYTHONPATH"] = new_pythonpath + assert os.environ.get("PYTHONPATH") == new_pythonpath + assert os.environ.get("PYTHONPATH") == old_pythonpath + assert os.environ.get("PYTHONPATH") == real_pythonpath -def test__test_environ_pythonpath() -> None: - python_path = os.environ.get("PYTHONPATH") - new_pythonpath = "./whatever/:" - with _test_environ_pythonpath(new_pythonpath): - assert os.environ.get("PYTHONPATH") == new_pythonpath - newer_pythonpath = "./something_else/:" - os.environ["PYTHONPATH"] = newer_pythonpath - assert os.environ.get("PYTHONPATH") == newer_pythonpath - assert os.environ.get("PYTHONPATH") == python_path +@pytest.mark.parametrize("old_pythonpath", ["./oldpath/:", None]) +def test__test_environ_pythonpath(old_pythonpath: str) -> None: + real_pythonpath = os.environ.get("PYTHONPATH") + with _test_environ_pythonpath(old_pythonpath): + new_pythonpath = "./whatever/:" + with _test_environ_pythonpath(new_pythonpath): + assert os.environ.get("PYTHONPATH") == new_pythonpath + newer_pythonpath = "./something_else/:" + os.environ["PYTHONPATH"] = newer_pythonpath + assert os.environ.get("PYTHONPATH") == newer_pythonpath + assert os.environ.get("PYTHONPATH") == old_pythonpath + assert os.environ.get("PYTHONPATH") == real_pythonpath diff --git a/tests/utils/unittest_ast_walker.py b/tests/utils/unittest_ast_walker.py index 43614c0ed..5a2dc6609 100644 --- a/tests/utils/unittest_ast_walker.py +++ b/tests/utils/unittest_ast_walker.py @@ -7,6 +7,7 @@ from __future__ import annotations import warnings import astroid +from astroid import nodes from pylint.checkers.base_checker import BaseChecker from pylint.checkers.utils import only_required_for_messages @@ -27,19 +28,23 @@ class TestASTWalker: self.called: set[str] = set() @only_required_for_messages("first-message") - def visit_module(self, module): # pylint: disable=unused-argument + def visit_module( + self, module: nodes.Module # pylint: disable=unused-argument + ) -> None: self.called.add("module") @only_required_for_messages("second-message") - def visit_call(self, module): + def visit_call(self, module: nodes.Call) -> None: raise NotImplementedError @only_required_for_messages("second-message", "third-message") - def visit_assignname(self, module): # pylint: disable=unused-argument + def visit_assignname( + self, module: nodes.AssignName # pylint: disable=unused-argument + ) -> None: self.called.add("assignname") @only_required_for_messages("second-message") - def leave_assignname(self, module): + def leave_assignname(self, module: nodes.AssignName) -> None: raise NotImplementedError def test_only_required_for_messages(self) -> None: @@ -59,7 +64,9 @@ class TestASTWalker: self.called = False @only_required_for_messages("first-message") - def visit_assname(self, node): # pylint: disable=unused-argument + def visit_assname( + self, node: nodes.AssignName # pylint: disable=unused-argument + ) -> None: self.called = True linter = self.MockLinter({"first-message": True}) diff --git a/towncrier.toml b/towncrier.toml index b25b28298..51ea3d078 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -7,9 +7,25 @@ issue_format = "`#{issue} <https://github.com/PyCQA/pylint/issues/{issue}>`_" wrap = true # Definition of fragment types. -# TODO: with the next towncrier release (21.9.1) it will be possible to define -# custom types as toml tables: -# https://github.com/twisted/towncrier#further-options +# We want the changelog to show in the same order as the fragment types +# are defined here. Therefore we have to use the array-style fragment definition. +# The table-style definition, although more concise, would be sorted alphabetically. +# https://github.com/twisted/towncrier/issues/437 +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "user_action" +name = "Changes requiring user actions" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "New Features" +showcontent = true + [[tool.towncrier.type]] directory = "new_check" name = "New Checks" @@ -1,6 +1,6 @@ [tox] minversion = 3.0 -envlist = formatting, py37, py38, py39, py310, pypy, benchmark +envlist = formatting, py37, py38, py39, py310, py311, pypy, benchmark skip_missing_interpreters = true requires = pip >=21.3.1 isolated_build = true |