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