6
6
import itertools
7
7
import tokenize
8
8
from functools import reduce
9
- from typing import List , Optional , Tuple , Union , cast
9
+ from typing import Dict , Iterator , List , NamedTuple , Optional , Tuple , Union , cast
10
10
11
11
import astroid
12
+ from astroid .util import Uninferable
12
13
13
14
from pylint import checkers , interfaces
14
15
from pylint import utils as lint_utils
41
42
"tarfile.TarFile" ,
42
43
"tarfile.TarFile.open" ,
43
44
"multiprocessing.context.BaseContext.Pool" ,
44
- "concurrent.futures.thread.ThreadPoolExecutor" ,
45
- "concurrent.futures.process.ProcessPoolExecutor" ,
46
45
"subprocess.Popen" ,
47
46
)
48
47
)
@@ -147,6 +146,33 @@ def _will_be_released_automatically(node: astroid.Call) -> bool:
147
146
return func .qname () in callables_taking_care_of_exit
148
147
149
148
149
+ class ConsiderUsingWithStack (NamedTuple ):
150
+ """Stack for objects that may potentially trigger a R1732 message
151
+ if they are not used in a ``with`` block later on."""
152
+
153
+ module_scope : Dict [str , astroid .NodeNG ] = {}
154
+ class_scope : Dict [str , astroid .NodeNG ] = {}
155
+ function_scope : Dict [str , astroid .NodeNG ] = {}
156
+
157
+ def __iter__ (self ) -> Iterator [Dict [str , astroid .NodeNG ]]:
158
+ yield from (self .function_scope , self .class_scope , self .module_scope )
159
+
160
+ def get_stack_for_frame (
161
+ self , frame : Union [astroid .FunctionDef , astroid .ClassDef , astroid .Module ]
162
+ ):
163
+ """Get the stack corresponding to the scope of the given frame."""
164
+ if isinstance (frame , astroid .FunctionDef ):
165
+ return self .function_scope
166
+ if isinstance (frame , astroid .ClassDef ):
167
+ return self .class_scope
168
+ return self .module_scope
169
+
170
+ def clear_all (self ) -> None :
171
+ """Convenience method to clear all stacks"""
172
+ for stack in self :
173
+ stack .clear ()
174
+
175
+
150
176
class RefactoringChecker (checkers .BaseTokenChecker ):
151
177
"""Looks for code which can be refactored
152
178
@@ -416,6 +442,7 @@ class RefactoringChecker(checkers.BaseTokenChecker):
416
442
def __init__ (self , linter = None ):
417
443
checkers .BaseTokenChecker .__init__ (self , linter )
418
444
self ._return_nodes = {}
445
+ self ._consider_using_with_stack = ConsiderUsingWithStack ()
419
446
self ._init ()
420
447
self ._never_returning_functions = None
421
448
@@ -425,6 +452,7 @@ def _init(self):
425
452
self ._nested_blocks_msg = None
426
453
self ._reported_swap_nodes = set ()
427
454
self ._can_simplify_bool_op = False
455
+ self ._consider_using_with_stack .clear_all ()
428
456
429
457
def open (self ):
430
458
# do this in open since config not fully initialized in __init__
@@ -543,7 +571,12 @@ def process_tokens(self, tokens):
543
571
if self .linter .is_message_enabled ("trailing-comma-tuple" ):
544
572
self .add_message ("trailing-comma-tuple" , line = token .start [0 ])
545
573
574
+ @utils .check_messages ("consider-using-with" )
546
575
def leave_module (self , _ ):
576
+ # check for context managers that have been created but not used
577
+ self ._emit_consider_using_with_if_needed (
578
+ self ._consider_using_with_stack .module_scope
579
+ )
547
580
self ._init ()
548
581
549
582
@utils .check_messages ("too-many-nested-blocks" )
@@ -593,7 +626,14 @@ def visit_excepthandler(self, node):
593
626
594
627
@utils .check_messages ("redefined-argument-from-local" )
595
628
def visit_with (self , node ):
596
- for _ , names in node .items :
629
+ for var , names in node .items :
630
+ if isinstance (var , astroid .Name ):
631
+ for stack in self ._consider_using_with_stack :
632
+ # We don't need to restrict the stacks we search to the current scope and outer scopes,
633
+ # as e.g. the function_scope stack will be empty when we check a ``with`` on the class level.
634
+ if var .name in stack :
635
+ del stack [var .name ]
636
+ break
597
637
if not names :
598
638
continue
599
639
for name in names .nodes_of_class (astroid .AssignName ):
@@ -818,9 +858,12 @@ def _check_simplifiable_ifexp(self, node):
818
858
self .add_message ("simplifiable-if-expression" , node = node , args = (reduced_to ,))
819
859
820
860
@utils .check_messages (
821
- "too-many-nested-blocks" , "inconsistent-return-statements" , "useless-return"
861
+ "too-many-nested-blocks" ,
862
+ "inconsistent-return-statements" ,
863
+ "useless-return" ,
864
+ "consider-using-with" ,
822
865
)
823
- def leave_functiondef (self , node ) :
866
+ def leave_functiondef (self , node : astroid . FunctionDef ) -> None :
824
867
# check left-over nested blocks stack
825
868
self ._emit_nested_blocks_message_if_needed (self ._nested_blocks )
826
869
# new scope = reinitialize the stack of nested blocks
@@ -830,6 +873,19 @@ def leave_functiondef(self, node):
830
873
# check for single return or return None at the end
831
874
self ._check_return_at_the_end (node )
832
875
self ._return_nodes [node .name ] = []
876
+ # check for context managers that have been created but not used
877
+ self ._emit_consider_using_with_if_needed (
878
+ self ._consider_using_with_stack .function_scope
879
+ )
880
+ self ._consider_using_with_stack .function_scope .clear ()
881
+
882
+ @utils .check_messages ("consider-using-with" )
883
+ def leave_classdef (self , _ : astroid .ClassDef ) -> None :
884
+ # check for context managers that have been created but not used
885
+ self ._emit_consider_using_with_if_needed (
886
+ self ._consider_using_with_stack .class_scope
887
+ )
888
+ self ._consider_using_with_stack .class_scope .clear ()
833
889
834
890
@utils .check_messages ("stop-iteration-return" )
835
891
def visit_raise (self , node ):
@@ -1021,6 +1077,10 @@ def _emit_nested_blocks_message_if_needed(self, nested_blocks):
1021
1077
args = (len (nested_blocks ), self .config .max_nested_blocks ),
1022
1078
)
1023
1079
1080
+ def _emit_consider_using_with_if_needed (self , stack : Dict [str , astroid .NodeNG ]):
1081
+ for node in stack .values ():
1082
+ self .add_message ("consider-using-with" , node = node )
1083
+
1024
1084
@staticmethod
1025
1085
def _duplicated_isinstance_types (node ):
1026
1086
"""Get the duplicated types from the underlying isinstance calls.
@@ -1282,12 +1342,22 @@ def _check_swap_variables(self, node):
1282
1342
message = "consider-swap-variables"
1283
1343
self .add_message (message , node = node )
1284
1344
1345
+ @utils .check_messages (
1346
+ "simplify-boolean-expression" ,
1347
+ "consider-using-ternary" ,
1348
+ "consider-swap-variables" ,
1349
+ "consider-using-with" ,
1350
+ )
1351
+ def visit_assign (self , node : astroid .Assign ) -> None :
1352
+ self ._append_context_managers_to_stack (node )
1353
+ self .visit_return (node ) # remaining checks are identical as for return nodes
1354
+
1285
1355
@utils .check_messages (
1286
1356
"simplify-boolean-expression" ,
1287
1357
"consider-using-ternary" ,
1288
1358
"consider-swap-variables" ,
1289
1359
)
1290
- def visit_assign (self , node ) :
1360
+ def visit_return (self , node : astroid . Return ) -> None :
1291
1361
self ._check_swap_variables (node )
1292
1362
if self ._is_and_or_ternary (node .value ):
1293
1363
cond , truth_value , false_value = self ._and_or_ternary_arguments (node .value )
@@ -1317,9 +1387,54 @@ def visit_assign(self, node):
1317
1387
)
1318
1388
self .add_message (message , node = node , args = (suggestion ,))
1319
1389
1320
- visit_return = visit_assign
1390
+ def _append_context_managers_to_stack (self , node : astroid .Assign ) -> None :
1391
+ if _is_inside_context_manager (node ):
1392
+ # if we are inside a context manager itself, we assume that it will handle the resource management itself.
1393
+ return
1394
+ if isinstance (node .targets [0 ], (astroid .Tuple , astroid .List , astroid .Set )):
1395
+ assignees = node .targets [0 ].elts
1396
+ value = utils .safe_infer (node .value )
1397
+ if value is None or not hasattr (value , "elts" ):
1398
+ # We cannot deduce what values are assigned, so we have to skip this
1399
+ return
1400
+ values = value .elts
1401
+ else :
1402
+ assignees = [node .targets [0 ]]
1403
+ values = [node .value ]
1404
+ if Uninferable in (assignees , values ):
1405
+ return
1406
+ for assignee , value in zip (assignees , values ):
1407
+ if not isinstance (value , astroid .Call ):
1408
+ continue
1409
+ inferred = utils .safe_infer (value .func )
1410
+ if not inferred or inferred .qname () not in CALLS_RETURNING_CONTEXT_MANAGERS :
1411
+ continue
1412
+ stack = self ._consider_using_with_stack .get_stack_for_frame (node .frame ())
1413
+ varname = (
1414
+ assignee .name
1415
+ if isinstance (assignee , astroid .AssignName )
1416
+ else assignee .attrname
1417
+ )
1418
+ if varname in stack :
1419
+ # variable was redefined before it was used in a ``with`` block
1420
+ self .add_message (
1421
+ "consider-using-with" ,
1422
+ node = stack [varname ],
1423
+ )
1424
+ stack [varname ] = value
1321
1425
1322
1426
def _check_consider_using_with (self , node : astroid .Call ):
1427
+ if _is_inside_context_manager (node ):
1428
+ # if we are inside a context manager itself, we assume that it will handle the resource management itself.
1429
+ return
1430
+ if (
1431
+ node
1432
+ in self ._consider_using_with_stack .get_stack_for_frame (
1433
+ node .frame ()
1434
+ ).values ()
1435
+ ):
1436
+ # the result of this call was already assigned to a variable and will be checked when leaving the scope.
1437
+ return
1323
1438
inferred = utils .safe_infer (node .func )
1324
1439
if not inferred :
1325
1440
return
@@ -1332,9 +1447,7 @@ def _check_consider_using_with(self, node: astroid.Call):
1332
1447
and not _is_part_of_with_items (node )
1333
1448
)
1334
1449
)
1335
- if could_be_used_in_with and not (
1336
- _is_inside_context_manager (node ) or _will_be_released_automatically (node )
1337
- ):
1450
+ if could_be_used_in_with and not _will_be_released_automatically (node ):
1338
1451
self .add_message ("consider-using-with" , node = node )
1339
1452
1340
1453
def _check_consider_using_join (self , aug_assign ):
0 commit comments