diff --git a/doc/whatsnew/3/3.2/index.rst b/doc/whatsnew/3/3.2/index.rst index c71ae72197..e8ee047d15 100644 --- a/doc/whatsnew/3/3.2/index.rst +++ b/doc/whatsnew/3/3.2/index.rst @@ -14,6 +14,29 @@ Summary -- Release highlights .. towncrier release notes start +What's new in Pylint 3.2.3? +--------------------------- +Release date: 2024-06-06 + + +False Positives Fixed +--------------------- + +- Classes with only an Ellipsis (``...``) in their body do not trigger 'multiple-statements' + anymore if they are inlined (in accordance with black's 2024 style). + + Closes #9398 (`#9398 `_) + +- Fix a false positive for ``redefined-outer-name`` when there is a name defined in an exception-handling block which shares the same name as a local variable that has been defined in a function body. + + Closes #9671 (`#9671 `_) + +- Fix a false positive for ``use-yield-from`` when using the return value from the ``yield`` atom. + + Closes #9696 (`#9696 `_) + + + What's new in Pylint 3.2.2? --------------------------- Release date: 2024-05-20 diff --git a/examples/pylintrc b/examples/pylintrc index 0a917e9cef..24d91bd4e9 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -87,8 +87,8 @@ load-plugins= # Pickle collected data for later comparisons. persistent=yes -# Resolve imports to .pyi stubs if available. May reduce no-member messages -# and increase not-an-iterable messages. +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. prefer-stubs=no # Minimum Python version to use for version dependent checks. Will default to diff --git a/examples/pyproject.toml b/examples/pyproject.toml index 68e8c66737..8b3c7b29f7 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -77,9 +77,9 @@ limit-inference-results = 100 # Pickle collected data for later comparisons. persistent = true -# Resolve imports to .pyi stubs if available. May reduce no-member messages -# and increase not-an-iterable messages. -prefer-stubs = false +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. +# prefer-stubs = # Minimum Python version to use for version dependent checks. Will default to the # version used to run pylint. diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index a898f5f7bd..e57a71404f 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "3.2.2" +__version__ = "3.2.3" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index f4de616598..f8aecbda64 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -522,9 +522,8 @@ def visit_default(self, node: nodes.NodeNG) -> None: def _check_multi_statement_line(self, node: nodes.NodeNG, line: int) -> None: """Check for lines containing multiple statements.""" - # Do not warn about multiple nested context managers - # in with statements. if isinstance(node, nodes.With): + # Do not warn about multiple nested context managers in with statements. return if ( isinstance(node.parent, nodes.If) @@ -539,16 +538,16 @@ def _check_multi_statement_line(self, node: nodes.NodeNG, line: int) -> None: ): return - # Functions stubs with ``Ellipsis`` as body are exempted. + # Functions stubs and class with ``Ellipsis`` as body are exempted. if ( - isinstance(node.parent, nodes.FunctionDef) - and isinstance(node, nodes.Expr) + isinstance(node, nodes.Expr) + and isinstance(node.parent, (nodes.FunctionDef, nodes.ClassDef)) and isinstance(node.value, nodes.Const) and node.value.value is Ellipsis ): return - self.add_message("multiple-statements", node=node) + self.add_message("multiple-statements", node=node, confidence=HIGH) self._visited_lines[line] = 2 def check_trailing_whitespace_ending(self, line: str, i: int) -> None: diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 94e722b177..9ffcecce12 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -1169,21 +1169,24 @@ def visit_yield(self, node: nodes.Yield) -> None: if not isinstance(node.value, nodes.Name): return - parent = node.parent.parent + loop_node = node.parent.parent if ( - not isinstance(parent, nodes.For) - or isinstance(parent, nodes.AsyncFor) - or len(parent.body) != 1 + not isinstance(loop_node, nodes.For) + or isinstance(loop_node, nodes.AsyncFor) + or len(loop_node.body) != 1 + # Avoid a false positive if the return value from `yield` is used, + # (such as via Assign, AugAssign, etc). + or not isinstance(node.parent, nodes.Expr) ): return - if parent.target.name != node.value.name: + if loop_node.target.name != node.value.name: return if isinstance(node.frame(), nodes.AsyncFunctionDef): return - self.add_message("use-yield-from", node=parent, confidence=HIGH) + self.add_message("use-yield-from", node=loop_node, confidence=HIGH) @staticmethod def _has_exit_in_scope(scope: nodes.LocalsDictNodeNG) -> bool: diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 6c33a05556..8447320b89 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1519,6 +1519,15 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: ): continue + # Suppress emitting the message if the outer name is in the + # scope of an exception assignment. + # For example: the `e` in `except ValueError as e` + global_node = globs[name][0] + if isinstance(global_node, nodes.AssignName) and isinstance( + global_node.parent, nodes.ExceptHandler + ): + continue + line = definition.fromlineno if not self._is_name_ignored(stmt, name): self.add_message( diff --git a/tbump.toml b/tbump.toml index e8a35d2fcc..1a1fbaec5c 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/pylint" [version] -current = "3.2.2" +current = "3.2.3" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/functional/m/multiple_statements.py b/tests/functional/m/multiple_statements.py index c3252f797c..cc0a53e111 100644 --- a/tests/functional/m/multiple_statements.py +++ b/tests/functional/m/multiple_statements.py @@ -4,13 +4,28 @@ from typing import overload +if True: print("Golfing sure is nice") # [multiple-statements] if True: pass # [multiple-statements] +if True: ... # [multiple-statements] + +if True: print("Golfing sure is nice") # [multiple-statements] +else: + pass if True: pass # [multiple-statements] else: pass +if True: ... # [multiple-statements] +else: + pass + +# The following difference in behavior is due to black 2024's style +# that reformat pass on multiple line but reformat "..." on a single line +# (only for classes, not for the examples above) +class MyException(Exception): print("Golfing sure is nice") # [multiple-statements] class MyError(Exception): pass # [multiple-statements] +class DebugTrueDetected(Exception): ... class MyError(Exception): a='a' # [multiple-statements] diff --git a/tests/functional/m/multiple_statements.txt b/tests/functional/m/multiple_statements.txt index 661314268d..6b5be1adf6 100644 --- a/tests/functional/m/multiple_statements.txt +++ b/tests/functional/m/multiple_statements.txt @@ -1,5 +1,10 @@ -multiple-statements:7:9:7:13::More than one statement on a single line:UNDEFINED -multiple-statements:9:9:9:13::More than one statement on a single line:UNDEFINED -multiple-statements:13:26:13:30:MyError:More than one statement on a single line:UNDEFINED -multiple-statements:15:26:15:31:MyError:More than one statement on a single line:UNDEFINED -multiple-statements:17:26:17:31:MyError:More than one statement on a single line:UNDEFINED +multiple-statements:7:9:7:38::More than one statement on a single line:HIGH +multiple-statements:8:9:8:13::More than one statement on a single line:HIGH +multiple-statements:9:9:9:12::More than one statement on a single line:HIGH +multiple-statements:11:9:11:38::More than one statement on a single line:HIGH +multiple-statements:15:9:15:13::More than one statement on a single line:HIGH +multiple-statements:19:9:19:12::More than one statement on a single line:HIGH +multiple-statements:26:30:26:59:MyException:More than one statement on a single line:HIGH +multiple-statements:27:26:27:30:MyError:More than one statement on a single line:HIGH +multiple-statements:30:26:30:31:MyError:More than one statement on a single line:HIGH +multiple-statements:32:26:32:31:MyError:More than one statement on a single line:HIGH diff --git a/tests/functional/m/multiple_statements_single_line.py b/tests/functional/m/multiple_statements_single_line.py index 93a470702c..8b39d631bd 100644 --- a/tests/functional/m/multiple_statements_single_line.py +++ b/tests/functional/m/multiple_statements_single_line.py @@ -4,19 +4,32 @@ from typing import overload +if True: print("Golfing sure is nice") if True: pass +if True: ... + +if True: print("Golfing sure is nice") # [multiple-statements] +else: + pass if True: pass # [multiple-statements] else: pass +if True: ... # [multiple-statements] +else: + pass + +class MyException(Exception): print("Golfing sure is nice") class MyError(Exception): pass +class DebugTrueDetected(Exception): ... + class MyError(Exception): a='a' class MyError(Exception): a='a'; b='b' # [multiple-statements] -try: +try: #@ pass except: pass diff --git a/tests/functional/m/multiple_statements_single_line.txt b/tests/functional/m/multiple_statements_single_line.txt index cac2f7eb2e..8d19c72516 100644 --- a/tests/functional/m/multiple_statements_single_line.txt +++ b/tests/functional/m/multiple_statements_single_line.txt @@ -1,2 +1,4 @@ -multiple-statements:9:9:9:13::More than one statement on a single line:UNDEFINED -multiple-statements:17:26:17:31:MyError:More than one statement on a single line:UNDEFINED +multiple-statements:11:9:11:38::More than one statement on a single line:HIGH +multiple-statements:15:9:15:13::More than one statement on a single line:HIGH +multiple-statements:19:9:19:12::More than one statement on a single line:HIGH +multiple-statements:30:26:30:31:MyError:More than one statement on a single line:HIGH diff --git a/tests/functional/r/redefined/redefined_except_handler.py b/tests/functional/r/redefined/redefined_except_handler.py index b774e32f2f..d621496263 100644 --- a/tests/functional/r/redefined/redefined_except_handler.py +++ b/tests/functional/r/redefined/redefined_except_handler.py @@ -70,3 +70,25 @@ def func(): # pylint:disable-next=invalid-name, unused-variable except IOError as CustomException: # [redefined-outer-name] pass + + +# https://github.com/pylint-dev/pylint/issues/9671 +def function_before_exception(): + """The local variable `e` should not trigger `redefined-outer-name` + when `e` is also defined in the subsequent exception handling block. + """ + e = 42 + return e + +try: + raise ValueError('outer') +except ValueError as e: + print(e) + + +def function_after_exception(): + """The local variable `e` should not trigger `redefined-outer-name` + when `e` is also defined in the preceding exception handling block. + """ + e = 42 + return e diff --git a/tests/functional/u/use/use_yield_from.py b/tests/functional/u/use/use_yield_from.py index 2ccbb6d77e..f4dcdc6b9e 100644 --- a/tests/functional/u/use/use_yield_from.py +++ b/tests/functional/u/use/use_yield_from.py @@ -57,3 +57,13 @@ async def async_for_yield(agen): async def async_yield(agen): for item in agen: yield item + + +# If the return from `yield` is used inline, don't suggest delegation. + +def yield_use_send(): + for item in (1, 2, 3): + _ = yield item + total = 0 + for item in (1, 2, 3): + total += yield item diff --git a/towncrier.toml b/towncrier.toml index 8df0009ec2..a25f83a95d 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,5 +1,5 @@ [tool.towncrier] -version = "3.2.2" +version = "3.2.3" directory = "doc/whatsnew/fragments" filename = "doc/whatsnew/3/3.2/index.rst" template = "doc/whatsnew/fragments/_template.rst"