Repository: wemake-services/wemake-python-styleguide

Issue:
WPS503 on for ... else with except in loop
### What's wrong

wps reports a useless else clause after a for, but I don't think the clause is useless

```
"""Test WPS503."""


def action_one(key: int) -> int:
    """
    Do some complicated logic for group of keys.

    Condition key % 2 != 0 and key < 10 is just example

    :param key: input_value
    :return: action result
    :raises ValueError: if key not in allowed list
    """
    if key % 2 != 0 and key < 10:
        raise ValueError()
    return key * 2


def action_two(key: int) -> int:
    """
    Do some complicated logic for group of keys.

    Condition key % 3 != 0 and key > 10 is just example

    :param key: input_value
    :return: action result
    :raises ValueError: if key not in allowed list
    """
    if key % 3 != 0 and key > 10:
        raise ValueError()
    return key * 3


# we have many actions
actions = [action_one, action_two]

input_value = 1

# We need to loop over actions, and stop after any action that can be applied.
# If not actions can be applied we raise ValueError
for action in actions:
    try:
        output_value = action(input_value)
    except ValueError:
        continue
    break
else:
    raise ValueError()

```

I got 
```
WPS503 Found useless returning `else` statement
```




### How it should be

I think the for ... else clause here is valid and should not be reported as a violation.

### Flake8 version and plugins

{
  "dependencies": [],
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.8.10",
    "system": "Linux"
  },
  "plugins": [
    {
      "is_local": false,
      "plugin": "flake8-bandit",
      "version": "2.1.2"
    },
    {
      "is_local": false,
      "plugin": "flake8-broken-line",
      "version": "0.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-bugbear",
      "version": "20.11.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-comprehensions",
      "version": "3.5.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-darglint",
      "version": "1.8.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-debugger",
      "version": "4.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-docstrings",
      "version": "1.6.0, pydocstyle: 6.1.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-eradicate",
      "version": "1.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-string-format",
      "version": "0.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_commas",
      "version": "2.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_isort",
      "version": "4.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_quotes",
      "version": "3.2.0"
    },
    {
      "is_local": false,
      "plugin": "mccabe",
      "version": "0.6.1"
    },
    {
      "is_local": false,
      "plugin": "naming",
      "version": "0.11.1"
    },
    {
      "is_local": false,
      "plugin": "pycodestyle",
      "version": "2.7.0"
    },
    {
      "is_local": false,
      "plugin": "pyflakes",
      "version": "2.3.1"
    },
    {
      "is_local": false,
      "plugin": "rst-docstrings",
      "version": "0.2.3"
    },
    {
      "is_local": false,
      "plugin": "wemake_python_styleguide",
      "version": "0.15.3"
    }
  ],
  "version": "3.9.2"
}


### pip information

pip 21.0.1 from /home/private_user_name/private_project_name/venv/lib/python3.8/site-packages/pip (python 3.8)
absl-py==0.13.0
adal==1.2.7
adlfs==0.7.1
aiohttp==3.7.4.post0
alembic==1.4.1
aliyun-python-sdk-core==2.13.35
aliyun-python-sdk-core-v3==2.13.32
aliyun-python-sdk-kms==2.14.0
anyio==3.3.1
appdirs==1.4.4
argcomplete==1.12.3
argon2-cffi==21.1.0
astor==0.8.1
astroid==2.5.6
astunparse==1.6.3
async-timeout==3.0.1
atpublic==2.3
attrs==21.2.0
azure-core==1.14.0
azure-datalake-store==0.0.52
azure-identity==1.6.0
azure-storage-blob==12.6.0
Babel==2.9.1
backcall==0.2.0
bandit==1.7.0
bcrypt==3.2.0
bleach==4.1.0
blis==0.7.4
boto3==1.17.76
botocore==1.20.76
cache-decorator==2.0.3
cached-property==1.5.2
cachetools==4.2.2
catalogue==2.0.6
certifi==2020.12.5
cffi==1.14.5
chardet==4.0.0
click==7.1.2
cloudpickle==2.0.0
colorama==0.4.4
commonmark==0.9.1
compress-json==1.0.4
compress-pickle==2.0.1
configobj==5.0.6
coverage==5.5
crcmod==1.7
cryptography==3.4.7
cymem==2.0.5
darglint==1.8.0
databricks-cli==0.15.0
debugpy==1.4.3
decorator==4.4.2
deflate-dict==1.0.9
defusedxml==0.7.1
dependency-injector==4.32.3
dict-hash==1.1.16
dictdiffer==0.8.1
diskcache==5.2.1
distro==1.5.0
docker==5.0.2
docopt==0.6.2
docutils==0.17.1
dpath==2.0.1
dulwich==0.20.21
dvc==2.1.0
entrypoints==0.3
environs==9.3.2
eradicate==2.0.0
flake8==3.9.2
flake8-bandit==2.1.2
flake8-broken-line==0.3.0
flake8-bugbear==20.11.1
flake8-commas==2.0.0
flake8-comprehensions==3.5.0
flake8-debugger==4.0.0
flake8-docstrings==1.6.0
flake8-eradicate==1.0.0
flake8-isort==4.0.0
flake8-polyfill==1.0.2
flake8-quotes==3.2.0
flake8-rst-docstrings==0.2.3
flake8-string-format==0.3.0
Flask==2.0.1
flatbuffers==1.12
flatten-dict==0.4.0
flufl.lock==3.2
fsspec==0.9.0
ftfy==6.0.1
funcy==1.16
future==0.18.2
gast==0.3.3
gcsfs==0.8.0
gitdb==4.0.7
GitPython==3.1.17
google-api-core==1.27.0
google-api-python-client==2.5.0
google-auth==1.30.0
google-auth-httplib2==0.1.0
google-auth-oauthlib==0.4.4
google-pasta==0.2.0
googleapis-common-protos==1.53.0
gql==2.0.0
gql-query-builder==0.1.5
grandalf==0.6
graphql-core==2.3.2
greenlet==1.1.0
grpcio==1.32.0
gunicorn==20.1.0
h5py==2.10.0
hdfs==2.5.8
httplib2==0.19.1
humanize==3.5.0
idna==2.10
importlib-metadata==4.8.1
iniconfig==1.1.1
invoke==1.5.0
ipykernel==6.4.1
ipython==7.27.0
ipython-genutils==0.2.0
ipywidgets==7.6.5
isodate==0.6.0
isort==5.8.0
itsdangerous==2.0.1
jedi==0.18.0
Jinja2==3.0.1
jmespath==0.10.0
joblib==1.0.1
json5==0.9.6
jsonpath-ng==1.5.2
jsonpickle==2.0.0
jsonschema==3.2.0
jupyter==1.0.0
jupyter-client==7.0.3
jupyter-console==6.4.0
jupyter-contrib-core==0.3.3
jupyter-contrib-nbextensions==0.5.1
jupyter-core==4.8.1
jupyter-highlight-selected-word==0.2.0
jupyter-latex-envs==1.4.6
jupyter-nbextensions-configurator==0.4.1
jupyter-server==1.11.0
jupyterlab==3.0.16
jupyterlab-pygments==0.1.2
jupyterlab-server==2.8.1
jupyterlab-widgets==1.0.2
Keras==2.4.3
Keras-Preprocessing==1.1.2
knack==0.8.2
lazy-object-proxy==1.6.0
lxml==4.6.3
mailchecker==4.0.7
Mako==1.1.5
Markdown==3.3.4
MarkupSafe==2.0.1
marshmallow==3.12.1
matplotlib-inline==0.1.3
mccabe==0.6.1
mistune==0.8.4
mlflow==1.20.2
msal==1.12.0
msal-extensions==0.3.0
msrest==0.6.21
msrestazure==0.6.4
multidict==5.1.0
murmurhash==1.0.5
mypy==0.812
mypy-extensions==0.4.3
nanotime==0.5.2
nbclassic==0.3.2
nbclient==0.5.4
nbconvert==6.1.0
nbformat==5.1.3
nest-asyncio==1.5.1
networkx==2.5.1
nltk==3.6.2
notebook==6.4.4
numpy==1.19.5
oauth2client==4.1.3
oauthlib==3.1.0
opt-einsum==3.3.0
oss2==2.6.1
packaging==20.9
pandas==1.2.4
pandocfilters==1.5.0
paramiko==2.7.2
parso==0.8.2
pathlib2==2.3.5
pathspec==0.8.1
pathy==0.6.0
pbr==5.6.0
pep8-naming==0.11.1
pexpect==4.8.0
phonenumbers==8.12.23
pickleshare==0.7.5
pluggy==0.13.1
ply==3.11
portalocker==1.7.1
preshed==3.0.5
prometheus-client==0.11.0
prometheus-flask-exporter==0.18.2
promise==2.3
prompt-toolkit==3.0.20
protobuf==3.17.0
psutil==5.8.0
ptyprocess==0.7.0
py==1.10.0
pyarrow==4.0.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycodestyle==2.7.0
pycparser==2.20
pycryptodome==3.10.1
pydantic==1.8.2
pydocstyle==6.1.1
pydot==1.4.2
PyDrive2==1.8.2
pyflakes==2.3.1
pygit2==1.5.0
Pygments==2.9.0
pygtrie==2.3.2
PyJWT==2.1.0
pylint==2.8.2
PyMySQL==1.0.2
PyNaCl==1.4.0
pyOpenSSL==20.0.1
pyparsing==2.4.7
pyrsistent==0.18.0
pytest==6.2.4
pytest-cov==2.12.0
pytest-lazy-fixture==0.6.3
python-benedict==0.24.0
python-dateutil==2.8.1
python-dotenv==0.17.1
python-editor==1.0.4
python-fsutil==0.5.0
python-slugify==5.0.2
pytz==2021.1
PyYAML==5.4.1
pyzmq==22.3.0
qtconsole==5.1.1
QtPy==1.11.1
querystring-parser==1.2.4
regex==2021.4.4
requests==2.25.1
requests-oauthlib==1.3.0
requests-unixsocket==0.2.0
restructuredtext-lint==1.3.2
rich==10.2.2
rsa==4.7.2
ruamel.yaml==0.17.4
ruamel.yaml.clib==0.2.2
Rx==1.6.1
s3transfer==0.4.2
scikit-learn==0.24.2
scipy==1.7.1
semver==2.9.0
Send2Trash==1.8.0
shortuuid==1.0.1
shtab==1.3.6
six==1.15.0
sklearn==0.0
smart-open==5.2.1
smmap==4.0.0
sniffio==1.2.0
snowballstemmer==2.1.0
solrcloudpy==4.0.1
spacy==3.1.1
spacy-legacy==3.0.8
SQLAlchemy==1.4.15
sqlalchemy2-stubs==0.0.2a1
sqlparse==0.4.2
srsly==2.4.1
stevedore==3.3.0
tabulate==0.8.9
tenacity==8.0.1
tensorboard==2.6.0
tensorboard-data-server==0.6.1
tensorboard-plugin-wit==1.8.0
tensorflow==2.4.2
tensorflow-estimator==2.4.0
termcolor==1.1.0
terminado==0.12.1
testfixtures==6.17.1
testpath==0.5.0
text-unidecode==1.3
thinc==8.0.10
threadpoolctl==2.2.0
toml==0.10.2
tornado==6.1
tqdm==4.60.0
traitlets==5.1.0
typed-ast==1.4.3
typer==0.3.2
typing-extensions==3.7.4.3
ujson==4.0.2
uritemplate==3.0.1
urllib3==1.26.4
voluptuous==0.12.1
wasabi==0.8.2
wcwidth==0.2.5
webdavclient3==3.14.5
webencodings==0.5.1
websocket-client==1.2.1
wemake-python-styleguide==0.15.3
Werkzeug==2.0.1
widgetsnbextension==3.5.1
wrapt==1.12.1
xmltodict==0.12.0
yarl==1.6.3
zc.lockfile==2.0
zipp==3.5.0


### OS information

Ubuntu 20.04.3 LTS


Discussion:
If it is really a bug, please assign it to me.
Hi, @whysage! Thanks a lot for raising this.

```python
for action in actions:
    try:
        output_value = action(input_value)
    except ValueError:
        continue
    break
else:
    raise ValueError()
```

What I see here:
1. Iterate over all actions
2. Contibue to the next one only if it raises `ValueError`
3. If any action does not raise `ValueError`, `break` and stop
4. If all actions do raise `ValueError`, go to `else` and raise new `ValueError`

Do I understand this correctly?

It looks like a valid case, I don't have any simple alternatives except using `all()`. You can totally work on this! 
Thank you for the detailed bug report and your willingness to help.
@sobolevn Thank you for quick reply! 
As I understand:
- you confirm that it is a bug
- this case should not be reported as a violation
- I can create a pull request to fix this.

Everything is correct? 
Yeap 🙂 

Resolving patch:
diff --git a/wemake_python_styleguide/visitors/ast/conditions.py b/wemake_python_styleguide/visitors/ast/conditions.py
--- a/wemake_python_styleguide/visitors/ast/conditions.py
+++ b/wemake_python_styleguide/visitors/ast/conditions.py
@@ -215,6 +215,12 @@ def _check_useless_loop_else(self, node: AnyLoop) -> None:
         # An else statement makes sense if we
         # want to execute something after breaking
         # out of the loop without writing more code
+        has_break = any(
+            walk.is_contained(sub, ast.Break)
+            for sub in node.body
+        )
+        if has_break:
+            return
         body_returning = any(
             walk.is_contained(sub, self._returning_nodes[1:])
             for sub in node.body


Test patch:
diff --git a/tests/test_visitors/test_ast/test_conditions/test_useless_else/test_useless_loop_else.py b/tests/test_visitors/test_ast/test_conditions/test_useless_else/test_useless_loop_else.py
--- a/tests/test_visitors/test_ast/test_conditions/test_useless_else/test_useless_loop_else.py
+++ b/tests/test_visitors/test_ast/test_conditions/test_useless_else/test_useless_loop_else.py
@@ -73,6 +73,30 @@ def function():
     return 1
 """
 
+correct_example9 = """
+def function():
+    while ...:
+        try:
+            ...
+        except ...:
+           continue
+        break
+    else:
+        raise ...
+"""
+
+correct_example10 = """
+def function():
+    for x in ...:
+        try:
+            ...
+        except ...:
+           continue
+        break
+    else:
+        raise ...
+"""
+
 
 @pytest.mark.parametrize('code', [
     correct_example1,
@@ -83,6 +107,8 @@ def function():
     correct_example6,
     correct_example7,
     correct_example8,
+    correct_example9,
+    correct_example10,
 ])
 def test_else_that_can_not_be_removed(
     assert_errors,