Skip to content

Commit 98c5af9

Browse files
Fix false-positive with contextmanager missing cleanup (#9654) (#9657)
1 parent 9a9db8f commit 98c5af9

File tree

7 files changed

+78
-15
lines changed

7 files changed

+78
-15
lines changed

doc/data/messages/c/contextmanager-generator-missing-cleanup/bad.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ def cm():
99
print("cm exit")
1010

1111

12-
def genfunc_with_cm(): # [contextmanager-generator-missing-cleanup]
13-
with cm() as context:
12+
def genfunc_with_cm():
13+
with cm() as context: # [contextmanager-generator-missing-cleanup]
1414
yield context * 2

doc/data/messages/c/contextmanager-generator-missing-cleanup/details.rst

+21
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,24 @@ because the ways to use a contextmanager are many.
88
A contextmanager can be used as a decorator (which immediately has ``__enter__``/``__exit__`` applied)
99
and the use of ``as ...`` or discard of the return value also implies whether the context needs cleanup or not.
1010
So for this message, warning the invoker of the contextmanager is important.
11+
12+
The check can create false positives if ``yield`` is used inside an ``if-else`` block without custom cleanup. Use ``pylint: disable`` for these.
13+
14+
.. code-block:: python
15+
16+
from contextlib import contextmanager
17+
18+
@contextmanager
19+
def good_cm_no_cleanup():
20+
contextvar = "acquired context"
21+
print("cm enter")
22+
if some_condition:
23+
yield contextvar
24+
else:
25+
yield contextvar
26+
27+
28+
def good_cm_no_cleanup_genfunc():
29+
# pylint: disable-next=contextmanager-generator-missing-cleanup
30+
with good_cm_no_cleanup() as context:
31+
yield context * 2

doc/data/messages/c/contextmanager-generator-missing-cleanup/good.py

+12
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,15 @@ def good_cm_finally():
4747
def good_cm_finally_genfunc():
4848
with good_cm_finally() as context:
4949
yield context * 2
50+
51+
52+
@contextlib.contextmanager
53+
def good_cm_no_cleanup():
54+
contextvar = "acquired context"
55+
print("cm enter")
56+
yield contextvar
57+
58+
59+
def good_cm_no_cleanup_genfunc():
60+
with good_cm_no_cleanup() as context:
61+
yield context * 2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Exclude context manager without cleanup from
2+
``contextmanager-generator-missing-cleanup`` checks.
3+
4+
Closes #9625

pylint/checkers/base/function_checker.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def _check_contextmanager_generator_missing_cleanup(
7272
if self._node_fails_contextmanager_cleanup(inferred_node, yield_nodes):
7373
self.add_message(
7474
"contextmanager-generator-missing-cleanup",
75-
node=node,
75+
node=with_node,
7676
args=(node.name,),
7777
)
7878

@@ -85,6 +85,7 @@ def _node_fails_contextmanager_cleanup(
8585
Current checks for a contextmanager:
8686
- only if the context manager yields a non-constant value
8787
- only if the context manager lacks a finally, or does not catch GeneratorExit
88+
- only if some statement follows the yield, some manually cleanup happens
8889
8990
:param node: Node to check
9091
:type node: nodes.FunctionDef
@@ -114,6 +115,19 @@ def check_handles_generator_exceptions(try_node: nodes.Try) -> bool:
114115
for yield_node in yield_nodes
115116
):
116117
return False
118+
119+
# Check if yield expression is last statement
120+
yield_nodes = list(node.nodes_of_class(nodes.Yield))
121+
if len(yield_nodes) == 1:
122+
n = yield_nodes[0].parent
123+
while n is not node:
124+
if n.next_sibling() is not None:
125+
break
126+
n = n.parent
127+
else:
128+
# No next statement found
129+
return False
130+
117131
# if function body has multiple Try, filter down to the ones that have a yield node
118132
try_with_yield_nodes = [
119133
try_node

tests/functional/c/contextmanager_generator_missing_cleanup.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def cm():
1414
print("cm exit")
1515

1616

17-
def genfunc_with_cm(): # [contextmanager-generator-missing-cleanup]
18-
with cm() as context:
17+
def genfunc_with_cm():
18+
with cm() as context: # [contextmanager-generator-missing-cleanup]
1919
yield context * 2
2020

2121

@@ -27,13 +27,13 @@ def name_cm():
2727
print("cm exit")
2828

2929

30-
def genfunc_with_name_cm(): # [contextmanager-generator-missing-cleanup]
31-
with name_cm() as context:
30+
def genfunc_with_name_cm():
31+
with name_cm() as context: # [contextmanager-generator-missing-cleanup]
3232
yield context * 2
3333

3434

35-
def genfunc_with_cm_after(): # [contextmanager-generator-missing-cleanup]
36-
with after_cm() as context:
35+
def genfunc_with_cm_after():
36+
with after_cm() as context: # [contextmanager-generator-missing-cleanup]
3737
yield context * 2
3838

3939

@@ -56,8 +56,8 @@ def cm_with_improper_handling():
5656
print("cm exit")
5757

5858

59-
def genfunc_with_cm_improper(): # [contextmanager-generator-missing-cleanup]
60-
with cm_with_improper_handling() as context:
59+
def genfunc_with_cm_improper():
60+
with cm_with_improper_handling() as context: # [contextmanager-generator-missing-cleanup]
6161
yield context * 2
6262

6363

@@ -175,3 +175,15 @@ def genfunc_with_cm_bare_handler():
175175
def genfunc_with_cm_base_exception_handler():
176176
with cm_base_exception_handler() as context:
177177
yield context * 2
178+
179+
180+
@contextlib.contextmanager
181+
def good_cm_no_cleanup():
182+
contextvar = "acquired context"
183+
print("cm enter")
184+
yield contextvar
185+
186+
187+
def good_cm_no_cleanup_genfunc():
188+
with good_cm_no_cleanup() as context:
189+
yield context * 2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
contextmanager-generator-missing-cleanup:17:0:17:19:genfunc_with_cm:The context used in function 'genfunc_with_cm' will not be exited.:UNDEFINED
2-
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
3-
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
4-
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
1+
contextmanager-generator-missing-cleanup:18:4:19:25:genfunc_with_cm:The context used in function 'genfunc_with_cm' will not be exited.:UNDEFINED
2+
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
3+
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
4+
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

0 commit comments

Comments
 (0)