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
Comments
- 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.
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.
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.
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
3.0
48.0
165 KB
Flake8 plugin for return expressions checking.
License: MIT License
Makefile 3.10%
flake8-return’s Introduction
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” в честь вот такой
штуки:
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