rspec/rules/S1143/python/rule.adoc
Fred Tingaud d3cfe19d7e
Fix broken or dangerous backquotes
Co-authored-by: Marco Borgeaud <89914223+marco-antognini-sonarsource@users.noreply.github.com>
2023-10-30 10:33:56 +01:00

165 lines
5.8 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

== Why is this an issue?
Using ``++return++``, ``++break++`` or ``++continue++`` in a ``++finally++`` block suppresses the propagation of any unhandled exception which was raised in the ``++try++``, ``++else++`` or ``++except++`` blocks. It will also ignore their return statements.
``https://docs.python.org/3/library/exceptions.html#SystemExit[SystemExit]`` is raised when ``++sys.exit()++`` is called. ``https://docs.python.org/3/library/exceptions.html#KeyboardInterrupt[KeyboardInterrupt]`` is raised when the user asks the program to stop by pressing interrupt keys. Both exceptions are expected to propagate up until the application stops. It is ok to catch them when a clean-up is necessary but they should be raised again immediately. They should never be ignored.
If you need to ignore every other exception you can simply catch the ``++Exception++`` class. However you should be very careful when you do this as it will ignore other important exceptions such as ``https://docs.python.org/3/library/exceptions.html#MemoryError[MemoryError]``
In python 2 it is possible to raise old style classes. You can use a bare ``++except:++`` statement to catch every exception. Remember to still reraise ``++SystemExit++`` and ``++KeyboardInterrupt++``.
This rule raises an issue when a jump statement (``++break++``, ``++continue++``, ``++return++``) would force the control flow to leave a finally block.
=== Noncompliant code example
[source,python]
----
def find_file_which_contains(expected_content, paths):
file = None
for path in paths:
try:
# "open" will raise IsADirectoryError if the provided path is a directory but it will be stopped by the "return" and "continue"
file = open(path, 'r')
actual_content = file.read()
except FileNotFoundError as exception:
# This exception will never pass the "finally" block because of "return" and "continue"
raise ValueError(f"'paths' should only contain existing files. File ${path} does not exist.")
finally:
file.close()
if actual_content != expected_content:
# Note that "continue" is allowed in a "finally" block only since python 3.8
continue # Noncompliant. This will prevent exceptions raised by the "try" block and "except" block from raising.
else:
return path # Noncompliant. Same as for "continue"
return None
# This will return None instead of raising ValueError from the "except" block
find_file_which_contains("some content", ["file_which_does_not_exist"])
# This will return None instead of raising IsADirectoryError from the "try" block
find_file_which_contains("some content", ["a_directory"])
import sys
while True:
try:
sys.exit(1)
except (SystemExit) as e:
print("Exiting")
raise
finally:
break # This will prevent SystemExit from raising
def continue_whatever_happens_noncompliant():
for i in range(10):
try:
raise ValueError()
finally:
continue # Noncompliant
----
=== Compliant solution
[source,python]
----
# Note that using "with open(...) as" would be better. We keep the example as is just for demonstration purpose.
def find_file_which_contains(expected_content, paths):
file = None
for path in paths:
try:
file = open(path, 'r')
actual_content = file.read()
if actual_content != expected_content:
continue
else:
return path
except FileNotFoundError as exception:
raise ValueError(f"'paths' should only contain existing files. File ${path} does not exist.")
finally:
if file:
file.close()
return None
# This raises ValueError
find_file_which_contains("some content", ["file_which_does_not_exist"])
# This raises IsADirectoryError
find_file_which_contains("some content", ["a_directory"])
import sys
while True:
try:
sys.exit(1)
except (SystemExit) as e:
print("Exiting")
raise # SystemExit is re-raised
import logging
def continue_whatever_happens_compliant():
for i in range(10):
try:
raise ValueError()
except Exception:
logging.exception("Failed") # Ignore all "Exception" subclasses yet allow SystemExit and other important exceptions to pass
----
== Resources
* Python documentation - https://docs.python.org/3/reference/compound_stmts.html#except[the ``++try++`` statement]
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
include::../message.adoc[]
'''
== Comments And Links
(visible only on this page)
=== on 3 Mar 2020, 17:27:45 Nicolas Harraudeau wrote:
Note: There is no need to create issues for ``++raise++`` statements because:
* there might be a good reason to raise an exception (Ex: cannot release resource)
* python will link the new exception and the old exception automatically:
----
In [1]: try:
...: raise ValueError("try block")
...: finally:
...: raise TypeError("finally block")
...:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-1-dcce5abc58e4> in <module>
1 try:
----> 2 raise ValueError("try block")
3 finally:
ValueError: try block
During handling of the above exception, another exception occurred:
TypeError Traceback (most recent call last)
<ipython-input-1-dcce5abc58e4> in <module>
2 raise ValueError("try block")
3 finally:
----> 4 raise TypeError("finally block")
5
TypeError: finally block
----
include::../comments-and-links.adoc[]
endif::env-github,rspecator-view[]