From 4c913c06cf654f1d4e4cc0a973a1d9159f60922a Mon Sep 17 00:00:00 2001 From: "pylint-backport[bot]" <212256041+pylint-backport[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:38:49 +0000 Subject: [PATCH 1/5] [Backport maintenance/4.0.x] Fix FP for invalid-name for TypedDicts (#10674) Fix FP for invalid-name for TypedDicts (#10673) (cherry picked from commit 4eabe269672110f83150e04a03a8f53499618237) Co-authored-by: Jacob Walls --- doc/whatsnew/fragments/10672.false_positive | 3 +++ pylint/checkers/base/name_checker/checker.py | 15 ++++++++------- tests/functional/i/invalid/invalid_name.py | 4 ++++ 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 doc/whatsnew/fragments/10672.false_positive diff --git a/doc/whatsnew/fragments/10672.false_positive b/doc/whatsnew/fragments/10672.false_positive new file mode 100644 index 0000000000..e27f916c29 --- /dev/null +++ b/doc/whatsnew/fragments/10672.false_positive @@ -0,0 +1,3 @@ +Fix false positive for ``invalid-name`` for ``TypedDict`` instances. + +Closes #10672 diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 2fcef7fc7b..29fb52d051 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -490,13 +490,14 @@ def visit_assignname( # pylint: disable=too-many-branches,too-many-statements # Check classes (TypeVar's are classes so they need to be excluded first) elif isinstance(inferred_assign_type, nodes.ClassDef) or ( isinstance(inferred_assign_type, bases.Instance) - and "EnumMeta" - in { - ancestor.name - for ancestor in cast( - InferenceResult, inferred_assign_type - ).mro() - } + and {"EnumMeta", "TypedDict"}.intersection( + { + ancestor.name + for ancestor in cast( + InferenceResult, inferred_assign_type + ).mro() + } + ) ): self._check_name("class", node.name, node) diff --git a/tests/functional/i/invalid/invalid_name.py b/tests/functional/i/invalid/invalid_name.py index 9e002fc3f0..7db42f7fb3 100644 --- a/tests/functional/i/invalid/invalid_name.py +++ b/tests/functional/i/invalid/invalid_name.py @@ -121,3 +121,7 @@ class FooBarSubclass(FooBar): from enum import Enum Color = Enum('Color', [('RED', 1), ('GREEN', 2), ('BLUE', 3)]) + + +from typing import TypedDict +MyExampleType = TypedDict("MyExampleType", {"some_field": str}) From 92a13dff01715c782a8321ac3dd9c5fe91dda19a Mon Sep 17 00:00:00 2001 From: "pylint-backport[bot]" <212256041+pylint-backport[bot]@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:33:26 +0000 Subject: [PATCH 2/5] [Backport maintenance/4.0.x] Fix FP for `invalid-name` on module-level constant with multiple branches (#10684) Fix FP for `invalid-name` on module-level constant with multiple branches (#10677) (cherry picked from commit 6678c90dc56c8c624317cdb1b53a6e1e78b0c4d3) Co-authored-by: Jacob Walls --- doc/whatsnew/fragments/10664.false_positive | 5 +++++ pylint/checkers/base/name_checker/checker.py | 22 ++++++++++++------- tests/functional/d/disallowed_name.py | 2 +- tests/functional/d/disallowed_name.txt | 1 - .../invalid_name/invalid_name_module_level.py | 14 +++++++++++- 5 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 doc/whatsnew/fragments/10664.false_positive diff --git a/doc/whatsnew/fragments/10664.false_positive b/doc/whatsnew/fragments/10664.false_positive new file mode 100644 index 0000000000..3e0716ac50 --- /dev/null +++ b/doc/whatsnew/fragments/10664.false_positive @@ -0,0 +1,5 @@ +Fix a false positive for ``invalid-name`` on exclusive module-level assignments +composed of three or more branches. We won't raise ``disallowed-name`` on module-level names that can't be inferred +until a further refactor to remove this false negative is done. + +Closes #10664 diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 29fb52d051..c2f2655282 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -523,16 +523,22 @@ def visit_assignname( # pylint: disable=too-many-branches,too-many-statements if ( (iattrs := tuple(node.frame().igetattr(node.name))) and util.Uninferable not in iattrs - and len(iattrs) == 2 - and astroid.are_exclusive(*iattrs) + and len(iattrs) > 1 + and all( + astroid.are_exclusive(*combo) + for combo in itertools.combinations(iattrs, 2) + ) ): node_type = "const" - self._check_name( - node_type, - node.name, - node, - disallowed_check_only=redefines_import, - ) + if not self._meets_exception_for_non_consts( + inferred_assign_type, node.name + ): + self._check_name( + node_type, + node.name, + node, + disallowed_check_only=redefines_import, + ) # Check names defined in function scopes elif isinstance(frame, nodes.FunctionDef): diff --git a/tests/functional/d/disallowed_name.py b/tests/functional/d/disallowed_name.py index a6155dd346..21ffffcc5c 100644 --- a/tests/functional/d/disallowed_name.py +++ b/tests/functional/d/disallowed_name.py @@ -6,6 +6,6 @@ def baz(): # [disallowed-name] class foo(): # [disallowed-name] pass -foo = {}.keys() # [disallowed-name] +foo = {}.keys() # Should raise disallowed-name once _check_name() is refactored. foo = 42 # [disallowed-name] aaa = 42 # [invalid-name] diff --git a/tests/functional/d/disallowed_name.txt b/tests/functional/d/disallowed_name.txt index 535510a532..728d58aa6d 100644 --- a/tests/functional/d/disallowed_name.txt +++ b/tests/functional/d/disallowed_name.txt @@ -1,5 +1,4 @@ disallowed-name:3:0:3:7:baz:"Disallowed name ""baz""":HIGH disallowed-name:6:0:6:9:foo:"Disallowed name ""foo""":HIGH -disallowed-name:9:0:9:3::"Disallowed name ""foo""":HIGH disallowed-name:10:0:10:3::"Disallowed name ""foo""":HIGH invalid-name:11:0:11:3::"Constant name ""aaa"" doesn't conform to UPPER_CASE naming style":HIGH diff --git a/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py b/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py index 8c2c2f2482..9012cea417 100644 --- a/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py +++ b/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py @@ -26,7 +26,19 @@ def A(): # [invalid-name] ASSIGNMENT_THAT_CRASHED_PYLINT = type(float.__new__.__code__) +# Exclusive assignment: uses const regex if CONST: OTHER_CONST = 1 -else: +elif CONSTA: OTHER_CONST = 2 +else: + OTHER_CONST = 3 + + +# Lists, sets, and objects can pass against the variable OR const regexes. +if CONST: + other_const = [1] +elif CONSTA: + other_const = [2] +else: + other_const = [3] From 4d527695093fa8c09922ef781fa44c3457f1ec8f Mon Sep 17 00:00:00 2001 From: "pylint-backport[bot]" <212256041+pylint-backport[bot]@users.noreply.github.com> Date: Sun, 19 Oct 2025 15:14:27 +0000 Subject: [PATCH 3/5] [Backport maintenance/4.0.x] Ignore unraisable exceptions on stdlib primer (#10686) Ignore unraisable exceptions on stdlib primer (#10683) Refs pylint-dev/pylint#9138 (cherry picked from commit e18b6f5fd440c89262cac4f8f2040508e5ce78cb) Co-authored-by: Jacob Walls --- tests/primer/test_primer_stdlib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/primer/test_primer_stdlib.py b/tests/primer/test_primer_stdlib.py index ea55817498..cd64c212b1 100644 --- a/tests/primer/test_primer_stdlib.py +++ b/tests/primer/test_primer_stdlib.py @@ -48,6 +48,7 @@ def _patch_stdout(out: io.StringIO) -> Iterator[None]: @pytest.mark.parametrize( ("test_module_location", "test_module_name"), MODULES_TO_CHECK, ids=MODULES_NAMES ) +@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") def test_primer_stdlib_no_crash( test_module_location: str, test_module_name: str, capsys: CaptureFixture ) -> None: From caf95d5985e0b3ad164de6b787e309cf748fb876 Mon Sep 17 00:00:00 2001 From: "pylint-backport[bot]" <212256041+pylint-backport[bot]@users.noreply.github.com> Date: Sun, 19 Oct 2025 16:22:25 +0000 Subject: [PATCH 4/5] [Backport maintenance/4.0.x] Fix FP for `invalid-name` for partially uninferable module-level name (#10687) Fix FP for `invalid-name` for partially uninferable module-level name (#10678) (cherry picked from commit 7ecfd9f2428384c1363d02428eb54497d79e97a1) Co-authored-by: Jacob Walls --- doc/whatsnew/fragments/10652.false_positive | 3 +++ pylint/checkers/base/name_checker/checker.py | 9 +++++++-- .../invalid/invalid_name/invalid_name_module_level.py | 11 ++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 doc/whatsnew/fragments/10652.false_positive diff --git a/doc/whatsnew/fragments/10652.false_positive b/doc/whatsnew/fragments/10652.false_positive new file mode 100644 index 0000000000..a5a7319a26 --- /dev/null +++ b/doc/whatsnew/fragments/10652.false_positive @@ -0,0 +1,3 @@ +Fix false positive for ``invalid-name`` on a partially uninferable module-level constant. + +Closes #10652 diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index c2f2655282..3cb1c336da 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -520,9 +520,14 @@ def visit_assignname( # pylint: disable=too-many-branches,too-many-statements self._check_name("const", node.name, node) else: node_type = "variable" + iattrs = tuple(node.frame().igetattr(node.name)) if ( - (iattrs := tuple(node.frame().igetattr(node.name))) - and util.Uninferable not in iattrs + util.Uninferable in iattrs + and self._name_regexps["const"].match(node.name) is not None + ): + return + if ( + util.Uninferable not in iattrs and len(iattrs) > 1 and all( astroid.are_exclusive(*combo) diff --git a/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py b/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py index 9012cea417..9eae16b7c1 100644 --- a/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py +++ b/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py @@ -1,5 +1,5 @@ """Tests for invalid name for names declared at module level""" -# pylint: disable=missing-class-docstring, too-few-public-methods, missing-function-docstring +# pylint: disable=missing-class-docstring, too-few-public-methods, missing-function-docstring, wrong-import-position import collections @@ -42,3 +42,12 @@ def A(): # [invalid-name] other_const = [2] else: other_const = [3] + + +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version + +try: + VERSION = version("ty") # uninferable +except PackageNotFoundError: + VERSION = "0.0.0" From 0eb92d25fd38ba5bad2f8d2ea7df63ad23e18ae3 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 20 Oct 2025 08:35:04 -0400 Subject: [PATCH 5/5] Bump pylint to 4.0.2, update changelog --- doc/whatsnew/4/4.0/index.rst | 24 +++++++++++++++++++++ doc/whatsnew/fragments/10652.false_positive | 3 --- doc/whatsnew/fragments/10664.false_positive | 5 ----- doc/whatsnew/fragments/10672.false_positive | 3 --- pylint/__pkginfo__.py | 2 +- tbump.toml | 2 +- 6 files changed, 26 insertions(+), 13 deletions(-) delete mode 100644 doc/whatsnew/fragments/10652.false_positive delete mode 100644 doc/whatsnew/fragments/10664.false_positive delete mode 100644 doc/whatsnew/fragments/10672.false_positive diff --git a/doc/whatsnew/4/4.0/index.rst b/doc/whatsnew/4/4.0/index.rst index bde8380720..4d7c20b612 100644 --- a/doc/whatsnew/4/4.0/index.rst +++ b/doc/whatsnew/4/4.0/index.rst @@ -74,6 +74,30 @@ to your liking. .. towncrier release notes start +What's new in Pylint 4.0.2? +-------------------------------- +Release date: 2025-10-20 + + +False Positives Fixed +--------------------- + +- Fix false positive for :ref:`invalid-name` on a partially uninferable module-level constant. + + Closes #10652 (`#10652 `_) + +- Fix a false positive for :ref:`invalid-name` on exclusive module-level assignments + composed of three or more branches. We won't raise :ref:`disallowed-name` on module-level names that can't be inferred + until a further refactor to remove this false negative is done. + + Closes #10664 (`#10664 `_) + +- Fix false positive for :ref:`invalid-name` for ``TypedDict`` instances. + + Closes #10672 (`#10672 `_) + + + What's new in Pylint 4.0.1? --------------------------- Release date: 2025-10-14 diff --git a/doc/whatsnew/fragments/10652.false_positive b/doc/whatsnew/fragments/10652.false_positive deleted file mode 100644 index a5a7319a26..0000000000 --- a/doc/whatsnew/fragments/10652.false_positive +++ /dev/null @@ -1,3 +0,0 @@ -Fix false positive for ``invalid-name`` on a partially uninferable module-level constant. - -Closes #10652 diff --git a/doc/whatsnew/fragments/10664.false_positive b/doc/whatsnew/fragments/10664.false_positive deleted file mode 100644 index 3e0716ac50..0000000000 --- a/doc/whatsnew/fragments/10664.false_positive +++ /dev/null @@ -1,5 +0,0 @@ -Fix a false positive for ``invalid-name`` on exclusive module-level assignments -composed of three or more branches. We won't raise ``disallowed-name`` on module-level names that can't be inferred -until a further refactor to remove this false negative is done. - -Closes #10664 diff --git a/doc/whatsnew/fragments/10672.false_positive b/doc/whatsnew/fragments/10672.false_positive deleted file mode 100644 index e27f916c29..0000000000 --- a/doc/whatsnew/fragments/10672.false_positive +++ /dev/null @@ -1,3 +0,0 @@ -Fix false positive for ``invalid-name`` for ``TypedDict`` instances. - -Closes #10672 diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 8564002737..877091ee89 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "4.0.1" +__version__ = "4.0.2" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/tbump.toml b/tbump.toml index 9d6f1da89b..c1797cca75 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/pylint" [version] -current = "4.0.1" +current = "4.0.2" regex = ''' ^(?P0|[1-9]\d*) \.