66import itertools
77import tokenize
88from functools import reduce
9- from typing import List , Optional , Tuple , Union , cast
9+ from typing import Dict , Iterator , List , NamedTuple , Optional , Tuple , Union , cast
1010
1111import astroid
12+ from astroid .util import Uninferable
1213
1314from pylint import checkers , interfaces
1415from pylint import utils as lint_utils
4142 "tarfile.TarFile" ,
4243 "tarfile.TarFile.open" ,
4344 "multiprocessing.context.BaseContext.Pool" ,
44- "concurrent.futures.thread.ThreadPoolExecutor" ,
45- "concurrent.futures.process.ProcessPoolExecutor" ,
4645 "subprocess.Popen" ,
4746 )
4847)
@@ -147,6 +146,33 @@ def _will_be_released_automatically(node: astroid.Call) -> bool:
147146 return func .qname () in callables_taking_care_of_exit
148147
149148
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+
150176class RefactoringChecker (checkers .BaseTokenChecker ):
151177 """Looks for code which can be refactored
152178
@@ -416,6 +442,7 @@ class RefactoringChecker(checkers.BaseTokenChecker):
416442 def __init__ (self , linter = None ):
417443 checkers .BaseTokenChecker .__init__ (self , linter )
418444 self ._return_nodes = {}
445+ self ._consider_using_with_stack = ConsiderUsingWithStack ()
419446 self ._init ()
420447 self ._never_returning_functions = None
421448
@@ -425,6 +452,7 @@ def _init(self):
425452 self ._nested_blocks_msg = None
426453 self ._reported_swap_nodes = set ()
427454 self ._can_simplify_bool_op = False
455+ self ._consider_using_with_stack .clear_all ()
428456
429457 def open (self ):
430458 # do this in open since config not fully initialized in __init__
@@ -543,7 +571,12 @@ def process_tokens(self, tokens):
543571 if self .linter .is_message_enabled ("trailing-comma-tuple" ):
544572 self .add_message ("trailing-comma-tuple" , line = token .start [0 ])
545573
574+ @utils .check_messages ("consider-using-with" )
546575 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+ )
547580 self ._init ()
548581
549582 @utils .check_messages ("too-many-nested-blocks" )
@@ -593,7 +626,14 @@ def visit_excepthandler(self, node):
593626
594627 @utils .check_messages ("redefined-argument-from-local" )
595628 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
597637 if not names :
598638 continue
599639 for name in names .nodes_of_class (astroid .AssignName ):
@@ -818,9 +858,12 @@ def _check_simplifiable_ifexp(self, node):
818858 self .add_message ("simplifiable-if-expression" , node = node , args = (reduced_to ,))
819859
820860 @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" ,
822865 )
823- def leave_functiondef (self , node ) :
866+ def leave_functiondef (self , node : astroid . FunctionDef ) -> None :
824867 # check left-over nested blocks stack
825868 self ._emit_nested_blocks_message_if_needed (self ._nested_blocks )
826869 # new scope = reinitialize the stack of nested blocks
@@ -830,6 +873,19 @@ def leave_functiondef(self, node):
830873 # check for single return or return None at the end
831874 self ._check_return_at_the_end (node )
832875 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 ()
833889
834890 @utils .check_messages ("stop-iteration-return" )
835891 def visit_raise (self , node ):
@@ -1021,6 +1077,10 @@ def _emit_nested_blocks_message_if_needed(self, nested_blocks):
10211077 args = (len (nested_blocks ), self .config .max_nested_blocks ),
10221078 )
10231079
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+
10241084 @staticmethod
10251085 def _duplicated_isinstance_types (node ):
10261086 """Get the duplicated types from the underlying isinstance calls.
@@ -1282,12 +1342,22 @@ def _check_swap_variables(self, node):
12821342 message = "consider-swap-variables"
12831343 self .add_message (message , node = node )
12841344
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+
12851355 @utils .check_messages (
12861356 "simplify-boolean-expression" ,
12871357 "consider-using-ternary" ,
12881358 "consider-swap-variables" ,
12891359 )
1290- def visit_assign (self , node ) :
1360+ def visit_return (self , node : astroid . Return ) -> None :
12911361 self ._check_swap_variables (node )
12921362 if self ._is_and_or_ternary (node .value ):
12931363 cond , truth_value , false_value = self ._and_or_ternary_arguments (node .value )
@@ -1317,9 +1387,54 @@ def visit_assign(self, node):
13171387 )
13181388 self .add_message (message , node = node , args = (suggestion ,))
13191389
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
13211425
13221426 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
13231438 inferred = utils .safe_infer (node .func )
13241439 if not inferred :
13251440 return
@@ -1332,9 +1447,7 @@ def _check_consider_using_with(self, node: astroid.Call):
13321447 and not _is_part_of_with_items (node )
13331448 )
13341449 )
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 ):
13381451 self .add_message ("consider-using-with" , node = node )
13391452
13401453 def _check_consider_using_join (self , aug_assign ):
0 commit comments