summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog4
-rw-r--r--doc/whatsnew/2.10.rst3
-rw-r--r--pylint/checkers/refactoring/refactoring_checker.py7
-rw-r--r--tests/functional/c/consider/consider_using_with_open.py93
-rw-r--r--tests/functional/c/consider/consider_using_with_open.txt11
5 files changed, 111 insertions, 7 deletions
diff --git a/ChangeLog b/ChangeLog
index 26c82cf1e..d514a10be 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -19,6 +19,10 @@ What's New in Pylint 2.10.3?
============================
Release date: TBA
+ * Fix false positive for ``consider-using-with`` if a context manager is assigned to a
+ variable in different paths of control flow (e. g. if-else clause).
+
+ Closes #4751
What's New in Pylint 2.10.2?
diff --git a/doc/whatsnew/2.10.rst b/doc/whatsnew/2.10.rst
index 85cba1c3f..09fc52870 100644
--- a/doc/whatsnew/2.10.rst
+++ b/doc/whatsnew/2.10.rst
@@ -82,6 +82,9 @@ Other Changes
* ``consider-using-with`` is no longer triggered if a context manager is returned from a function.
+* Fix false positive for ``consider-using-with`` if a context manager is assigned to a
+ variable in different paths of control flow (e. g. if-else clause).
+
* pylint does not crash with a traceback anymore when a file is problematic. It
creates a template text file for opening an issue on the bug tracker instead.
The linting can go on for other non problematic files instead of being impossible.
diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py
index 7a3048ab4..9ce42e395 100644
--- a/pylint/checkers/refactoring/refactoring_checker.py
+++ b/pylint/checkers/refactoring/refactoring_checker.py
@@ -1442,10 +1442,15 @@ class RefactoringChecker(checkers.BaseTokenChecker):
else assignee.attrname
)
if varname in stack:
+ existing_node = stack[varname]
+ if astroid.are_exclusive(node, existing_node):
+ # only one of the two assignments can be executed at runtime, thus it is fine
+ stack[varname] = value
+ continue
# variable was redefined before it was used in a ``with`` block
self.add_message(
"consider-using-with",
- node=stack[varname],
+ node=existing_node,
)
stack[varname] = value
diff --git a/tests/functional/c/consider/consider_using_with_open.py b/tests/functional/c/consider/consider_using_with_open.py
index ca807459e..7c584d8f5 100644
--- a/tests/functional/c/consider/consider_using_with_open.py
+++ b/tests/functional/c/consider/consider_using_with_open.py
@@ -1,4 +1,4 @@
-# pylint: disable=missing-function-docstring, missing-module-docstring, invalid-name, import-outside-toplevel
+# pylint: disable=missing-function-docstring, missing-module-docstring, invalid-name, import-outside-toplevel, no-self-use
# pylint: disable=missing-class-docstring, too-few-public-methods, unused-variable, multiple-statements, line-too-long
"""
The functional test for the standard ``open()`` function has to be moved in a separate file,
@@ -7,6 +7,7 @@ However, all remaining checks for consider-using-with work in PyPy, so we do not
PyPy from ALL functional tests.
"""
from contextlib import contextmanager
+from pathlib import Path
myfile = open("test.txt", encoding="utf-8") # [consider-using-with]
@@ -68,3 +69,93 @@ def test_multiline_with_items(file1, file2, which):
def test_suppress_on_return():
return open("foo", encoding="utf8") # must not trigger
+
+
+class TestControlFlow:
+ """
+ The message is triggered if a context manager is assigned to a variable, which name is later
+ reassigned without the variable being used inside a ``with`` first.
+ E.g. the following would trigger the message:
+
+ a = open("foo") # <-- would trigger here
+ a = "something new"
+
+ But it must not happen that the logic which checks if the same variable is assigned multiple
+ times in different code branches where only one of those assign statements is hit at runtime.
+ For example, the variable could be assigned in an if-else construct.
+
+ These tests check that the message is not triggered in those circumstances.
+ """
+
+ def test_defined_in_if_and_else(self, predicate):
+ if predicate:
+ file_handle = open("foo", encoding="utf8") # must not trigger
+ else:
+ file_handle = open("bar", encoding="utf8") # must not trigger
+ with file_handle:
+ return file_handle.read()
+
+ def test_defined_in_else_only(self, predicate):
+ if predicate:
+ result = "shiny watermelon"
+ else:
+ file_handle = open("foo", encoding="utf8") # must not trigger
+ with file_handle:
+ result = file_handle.read()
+ return result
+
+ def test_defined_in_if_only(self, predicate):
+ if predicate:
+ file_handle = open("foo", encoding="utf8") # must not trigger
+ with file_handle:
+ result = file_handle.read()
+ else:
+ result = "shiny watermelon"
+ return result
+
+ def test_triggers_if_reassigned_after_if_else(self, predicate):
+ if predicate:
+ file_handle = open("foo", encoding="utf8")
+ else:
+ file_handle = open( # [consider-using-with]
+ "bar", encoding="utf8"
+ )
+ file_handle = None
+ return file_handle
+
+ def test_defined_in_try_and_except(self):
+ try:
+ file_handle = open("foo", encoding="utf8") # must not trigger
+ except FileNotFoundError:
+ file_handle = open("bar", encoding="utf8") # must not trigger
+ with file_handle:
+ return file_handle.read()
+
+ def test_defined_in_try_and_finally(self):
+ try:
+ file_handle = open("foo", encoding="utf8") # must not trigger
+ except FileNotFoundError:
+ Path("foo").touch()
+ finally:
+ file_handle.open("foo", encoding="utf") # must not trigger
+ with file_handle:
+ return file_handle.read()
+
+ def test_defined_in_different_except_handlers(self, a, b):
+ try:
+ result = a/b
+ except ZeroDivisionError:
+ logfile = open("math_errors.txt", encoding="utf8") # must not trigger
+ result = "Can't divide by zero"
+ except TypeError:
+ logfile = open("type_errors.txt", encoding="utf8") # must not trigger
+ result = "Wrong types"
+ else:
+ logfile = open("results.txt", encoding="utf8") # must not trigger
+ with logfile:
+ logfile.write(result)
+
+ def test_multiple_return_statements(self, predicate):
+ if predicate:
+ return open("foo", encoding="utf8") # must not trigger
+ return open("bar", encoding="utf8") # must not trigger
diff --git a/tests/functional/c/consider/consider_using_with_open.txt b/tests/functional/c/consider/consider_using_with_open.txt
index ea3503123..cf3fdc1c8 100644
--- a/tests/functional/c/consider/consider_using_with_open.txt
+++ b/tests/functional/c/consider/consider_using_with_open.txt
@@ -1,5 +1,6 @@
-consider-using-with:11:9::Consider using 'with' for resource-allocating operations
-consider-using-with:15:9:test_open:Consider using 'with' for resource-allocating operations
-consider-using-with:45:4:test_open_outside_assignment:Consider using 'with' for resource-allocating operations
-consider-using-with:46:14:test_open_outside_assignment:Consider using 'with' for resource-allocating operations
-consider-using-with:51:8:test_open_inside_with_block:Consider using 'with' for resource-allocating operations
+consider-using-with:12:9::Consider using 'with' for resource-allocating operations:HIGH
+consider-using-with:16:9:test_open:Consider using 'with' for resource-allocating operations:HIGH
+consider-using-with:46:4:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:HIGH
+consider-using-with:47:14:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:HIGH
+consider-using-with:52:8:test_open_inside_with_block:Consider using 'with' for resource-allocating operations:HIGH
+consider-using-with:120:26:TestControlFlow.test_triggers_if_reassigned_after_if_else:Consider using 'with' for resource-allocating operations:HIGH