black
black copied to clipboard
Infer target version based on project metadata
Description
Fixes #3124
The logic implemented is as follows:
- If Black's
target_versionis specified in thepyproject.toml, we ignore the project metadata. - If it is not specified, we try to read and parse the project metadata to determine a valid list of
TargetVersion. If we fail for whatever reason, we continue without atarget_version. - The list of target versions should include all major Python versions allowed by the
requires-pythonspecifier.3.8.5becomes["py38"]>3.6,<3.11becomes["py37", "py38", "py39", "py310"]<3.7becomes["py33", "py34", "py35", "py36"](Python 3.3 is the minimal supported version for Black)
(for more examples, check the test cases)
Changes:
- Updated
parse_pyproject_toml- When reading the TOML file, we check if
target_versionis specified in Black's config. If not, we try to infer it from the project metadata. - This seemed like the logical place - since we are reading the file anyway, we might aswell extract the project metadata if we need it.
- When reading the TOML file, we check if
- Added basic functionality for reading project metadata.
- Only the PyPA standard is now supported:
project->requires-python. - We could add support for Poetry or other tools later.
- Only the PyPA standard is now supported:
- Added parsing of the version / specifier based on
packaging'sVersionandSpecifierSet- There is some black magic (pun intended) going on in the
strip_specifier_setfunction that I wrote. Unfortunately, working with these kinds of specifiers is nasty when it comes to edge cases. My implementation satisfies all specifier sets I could come up with, but there are probably some corner cases that I did not cover...
- There is some black magic (pun intended) going on in the
- Added
packaging>=20.0as a dependency. This is the minimum version that includes the functionality needed for this implementation. - Updated the help text for the CLI command.
Checklist - did you ...
- [x] Add a CHANGELOG entry if necessary?
- [x] Add / update tests if necessary?
- [x] Add new / update outdated documentation? --> Updated the CLI argument help text
Add new / update outdated documentation? --> There isn't good documentation on --target-version, in my opinion. I could add this, but that could be considered out of scope for this PR. Let me know what you prefer!
Drive-by comment, for the time being you could document this in the help for --target-version in the Click CLI setup decorators applied to black.main, but until I get #2839 landed, there are not many places you can document it. Getting that PR landed is on my priority to-do list, but I don't know whether I'll get it in before this PR.
Thanks for the comments so far! I came up with the missing logic for handling some of the special cases. The result isn't super pretty, but I believe it works. It covers all the test cases I could think of.
I set the PR to 'ready to review'. Happy to address any concerns you may have!
@ichard26 Would you be willing to take another look at this?
We can also conclude that this kind of complexity is not what we want right now, for the small benefit it provides. Then we can close the PR. But would be nice to have some kind of decision on this!
Hi sorry, I know it's been a long while since I've last said anything. I promise I haven't forgotten this PR, I'll try my best to review this PR in the coming days! On initial thought, I think this will be an useful QOL feature that the extra complexity is worth it.
I saw I missed a mypy linting error. I rebased the branch and added a commit to fix the issue.
The diff-shades checks are still failing, but I'm not sure how to fix those.
diff-shades results comparing this PR (1b5b6f45409c074b518e5e4861d156ffb1f6e13a) to main (226cbf0226ee3bc26972357ba54c36409e9a84ae). The full diff is available in the logs under the "Generate HTML diff report" step.
╭─────────────────────── Summary ────────────────────────╮
│ 1 projects & 1 files changed / 2 changes [+1/-1] │
│ │
│ ... out of 2 395 309 lines, 11 482 files & 23 projects │
╰────────────────────────────────────────────────────────╯
Differences found.
Had to rebase due to #3233 . Should be good to go!
I've been a really bad maintainer, but I promise I've started reviewing this. It overall looks great, I just have a few code style remarks and I want to play around with it locally before approving it. Thank you so much for being patient (way beyond what's reasonable!).
I've been a really bad maintainer, but I promise I've started reviewing this. It overall looks great, I just have a few code style remarks and I want to play around with it locally before approving it. Thank you so much for being patient (way beyond what's reasonable!).
Don't worry about it! Life gets in the way. We'll just pick it back up whenever you're ready 👍
Thanks for your comments! I just got back from vacation and will take a look at this tomorrow.
Should we emit a message telling the user what we inferred the target version to be as part of
--verbose?
Great suggestion, I will figure out how to do this and add a commit for it.
- By inferring a default for
--target-version, Black will never do per-file auto-detection ifproject.requires-pythonexists and is valid. This is certainly better than the status quo for the lower bound, but it can cause some parsing trouble if the file in question is actually using more modern syntax than specified. [...] The main two ways to fix this are either to infer a list of all supported target versions or actually implement this suggestion to make--target-versiona lower-bound by default. The latter is the long-term fix, but I'm not sure when that will be implemented so the former might be necessary as a temporary fix.
I hadn't considered this; thanks for explaining this. I think it shouldn't be hard to rewrite my logic to generate the full list of target versions. I'll figure this out and add a commit for this.
Hey @ichard26 , sorry I have been sidetracked with some other things - I will complete this sometime next week.
Indeed my two open points are still:
- verbose flag output -> I added this, but I couldn't work out how to nicely specify whether the versions were inferred from project metadata or not. It just prints the versions that are active.
- weird invalid specifiers -> Done; will now default to per-file-autodetection when the metadata field contains invalid specifiers.
I've looked at both but there are some tricky things to consider. I just have to sit down and finish it 😸
@ichard26 I addressed your comments, I think this handles all cases properly now. Happy to address any other feedback.
I would also be interested in doing some work as a follow-up to make Black use a single target version, as discussed previously.
Thanks! I'll do a rebase to resolve a conflict in the changelog.
packaging version 22.0 removed some deprecated behaviour - that makes this implementation a bit cleaner. Had to do minor adjustments to get everything to work nice.
Note that lint now fails because of a new lint added by flake8-bugbear. I'll rerun this check once it's fixed.
Oh gosh I've ignored this for two months. If I don't review this by this weekend, everyone has my consent to ping me about this PR. I imagine it'd be a simple approval, but yeah I've been busy with life.
No worries, I've been busy working on Polars myself.
Lint passes now after rebase. I don't know how to make diff-shades happy, though.
@ichard26 any chance this could make it into the upcoming release ? :-)
Wait, it seems like the attrs failure on diff-shades is genuine. I thought it was just a fluke, but it turns out attrs doesn't specify Black's target-version in pyproject.toml so evidently this PR is changing behaviour (in a bad way). I've marked this as draft as I investigate.
Alright, so the reason why this is breaking attrs is that a non-empty mode.target_versions causes black.parsing.get_grammars() to be stricter with what grammars it returns. I haven't checked, but it's returning only this one probably:
https://github.com/psf/black/blob/f16333e78ba77ab29ab0b75e10891e6c65c6da7e/src/black/parsing.py#L66-L69
I miscontextualized how get_grammars() works. I thought as long as the 3.10 grammar might be supported, it'll be added to the list to try... that's not the case! Instead it checks for strict compatibility (using black.mode.supports_feature()) so if there's a target-version that doesn't support the 3.10 grammar, it won't be added.
Attrs is formatting fine right now because if mode.target_versions is empty, all grammars get picked:
https://github.com/psf/black/blob/f16333e78ba77ab29ab0b75e10891e6c65c6da7e/src/black/parsing.py#L50-L59
So technically this PR doesn't introduce any more regressions, but it just exposes this pitfall... I'm not entirely sure what the best way of fixing this is. My suggestion would be to add the 3.10 grammar if any of the target versions support Feature.PATTERN_MATCHING, not all.
[attrs - https://github.com/python-attrs/attrs.git]
╰─> [revision 9d23ae9536beb3cd30241f05123190ec1ecc20c1](https://github.com/python-attrs/attrs/tree/9d23ae9536beb3cd30241f05123190ec1ecc20c1)
--- a/attrs:src/attr/validators.pyi
+++ b/attrs:src/attr/validators.pyi
@@ -80,7 +80,7 @@
def min_len(length: int) -> _ValidatorType[_T]: ...
def not_(
validator: _ValidatorType[_T],
*,
msg: Optional[str] = None,
- exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ...
+ exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ...,
) -> _ValidatorType[_T]: ...
I don't have time to investigate, but presumably this is due to better inference of the target version. Not ideal, but as long as we land this in 23.1.0 where we're changing the stable style anyway, it should be fine.
Thanks again @stinodego. It's been great working with you :black_heart:
Thanks for putting the finishing touches on this, @ichard26 ! Glad to have this part of the latest release.
If any issues pop up, do not hesitate to tag me.