Today we released Black 23.1.0 🎉.
While Black does not follow SemVar, it is indeed a major release as it ships with the 2023 stable style. Many changes made to the preview style over the past year have been finally promoted to the stable style.
You can install it by running this command:
python -m pip install black==23.1.0
Below I’ll cover the important changes worth mentioning. If you’d like the full changelog for 23.1.0, please refer to the link above.
Code style
The 2023 stable style
We didn’t make any major changes to the 2023 stable style draft since 23.1a1.
So for the major parts of the new stable style, please refer to the 23.1a1 post. Everything under the “promoted changes” header were promoted officially in 23.1.0. The TL;DR is:
- Improved empty line handling (mostly removing unnecessary ones)
- Removal of redundant parentheses in several contexts
We did also fix quite a few bugs and crashes, including these ones reporting by users testing out 23.1a1:
- Preview style: Crash on walrus in with statement (#3472)
- Extra newlines added before function with fmt skip on decorator (#3454)
- Preview style incorrectly removes parens from walrus in annotation (#3419)
For the rest, please refer to the changelog.
The 2023 preview style
Conditional expressions are parenthesized if needed now
If a conditional expression is too long, it will be wrapped in parentheses instead of whatever nonsense Black did before.
I’ll just let this diff speak for itself :)
--- pyanalyze/typeshed.py 2021-05-30 03:06:13.755715 +0000
+++ pyanalyze/typeshed.py 2021-05-30 03:06:32.534930 +0000
@@ -448,13 +448,13 @@
arg = arg.replace(kind=SigParameter.POSITIONAL_OR_KEYWORD)
cleaned_arguments.append(arg)
return Signature.make(
cleaned_arguments,
callable=obj,
- return_annotation=GenericValue(Awaitable, [return_value])
- if is_async_fn
- else return_value,
+ return_annotation=(
+ GenericValue(Awaitable, [return_value]) if is_async_fn else return_value
+ ),
)
def _parse_param_list(
self,
args: Iterable[ast3.arg],
There are certain cases where it might be overkill, like this one, but overall, I consider it a major improvement.
normalized = [
- (source, source)
- if source == "-"
- else (normalize_path_maybe_ignore(Path(source), root), source)
+ (
+ (source, source)
+ if source == "-"
+ else (normalize_path_maybe_ignore(Path(source), root), source)
+ )
for source in src
]
Wrap multiple context managers in parentheses if targeting Python 3.9+
Since Python 3.91, you can break long with statements using parentheses. Thanks to work done by @yilei, Black will now use this style as long as the lowest targeted version is 3.9.
Source:
with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
Before: unchanged!!
After:
with (
make_context_manager1() as cm1,
make_context_manager2() as cm2,
make_context_manager3() as cm3,
make_context_manager4() as cm4,
):
pass
Black still doesn’t have a good story for formatting such with statements when Python <3.9 support is requested. All of the gory details can be found in issue #664. (The summary is that no one has stepped up to implement the proposed style.)
Packaging
mypyc wheels are available for all platforms (once again)
Long story short, recent versions of packaging and hatchling interacted with ways that led to errors when trying to build macOS mypyc wheels. The essence is that the wrong platform tag was being chosen which made the wheels uninstallable in certain situations (including on the same machine that built wheels >.<).
Anyway that’s all fixed now and 23.1.0 ships with macOS wheels, bringing us back to the full set of compiled wheels.
This affected the 22.10.0, 22.12.0, and 23.1a1 releases.
mypycified Black can be now built on armv7
We upgraded mypy/c from 0.971
which 0.991
which allows mypycified Black to be built on
armv7.
Conveniently, this also fixes some segfaults caused by mypyc:
- Segfault with black 22.1.0 (#2845)
- black running on address sanitized Python interpreter crashes (#2867)
- same underlying issue as the one above
Output
No longer incorrectly claim that symbolic links exist when formatting from outside a project
When trying to format a project from the outside, the verbose output shows says that there are symbolic links that points outside of the project, but displays the wrong project path. This behavior was triggered when Black is executed from outside the project’s root.
Consider the following tree:
.
└── home/
└── project/
├── .git
└── dir/
└── main.py
When trying to format a folder from home, this is the output:
$ black ./project --check --verbose
Identified `/path/to/home/project` as project root containing a .git directory.
Sources to be formatted: "."
project/.git ignored: is a symbolic link that points outside /path/to/home/project/project
project/.git ignored: matches the --exclude regular expression
project/dir ignored: is a symbolic link that points outside /path/to/home/project/project
project/dir/file.py ignored: is a symbolic link that points outside /path/to/home/project/project
would reformat project/dir/file.py
Oh no! đź’Ą đź’” đź’Ą
1 file would be reformatted.
Notice that the message
$FILEPATH ignored: is a symbolic link that points outside $PROJECTPATH
displays a wrong
$PROJECTPATH (should be /path/to/home/project/
instead of
/path/to/home/project/project
).
Anyway, Black will no longer emit these superfluous and incorrect messages. All thanks goes to @aaossa.
Verbose output now shows the full loaded configuration
It’s easier to just copy and paste part of the issue to explain this feature:
Sometimes when dealing with multiple environment settings, IDEs, and pyproject.toml files etc. etc. is beneficial for a project team to have a confirmation about the actual applied black-settings (“line length”) in the CI CD pipeline.
black -v
will show a line like this:Using configuration from project root.
. However, it doesn’t tell us what the settings are […]
… so we changed Black to show the loaded configuration if -v
/ --verbose
is passed.
$ black src/black/parsing.py --verbose
Identified `/home/ichard26/programming/oss/black` as project root containing a .git directory.
Sources to be formatted: "src/black/parsing.py"
Using configuration from project root.
line_length: 88
target_version: ['py37', 'py38']
include: \.pyi?$
extend_exclude: /(
# The following are specific to Black, you probably don't want those.
| blib2to3
| tests/data
| profiling
)/
preview: True
src/black/parsing.py already well formatted, good job.
All done! ✨ 🍰 ✨
1 file left unchanged.
Some cleaning up can definitely be done to the verbose output, but at least it’s there now.
Configuration
Better default for –target-version if PEP 621 python-requires is available
Black will now infer a default set of target versions using the project.requires-python
field in pyproject.toml
if possible.
For example, these are the inferred default for --target-version
for three different
requires-python
values:
3.8.5
becomes["py38"]
>3.6,<3.11
becomes["py37", "py38", "py39", "py310"]
<3.7
becomes["py33", "py34", "py35", "py36"]
- Python 3.3 is the minimal supported version for Black
If you’re curious what Black infers for your project, make sure to comment out any
--target-version
configuration you have set and run Black with --verbose
. Thanks to
the change mentioned previously, there should be a line showing the (inferred)
target-version config value.
If --target-version
is explicitly configured on the command line or in pyproject.toml
,
it will trump the inferred configuration. And if Black encounters any error inferring a
better default, it will simply fallback to per-file autodetection (like it did before).
To make this work, Black now requires packaging (version 22 and up). It’s a tiny dependency and often installed with other development tools so it shouldn’t be a big deal.
In my opinion, the general hope is that the vast majority of projects will eventually
use Black’s inference capabilities instead of setting --target-version
separately.
It’s less work (no need to update it!) and should be less error prone.
At the bare minimum, new projects shouldn’t have to configure --target-version
anymore as long as they define their project metadata
in the standardized way.
This was contributed by the lovely @stinodego.
A few final words
We tried to do some community outreach in an attempt to iron out issues before cutting 23.1.0 and make sure the 2023 stable style wouldn’t be a failure. We got some helpful feedback, but I realize we could’ve done more.
If you have any feedback for this release or suggestions for next time, please let us know! Feel free to open an issue, email me, message us on Discord, whatever.
Anyway, thanks for reading this. On behalf of the maintainer team, I hope you enjoy the new release!
Well officially speaking, this is only supported starting in Python 3.10 and higher, but GvR and co. might have snuck it into 3.9 :P ↩︎