Writing better Python code, automatically
Updated 2021-10-13: Just fixed a few typos and made some minor updates. If you're reading this in 2021 (or later) you should probably use poetry instead of pip and configure your tools with pyproject.toml
instead of setup.cfg
, but otherwise this is still pretty good material.
As a software developer, your time, focus, and mental stamina are precious resources. Modern tooling can free you from having to think about all sorts of menial tasks, letting you write better code with less effort. This post outlines a process for gradually introducing such tooling into an established project.
Step 1: catching bugs and anti-patterns
Flake8 is a Python linter: a tool which checks Python code for errors and evaluates it for quality and style. There is an ecosystem of plugins which add further checks or integrate other tools, notably:
- flake8-bugbear, which has rules that are considered too subjective for the main Flake8 distribution,
- flake8-comprehensions, which checks for issues around comprehensions, generators, and initialisation,
- flake8-docstrings, which uses pydocstyle to enforce documentation presence and style,
- flake8-requirements, which checks for missing or unused dependencies, and
- pep8-naming, which checks names against the PEP8 naming conventions.
Code that passes Flake8 is free of a range of issues that would otherwise not show up until later in the development cycle, which saves a lot of time. Since Python is an interpreted language, it's easy to introduce errors which even a reasonably thorough testing procedure wouldn't detect. For example, consider this simple refactoring:
# Before
def main():
try:
cfg = parse_config(open("config.txt").read())
except FileNotFoundError:
cfg = {}
run(cfg)
# After
def main():
try:
config = parse_config(open("config.txt").read())
except FileNotFoundError:
cfg = {}
run(config)
The new main
appears to work normally, but if the config file is not present it fails with UnboundLocalError
.
Flake8 catches the problem, warning F841 local variable 'cfg' is assigned to but never used
.
Although this is a contrived example to demonstrate one particular rule, this kind of error doesn't necessarily show up in manual testing and can even get through moderately thorough unit test suites.
However, adding Flake8 to an existing codebase tends to produce a sea of violations, which can make it hard to integrate into an existing workflow. With that in mind, a good way to transition a codebase into passing Flake8 is to start with a subset of rules that catches only the most serious errors. First, install Flake8:
$ pip install flake8 flake8-bugbear flake8-requirements
Then configure it in setup.cfg
:
# setup.cfg
[flake8]
select =
# pycodestyle
E112,E113,E9,W6,
# pyflakes
F402,F404,F406,F407,F5,F6,F7,F821,F823,F831,
# flake8-bugbear
B,B902,
# flake8-requirements
I900
ignore =
# pycodestyle
E722,W605,
# pyflakes
F504,F522,F523,F541,F705,
# flake8-bugbear
B001,B005,B007,B013,B014,B015
Then run it on your code:
$ flake8
On a large codebase this can turn up a lot of violations, and many of them will be real bugs.
Go through them all and either fix them or, if you understand them and you're confident you know better, ignore them one-by-one with # noqa: <code>
comments.
Once the whole codebase passes, we need to make sure it stays that way. It's helpful to set up your editor or IDE to run Flake8 as you type (most editors and IDEs can do this). It's easy to miss a warning that way though, so it's a good idea to also add a Git hook which automatically runs Flake8 before you commit; that way, you won't be able to commit new violations. You can do this easily with pre-commit. First, install it and add it to your repo:
$ pip install pre-commit
$ pre-commit install
Then configure pre-commit to run Flake8, and also a long list of other checks and fixes that are occasionally helpful (see here for details of the other hooks):
# .pre-commit-config.yaml
repos:
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear==20.11.1
- flake8-requirements==1.3.3
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
hooks:
- id: fix-byte-order-marker
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-xml
- id: check-yaml
- id: detect-private-key
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.7.0
hooks:
- id: python-check-blanket-noqa
- id: python-check-mock-methods
- id: python-no-eval
- id: python-no-log-warn
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
- id: text-unicode-replacement-char
Now as you long as you remember to run pre-commit install
after checking out the repo,1 git commit
will fail on code that doesn't pass Flake8.
To be extra safe, you should also run the checks in continuous integration.
That way, a contributor who hasn't followed your developer instructions and thus doesn't have pre-commit installed will have their code checked (and probably rejected) without your involvement.
If you don't have CI set up and you're using GitHub, just add this in .github/workflows
:
# .github/workflows/checks.yml
name: Run checks
on:
push:
jobs:
checks:
runs-on: ubuntu-18.04
steps:
- name: Check out
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- name: Install pre-commit
run: pip install pre-commit && pre-commit install
- name: Run pre-commit on all files
run: pre-commit run -a
While you're there you may as well run your test suite, which probably looks something like this:
# .github/workflows/tests.yml
name: Run tests
on:
push:
jobs:
tests:
strategy:
fail-fast: false
matrix:
os: [
ubuntu-16.04, ubuntu-18.04, ubuntu-20.04,
macos-10.15, windows-2019
]
python-version: [3.7, 3.8, 3.9]
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: pip install .
- name: Run tests
run: pytest
Note that the pre-commit configuration pins each tool to a specific version in order to prevent surprises.
You can have it update them all for you with pre-commit autoupdate
.2
Step 2: enforcing best practices
Now that we have a system in place, we can extend Flake8's scope to not only catch likely bugs but also enforce best practises and general tidiness.
# In .pre-commit-config.yaml (replacing the previous
# flake8 config)
- id: flake8
additional_dependencies:
- flake8-bugbear==20.11.1
- flake8-comprehensions==3.3.0
- flake8-docstrings==1.5.0
- flake8-requirements==1.3.3
- pep8-naming==0.11.1
- pydocstyle==5.1.1
# In setup.cfg, replacing the previous flake8 config
[flake8]
# flake8-docstrings config
docstring-convention = google
# rules
select =
# pycodestyle
E112,E113,E71,E72,E74,E9,W6,
# pyflakes
F,
# flake8-bugbear
B,B902,
# flake8-comprehensions
C4,
# flake8-docstrings
D1,
# flake8-requirements
I,
# pep8-naming
N807,
ignore =
# pycodestyle (overlaps with B904)
E722,
# pyflakes (allow star imports)
F403,F405,
# flake8-comprehensions (allow dict() calls)
C408,
# pydocstyle
# (allow __init__ without docstring)
D107,
# (allow first line of docstring to wrap)
D415,
# pep8-naming (overlaps with B902)
D404,D405,
Now Flake8 will catch all the same bugs as before as well as a lot of anti-patterns, dead code, missing documentation and so on. Passing all these checks should make your code pretty functional overall. More importantly, it should stay that way without much effort on your part, and anyone who sends you a PR will have their code held to the same standard automatically.
Step 3: maintaining a consistent style
So far we've set up tooling to help write code that works better. Another way we can save effort is by automating the way code looks. Traditionally, software engineering teams have a style guide which everyone consciously follows, and code is manually checked for conformance during code review. This results in a consistent code base which is easy to read and comfortable to work in at the expense of constant minor effort and occasional bitter debates. With modern tooling, those downsides go away.
The easiest way to maintain a consistent code style is to use Black, the Python formatter with basically no options.
Black's code style is an opinionated subset of PEP8.
I don't always appreciate its style choices, particularly how it indents deeply-nested data structures and how it formats math, but in my opinion the benefits outweigh the minor annoyances.
It's the most popular Python formatter by a wide margin, with over 35k 83k projects on GitHub using it, so there's a strong argument that getting comfortable with the Black style will pay off if you want to be part of the greater Python community.
Not everyone is a fan though; if you have strong feelings about single quotes or where brackets should go you might prefer Google's yapf, which is extremely configurable, or if you don't believe in auto-formatting at all then you might prefer to just use strict Flake8 checks (like the ones we're about to set up).
While we're making sweeping code changes, we may as well use isort to keep package imports sorted3 and yesqa to automatically remove unnecessary # noqa
comments:4
# In the flake8 config in .pre-commit-config.yaml
- id: flake8
additional_dependencies: &flake8-deps
# Under repos: in .pre-commit-config.yaml
- repo: https://github.com/ambv/black
rev: 20.8b1
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.6.4
hooks:
- id: isort
- repo: https://github.com/asottile/yesqa
rev: v1.2.2
hooks:
- id: yesqa
additional_dependencies: *flake8-deps
# In setup.cfg, replacing the previous flake8 config
[flake8]
max-line-length = 88
# mccabe config
max-complexity = 12
# flake8-docstrings config
docstring-convention = google
# rules
select =
# mccabe
C9,
# pycodestyle
E,W,
# pyflakes
F,
# flake8-bugbear
B,B9,
# flake8-comprehensions
C4,
# flake8-docstrings
D,
# flake8-requirements
I,
# pep8-naming
N,
ignore =
# pycodestyle (overlaps with B904)
E722,
# pycodestyle (for black)
E203,W503,
# pyflakes (allow star imports)
F403,F405,
# flake8-bugbear (overlaps with E501)
B950,
# flake8-comprehensions (allow dict() calls)
C408,
# pydocstyle
# (allow __init__ without docstring)
D107,
# (allow first line of docstring to wrap)
D415,
# pep8-naming (overlaps with B902)
D404,D405,
# Also in setup.cfg
[isort]
profile=black
Black should fix most of the style issues Flake8 checks, so now Flake8 is turned on all the way apart from where tools clash and a few things I find too strict. Now when you go to commit, all the files you touched are reformatted with a standard code style, all the docstrings are checked for conformance with the Google docstring convention, all the names are checked against the PEP8 naming conventions, and all the functions are evaluated for complexity.
Getting the code style consistent is undeniably disruptive though.
Reformatting the whole codebase in one go and using git config blame.ignoreRevsFile
5 (as the Black documentation recommends) leaves git blame
mostly working,6 but will still show up in every file's history and probably mess up merging any active branches.
Reformatting each file as you touch it leaves the history tidy but ruins git blame
.
Reformatting each file in a separate commit before you touch it and adding every single reformatting commit to the ignore-revs file is arguably the best of both worlds, but is also more work and easy to mess up.
Black and pre-commit can help with any of those approaches, but for anything other than doing it all at once you'll have to take pre-commit out of CI and run the other checks manually.
Step 4: bonus points
If you stop here you'll be pretty well set up, but there are a few more tools you can use that require a bit more effort.
More linting
Pylint is the original Python linter. It produces more false positives than Flake8 but also catches more bugs, so if you're starting fresh or don't mind going through a lot of minor issues then it's worth a go.
# In setup.cfg
[pylint.MASTER]
disable =
# Fails in pre-commit venv
import-error,
# Conflicts with or covered by other tools
bad-continuation,
line-too-long,
missing-docstring,
ungrouped-imports,
wildcard-import,
wrong-import-order,
# Annoying
fixme,
no-self-use,
too-few-public-methods,
unused-wildcard-import,
# Under repos: in .pre-commit-config.yaml
- repo: https://github.com/PyCQA/pylint
rev: pylint-2.6.0
hooks:
- id: pylint
There are also other linters I haven't used, notably flake8-pytest-style, which checks pytest
tests, Radon, which complains if your code is too complex, and Bandit, which checks for security flaws.
Static typing
Mypy is a static type checker which often catches impressively subtle bugs. Static type checkers for dynamic languages rely on the assumption that variables really only have one type at a time, which turns out to be usually true. Many commonly-used libraries have type information available already, so out of the box Mypy will warn you about calling library functions with the wrong arguments, using their return values incorrectly, and so on. If you add type annotations to your code then it will do the same for you. Ideally, every function should have parameter and return type annotations.
# Under repos: in .pre-commit-config.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.720
hooks:
- id: mypy
# In setup.cfg
[mypy]
# Don't warn when an import cannot be resolved
ignore_missing_imports = True
# Check the body of every function, regardless of
# whether it has type annotations
check_untyped_defs = True
# Warn about casts that do nothing
warn_redundant_casts = True
# Warn about "type: ignore" comments that do nothing
warn_unused_ignores = True
# Warn when a function is missing return statements in
# some execution paths
warn_no_return = True
# Warn about code determined to be unreachable or
# redundant after performing type analysis
warn_unreachable = True
# Allow variables to be redefined with a different type
allow_redefinition = True
# Prefixes each error with the relevant context
show_error_context = True
# Shows error codes in error messages, so you can use
# specific ignore comments
# i.e., "type: ignore[code]"
show_error_codes = True
# Use visually nicer output in error messages
pretty = True
You can leave most things un-annotated and still get a lot out of it, but it will occasionally complain. For example:
values = {"test": 1}
# The type of values is inferred as Dict[str, int]
values["test2"] = "test"
# Mypy gives error: Incompatible types in assignment
# (expression has type "str", target has type "int")
The error is that the dictionary was used differently to how Mypy assumed it would be; you can fix it by adding a type annotation:
from typing import Dict, Any
values = {"test": 1} # type: Dict[str, Any]
values["test2"] = "test"
Mypy was developed by a team at Dropbox in the early 2010s. Lately, a few competitors have arrived: Pytype from Google, Pyre from Facebook, and Pyright from Microsoft. I haven't used them, but you might prefer them; Pyright, in particular, integrates nicely with VS Code.
Conclusion
In this post, I've shown a workflow that uses an assortment of tools to catch bugs early, ensure code follows best practises, and maintain a consistent style, all while minimising the amount of effort required. The code this workflow produces is not automatically perfect: there is more to writing good code than what a tool can automate. Following the workflow is more effort than just writing bad code, and you might disagree with some of the specifics, but if your aim is to write the best code you can then these tools can help.
Here are the final config files:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/ambv/black
rev: 20.8b1
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4
hooks:
- id: flake8
additional_dependencies: &flake8-deps
- flake8-bugbear==20.11.1
- flake8-comprehensions==3.3.0
- flake8-docstrings==1.5.0
- flake8-requirements==1.3.3
- pep8-naming==0.11.1
- pydocstyle==5.1.1
- repo: https://github.com/pycqa/isort
rev: 5.6.4
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
hooks:
- id: check-builtin-literals
- id: check-case-conflict
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-xml
- id: check-yaml
- id: detect-private-key
- id: end-of-file-fixer
- id: fix-byte-order-marker
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.7.0
hooks:
- id: python-check-blanket-noqa
- id: python-check-mock-methods
- id: python-no-eval
- id: python-no-log-warn
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
- id: text-unicode-replacement-char
- repo: https://github.com/PyCQA/pylint
rev: pylint-2.6.0
hooks:
- id: pylint
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.790
hooks:
- id: mypy
- repo: https://github.com/asottile/yesqa
rev: v1.2.2
hooks:
- id: yesqa
additional_dependencies: *flake8-deps
# setup.cfg
[flake8]
max-line-length = 88
# mccabe config
max-complexity = 12
# flake8-docstrings config
docstring-convention = google
# rules
select =
# mccabe
C9,
# pycodestyle
E,W,
# pyflakes
F,
# flake8-bugbear
B,B9,
# flake8-comprehensions
C4,
# flake8-docstrings
D,
# flake8-requirements
I,
# pep8-naming
N,
ignore =
# pycodestyle (overlaps with B904)
E722,
# pycodestyle (for black)
E203,W503,
# pyflakes (allow star imports)
F403,F405,
# flake8-bugbear (overlaps with E501)
B950,
# flake8-comprehensions (allow dict() calls)
C408,
# pydocstyle
# (allow __init__ without docstring)
D107,
# (allow first line of docstring to wrap)
D415,
# pep8-naming (overlaps with B902)
D404,D405,
[isort]
profile = black
[pylint.MASTER]
disable =
# Fails in pre-commit venv
import-error,
# Conflicts with or covered by other tools
bad-continuation,
line-too-long,
missing-docstring,
ungrouped-imports,
wildcard-import,
wrong-import-order,
# Annoying
fixme,
no-self-use,
too-few-public-methods,
unused-wildcard-import,
[mypy]
# Don't warn when an import cannot be resolved
ignore_missing_imports = True
# Check the body of every function, regardless of
# whether it has type annotations
check_untyped_defs = True
# Warn about casts that do nothing
warn_redundant_casts = True
# Warn about "type: ignore" comments that do nothing
warn_unused_ignores = True
# Warn when a function is missing return statements in
# some execution paths
warn_no_return = True
# Warn about code determined to be unreachable or
# redundant after performing type analysis
warn_unreachable = True
# Allow variables to be redefined with a different type
allow_redefinition = True
# Prefixes each error with the relevant context
show_error_context = True
# Shows error codes in error messages, so you can use
# specific ignore comments
# i.e., "type: ignore[code]"
show_error_codes = True
# Use visually nicer output in error messages
pretty = True
# .github/workflows/checks.yml
name: Run checks
on:
push:
jobs:
checks:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v2
- name: Set up Python
- name: Install pre-commit
run: pip install pre-commit && pre-commit install
- name: Run pre-commit on all files
run: pre-commit run -a
Appendix: Flake8 rules by importance
As of flake8 3.8.4, flake8-bugbear 20.11.1, flake8-comprehensions 3.3.0, pydocstyle 5.1.1, flake8-requirements 1.3.3, and pep8-naming 0.11.1, here are the rules I think are important. I've abbreviated some of the flake8-bugbear and flake8-comprehensions rules; see their docs for full explanations. For full explanations of many of the core Flake8 rules, see Grant McConnaughey's Big Ol' List of Rules. Note that many pydocstyle rules are disabled by default depending on the docstring convention selected.
Rules that catch bugs:
E112: Expected an indented block
E113: Unexpected indentationE901:
SyntaxError
orIndentationError
E902:IOError
E999:SyntaxError
W601:
.has_key()
is deprecated, usein
W602: Deprecated form of raising exception
W603:<>
is deprecated, use!=
W604: Backticks are deprecated, userepr()
W606:async
andawait
are reserved keywords starting with Python 3.7F402: Import
module
from lineN
shadowed by loop variable
F404: Future import(s)name
after other statements
F406:from module import *
only allowed at module level
F407: An undefined__future__
feature name was importedF501: Invalid
%
format literal
F502:%
format expected mapping but got sequence
F503:%
format expected sequence but got mapping
F505:%
format missing named arguments
F506:%
format mixed positional and named arguments
F507:%
format mismatch of placeholder and argument count
F508:%
format with*
specifier requires a sequence
F509:%
format with unsupported format character
F521:.format(...)
invalid format string
F524:.format(...)
missing argument
F525:.format(...)
mixing automatic and manual numberingF601: Dictionary key
name
repeated with different values
F602: Dictionary key variablename
repeated with different values
F621: Too many expressions in an assignment with star-unpacking
F622: Two or more starred expressions in an assignment(a, *b, *c = d)
F631: Assertion test is a tuple, which is alwaysTrue
F632: Use==/!=
to comparestr
,bytes
, andint
literals
F633: Use of>>
is invalid with
F634:if
test is a tuple, which is alwaysTrue
F701: A
break
statement outside of awhile
orfor
loop
F702: Acontinue
statement outside of awhile
orfor
loop
F703: Acontinue
statement in afinally
block in a loop
F704: Ayield
oryield from
statement outside of a function
F706: areturn
statement outside of a function/method
F707: Anexcept:
block as not the last exception handler
F721: Syntax error in doctest
F722: Syntax error in forward annotation
F723: Syntax error in type commentF821: Undefined name
name
F823: Local variablename
… referenced before assignment
F831: Duplicate argumentname
in function definitionB002: Python does not support the unary prefix increment
B003: Assigning toos.environ
doesn't clear the environment
B004: Usinghasattr(x, '__call__')
to test if x is callable is unreliable
B006: Do not use mutable data structures for argument defaults
B008: Do not perform function calls in argument defaults
B009: Do not callgetattr(x, 'attr')
B010: Do not callsetattr(x, 'attr', val)
B011: Do not callassert False
B012: Use ofbreak
,continue
orreturn
insidefinally
blocks will silence exceptions or override return values from thetry
orexcept
blocks
B016: Cannot raise a literalB301: Python 3 does not include
.iter*
methods on dictionaries
B302: Python 3 does not include.view*
methods on dictionaries
B303: The__metaclass__
attribute on a class definition does nothing on Python 3
B304:sys.maxint
is not a thing on Python 3
B305:.next()
is not a thing on Python 3
B306:BaseException.message
has been deprecated as of Python 2.6 and is removed in Python 3B902: Invalid first argument used for method
I900: Package is not listed as a requirement
Rules that make your code better:
E711: Comparison to none should be
if cond is none:
E712: Comparison to true should beif cond is true:
orif cond:
E713: Test for membership should benot in
E714: Test for object identity should beis not
E721: Do not compare types, useisinstance()
E722: Do not use bareexcept
, specify exception instead
E741: Do not use variables namedI
,O
, orl
E742: Do not define classes namedI
,O
, orl
E743: Do not define functions namedI
,O
, orl
W605: invalid escape sequence
x
F401:
module
imported but unusedF504:
%
format unused named arguments
F522:.format(...)
unused named arguments
F523:.format(...)
unused positional arguments
F541: F-string without any placeholdersF705: A
return
statement with arguments inside a generatorF811: Redefinition of unused
name
from lineN
F812: List comprehension redefinesname
from lineN
F822: Undefined namename
in__all__
F841: Local variablename
is assigned to but never usedF901:
raise NotImplemented
should beraise NotImplementedError
B001: Do not use bare
except:
, it also catches unexpected events like memory errors, interrupts, system exit, and so on
B005: Using.strip()
with multi-character strings is misleading the reader
B007: Loop control variable not used within the loop body
B013: A length-one tuple literal is redundant inexcept
statements
B014: Redundant exception types inexcept (Exception, TypeError):
B015: Pointless comparisonC400-C402: Unnecessary generator
C403-C404: Unnecessary list comprehension
C405-C406: Unnecessary (list/tuple) literal
C407: Unnecessary (dict/list) comprehension
C409: Unnecessary (list/tuple) passed totuple()
C410: Unnecessary (list/tuple) passed tolist()
C412: Unnecessary (dict/list/set) comprehension
C413: Unnecessary list call aroundsorted()
C413: Unnecessary reversed call aroundsorted()
C415: Unnecessary subscript reversal of iterable withinreversed/set/sorted()
C416: Unnecessary (list/set) comprehensionD100: Missing docstring in public module
D101: Missing docstring in public class
D102: Missing docstring in public method
D103: Missing docstring in public function
D104: Missing docstring in public package
D105: Missing docstring in magic method
D106: Missing docstring in public nested classI901: Package is required but not used
N807: function name should not start and end with
__
Rules that make your code better but can be a lot of effort to fix on an existing codebase:
F403:
from module import *
used; unable to detect undefined names
F405:name
may be undefined, or defined from star imports:module
The other rules are all for code style; many are important but none affect correctness.
-
You can also use the git template directory to have
git clone
andgit init
automatically install pre-commit. ↩ -
Unfortunately,
pre-commit autoupdate
will not update the flake8 plugins (flake8#1351). ↩ -
PEP8 specifies a standard order for package imports: standard library imports, then third party imports, then local imports. If your project structure is complicated, you may have to inform isort which modules are yours using the
known_first_party
option. ↩ -
In the pre-commit config,
&flake8-deps
is an anchor and*flake8-deps
is an alias. This is a YAML feature which enables sections of the config to be reused, in this avoiding repetition of the list of Flake8 plugins. ↩ -
This Moxio blog post is a good explanation of
blame.ignoreRevsFile
. ↩ -
Unfortunately neither GitHub nor GitLab respect
blame.ignoreRevsFile
; local tools are more likely to support it. ↩
Comments