Introducing Noir
tl;dr
Noir is a drop-in replacement for Black (the uncompromising
code formatter), with the default line length set to PEP-8’s preferred
79 characters. If you want to use it, just replace black
with noir
in your
requirements.txt
and/or setup.py
and you’re good to go.
Black is a Python code formatter that reformats your code to make it more PEP-8 compliant. It implements a subset of PEP-8, most notably it deliberately ignores PEP-8’s suggestion for a line length of 79 characters and defaults to a length of 88. I find the decision and the reasoning behind that somewhat arbitrary. PEP-8 is a good standard and there’s a lot of value in having a style guide that is generally accepted and has a lot of tooling to support it.
When people ask to change Black’s default line length to
79, the issue is usually closed with a reference to the reasoning in the
README
. But Black’s developers are at least aware of this controversial
decision, as Black’s only option that allows to configure the (otherwise
uncompromising) code formatter, is in fact the line length.
Apart from that, Black is a good formatter that’s gaining more and more popularity. And, of course, the developers have every right to follow their own taste. However, since Black is licensed under the terms of the MIT license, I tried to see what needs to be done in order to fix the line length issue.
Step 1: Changing the Default
This is the easiest part. You only have to change the
DEFAULT_LINE_LENGTH
value in black.py
from 88
to 79
, and black works
as expected. Bonus points for doing the same in black.vim
and
pyproject.toml
, but not strictly necessary.
Step 2a: Fixing the Tests
Now comes the fun part. Black has an extensive test suite and suddenly a lot of tests are failing because the fixtures that compare the unformatted input with the expected, formatted output were written with a line length of 88 characters in mind. To make it more interesting the expected output comes in two forms: (1) as normal reformatted Python code (which is rather easy to fix) and (2) as a diff between the input and the expected output. The latter was really painful to fix – although I’m very much used to reading diffs, I don’t usually write them.
Step 2b: Fixing the Tests
After all fixtures were updated, some tests were still failing. And it turned
out that Black is running itself on its own source code as part of its test
suite, making the tests fail if Black’s code does not conform to Black’s
coding standards. While this is a genius idea, it meant that I had to reformat
Black’s code to match the new 79 characters line length, generating a giant
diff, that is functionally unrelated to the fix I wanted to make but
now part of the fix anyway. This of course makes the whole patch horrible to
maintain if you plan to follow along upstream’s master
branch.
Step 3: Publish
Since we already got this far, why not publish the fixed version of Black? To my surprise the name noir was still available on PyPi, so I renamed my version of Black to Noir and uploaded it to PyPi.
You can install it via:
$ pip install noir
Since I didn’t change anything else, this is literally a drop-in replacement
for Black. All you have to do is replace black
with noir
in your
requirements.txt
and/or setup.py
and you’re good to go. The script that
executes Black is still called black
and the server is still called
blackd
.
Outlook
While this was a fun exercise, the question remains what to do with it. I’ll try to follow upstream and update my patch whenever a new version will come out. As new versions of Black are released only a handful of times a year, this might be feasible.
Depending on how painful it is to maintain the patch for the tests, I might
either drop the tests altogether, relying on upstream’s tests passing on their
side and just maintaining the trivial patch from Step 1: Changing the
DEFAULT_LINE_LENGTH
. The latter can probably be automated somehow using
github actions – and I’ll probably look into that at some point.
Best case scenario, of course, would be if Python changes its recommended line length to 88 and I wouldn’t have to maintain noir in the first place :)