Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid `except TypeError or ValueError`
This code should be invalid:

```python
try:
    ...
except TypeError or ValueError:
    ...

```


Discussion:
Basically, all expressions except `ast.Name` and `ast.Attribute`, and `ast.Tuple` (with names and attributes) should be banned in `except` name.
@sobolevn can you provide valid example, cause it's not clear which one should be accepted.
```python
try:
    ...
except (TypeError, ValueError):
    ...

try:
    ...
except TypeError:
    ...
except ValueError:
    ...
```
Both! I have updated the description. I forgot about `ast.Tuple`
> can you provide valid example, cause it's not clear which one should be accepted

Both your examples are valid. The issue is about having an expression inside the `except`. Another example of a bad code:

```python
def f():
    return ValueError

try:
    raise ValueError
except f():
    print('triggered')
```

I've never seen a dynamically defined `except` like in the example above and I don't have in mind when it could be useful. Exceptions that should be caught should be explicitly listed.
Hi!
I am a student looking to contribute, i am sorry but i don't understand this completely, would you mind explaining a bit more on what needs to be achieved so i can send a PR.
@loukikstudent Hi! Awesome that you are interested in contributing to our project!

We need a new violation and a new check for `except` `ast` node.
You can start with looking at similar examples: https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/ast/exceptions.py
@sobolevn Thank you so much for to revert back. I'll definitely look into it!
Hello. Can I take this over?
🤗 
There is a problem with the case of `ast.Tuple` containing anything other than class name or `ast.Attribute`: source code that uses such constructs in *except* handler actually trips assertion checks inside `flake8-bugbear` visitors:

Test file:
```python
try:
    pass
except (ValueError, "oops"):
    print(123)
```

Error output:
```shell
❯ flake8 1.py
Traceback (most recent call last):
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/bin/flake8", line 10, in <module>
    sys.exit(main())
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/flake8/main/cli.py", line 22, in main
    app.run(argv)
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/flake8/main/application.py", line 360, in run
    self._run(argv)
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/flake8/main/application.py", line 348, in _run
    self.run_checks()
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/flake8/main/application.py", line 262, in run_checks
    self.file_checker_manager.run()
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/flake8/checker.py", line 325, in run
    self.run_serial()
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/flake8/checker.py", line 309, in run_serial
    checker.run_checks()
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/flake8/checker.py", line 589, in run_checks
    self.run_ast_checks()
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/flake8/checker.py", line 496, in run_ast_checks
    for (line_number, offset, text, _) in runner:
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/bugbear.py", line 36, in run
    visitor.visit(self.tree)
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/bugbear.py", line 156, in visit
    super().visit(node)
  File "/Users/lensvol/.pyenv/versions/3.7.4/lib/python3.7/ast.py", line 262, in visit
    return visitor(node)
  File "/Users/lensvol/.pyenv/versions/3.7.4/lib/python3.7/ast.py", line 270, in generic_visit
    self.visit(item)
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/bugbear.py", line 156, in visit
    super().visit(node)
  File "/Users/lensvol/.pyenv/versions/3.7.4/lib/python3.7/ast.py", line 262, in visit
    return visitor(node)
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/bugbear.py", line 292, in visit_Try
    self.generic_visit(node)
  File "/Users/lensvol/.pyenv/versions/3.7.4/lib/python3.7/ast.py", line 270, in generic_visit
    self.visit(item)
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/bugbear.py", line 156, in visit
    super().visit(node)
  File "/Users/lensvol/.pyenv/versions/3.7.4/lib/python3.7/ast.py", line 262, in visit
    return visitor(node)
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/bugbear.py", line 165, in visit_ExceptHandler
    names = [_to_name_str(e) for e in node.type.elts]
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/bugbear.py", line 165, in <listcomp>
    names = [_to_name_str(e) for e in node.type.elts]
  File "/Users/lensvol/Library/Caches/pypoetry/virtualenvs/wemake-python-styleguide-py3.7/lib/python3.7/site-packages/bugbear.py", line 130, in _to_name_str
    assert isinstance(node, ast.Attribute)
AssertionError
``` 

So it does not make sense to implement support for them right now, as our code will never be reached. Should I proceed without them?
Can you please report this to `bugbear`? And let's continue without them! 

Resolving patch:
diff --git a/wemake_python_styleguide/logic/tree/exceptions.py b/wemake_python_styleguide/logic/tree/exceptions.py
--- a/wemake_python_styleguide/logic/tree/exceptions.py
+++ b/wemake_python_styleguide/logic/tree/exceptions.py
@@ -3,6 +3,8 @@
 from typing import List, Mapping, Optional, Tuple
 
 from wemake_python_styleguide.logic import source
+from wemake_python_styleguide.logic.walk import is_contained
+from wemake_python_styleguide.types import AnyNodes
 
 
 def get_exception_name(node: ast.Raise) -> Optional[str]:
@@ -63,3 +65,27 @@ def traverse_exception(
         traverse_exception(exc, builtin_exceptions)
 
     return builtin_exceptions
+
+
+def find_returning_nodes(
+    node: ast.Try,
+    bad_returning_nodes: AnyNodes,
+) -> Tuple[bool, bool, bool, bool]:
+    """Find nodes that return value and are inside try/except/else/finally."""
+    try_has = any(
+        is_contained(line, bad_returning_nodes)
+        for line in node.body
+    )
+    except_has = any(
+        is_contained(except_handler, bad_returning_nodes)
+        for except_handler in node.handlers
+    )
+    else_has = any(
+        is_contained(line, bad_returning_nodes)
+        for line in node.orelse
+    )
+    finally_has = any(
+        is_contained(line, bad_returning_nodes)
+        for line in node.finalbody
+    )
+    return try_has, except_has, else_has, finally_has
diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py
--- a/wemake_python_styleguide/violations/best_practices.py
+++ b/wemake_python_styleguide/violations/best_practices.py
@@ -71,6 +71,7 @@
    LoopControlFinallyViolation
    ShebangViolation
    BaseExceptionRaiseViolation
+   NonTrivialExceptViolation
 
 Best practices
 --------------
@@ -130,6 +131,7 @@
 .. autoclass:: LoopControlFinallyViolation
 .. autoclass:: ShebangViolation
 .. autoclass:: BaseExceptionRaiseViolation
+.. autoclass:: NonTrivialExceptViolation
 
 """
 
@@ -2149,3 +2151,46 @@ class BaseExceptionRaiseViolation(ASTViolation):
 
     error_template = 'Found wrong `raise` exception type: {0}'
     code = 454
+
+
+@final
+class NonTrivialExceptViolation(ASTViolation):
+    """
+    Forbids to use non-trivial expressions as a parameter for ``except``.
+
+    Reasoning:
+        Expressions used as an argument for ``except`` could be hard to read
+        and hide real list of exceptions being expected to occur in the outlined
+        code block.
+
+    Solution:
+        Use separate ``except`` blocks for each exception or provide a tuple
+        of exception classes.
+
+    Example::
+
+        # Correct:
+        try:
+            ...
+        except ValueError:
+            ...
+        except TypeError:
+            ...
+
+        try:
+            ...
+        except (TypeError, ValueError):
+            ...
+
+        # Wrong:
+        try:
+            ...
+        except TypeError or ValueError:
+            ...
+
+    .. versionadded:: 0.16.0
+
+    """
+
+    error_template = 'Found non-trivial expression as an argument for "except"'
+    code = 455
diff --git a/wemake_python_styleguide/visitors/ast/exceptions.py b/wemake_python_styleguide/visitors/ast/exceptions.py
--- a/wemake_python_styleguide/visitors/ast/exceptions.py
+++ b/wemake_python_styleguide/visitors/ast/exceptions.py
@@ -1,12 +1,13 @@
 import ast
 from collections import Counter
-from typing import ClassVar, Set, Tuple
+from typing import ClassVar, Set
 
 from typing_extensions import final
 
 from wemake_python_styleguide.compat.aliases import FunctionNodes
 from wemake_python_styleguide.logic import nodes
 from wemake_python_styleguide.logic.tree import exceptions
+from wemake_python_styleguide.logic.tree.exceptions import find_returning_nodes
 from wemake_python_styleguide.logic.walk import is_contained
 from wemake_python_styleguide.types import AnyNodes
 from wemake_python_styleguide.violations.best_practices import (
@@ -14,6 +15,7 @@
     DuplicateExceptionViolation,
     IncorrectExceptOrderViolation,
     LoopControlFinallyViolation,
+    NonTrivialExceptViolation,
     TryExceptMultipleReturnPathViolation,
 )
 from wemake_python_styleguide.violations.consistency import (
@@ -26,29 +28,6 @@
 from wemake_python_styleguide.visitors.base import BaseNodeVisitor
 
 
-def _find_returing_nodes(
-    node: ast.Try,
-    bad_returning_nodes: AnyNodes,
-) -> Tuple[bool, bool, bool, bool]:
-    try_has = any(
-        is_contained(line, bad_returning_nodes)
-        for line in node.body
-    )
-    except_has = any(
-        is_contained(except_handler, bad_returning_nodes)
-        for except_handler in node.handlers
-    )
-    else_has = any(
-        is_contained(line, bad_returning_nodes)
-        for line in node.orelse
-    )
-    finally_has = any(
-        is_contained(line, bad_returning_nodes)
-        for line in node.finalbody
-    )
-    return try_has, except_has, else_has, finally_has
-
-
 @final
 class WrongTryExceptVisitor(BaseNodeVisitor):
     """Responsible for examining ``try`` and friends."""
@@ -100,7 +79,7 @@ def _check_duplicate_exceptions(self, node: ast.Try) -> None:
                 )
 
     def _check_return_path(self, node: ast.Try) -> None:
-        try_has, except_has, else_has, finally_has = _find_returing_nodes(
+        try_has, except_has, else_has, finally_has = find_returning_nodes(
             node, self._bad_returning_nodes,
         )
 
@@ -160,6 +139,8 @@ class WrongExceptHandlerVisitor(BaseNodeVisitor):
 
     _base_exception: ClassVar[str] = 'BaseException'
 
+    _trivial_except_arg_nodes: ClassVar[AnyNodes] = (ast.Name, ast.Attribute)
+
     def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
         """
         Checks all ``ExceptionHandler`` nodes.
@@ -171,6 +152,7 @@ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
         """
         self._check_useless_except(node)
         self._check_exception_type(node)
+        self._check_except_expression(node)
         self.generic_visit(node)
 
     def _check_useless_except(self, node: ast.ExceptHandler) -> None:
@@ -198,3 +180,21 @@ def _check_exception_type(self, node: ast.ExceptHandler) -> None:
         exception_id = getattr(exception_name, 'id', None)
         if exception_id == self._base_exception:
             self.add_violation(BaseExceptionViolation(node))
+
+    def _check_except_expression(self, node: ast.ExceptHandler) -> None:
+        # Catch-all 'except' is actually okay in this case
+        if node.type is None:
+            return
+
+        if isinstance(node.type, self._trivial_except_arg_nodes):
+            return
+
+        if isinstance(node.type, ast.Tuple):
+            all_elements_are_trivial = all((
+                isinstance(element, self._trivial_except_arg_nodes)
+                for element in node.type.elts
+            ))
+            if all_elements_are_trivial:
+                return
+
+        self.add_violation(NonTrivialExceptViolation(node.type))


Test patch:
diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py
--- a/tests/fixtures/noqa/noqa.py
+++ b/tests/fixtures/noqa/noqa.py
@@ -715,3 +715,10 @@ def consecutive_yields():
 
 def raise_bad_exception():
     raise Exception  # noqa: WPS454
+
+
+try:
+    cause_errors()
+except ValueError or TypeError:  # noqa: WPS455
+    my_print("Oops.")
+
diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py
--- a/tests/test_checker/test_noqa.py
+++ b/tests/test_checker/test_noqa.py
@@ -223,6 +223,7 @@
     'WPS452': 1,  # also defined in version specific table.
     'WPS453': 0,
     'WPS454': 1,
+    'WPS455': 1,
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_exceptions/test_except_block_expression.py b/tests/test_visitors/test_ast/test_exceptions/test_except_block_expression.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_exceptions/test_except_block_expression.py
@@ -0,0 +1,90 @@
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    NonTrivialExceptViolation,
+)
+from wemake_python_styleguide.visitors.ast.exceptions import (
+    WrongExceptHandlerVisitor,
+)
+
+use_complex_expression = """
+try:
+    ...
+except ValueError or TypeError:
+    ...
+"""
+
+use_name_expression = """
+try:
+    ...
+except ValueError:
+    ...
+"""
+
+use_attribute_expression = """
+try:
+    ...
+except exceptions.CustomException:
+    ...
+"""
+
+use_null_expression = """
+try:
+    ...
+except:
+    ...
+"""
+
+use_tuple_with_allowed_expression = """
+try:
+    ...
+except (ValueError, TypeError):
+    ...
+"""
+
+use_tuple_with_forbidden_expressions = """
+try:
+    ...
+except (ValueError, 1 + 2, "sad"):
+    ...
+"""
+
+
+@pytest.mark.parametrize('code', [
+    use_complex_expression,
+    use_tuple_with_forbidden_expressions,
+])
+def test_use_base_exception(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing that `except ValueError or TypeError:` is restricted."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongExceptHandlerVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [NonTrivialExceptViolation])
+
+
+@pytest.mark.parametrize('code', [
+    use_name_expression,
+    use_attribute_expression,
+    use_tuple_with_allowed_expression,
+    use_null_expression,
+])
+def test_use_exception(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing that trivial cases are allowed."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongExceptHandlerVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])