Ошибка r503 flake8 как исправить

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

Assignees

@afonasev

Comments

@peterschutt

  • Date you used flake8-return: 2023/02/02
  • flake8-return version used, if any: 1.2.0
  • Python version, if any: 3.11.1
  • Operating System: ubuntu linux

Description

Running through pre-commit with flake8-return listed as an additional dependency for the flake8 repo.

This function:

def get_service_for_object(
    obj: events.EventWrapper | markets.MarketWrapper,
) -> events.Service | markets.Service:
    """
    Args:
        obj: The parsed ZF push object.

    Returns:
        Relevant service object for the type.
    """
    match obj:
        case events.EventWrapper():
            return events.Service(obj)
        case markets.MarketWrapper():
            return markets.Service(obj)
        case _:  # pragma: no cover
            raise RuntimeError("Unexpected object type.")

raises R503 missing explicit return at the end of function able to return non-None value.

@afonasev

Thank you for your issue! Currently, the plugin does not support matching/case expressions. I would be glad to accept your contributions. I’m not sure I’ll be able to fix this anytime soon.

@peterschutt

No worries @afonasev – this is something I’d be interested in looking at but my available bandwidth is also very low.

If I do pick it up I’ll ping you again in here to let you know I’m working on it.

@ericbn

W503 rule and W504 rule of flake8 are conflicted to each other. I recommend you to add one of them into your .flake8‘s ignore list.

W503: line break before binary operator

W504: line break after binary operator

ignore = D400,D300,D205,D200,D105,D100,D101,D103,D107,W503,E712

The below code is prettified:

def check_actionable(self, user_name, op, changes_table):
    return any(user_name in row.inner_text() and
               row.query_selector(self.OPS[op]) is not None and
               row.query_selector(self.DISABLED) is not None for row in changes_table)

Some explanation on W503 and W504:

binary operator: +, -, /, and, or, …

To pass W503, your code should be like this:

x = (1
     + 2)

To pass W504, your code should be like this:

x = (1 +
     2)

afonasev / flake8-return
Goto Github
PK

View Code? Open in Web Editor
NEW

47.0
3.0
48.0
165 KB

Flake8 plugin for return expressions checking.

License: MIT License

Python 96.90%
Makefile 3.10%
flake8-plugin

flake8-return’s Introduction

pypi
Python: 3.6+
Downloads
Build Status
Code coverage
License: MIT
Code style: black

Flake8 plugin that checks return values.
Flake8-return rule set is supported in ruff.

Installation

pip install flake8-return

Errors

  • R501 do not explicitly return None in function if it is the only possible return value.
def x(y):
    if not y:
        return
    return None  # error!
  • R502 do not implicitly return None in function able to return non-None value.
def x(y):
    if not y:
        return  # error!
    return 1
  • R503 missing explicit return at the end of function able to return non-None value.
def x(y):
    if not y:
        return 1
    # error!
  • R504 unnecessary variable assignment before return statement.
def x():
    a = 1
    # some code that not using `a`
    print('test')
    return a  # error!
  • R505 unnecessary else after return statement.
def x(y, z):
    if y:  # error!
        return 1
    else:
        return z
  • R506 unnecessary else after raise statement.
def x(y, z):
    if y:  # error!
        raise Exception(y)
    else:
        raise Exception(z)
  • R507 unnecessary else after continue statement.
def x(y, z):
    for i in y:
        if i < z:  # error!
            continue
        else:
            a = 0
  • R508 unnecessary else after break statement.
def x(y, z):
    for i in y:
        if i > z:  # error!
            break
        else:
            a = 0

Returns in asyncio coroutines also supported.

For developers

Show help

Create venv and install deps

Install git precommit hook

Run linters, autoformat, tests etc

Bump new version

make bump_major
make bump_minor
make bump_patch

Change Log

Unreleased

1.2.0 — 2022-10-28

  • Port no-else-break, no-else-continue, no-else-raise, no-else-return from pylint (#122) Calum Young
  • PEP 621: Migrate more config to pyproject.toml (#123) Christian Clauss
  • Fix/116/R504-try-except (#120) Calum Young
  • Update ci (#119) Calum Young
  • Fix/47/Update-R504-for-assignment-value (#117) Calum Young
  • Upgrade GitHub Actions (#113) Christian Clauss
  • Add a space to avoid a typo in R503 (#98) Christian Clauss
  • GitHub Action to lint Python code (#97) Christian Clauss
  • Typo fixes (#92) Aarni Koskela
  • Create codeql-analysis.yml Afonasev Evgeniy
  • Bump flake8-plugin-utils from 1.1.1 to 1.3.2 (#87) dependabot
  • Bump mypy from 0.812 to 0.971 (#114) dependabot
  • Bump pytest-cov from 3.0.0 to 4.0.0 (#124) dependabot
  • Bump pytest-cov from 2.11.1 to 3.0.0 (#102) dependabot
  • Bump pytest-mock from 3.6.0 to 3.6.1 (#91) dependabot
  • Bump pytest from 6.2.4 to 6.2.5 (#99) dependabot
  • Bump pylint from 2.8.2 to 2.10.2 (#100) dependabot
  • Bump pytest from 6.2.3 to 6.2.4 (#86) dependabot

1.1.3 — 2021-05-05

  • Error clarifications (#77) Clément Robert
  • fix linting (migrate to black 20.0b1) (#78) Clément Robert

1.1.2 — 2020-07-09

  • Make R504 visitors handle while loops (#56) Frank Tackitt
  • Rename allows-prereleases to allow-prereleases (#55) Frank Tackitt
  • Fix typo: → haven’t (#24) Jon Dufresne

1.1.1 — 2019-09-21

  • fixed #3 The R504 doesn’t detect that the variable is modified in loop
  • fixed #4 False positive with R503 inside async with clause

1.1.0 — 2019-05-23

  • update flask_plugin_utils version to 1.0

1.0.0 — 2019-05-13

  • skip assign after unpacking while unnecessary assign checking «(x, y = my_obj)»

0.3.2 — 2019-04-01

  • allow «assert False» as last function return

0.3.1 — 2019-03-11

  • add pypi deploy into travis config
  • add make bump_version command

0.3.0 — 2019-02-26

  • skip functions that consist only return None
  • fix false positive when last return inner with statement
  • add unnecessary assign error
  • add support tuple in assign or return expressions
  • add support asyncio coroutines

0.2.0 — 2019-02-21

  • fix explicit/implicit
  • add flake8-plugin-utils as dependency
  • allow raise as last function return
  • allow no return as last line in while block
  • fix if/elif/else cases

0.1.1 — 2019-02-10

  • fix error messages

0.1.0 — 2019-02-10

  • initial

flake8-return’s People

flake8-return’s Issues

Should allow raise at end of function, in addition to assert

flake8-return version 1.1.3 allows this construct

def myfunc(x):
    if x == 5:
        return None
    assert False

but disallows this one

def myfunc(x):
    if x == 5:
        return None
    raise NotImplementedError()

I think they should be treated the same?

False positive on R504

  • Date you used flake8-return: 2019-05-16
  • flake8-return version used, if any: 1.0.0
  • Python version, if any: 3.7.1
  • Operating System: Debian

Description

The R504 doesn’t detect that the variable is conditionally modified prior to return.

What I Did

$ flake8 test.py 
test.py:5:12: R504 you shouldn`t assign value to variable if it will be use only as return value
$ cat test.py
def foo(bar):
    value = []
    if bar:
        value = value[:1]
    return value

R506 false positive when raise is used in elif

  • Date you used flake8-return: 15th December 2022
  • flake8-return version used, if any: 1.2.0
  • Python version, if any: 3.9.2
  • Operating System: Linux

Description

Linting following code:

def test_2(bar):
    if bar == 3:
        baz = 1
    elif bar == 4:
        raise Exception()
    else:
        baz = 2
    return baz

What I Did

$ flake8 test.py 
test.py:4:5: R506 unnecessary else after raise statement.

The else is necessary as there is additional if before which has side effects and removing else would overwrite that. The code is simplified from real-world code.

Incorrectly marks variables updated in a while loop

  • Date you used flake8-return: Jul 6, 2020
  • flake8-return version used, if any: 1.1.1
  • Python version, if any: Python 3.8.3
  • Operating System: Arch Linux on Linux 5.7.4

Description

flake8-return seemingly fails to detect changes to a variable modified in a while loop.

What I Did

def doit():
    test = 1
    i = 4

    while i := i - 1:
        test = test + 1

    return test

class Node:
    def __init__(self, v, parent=None):
        self.v = v
        self.parent = parent
        self.child = Node(v - 1, parent=self) if v else None

    def __str__(self):
        name = f"Node<{self.v}>"
        node = self

        while node := node.parent:
            name = f"Node<{node.v}>.{name}"

        return name

    def tail(self):
        node = self
        while node.child:
            node = node.child
        return node

print(Node(5).tail())

A R504 error is detected at each return there, in doit(), Node.__str__, and Node.tail

flake8-return wrongly indicates R504

  • Date you used flake8-return: 2020-06-18
  • flake8-return version used, if any: 1.1.1
  • Python version: 3.8.0
  • Operating System: Windows 10

Description

flake8-return wrongly indicates R504 for the following samples:

    formatted = _USER_AGENT_FORMATTER.format(format_string, **values)
    # clean up after any blank components
    formatted = formatted.replace('()', '').replace('  ', ' ').strip()
    return formatted  # <<< wrongly indicated as R504 issue
def user_agent_username(username=None):

    if not username:
        return ''

    username = username.replace(' ', '_')  # Avoid spaces or %20.
    try:
        username.encode('ascii')  # just test, but not actually use it
    except UnicodeEncodeError:
        username = quote(username.encode('utf-8'))
    else:
        # % is legal in the default $wgLegalTitleChars
        # This is so that ops know the real pywikibot will not
        # allow a useragent in the username to allow through a hand-coded
        # percent-encoded value.
        if '%' in username:
            username = quote(username)
    return username  # <<< wrongly indicated as R504 issue

flake8-return does not recognize `assert False` as an implicit return.

  • Date you used flake8-return: today
  • flake8-return version used, if any: 0.3.1
  • Python version, if any: Python 3.7.2
  • Operating System: Linux

Description

We sometimes end a method with assert False, "some reason" as a sanity check. The current version of flake8-return sees this as a violation of R503.

def some_method(parameter):
   if some_test:
     return some_value
   elif another_test:
     return another_value
   else:
     assert False, "parameter was not some_value nor another_value"

This assert False is equivalent to raise AssertionError which flake8-return does recognize. We would like to see flake8-return treat both statements as equivalent.

Invalid error reported for R505 simple if — elif pattern

  • Date you used flake8-return: 2022-10-31
  • flake8-return version used, if any: 1.2.0
  • Python version, if any: Python 3.10.6
  • Operating System: macOS

Description

Invalid error reported for R505 simple if — elif pattern

def get_name(name):
    if name == 'foo':
        return 'foo'
    elif name == 'bar':
        return 'bar'

    return 'baz'

What I Did

a.py:2:5: R505 unnecessary elif after return statement.

I think this pattern should be passed.

Initial Update

The bot created this issue to inform you that pyup.io has been set up on this repo.
Once you have closed it, the bot will open pull requests for updates as soon as they are available.

False positive with R503 inside with clause

  • Date you used flake8-return: 8/16/2019
  • flake8-return version used, if any: 1.1.0
  • Python version, if any: 3.6.6
  • Operating System: OSX

Description

With a function like the following:

async def function():
    async with thing as foo:
        return foo.bar

This is valid because there can be no other return than the one inside the with context. If enter/exit raise then nothing after it will run. The one way this is not true is if you capture the exeption with a try/except around the async with clause.

Variable assignment inside try/except blocks is not handled properly

  • Date you used flake8-return: 09-09-2022
  • flake8-return version used, if any: 1.1.3
  • Python version, if any: 3.10.6
  • Operating System: macOS Monterey 12.5.1

Description

Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen.

Consider the following functions

def no_exception_loop():
    success = False
    for _ in range(10):
        try:
            success = True
        except Exception:
            print("exception")  # noqa: T201
    return success


def no_exception():
    success = False
    try:
        success = True
    except Exception:
        print("exception")  # noqa: T201
    return success


def exception():
    success = True
    try:
        print("raising")  # noqa: T201
        raise Exception
    except Exception:
        success = False
    return success

The variable success is changed (or may be changed) inside try/except blocks. However, running flake8 generates the following error: R504 unecessary variable assignement before return statement.

What I Did

$ poetry run flake8 --show-source debug.py
Unable to find qualified name for module: debug.py
debug.py:8:12: R504 unecessary variable assignement before return statement.
    return success
           ^
debug.py:17:12: R504 unecessary variable assignement before return statement.
    return success
           ^
debug.py:27:12: R504 unecessary variable assignement before return statement.
    return success
           ^

In some circumstances the error R506 can be dangerous if the `raise` will be changed later

  • Date you used flake8-return: 28.10.2022
  • flake8-return version used, if any: 1.2.0
  • Python version, if any: 3.10.8
  • Operating System: MacOS/Linux

Description

The error reported for R506 can be dangerous in fast changing development environment

What I Did

Let assume we have this piece of code

def main():
    for a in range(10):                             # | 0 exception bad value
        try:                                        # | 1 ordinary value
            if a % 5 == 0:                          # | 2 good value
                raise ValueError('bad value')       # | 3 ordinary value
            elif a % 2 == 0:                        # | 4 good value
                print(a, 'good value')              # | 5 exception bad value
            else:                                   # | 6 good value
                print(a, 'ordinary value')          # | 7 ordinary value
        except ValueError as e:                     # | 8 good value
            print(a, 'exception', e)                # | 9 ordinary value


if __name__ == '__main__':
    main()

The error reported is the following:

myfile.py:4:13: R506 unnecessary elif after raise statement.
            if a % 5 == 0:

Now let assume we change it according to the proposal, but after short period of time another developer come and changed the code slightly (because IF can be quite big and not intuitive), he removed raise and add the handling of situation inside and removed try...except all together. Now let’s see the result:

def main():                                         # | 0 exception bad value
    for a in range(10):                             # | 0 good value
        if a % 5 == 0:                              # | 1 ordinary value
            print(a, 'exception', 'bad value')      # | 2 good value
        if a % 2 == 0:                              # | 3 ordinary value
            print(a, 'good value')                  # | 4 good value
        else:                                       # | 5 exception bad value
            print(a, 'ordinary value')              # | 5 ordinary value
                                                    # | 6 good value
                                                    # | 7 ordinary value
if __name__ == '__main__':                          # | 8 good value
    main()                                          # | 9 ordinary value

As we can see the result is not what the developer expected, but… sometimes it is very difficult to notice such issues. As elif has SOME MEANING behind, usually…

Incorrect error reporting R507 for `elif` + `continue` if after the `continue` statement `elif` is following

  • Date you used flake8-return: 28.10.2022
  • flake8-return version used, if any: 1.2.0
  • Python version, if any: 3.10.8
  • Operating System: MacOS/Linux

Description

Invalid error reported for R507 because of some complex context

What I Did

We have some if...elif... clause like this:

def main():
    for a in range(10):                     # | 0 div 4
        if a % 4 == 0:                      # | 1 div nothing
            print(a, 'div', 4)              # | 2 div 2
        elif a % 3 == 0:                    # | 3 div 3
            print(a, 'div', 3)              # | 4 div 4
            continue                        # | 5 div nothing
        elif a % 2 == 0:                    # | 6 div 3
            print(a, 'div', 2)              # | 7 div nothing
        else:                               # | 8 div 4
            print(a, 'div', 'nothing')      # | 9 div 3
    return 0



if __name__ == '__main__':
    main()

For this case we are getting this error:

myfile.py:5:9: R507 unnecessary elif after continue statement.
        elif a % 3 == 0:
        ^

If we follow the suggestion and fix the issue, then we have different output:

def main():                                 # | 0 div 4
    for a in range(10):                     # | 0 div 2
        if a % 4 == 0:                      # | 1 div nothing
            print(a, 'div', 4)              # | 2 div 2
        elif a % 3 == 0:                    # | 3 div 3
            print(a, 'div', 3)              # | 4 div 4
            continue                        # | 4 div 2
        if a % 2 == 0:                      # | 5 div nothing
            print(a, 'div', 2)              # | 6 div 3
        else:                               # | 7 div nothing
            print(a, 'div', 'nothing')      # | 8 div 4
    return 0                                # | 8 div 2
                                            # | 9 div 3

if __name__ == '__main__':
    main()

Can we do something to this? I have tried to apply the suggested errors on our codebase and this issue appeared.

Possible False Positive for R504

  • Date you used flake8-return: Oct 25th 2022
  • flake8-return version used, if any: 1.1.3
  • Python version, if any: 3.10.7
  • Operating System: Linux

Description

I have a fixture that creates a mock object, patches a library to return that mock object for a specific method and then returns the mock object so that assertions can be made on it. It seems quite clear that I am in fact using the variable, not sure why it’s triggering the linting rule.

I was able to refactor the code to make the lint pass (the second example below), but that code is much less intuitive to me and messy.

What I Did

@pytest.fixture()
def example_mock_fails_linting(mocker):
    mock = mocker.MagicMock()
    mocker.patch("path.to.mocked.method.that.returns.an.object").return_value = mock
    return mock # R504 triggers for this line


@pytest.fixture()
def example_mock_lints_clean(mocker):
    example = mocker.patch("path.to.mocked.method.that.returns.an.object")
    example.return_value = mocker.MagicMock()
    return example.return_value # R504 doesn't trigger for this line

Support match/case operator

  • Date you used flake8-return: 26-08-22
  • flake8-return version used, if any: 1.1.3
  • Python version, if any: 3.10.2
  • Operating System: MacOS 12.5.1

Description

match/case construction not supported

What I Did

def test():
    a = 1
    match a:  # <---- D400 First line should end with a period
      case 0:
        return False
      case 1:
        return True
      case _*:
        return False

Feature request: taking type hints into account ?

  • Date you used flake8-return: feb 6 2021
  • flake8-return version used, if any: 1.1.2
  • Python version, if any: 3.8.5
  • Operating System: macOS

Description

I have a decorator that’s specifically designed to return either None or a function depending on the CPU rank so that only the root one exectutes the decoreated function. Here’s the gist of it

from functools import wraps

def rootonly(func):
    @wraps(func)
    def check_parallel_rank(*args, **kwargs):
        global topcomm_parallel_rank
        if topcomm_parallel_rank > 0:
            return
        return func(*args, **kwargs)
    return check_parallel_rank

I’m getting a R502 error of course, so I tried adding type hints to avoid using noqas

from typing import Optional
from types import FunctionType
# ...
    def check_parallel_rank(*args, **kwargs) -> Optional[FunctionType]:
# ...

but the error persists, though I’d argue it shouldn’t (in an ideal world).
I know that this is a sizeable feature to ask but is there any chance you’d consider taking type hints into account ?
In any case thanks for the tool, it’s worth using for 504 alone !

Bad `R504` for assignment in a loop body

Running flake8-return with Python 3.7, I get an R504 error for the following code:

def close(self):
    any_failed = False
    for task in self.tasks:
        try:
            task()
        except BaseException:
            any_failed = True
            report(traceback.format_exc())
    return any_failed  # R504 you shouldn't assign value to variable if it will be use only as return value

While any_failed is not used in any other expressions before being returned, it’s still important to run all cleanup tasks instead of returning immediately after the first error. Perhaps R504 should be disabled when the assignment is in a loop and the return statement is not?

Plugin crashes when used with stdin

  • Date you used flake8-return: 27-11-2020
  • flake8-return version used, if any: 1.1.2
  • Python version, if any: Python 3.86
  • Operating System: Ubuntu 20.4

Description

When emacs’ lsp-mode calls flake 8 on a file (in order to lint it), the command errors out. I strongly suspect the issue is the same as the one reported here: sco1/flake8-annotations#52
My reason for suspecting the error lies within this plugin is the fact that the error goes away when I remove this plugin.

What I Did

Opened the file, activated the virtual env, and started the language server (which called flake8)

What I expected

Flake8 gives results and the pyls lints my code

What I got

Stack trace below

2020-11-27 12:43:25,280 UTC - ERROR - pyls.plugins.flake8_lint - Error while running flake8 'Traceback (most recent call last):
  File "some_code/.venv/bin/flake8", line 8, in <module>
    sys.exit(main())
  File "some_code/.venv/lib/python3.8/site-packages/flake8/main/cli.py", line 22, in main
    app.run(argv)
  File "some_code/.venv/lib/python3.8/site-packages/flake8/main/application.py", line 363, in run
    self._run(argv)
  File "some_code/.venv/lib/python3.8/site-packages/flake8/main/application.py", line 351, in _run
    self.run_checks()
  File "some_code/.venv/lib/python3.8/site-packages/flake8/main/application.py", line 264, in run_checks
    self.file_checker_manager.run()
  File "some_code/.venv/lib/python3.8/site-packages/flake8/checker.py", line 323, in run
    self.run_serial()
  File "some_code/.venv/lib/python3.8/site-packages/flake8/checker.py", line 307, in run_serial
    checker.run_checks()
  File "some_code/.venv/lib/python3.8/site-packages/flake8/checker.py", line 589, in run_checks
    self.run_ast_checks()
  File "some_code/.venv/lib/python3.8/site-packages/flake8/checker.py", line 494, in run_ast_checks
    for (line_number, offset, text, _) in runner:
  File "some_code/.venv/lib/python3.8/site-packages/flake8_plugin_utils/plugin.py", line 75, in run
    self._load_file()
  File "some_code/.venv/lib/python3.8/site-packages/flake8_plugin_utils/plugin.py", line 87, in _load_file
    with open(self._filename, 'rb') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'stdin'
'

port no-else-break, no-else-continue, no-else-raise, no-else-return from pylint

Description

This fixes the extremely annoying construct:

ret = fn()
if bad:
    raise BadThingHappenedError(msg)
elif okish:
    return fixit(ret)
else:
    return ret

to favour

ret = fn()
if bad:
    raise BadThingHappenedError(msg)
if okish:
    return fixit(ret)
return ret

What I Did

this is a feature request

Create v1.1.3 tag

  • Date you used flake8-return: 28/10/2022
  • flake8-return version used, if any: Hoping to use v1.1.3
  • Python version, if any: 3.8
  • Operating System: WSL

Description

I was updating our pre-commit hooks to use flake8-return (rather than a forked version), but as the v1.1.3 tag has not been created on Github, the pre-commit hook does not make use of the changes introduced in v1.1.3 on PyPI.

@afonasev, please could you create a tag for v1.1.3 so that pre-commit makes use of the recent updates?

What I Did

The abridged pre-commit config used below throws a false positive for R504, which has been fixed in the version released on PyPI.

repos:
  ...
  - repo: https://github.com/PyCQA/flake8
    rev: 5.0.4
    hooks:
      - id: flake8
        additional_dependencies:
          - flake8-return
          ...
        args: [--config, setup.cfg]

In GitLab by @TJC666 on May 13, 2019, 10:39

Please describe how you installed Flake8

It’s in my tox:

  [testenv:flake8]
  basepython = python3.7
  skip_install = true
  deps = flake8
  commands = flake8 setup.py a1 tests
 
  [flake8]
  ignore = E501

Note: Some *nix distributions patch Flake8 arbitrarily to accommodate incompatible software versions. If you’re on one of those distributions, your issue may be closed and you will be asked to open an issue with your distribution package maintainers instead.

Please provide the exact, unmodified output of flake8 --bug-report

{
  "dependencies": [
    {
      "dependency": "entrypoints",
      "version": "0.3"
    }
  ],
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.7.3",
    "system": "Linux"
  },
  "plugins": [
    {
      "is_local": false,
      "plugin": "mccabe",
      "version": "0.6.1"
    },
    {
      "is_local": false,
      "plugin": "pycodestyle",
      "version": "2.5.0"
    },
    {
      "is_local": false,
      "plugin": "pyflakes",
      "version": "2.1.1"
    }
  ],
  "version": "3.7.7"
}

Please describe the problem or feature

Flake8 3.7.7 still complains about W503. This seems to have been discussed (but closed?) https://gitlab.com/pycqa/flake8/issues/139

W503 seems inconsistent with PEP8.

If this is a bug report, please explain with examples (and example code) what you expected to happen and what actually happened.

The following code should be valid:

     elif (
         summary["message type"] == expected_ack_message_type
         and summary["message state"] == 0
         and summary["message status"] == "RMR_OK"
     ):

But flake8 bombs:

flake8 installed: a1==0.5.1,entrypoints==0.3,flake8==3.7.7,mccabe==0.6.1,pycodestyle==2.5.0,pyflakes==2.1.1
flake8 run-test-pre: PYTHONHASHSEED='2042587310'
flake8 runtests: commands[0] | flake8 setup.py a1 tests
a1/a1rmr.py:103:13: W503 line break before binary operator
a1/a1rmr.py:104:13: W503 line break before binary operator
ERROR: InvocationError for command '/home/tommy/workdir/RIC/ric-a1/.tox/flake8/bin/flake8 setup.py a1 tests' (exited with 
code 1)

Линтеры

В сообществе Python, как и в любой другой группе людей, существует некое
коллективное знание. Множество людей прошлось по всем возможным граблям
и получило опыт через набитые шишки. Затем через какое-то время,
благодаря выступлениям на конференциях, официальным заявлениям,
документам, статьям в блогах, код-ревью и личному общению,
это знание стало коллективным. Теперь мы просто называем его
“хорошими практиками”.

К таким хорошим практикам можно отнести, например, следующие.

  • Форматировать код по PEP8
    — если этого не делать, то другим людям будет намного сложнее понимать
    ваш код; в плохо оформленном коде сложнее увидеть суть,
    потому что мозг постоянно отвлекается на не несущие смысловой нагрузки
    особенности оформления.
  • Не допускать объявленных, но неиспользуемых переменных/функций/импортов
    — опять же, это усложняет восприятие кода; читателю потребуется потратить
    время на то, чтобы осознать, что вот на эту сущность обращать внимания не
    нужно.
  • Писать короткие функции — слишком сложные функции с большим
    количеством ветвлений и циклов тяжело понимать.
  • Не использовать изменяемый объект в качестве значения аргумента
    функции по умолчанию — иначе в результате можно получить
    очень неожиданные эффекты.

Соблюдать (и даже просто помнить) все хорошие практики — не самая простая
задача. Зачастую люди плохо справляются с тем, чтобы отсчитывать пробелы
и контролировать переменные, и вообще склонны допускать ошибки по
невнимательности. Таковы люди, ничего не поделаешь. Машины, наоборот,
прекрасно справляются с такими хорошо определёнными задачами, поэтому
появились инструменты, которые контролируют следование хорошим практикам.

В компилируемых языках ещё на этапе компиляции программист может получить
по щщам первый полезный фидбэк о написанном коде.
Компилятор проверит, что код валиден и может быть скомпилирован, а также может
выдать предупреждения и рекомендации, как сделать код лучше или читаемее.
Т.к. Python является интерпретируемым языком, где этап компиляции как таковой
отсутствует, линтеры особенно полезны. На самом деле, это очень важно и
круто — узнать, что твой код как минимум является валидным Python-кодом,
даже не запуская его.

В этом посте я рассмотрю два самых популярных линтера для Python:

  • flake8;
  • pylint.

Термин “lint” впервые начал использоваться в таком значении в 1979 году.
Так называлась программа для статического анализа кода на C,
которая предупреждала об использовании непортабельных на другие архитектуры
языковых конструкций. С тех пор “линтерами” называют любые статические
анализаторы кода, которые помогают находить распространённые ошибки, делать
его однообразным и более читаемым. А названо оно “lint” в честь вот такой
штуки:

lint roller

flake8

flake8 — это утилита-комбайн, которая органично объединяет в себе несколько
других анализаторов кода (pycodestyle, pyflakes и mccabe), а также
имеет огромную экосистему плагинов, которые могут добавить к стандартной
поставке ещё кучу различных проверок. На данный момент, это самый
популярный линтер для Python-кода. Кроме того, он предельно прост в
настройке и использовании.

Установка

flake8 устанавливается, как и любой другой Python-пакет,
через pip. Внутри виртуального окружения проекта выполните:

Если вы пользуетесь pipenv, то flake8 нужно устанавливать
как dev-зависимость (ведь для работы программы линтер не нужен,
он нужен только для разработчика):

$ pipenv install --dev flake8

Аналогично с poetry:

$ poetry add --dev flake8

Проверим установку:

$ flake8 --version
3.8.1 (mccabe: 0.6.1, pycodestyle: 2.6.0, pyflakes: 2.2.0) CPython 3.8.2 on Linux

Использование

Для работы flake8 нужно просто указать файл или директорию, которые
нужно проверять, например:

# проверить один файл
$ flake8 file.py

# проверить директорию рекурсивно 
$ flake8 src/

# проверить текущую директорию рекурсивно
$ flake8 .

Давайте для демонстрации попытаемся написать программу с как можно большим
количеством “плохих практик”:

Возможно, вам не видно всего, но в этом коде точно есть следующие “запахи кода”:

  • import * — импортирование всех имен из модуля, хотя используется
    из них только одно;
  • import itertools — ненужный импорт;
  • во множестве мест стоят лишние или отсутствующие пробелы;
  • название функции написано в стиле PascalCase;
  • в некоторых местах используются табы для отступов;
  • используется список (изменяемый объект) в качестве значения аргумента
    функции по умолчанию;
  • используется слишком “широкое” выражение except: без указания
    конкретного исключения.

Давайте посмотрим, что flake8 скажет по поводу этого файла:

$ flake8 bad_code.py
bad_code.py:1:1: F403 'from math import *' used; unable to detect undefined names
bad_code.py:2:1: F401 'itertools' imported but unused
bad_code.py:4:1: E302 expected 2 blank lines, found 1
bad_code.py:4:4: E271 multiple spaces after keyword
bad_code.py:4:25: E211 whitespace before '('
bad_code.py:4:33: E202 whitespace before ')'
bad_code.py:5:1: W191 indentation contains tabs
bad_code.py:5:8: E271 multiple spaces after keyword
bad_code.py:5:10: F405 'sqrt' may be undefined, or defined from star imports: math
bad_code.py:5:21: E202 whitespace before ')'
bad_code.py:7:1: E302 expected 2 blank lines, found 1
bad_code.py:7:23: E741 ambiguous variable name 'l'
bad_code.py:8:1: E101 indentation contains mixed spaces and tabs
bad_code.py:9:1: E101 indentation contains mixed spaces and tabs
bad_code.py:11:1: E305 expected 2 blank lines after class or function definition, found 1
bad_code.py:12:1: E101 indentation contains mixed spaces and tabs
bad_code.py:13:1: E101 indentation contains mixed spaces and tabs
bad_code.py:13:20: E225 missing whitespace around operator
bad_code.py:14:1: E101 indentation contains mixed spaces and tabs
bad_code.py:14:67: W291 trailing whitespace
bad_code.py:15:1: E101 indentation contains mixed spaces and tabs
bad_code.py:15:14: W291 trailing whitespace
bad_code.py:16:1: E101 indentation contains mixed spaces and tabs
bad_code.py:16:5: E722 do not use bare 'except'
bad_code.py:17:1: E101 indentation contains mixed spaces and tabs

Как видите, flake8 нашёл кучу ошибок. Для каждой ошибки указана строка
и номер символа в строке (не всегда точный), где произошла ошибка.
Также у каждой категории ошибок есть свой код: E101, W291 и т.д.
Эти коды ошибок могут использоваться для включения/отключения правил.
Тем не менее, не все ошибки были найдены. Давайте установим пару плагинов,
чтобы добавить ещё правил!

Плагины

Как я уже говорил, для flake8 написано множество плагинов.
Обычно плагины легко гуглятся или находятся в списках плагинов.
Есть плагины для всех популярных фреймворков и библиотек — пользуйтесь ими!
Давайте для нашего простого примера установим
flake8-bugbear
(находит распространённые логические ошибки) и
pep8-naming
(проверяет имена на соответствие PEP8).

Плагины устанавливаются так же, как и сам flake8 (для краткости я
не буду писать примеры для pipenv и poetry — сами сможете обобщить):

$ pip install flake8-bugbear pep8-naming

Давайте убедимся, что плагины действительно установились
и flake8 может их найти:

$ flake8 --version
3.8.1 (flake8-bugbear: 20.1.4, mccabe: 0.6.1, naming: 0.10.0, pycodestyle: 2.6.0, pyflakes: 2.2.0) CPython 3.8.2 on Linux

Если вы видите в списке в скобках названия ваших плагинов, то всё хорошо.

Теперь снова проверим наш файл:

$ flake8 bad_code.py
bad_code.py:1:1: F403 'from math import *' used; unable to detect undefined names
bad_code.py:2:1: F401 'itertools' imported but unused
bad_code.py:4:1: E302 expected 2 blank lines, found 1
bad_code.py:4:4: E271 multiple spaces after keyword
bad_code.py:4:6: N802 function name 'CalculateSquareRoot' should be lowercase
bad_code.py:4:25: E211 whitespace before '('
bad_code.py:4:28: N803 argument name 'Number' should be lowercase
bad_code.py:4:33: E202 whitespace before ')'
bad_code.py:5:1: W191 indentation contains tabs
bad_code.py:5:8: E271 multiple spaces after keyword
bad_code.py:5:10: F405 'sqrt' may be undefined, or defined from star imports: math
bad_code.py:5:21: E202 whitespace before ')'
bad_code.py:7:1: E302 expected 2 blank lines, found 1
bad_code.py:7:23: E741 ambiguous variable name 'l'
bad_code.py:7:25: B006 Do not use mutable data structures for argument defaults.  They are created during function definition time. All calls to the function reuse this one instance of that data structure, persisting changes between them.
bad_code.py:8:1: E101 indentation contains mixed spaces and tabs
bad_code.py:9:1: E101 indentation contains mixed spaces and tabs
bad_code.py:11:1: E305 expected 2 blank lines after class or function definition, found 1
bad_code.py:12:1: E101 indentation contains mixed spaces and tabs
bad_code.py:13:1: E101 indentation contains mixed spaces and tabs
bad_code.py:13:20: E225 missing whitespace around operator
bad_code.py:14:1: E101 indentation contains mixed spaces and tabs
bad_code.py:14:67: W291 trailing whitespace
bad_code.py:15:1: E101 indentation contains mixed spaces and tabs
bad_code.py:15:14: W291 trailing whitespace
bad_code.py:16:1: E101 indentation contains mixed spaces and tabs
bad_code.py:16:5: E722 do not use bare 'except'
bad_code.py:16:5: B001 Do not use bare `except:`, it also catches unexpected events like memory errors, interrupts, system exit, and so on.  Prefer `except Exception:`.  If you're sure what you're doing, be explicit and write `except BaseException:`.
bad_code.py:17:1: E101 indentation contains mixed spaces and tabs

В выводе появились новые категории ошибок (N802, B006)
— они как раз добавлены плагинами. На этот раз, как мне кажется,
найдены все ошибки. К сожалению, flake8 не умеет сам чинить
найденные ошибки, поэтому давайте сделаем это вручную:

Обратите внимание на строки 8 и 10, там содержится комментарии # noqa.
При помощи этих комментариев можно заставить flake8 игнорировать ошибки.
Это бывает полезно, когда по какой-то причине код должен остаться именно
таким, например:

  • он автоматически сгенерирован и исправление в нём ошибок не имеет смысла;
  • исправление этой ошибки породит куда более уродливый код,
    чем комментарий # noqa;
  • у вас просто сейчас нет времени, чтобы исправлять эту ошибку
    (плохая отмазка, серьёзно).

Если не указать код ошибки, то будут проигнорированы все ошибки в строке
— я не рекомендую так делать, потому что так можно пропустить
и на самом деле плохие ошибки. Если указать номер правила, то
flake8 будет игнорировать только указанную категорию,
а о других ошибках в этой же строке доложит.
Вообще, комментариями # noqa нужно пользоваться с большой осторожностью.
Считайте, что каждый раз, когда вы это делаете, вы берёте на
себя ответственность за эту строку кода. Если программа сломается
в этом месте, то пеняйте на себя — минздрав линтер вас предупреждал.

Конфигурация

flake8 для работы не требует никакой конфигурации.
Он имеет достаточно (но не слишком) строгие настройки по умолчанию,
которые подойдут большинству пользователей, но иногда бывает нужно
отключить (или наоборот включить) определённые правила на уровне всего проекта.
Сделать это можно через файлы .flake8 или setup.cfg в корне проекта.
Если у вас в проекте уже есть файл setup.cfg, то можно добавить конфигурацию
flake8 в него. Если вы предпочитаете для каждой утилиты держать
отдельный файл конфигурации, то используйте .flake8. В любом случае,
формат для обоих этих файлов совпадает:

[flake8]
ignore = D203,E741
exclude =
    # No need to traverse our git directory
    .git,
    # There's no value in checking cache directories
    __pycache__,
    # The conf file is mostly autogenerated, ignore it
    docs/source/conf.py,
    # The old directory contains Flake8 2.0
    old,
    # This contains our built documentation
    build,
    # This contains builds of flake8 that we don't want to check
    dist
max-complexity = 10

В конфигурации можно перечислить игнорируемые правила и директории,
в которые flake8 заглядывать не будет, а также максимальную
цикломатическую сложность
для функций. Все эти настройки будут автоматически применяться
к запускам flake8 во всех поддиректориях проекта.

Если же вам не хватает какого-нибудь правила, и его нет даже в уже
готовых плагинах, то написание собственного плагина
— не такая уж и сложная задача.
Я попробовал,
у меня на это ушло 2-3 часа.

pylint

pylint — это ещё один популярный линтер для Python.
Этот линтер значительно умнее и продвинутее flake8.
В pylint из коробки заложено очень много правил и рекомендаций,
и по умолчанию они все включены, так что он достаточно строгий и придирчивый.
Чтобы интегрировать его в существующий большой проект придётся потратить
некоторое время, чтобы выбрать те правила, которые для вас важны.
Так же как и flake8, pylint поддерживает плагины для расширения
базовой функциональности, но насколько я вижу, экосистема плагинов у pylint
значительно беднее.

Также при каждом запуске pylint выводит оценку качества кода
по десятибалльной шкале, а также следит, как эта оценка меняется
с течением времени. Достичь десятки очень сложно, но это благородная цель,
к которой нужно стремиться.

Установка

Установка pylint принципиально ничем не отличается от установки flake8.
Выполнить внутри виртуального окружения проекта:

Для pipenv:

$ pipenv install --dev pylint

Для poetry:

$ poetry add --dev pylint

Использование

pylint можно натравить на определённый файл:

С директориями у pylint дела обстоят чуть сложнее. Все директории он
обрабатывает как питоновские модули, поэтому если в директории нет хотя бы
пустого файла __init__.py, то работать с ней pylint не сможет. Имейте
это ввиду.

Давайте попросим pylint прокомментировать файл с плохими практиками
из предыдущего примера:

$ pylint bad_code.py
************* Module bad_code
bad_code.py:4:25: C0326: No space allowed before bracket
def  CalculateSquareRoot (Number ):
                         ^ (bad-whitespace)
bad_code.py:4:33: C0326: No space allowed before bracket
def  CalculateSquareRoot (Number ):
                                 ^ (bad-whitespace)
bad_code.py:5:0: W0312: Found indentation with tabs instead of spaces (mixed-indentation)
bad_code.py:5:21: C0326: No space allowed before bracket
    return  sqrt(Number )
                     ^ (bad-whitespace)
bad_code.py:13:19: C0326: Exactly one space required around assignment
        your_number=float(input('Enter your number: '))
                   ^ (bad-whitespace)
bad_code.py:14:66: C0303: Trailing whitespace (trailing-whitespace)
bad_code.py:15:13: C0303: Trailing whitespace (trailing-whitespace)
bad_code.py:1:0: W0622: Redefining built-in 'pow' (redefined-builtin)
bad_code.py:1:0: C0114: Missing module docstring (missing-module-docstring)
bad_code.py:1:0: W0401: Wildcard import math (wildcard-import)
bad_code.py:4:0: C0103: Function name "CalculateSquareRoot" doesn't conform to snake_case naming style (invalid-name)
bad_code.py:4:0: C0103: Argument name "Number" doesn't conform to snake_case naming style (invalid-name)
bad_code.py:4:0: C0116: Missing function or method docstring (missing-function-docstring)
bad_code.py:7:0: W0102: Dangerous default value [] as argument (dangerous-default-value)
bad_code.py:7:0: C0103: Argument name "l" doesn't conform to snake_case naming style (invalid-name)
bad_code.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
bad_code.py:16:4: W0702: No exception type(s) specified (bare-except)
bad_code.py:1:0: W0614: Unused import acos from wildcard import (unused-wildcard-import)
bad_code.py:1:0: W0614: Unused import acosh from wildcard import (unused-wildcard-import)
bad_code.py:1:0: W0614: Unused import asin from wildcard import (unused-wildcard-import)
bad_code.py:1:0: W0614: Unused import asinh from wildcard import (unused-wildcard-import)
...
bad_code.py:2:0: W0611: Unused import itertools (unused-import)
-------------------------------------
Your code has been rated at -41.43/10

Я немного сократил вывод. Как видите, даже без плагинов pylint нашёл
все ожидаемые ошибки, и даже больше — например, он даже предлагает написать
документацию.

По каждой ошибке можно запросить более подробную справку, используя
название правила из конца строки с ошибкой или код:

$ pylint --help-msg=missing-docstring
$ pylint --help-msg=R0902

Вот какие ошибки pylint находит для файла, который с точки зрения flake8
не содержит никаких ошибок:

$ pylint not_so_bad_code.py 
************* Module not_so_bad_code
not_so_bad_code.py:1:0: C0114: Missing module docstring (missing-module-docstring)
not_so_bad_code.py:4:0: C0116: Missing function or method docstring (missing-function-docstring)
not_so_bad_code.py:8:0: C0103: Argument name "l" doesn't conform to snake_case naming style (invalid-name)
not_so_bad_code.py:8:0: C0116: Missing function or method docstring (missing-function-docstring)
not_so_bad_code.py:20:11: W0703: Catching too general exception Exception (broad-except)
-----------------------------------
Your code has been rated at 6.67/10

А вот так в pylint можно игнорировать отдельную ошибку на строке прямо в файлах
с кодом:

def append_item(item, l=None):  # pylint: disable=C0103
   ...

Ещё pylint умеет игнорировать ошибки в блоках кода:

def test():
    # Disable all the no-member violations in this function
    # pylint: disable=no-member
    ...

И для файлов целиком. Вот так можно отключить все ошибки из категорий
Warning, Convention и Refactor:

А можно не проверять файл вообще:

Подробнее о правилах управления сообщениями
смотрите в документации.
Для более сложной настройки правил, придётся по-настоящему сконфигурировать
pylint.

Конфигурация

pylint настраивается через файл .pylintrc в корне проекта. Чтобы создать
дефолтный файл конфигурации, нужно выполнить следующую команду:

$ pylint --generate-rcfile > .pylintrc

Созданный файл содержит все поддерживаемые pylint опции с довольно
подробными комментариями, так что углубляться я не буду.

Плагины

Давайте установим какой-нибудь популярный плагин, например,
pylint-django:

$ pip install pylint-django

Теперь запускать pylint нужно вот так:

$ pylint --load-plugins pylint_django [..other options..] <path_to_your_sources>

либо в .pylintrc нужно исправить директиву load-plugins:

load-plugins=pylint_django

Интеграция линтера в проект

Интегрировать линтер в проект можно на трёх уровнях.
Я рекомендую по возможности использовать все три, но обязательным
является как минимум один (лучше всего, чтобы это была CI система).

Редактор кода или IDE

Популярные IDE для Python умеют легко интегрировать с линтерами и
подсвечивать ошибки линтера прямо в редактируемом файле.
Это удобно, потому что позволяет не выходя из редактора получить
полезную обратную связь.

PyCharm автоматически находить установленные flake8 и pylint внутри
интерпретатора проекта
и подключается к ним.

VS Code требует небольшой настройки, которая
описана здесь.

Git-хуки

Также читайте пост про Git-хуки и pre-commit.

В git есть возможность запрограммировать
определенные скрипты (хуки) в ответ на действия пользователя.
Например, можно запускать
какие-нибудь проверки перед коммитом, заново скачивать зависимости проекта
при переключении веток, высылать сообщение в рабочий чат
после пуша в удалённый репозиторий и вообще что угодно.

я запушель

Нас интересует возможность запускать линтер перед коммитом так,
чтобы если линтер найдёт какие-нибудь проблемы, операция коммита прерывалась.
Git-хуки можно настроить, написав несложный shell-скрипт,
но я рекомендую использовать для этого специальные утилиты,
такие как pre-commit.
Вот здесь
можно найти описание процесса настройки запуска flake8 через pre-commit.

Обратите внимание, что Git-хуки нужно будет настроить на машине каждого
разработчика в проекте.

Continuous Integration (CI)

Последний эшелоном защиты от попадания “сломанного” кода в основную ветку
репозитория является система непрерывной интеграции (CI) — такая, как:

  • GitHub Actions;
  • GitLab CI
    (а ещё читайте пост в блоге моего хорошего товарища про
    основы GitLab CI);
  • Travis CI;
  • или другая.

На каждый пуш в репозиторий система непрерывной интеграции должна
запускать проверки (включая все линтеры и тесты), и если что-то идёт
не так, рядом с коммитом должен появиться красный крестик.
Ветку с таким коммитом на конце нельзя будет слить с основной
веткой проекта через пулл-реквест на GitHub (или мёрдж-реквест на GitLab).
Пример того, как настроить GitHub Actions
для запуска flake8 и других питоновских проверок.

CI — это единственный надёжный способ обеспечить качество кода.
Предыдущие способы нужны скорее для удобства разработчика, чтобы он
как можно скорее получал обратную связь, но разработчик вправе проигнорировать
или отключить эти предупреждения.

Заключение

В подзаголовке этой статьи я написал фразу, что линтер способен
сэкономить разработчику один день жизни в месяц. Фраза может показаться
кликбейтной, но, поверьте мне, это так это и работает.
Возможно, я даже преуменьшил.
Чем раньше найдена ошибка, тем быстрее идёт разработка.
Иногда линтер предотвращает баги, иногда спасает от мучительного
траблшутинга. Линтеры абсолютно точно значительно сокращают время,
потраченное коллегами на код-ревью, потому что все тривиальные
ошибки будут отловлены автоматикой.

Не стоит недооценивать линтеры. Это те инструменты,
которые делают из “кодера” настоящего “software engineer”,
из мальчика — мужчину. Если вы до сих пор не пользуетесь каким-нибудь
линтером, то рекомендую всерьез задуматься над внедрением!

Я предпочитаю использовать flake8, потому что он простой
и понятный, как топор. С ним легко работать, его легко настроить
под свои нужды, а почти любые недостающие правила можно получить
через уже готовые плагины.

У pylint тоже есть свои последователи. Его ценят за подробный вывод
и большое количество правил в стандартной поставке.
Мне же pylint всегда казался слишком сложным в эксплуатации.

А кто-то вообще рекомендует устанавливать flake8 и pylint параллельно.

Если понравилась статья, то
подпишитесь на уведомления
о новых постах в блоге, чтобы ничего не пропустить!

Дополнительное чтение

  • документация flake8;
  • исходный код flake8;
  • список плагинов flake8;
  • сайт, где можно посмотреть правила flake8;
  • документация pylint;
  • исходный код pylint;
  • обсуждение “flake8 vs pylint” на Reddit;
  • пост на RealPython про качество кода;
  • статья на Хабре про линтеры.

Обложка: Sa Mu, Traffic Light

Добавить комментарий