Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/whatsnew/fragments/9294.false_positive
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix a false positive for ``unnecessary-direct-lambda-call`` when a directly called
lambda in a class body wraps a comprehension containing an assignment expression.
PEP 572 makes that a ``SyntaxError`` without the lambda's scope, so following the
message produced code that would not compile.

Closes #9294
38 changes: 37 additions & 1 deletion pylint/checkers/lambda_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
from pylint.checkers import BaseChecker
from pylint.interfaces import HIGH

# PEP 572 forbids an assignment expression inside a comprehension whose
# containing scope is a class body, and every comprehension form is covered.
COMPREHENSION_NODES = (
nodes.DictComp,
nodes.GeneratorExp,
nodes.ListComp,
nodes.SetComp,
)

if TYPE_CHECKING:
from pylint.lint import PyLinter

Expand Down Expand Up @@ -82,13 +91,40 @@ def visit_namedexpr(self, node: nodes.NamedExpr) -> None:

def visit_call(self, node: nodes.Call) -> None:
"""Check if lambda expression is called directly."""
if isinstance(node.func, nodes.Lambda):
if isinstance(node.func, nodes.Lambda) and not _lambda_scope_is_required(node):
self.add_message(
"unnecessary-direct-lambda-call",
node=node,
confidence=HIGH,
)


def _lambda_scope_is_required(node: nodes.Call) -> bool:
"""Whether removing this directly called lambda would not compile.

A comprehension carrying an assignment expression is a SyntaxError when its
containing scope is a class body (PEP 572). The lambda supplies a function
scope in between, so removing it, which is exactly what the message advises,
turns working code into code that does not parse.

Only a comprehension that would land in the class body itself counts. One
nested inside a further lambda or function keeps a scope of its own after
the removal, so the call around it is still reported.
"""
if not isinstance(node.frame(), nodes.ClassDef):
return False
for comprehension in node.func.body.nodes_of_class(COMPREHENSION_NODES):
if not any(True for _ in comprehension.nodes_of_class(nodes.NamedExpr)):
continue
enclosing_scope = comprehension.parent
while enclosing_scope is not None and not isinstance(
enclosing_scope, (nodes.Lambda, nodes.FunctionDef)
):
enclosing_scope = enclosing_scope.parent
if enclosing_scope is node.func:
return True
return False


def register(linter: PyLinter) -> None:
linter.register_checker(LambdaExpressionChecker(linter))
26 changes: 26 additions & 0 deletions tests/functional/u/unnecessary/unnecessary_direct_lambda_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,29 @@

y = (lambda x: x**2 + 2*x + 1)(a) # [unnecessary-direct-lambda-call]
y = max((lambda x: x**2)(a), (lambda x: x+1)(a)) # [unnecessary-direct-lambda-call,unnecessary-direct-lambda-call]


class ClassBodyWalrus: # pylint: disable=too-few-public-methods
"""The lambda supplies the scope PEP 572 requires.

An assignment expression inside a comprehension is a SyntaxError when the
containing scope is a class body, so inlining these would not compile.
"""

generator = (lambda: all((x := object()) is x for _ in range(1)))()
listcomp = (lambda: [(x := o) for o in range(1)])()
setcomp = (lambda: {(x := o) for o in range(1)})()
dictcomp = (lambda: {(x := o): o for o in range(1)})()

# Still reported: no comprehension, so the walrus is legal in a class body.
bare_walrus = (lambda: (x := 1))() # [unnecessary-direct-lambda-call]
# Still reported: no assignment expression in the comprehension.
plain_comprehension = (lambda: [o + 1 for o in range(1)])() # [unnecessary-direct-lambda-call]
# Still reported for both calls: after inlining the outer lambda, the
# comprehension is still inside the inner one, which keeps its own scope.
nested = (lambda: [(lambda: [(x := o) for o in range(1)])()])() # [unnecessary-direct-lambda-call,unnecessary-direct-lambda-call]


def outside_a_class_body():
"""Only a class body carries the restriction."""
return (lambda: all((x := object()) is x for _ in range(1)))() # [unnecessary-direct-lambda-call]
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
unnecessary-direct-lambda-call:4:4:4:33::Lambda expression called directly. Execute the expression inline instead.:HIGH
unnecessary-direct-lambda-call:5:8:5:27::Lambda expression called directly. Execute the expression inline instead.:HIGH
unnecessary-direct-lambda-call:5:29:5:47::Lambda expression called directly. Execute the expression inline instead.:HIGH
unnecessary-direct-lambda-call:21:18:21:38:ClassBodyWalrus:Lambda expression called directly. Execute the expression inline instead.:HIGH
unnecessary-direct-lambda-call:23:26:23:63:ClassBodyWalrus:Lambda expression called directly. Execute the expression inline instead.:HIGH
unnecessary-direct-lambda-call:26:13:26:67:ClassBodyWalrus:Lambda expression called directly. Execute the expression inline instead.:HIGH
unnecessary-direct-lambda-call:26:23:26:63:ClassBodyWalrus.<lambda>:Lambda expression called directly. Execute the expression inline instead.:HIGH
unnecessary-direct-lambda-call:31:11:31:66:outside_a_class_body:Lambda expression called directly. Execute the expression inline instead.:HIGH