- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 1.2k
 
[contextmanager-generator-missing-cleanup][new feature] Warn about @contextlib.contextmanager without try/finally in generator functions #9133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
          
     Merged
      
      
            jacobtylerwalls
  merged 32 commits into
  pylint-dev:main
from
rhyn0:contextmanager-generator-2832
  
      
      
   
  May 12, 2024 
      
    
  
     Merged
                    Changes from all commits
      Commits
    
    
            Show all changes
          
          
            32 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      062373e
              
                add: initial layout for new error
              
              
                rhyn0 b6774f6
              
                add: test cases for new checker
              
              
                rhyn0 b0547eb
              
                add: first pass at new checker
              
              
                rhyn0 72068a2
              
                add: towncrier fragment for changes
              
              
                rhyn0 81c7ab6
              
                add: suggestions, better logic for this warning
              
              
                rhyn0 9be51c2
              
                add: good and bad example under doc
              
              
                rhyn0 314158b
              
                update: functional tests to use new message
              
              
                rhyn0 74841ab
              
                update: other tests to exclude new warning
              
              
                rhyn0 f0d9fad
              
                change: keep new checker name under 'basic'
              
              
                rhyn0 06204d7
              
                change: update positive/negative examples to new spec for this feature
              
              
                rhyn0 9928473
              
                Fix other pylint rule violations in test data, update expected output
              
              
                rhyn0 6c95035
              
                change: new checker logic for refined 'contextmanager_generator_missi…
              
              
                rhyn0 eef0ac0
              
                update: test files with unused suppress flags, add suppress flags
              
              
                rhyn0 8022d8d
              
                fix: CI docstring spelling errors
              
              
                rhyn0 3ec527d
              
                add: test cases for code coverage, order of functions and various Wit…
              
              
                rhyn0 f1b6eb9
              
                disable wrong spelling for AsyncWith
              
              
                rhyn0 7f3874d
              
                add more test cases from Primer errors
              
              
                rhyn0 eb40479
              
                remove 'spelling mistakes' sections, handle Primer errors
              
              
                rhyn0 1558bcd
              
                remove unused suppressions from 'eef0ac00d'
              
              
                rhyn0 4ac898a
              
                add negative test case for decorated usage of contextmanager
              
              
                rhyn0 5c50505
              
                rework checker to use simpler logic from utils.safe_infer
              
              
                rhyn0 de54fc4
              
                reword feature fragment, explain purpose more in depth
              
              
                rhyn0 f3f0876
              
                fix comment in good examples
              
              
                rhyn0 3c365ce
              
                use inference to check for handler of GeneratorExit
              
              
                rhyn0 8730bc7
              
                don't raise for contextmanagers that handle base Exception or bare Ex…
              
              
                rhyn0 1f3cb77
              
                reword top level error message
              
              
                rhyn0 03c5562
              
                remove unneccessary disable comment, update expected output
              
              
                rhyn0 d87ebac
              
                add details and related links to error message documentation
              
              
                rhyn0 4d36753
              
                remove TODO from checker
              
              
                rhyn0 db8db8f
              
                revert test breaking formatting changes
              
              
                rhyn0 b89e7df
              
                update: documentation about new checker, grammar
              
              
                rhyn0 3bd7132
              
                fix: make checking for generators with try blocks cheaper
              
              
                rhyn0 File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
There are no files selected for viewing
        
          
          
            14 changes: 14 additions & 0 deletions
          
          14 
        
  doc/data/messages/c/contextmanager-generator-missing-cleanup/bad.py
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import contextlib | ||
| 
     | 
||
| 
     | 
||
| @contextlib.contextmanager | ||
| def cm(): | ||
| contextvar = "acquired context" | ||
| print("cm enter") | ||
| yield contextvar | ||
| print("cm exit") | ||
| 
     | 
||
| 
     | 
||
| def genfunc_with_cm(): # [contextmanager-generator-missing-cleanup] | ||
| with cm() as context: | ||
| yield context * 2 | ||
        
          
          
            10 changes: 10 additions & 0 deletions
          
          10 
        
  doc/data/messages/c/contextmanager-generator-missing-cleanup/details.rst
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| Instantiating and using a contextmanager inside a generator function can | ||
| result in unexpected behavior if there is an expectation that the context is only | ||
| available for the generator function. In the case that the generator is not closed or destroyed | ||
| then the context manager is held suspended as is. | ||
| 
     | 
||
| This message warns on the generator function instead of the contextmanager function | ||
| because the ways to use a contextmanager are many. | ||
| A contextmanager can be used as a decorator (which immediately has ``__enter__``/``__exit__`` applied) | ||
| and the use of ``as ...`` or discard of the return value also implies whether the context needs cleanup or not. | ||
| So for this message, warning the invoker of the contextmanager is important. | ||
                
      
                  rhyn0 marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
        
          
          
            49 changes: 49 additions & 0 deletions
          
          49 
        
  doc/data/messages/c/contextmanager-generator-missing-cleanup/good.py
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import contextlib | ||
| 
     | 
||
| 
     | 
||
| @contextlib.contextmanager | ||
| def good_cm_except(): | ||
| contextvar = "acquired context" | ||
| print("good cm enter") | ||
| try: | ||
| yield contextvar | ||
| except GeneratorExit: | ||
| print("good cm exit") | ||
| 
     | 
||
| 
     | 
||
| def genfunc_with_cm(): | ||
| with good_cm_except() as context: | ||
| yield context * 2 | ||
| 
     | 
||
| 
     | 
||
| def genfunc_with_discard(): | ||
| with good_cm_except(): | ||
| yield "discarded" | ||
| 
     | 
||
| 
     | 
||
| @contextlib.contextmanager | ||
| def good_cm_yield_none(): | ||
| print("good cm enter") | ||
| yield | ||
| print("good cm exit") | ||
| 
     | 
||
| 
     | 
||
| def genfunc_with_none_yield(): | ||
| with good_cm_yield_none() as var: | ||
| print(var) | ||
| yield "constant yield" | ||
| 
     | 
||
| 
     | 
||
| @contextlib.contextmanager | ||
| def good_cm_finally(): | ||
| contextvar = "acquired context" | ||
| print("good cm enter") | ||
| try: | ||
| yield contextvar | ||
| finally: | ||
| print("good cm exit") | ||
| 
     | 
||
| 
     | 
||
| def good_cm_finally_genfunc(): | ||
| with good_cm_finally() as context: | ||
| yield context * 2 | 
        
          
          
            2 changes: 2 additions & 0 deletions
          
          2 
        
  doc/data/messages/c/contextmanager-generator-missing-cleanup/related.rst
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| - `Rationale <https://discuss.python.org/t/preventing-yield-inside-certain-context-managers/1091>`_ | ||
| - `CPython Issue <https://github.com/python/cpython/issues/81924#issuecomment-1093830682>`_ | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| Checks for generators that use contextmanagers that don't handle cleanup properly. | ||
| Is meant to raise visibilty on the case that a generator is not fully exhausted and the contextmanager is not cleaned up properly. | ||
| A contextmanager must yield a non-constant value and not handle cleanup for GeneratorExit. | ||
| The using generator must attempt to use the yielded context value `with x() as y` and not just `with x()`. | ||
| 
     | 
||
| Closes #2832 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html | ||
| # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE | ||
| # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt | ||
| 
     | 
||
| """Function checker for Python code.""" | ||
| 
     | 
||
| from __future__ import annotations | ||
| 
     | 
||
| from itertools import chain | ||
| 
     | 
||
| from astroid import nodes | ||
| 
     | 
||
| from pylint.checkers import utils | ||
| from pylint.checkers.base.basic_checker import _BasicChecker | ||
| 
     | 
||
| 
     | 
||
| class FunctionChecker(_BasicChecker): | ||
| """Check if a function definition handles possible side effects.""" | ||
| 
     | 
||
| msgs = { | ||
| "W0135": ( | ||
| "The context used in function %r will not be exited.", | ||
| "contextmanager-generator-missing-cleanup", | ||
| "Used when a contextmanager is used inside a generator function" | ||
| " and the cleanup is not handled.", | ||
| ) | ||
| } | ||
| 
     | 
||
| @utils.only_required_for_messages("contextmanager-generator-missing-cleanup") | ||
| def visit_functiondef(self, node: nodes.FunctionDef) -> None: | ||
| self._check_contextmanager_generator_missing_cleanup(node) | ||
| 
     | 
||
| @utils.only_required_for_messages("contextmanager-generator-missing-cleanup") | ||
| def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None: | ||
| self._check_contextmanager_generator_missing_cleanup(node) | ||
| 
     | 
||
| def _check_contextmanager_generator_missing_cleanup( | ||
| self, node: nodes.FunctionDef | ||
| ) -> None: | ||
| """Check a FunctionDef to find if it is a generator | ||
| that uses a contextmanager internally. | ||
| 
     | 
||
| If it is, check if the contextmanager is properly cleaned up. Otherwise, add message. | ||
| 
     | 
||
| :param node: FunctionDef node to check | ||
| :type node: nodes.FunctionDef | ||
| """ | ||
| # if function does not use a Yield statement, it cant be a generator | ||
| with_nodes = list(node.nodes_of_class(nodes.With)) | ||
| if not with_nodes: | ||
| return | ||
| # check for Yield inside the With statement | ||
| yield_nodes = list( | ||
| chain.from_iterable( | ||
| with_node.nodes_of_class(nodes.Yield) for with_node in with_nodes | ||
| ) | ||
| ) | ||
| if not yield_nodes: | ||
| return | ||
| 
     | 
||
| # infer the call that yields a value, and check if it is a contextmanager | ||
| for with_node in with_nodes: | ||
| for call, held in with_node.items: | ||
| if held is None: | ||
| # if we discard the value, then we can skip checking it | ||
| continue | ||
| 
     | 
||
| # safe infer is a generator | ||
| inferred_node = getattr(utils.safe_infer(call), "parent", None) | ||
| if not isinstance(inferred_node, nodes.FunctionDef): | ||
| continue | ||
| if self._node_fails_contextmanager_cleanup(inferred_node, yield_nodes): | ||
| self.add_message( | ||
| "contextmanager-generator-missing-cleanup", | ||
| node=node, | ||
| args=(node.name,), | ||
| ) | ||
| 
     | 
||
| @staticmethod | ||
| def _node_fails_contextmanager_cleanup( | ||
| node: nodes.FunctionDef, yield_nodes: list[nodes.Yield] | ||
| ) -> bool: | ||
| """Check if a node fails contextmanager cleanup. | ||
| 
     | 
||
| Current checks for a contextmanager: | ||
| - only if the context manager yields a non-constant value | ||
| - only if the context manager lacks a finally, or does not catch GeneratorExit | ||
| 
     | 
||
| :param node: Node to check | ||
| :type node: nodes.FunctionDef | ||
| :return: True if fails, False otherwise | ||
| :param yield_nodes: List of Yield nodes in the function body | ||
| :type yield_nodes: list[nodes.Yield] | ||
| :rtype: bool | ||
| """ | ||
| 
     | 
||
| def check_handles_generator_exceptions(try_node: nodes.Try) -> bool: | ||
| # needs to handle either GeneratorExit, Exception, or bare except | ||
| for handler in try_node.handlers: | ||
| if handler.type is None: | ||
| # handles all exceptions (bare except) | ||
| return True | ||
| inferred = utils.safe_infer(handler.type) | ||
| if inferred and inferred.qname() in { | ||
| "builtins.GeneratorExit", | ||
| "builtins.Exception", | ||
| }: | ||
| return True | ||
| return False | ||
| 
     | 
||
| # if context manager yields a non-constant value, then continue checking | ||
| if any( | ||
| yield_node.value is None or isinstance(yield_node.value, nodes.Const) | ||
| for yield_node in yield_nodes | ||
| ): | ||
| return False | ||
| # if function body has multiple Try, filter down to the ones that have a yield node | ||
| try_with_yield_nodes = [ | ||
| try_node | ||
| for try_node in node.nodes_of_class(nodes.Try) | ||
| if any(try_node.nodes_of_class(nodes.Yield)) | ||
| ] | ||
| if not try_with_yield_nodes: | ||
| # no try blocks at all, so checks after this line do not apply | ||
| return True | ||
| # if the contextmanager has a finally block, then it is fine | ||
| if all(try_node.finalbody for try_node in try_with_yield_nodes): | ||
| return False | ||
| # if the contextmanager catches GeneratorExit, then it is fine | ||
| if all( | ||
                
      
                  rhyn0 marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| check_handles_generator_exceptions(try_node) | ||
| for try_node in try_with_yield_nodes | ||
| ): | ||
| return False | ||
| return True | ||
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| consider-using-with:10:9:10:43::Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:14:9:14:43:test_open:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:44:4:44:33:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:45:14:45:43:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:50:8:50:37:test_open_inside_with_block:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:118:26:120:13:TestControlFlow.test_triggers_if_reassigned_after_if_else:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| used-before-assignment:139:12:139:23:TestControlFlow.test_defined_in_try_and_finally:Using variable 'file_handle' before assignment:CONTROL_FLOW | ||
| consider-using-with:11:9:11:43::Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:15:9:15:43:test_open:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:45:4:45:33:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:46:14:46:43:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:51:8:51:37:test_open_inside_with_block:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| consider-using-with:119:26:121:13:TestControlFlow.test_triggers_if_reassigned_after_if_else:Consider using 'with' for resource-allocating operations:UNDEFINED | ||
| used-before-assignment:140:12:140:23:TestControlFlow.test_defined_in_try_and_finally:Using variable 'file_handle' before assignment:CONTROL_FLOW | 
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
Uh oh!
There was an error while loading. Please reload this page.