From 9ce75726fcf1f3a2617e873d925384fd6b876dcb Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 19 Dec 2022 18:08:13 -0800 Subject: [PATCH 01/17] Do not move docker `latest_release` tag for Pre-Releases (#3461) * Do not move docker `latest_release` tag for Pre-Releases - When we do a pre-release lets not move the latest_release tag - This tag should only move on official real releases Fixes #3453 * Make it prettier - TIL we format our yaml --- .github/workflows/docker.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a3106d04aae..855186f9bf1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -44,7 +44,9 @@ jobs: tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} - name: Build and push latest_release tag - if: ${{ github.event_name == 'release' && github.event.action == 'published' }} + if: + ${{ github.event_name == 'release' && github.event.action == 'published' && + !github.event.release.prerelease }} uses: docker/build-push-action@v3 with: context: . From 1e8217fd6284bdb020e7ca70964d677a3016f914 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 20 Dec 2022 06:36:42 -0800 Subject: [PATCH 02/17] Fix an f-string crash in ESP. (#3463) --- CHANGES.md | 2 ++ src/black/trans.py | 9 ++++-- .../data/preview/long_strings__regression.py | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f786f1a1fed..61608c361cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,8 @@ - Fix a crash in preview style with assert + parenthesized string (#3415) - Fix crashes in preview style with walrus operators used in function return annotations and except clauses (#3423) +- Fix a crash in preview advanced string processing where mixed implicitly concatenated + regular and f-strings start with an empty span (#3463) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) - Long values in dict literals are now wrapped in parentheses; correspondingly diff --git a/src/black/trans.py b/src/black/trans.py index b08a6d243d8..25d35afe74d 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1359,9 +1359,14 @@ def more_splits_should_be_made() -> bool: # prefix, and the current custom split did NOT originally use a # prefix... if ( - next_value != self._normalize_f_string(next_value, prefix) - and use_custom_breakpoints + use_custom_breakpoints and not csplit.has_prefix + and ( + # `next_value == prefix + QUOTE` happens when the custom + # split is an empty string. + next_value == prefix + QUOTE + or next_value != self._normalize_f_string(next_value, prefix) + ) ): # Then `csplit.break_idx` will be off by one after removing # the 'f' prefix. diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 8b8fc179147..5e8f012bc3e 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -531,6 +531,18 @@ async def foo(self): r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t", } +# Regression test for https://github.com/psf/black/issues/3459. +xxxx( + empty_str_as_first_split='' + f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx ' + 'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. ' + f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}', + empty_u_str_as_first_split=u'' + f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx ' + 'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. ' + f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}', +) + # output @@ -1193,3 +1205,19 @@ async def foo(self): r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" ), } + +# Regression test for https://github.com/psf/black/issues/3459. +xxxx( + empty_str_as_first_split=( + "" + f"xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx " + "xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. " + f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}" + ), + empty_u_str_as_first_split=( + "" + f"xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx " + "xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. " + f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}" + ), +) From a44dc3d59eb46901f9fe893727280903df41fc20 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 20 Dec 2022 13:38:35 -0800 Subject: [PATCH 03/17] Exclude string type annotations from ESP (#3462) --- CHANGES.md | 2 + src/black/brackets.py | 16 ++++- src/black/nodes.py | 12 ++++ src/black/trans.py | 23 ++++---- .../preview/long_strings__type_annotations.py | 59 +++++++++++++++++++ 5 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 tests/data/preview/long_strings__type_annotations.py diff --git a/CHANGES.md b/CHANGES.md index 61608c361cb..c29933fe5d9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ - Long values in dict literals are now wrapped in parentheses; correspondingly unnecessary parentheses around short values in dict literals are now removed; long string lambda values are now wrapped in parentheses (#3440) +- Exclude string type annotations from improved string processing; fix crash when the + return type annotation is stringified and spans across multiple lines (#3462) ### Configuration diff --git a/src/black/brackets.py b/src/black/brackets.py index ec9708cb08a..343f0608d50 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -80,9 +80,12 @@ def mark(self, leaf: Leaf) -> None: within brackets a given leaf is. 0 means there are no enclosing brackets that started on this line. - If a leaf is itself a closing bracket, it receives an `opening_bracket` - field that it forms a pair with. This is a one-directional link to - avoid reference cycles. + If a leaf is itself a closing bracket and there is a matching opening + bracket earlier, it receives an `opening_bracket` field with which it forms a + pair. This is a one-directional link to avoid reference cycles. Closing + bracket without opening happens on lines continued from previous + breaks, e.g. `) -> "ReturnType":` as part of a funcdef where we place + the return type annotation on its own line of the previous closing RPAR. If a leaf is a delimiter (a token on which Black can split the line if needed) and it's on depth 0, its `id()` is stored in the tracker's @@ -91,6 +94,13 @@ def mark(self, leaf: Leaf) -> None: if leaf.type == token.COMMENT: return + if ( + self.depth == 0 + and leaf.type in CLOSING_BRACKETS + and (self.depth, leaf.type) not in self.bracket_match + ): + return + self.maybe_decrement_after_for_loop_variable(leaf) self.maybe_decrement_after_lambda_arguments(leaf) if leaf.type in CLOSING_BRACKETS: diff --git a/src/black/nodes.py b/src/black/nodes.py index aeb2be389c8..a11fb7cc071 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -848,3 +848,15 @@ def is_string_token(nl: NL) -> TypeGuard[Leaf]: def is_number_token(nl: NL) -> TypeGuard[Leaf]: return nl.type == token.NUMBER + + +def is_part_of_annotation(leaf: Leaf) -> bool: + """Returns whether this leaf is part of type annotations.""" + ancestor = leaf.parent + while ancestor is not None: + if ancestor.prev_sibling and ancestor.prev_sibling.type == token.RARROW: + return True + if ancestor.parent and ancestor.parent.type == syms.tname: + return True + ancestor = ancestor.parent + return False diff --git a/src/black/trans.py b/src/black/trans.py index 25d35afe74d..a5cf4955f13 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -30,7 +30,6 @@ from mypy_extensions import trait -from black.brackets import BracketMatchError from black.comments import contains_pragma_comment from black.lines import Line, append_leaves from black.mode import Feature @@ -41,6 +40,7 @@ is_empty_lpar, is_empty_par, is_empty_rpar, + is_part_of_annotation, parent_type, replace_child, syms, @@ -351,7 +351,7 @@ class StringMerger(StringTransformer, CustomSplitMapMixin): Requirements: (A) The line contains adjacent strings such that ALL of the validation checks - listed in StringMerger.__validate_msg(...)'s docstring pass. + listed in StringMerger._validate_msg(...)'s docstring pass. OR (B) The line contains a string which uses line continuation backslashes. @@ -377,6 +377,8 @@ def do_match(self, line: Line) -> TMatchResult: and is_valid_index(i + 1) and LL[i + 1].type == token.STRING ): + if is_part_of_annotation(leaf): + return TErr("String is part of type annotation.") return Ok(i) if leaf.type == token.STRING and "\\\n" in leaf.value: @@ -454,7 +456,7 @@ def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: Returns: Ok(new_line), if ALL of the validation checks found in - __validate_msg(...) pass. + _validate_msg(...) pass. OR Err(CannotTransform), otherwise. """ @@ -608,7 +610,7 @@ def make_naked(string: str, string_prefix: str) -> str: def _validate_msg(line: Line, string_idx: int) -> TResult[None]: """Validate (M)erge (S)tring (G)roup - Transform-time string validation logic for __merge_string_group(...). + Transform-time string validation logic for _merge_string_group(...). Returns: * Ok(None), if ALL validation checks (listed below) pass. @@ -622,6 +624,11 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]: - The set of all string prefixes in the string group is of length greater than one and is not equal to {"", "f"}. - The string group consists of raw strings. + - The string group is stringified type annotations. We don't want to + process stringified type annotations since pyright doesn't support + them spanning multiple string values. (NOTE: mypy, pytype, pyre do + support them, so we can change if pyright also gains support in the + future. See https://github.com/microsoft/pyright/issues/4359.) """ # We first check for "inner" stand-alone comments (i.e. stand-alone # comments that have a string leaf before them AND after them). @@ -812,13 +819,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: new_line = line.clone() new_line.comments = line.comments.copy() - try: - append_leaves(new_line, line, LL[: string_idx - 1]) - except BracketMatchError: - # HACK: I believe there is currently a bug somewhere in - # right_hand_split() that is causing brackets to not be tracked - # properly by a shared BracketTracker. - append_leaves(new_line, line, LL[: string_idx - 1], preformatted=True) + append_leaves(new_line, line, LL[: string_idx - 1]) string_leaf = Leaf(token.STRING, LL[string_idx].value) LL[string_idx - 1].remove() diff --git a/tests/data/preview/long_strings__type_annotations.py b/tests/data/preview/long_strings__type_annotations.py new file mode 100644 index 00000000000..41d7ee2b67b --- /dev/null +++ b/tests/data/preview/long_strings__type_annotations.py @@ -0,0 +1,59 @@ +def func( + arg1, + arg2, +) -> Set["this_is_a_very_long_module_name.AndAVeryLongClasName" + ".WithAVeryVeryVeryVeryVeryLongSubClassName"]: + pass + + +def func( + argument: ( + "VeryLongClassNameWithAwkwardGenericSubtype[int] |" + "VeryLongClassNameWithAwkwardGenericSubtype[str]" + ), +) -> ( + "VeryLongClassNameWithAwkwardGenericSubtype[int] |" + "VeryLongClassNameWithAwkwardGenericSubtype[str]" +): + pass + + +def func( + argument: ( + "int |" + "str" + ), +) -> Set["int |" + " str"]: + pass + + +# output + + +def func( + arg1, + arg2, +) -> Set[ + "this_is_a_very_long_module_name.AndAVeryLongClasName" + ".WithAVeryVeryVeryVeryVeryLongSubClassName" +]: + pass + + +def func( + argument: ( + "VeryLongClassNameWithAwkwardGenericSubtype[int] |" + "VeryLongClassNameWithAwkwardGenericSubtype[str]" + ), +) -> ( + "VeryLongClassNameWithAwkwardGenericSubtype[int] |" + "VeryLongClassNameWithAwkwardGenericSubtype[str]" +): + pass + + +def func( + argument: ("int |" "str"), +) -> Set["int |" " str"]: + pass From 73c2d5514ce604141abe176b1f3e5cd35ff51d56 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 20 Dec 2022 14:59:38 -0800 Subject: [PATCH 04/17] Fix a crash in ESP where a standalone comment is placed before a dict's value (#3469) --- CHANGES.md | 2 ++ src/black/trans.py | 2 +- tests/data/preview/long_strings__regression.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c29933fe5d9..1e51f3e9108 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,8 @@ and except clauses (#3423) - Fix a crash in preview advanced string processing where mixed implicitly concatenated regular and f-strings start with an empty span (#3463) +- Fix a crash in preview advanced string processing where a standalone comment is placed + before a dict's value (#3469) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) - Long values in dict literals are now wrapped in parentheses; correspondingly diff --git a/src/black/trans.py b/src/black/trans.py index a5cf4955f13..0eb53e2b098 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1866,7 +1866,7 @@ def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]: for i, leaf in enumerate(LL): # We MUST find a colon, it can either be dict's or lambda's colon... - if leaf.type == token.COLON: + if leaf.type == token.COLON and i < len(LL) - 1: idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 # That colon MUST be followed by a string... diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 5e8f012bc3e..ef9007f4ce1 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -543,6 +543,13 @@ async def foo(self): f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}', ) +# Regression test for https://github.com/psf/black/issues/3455. +a_dict = { + "/this/is/a/very/very/very/very/very/very/very/very/very/very/long/key/without/spaces": + # And there is a comment before the value + ("item1", "item2", "item3"), +} + # output @@ -1221,3 +1228,10 @@ async def foo(self): f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}" ), ) + +# Regression test for https://github.com/psf/black/issues/3455. +a_dict = { + "/this/is/a/very/very/very/very/very/very/very/very/very/very/long/key/without/spaces": + # And there is a comment before the value + ("item1", "item2", "item3"), +} From 59f03d1b9d3c77f214087948b4d5e3dbb024d0b3 Mon Sep 17 00:00:00 2001 From: Matthew Armand Date: Tue, 20 Dec 2022 18:00:06 -0500 Subject: [PATCH 05/17] Vim plugin docs improvements (#3468) * Organize vim plugin section with headers to separate out Installation, Usage, and Troubleshooting for readability and easy linking * Add missing plugin configuration options, with current defaults * Add installation note for Arch Linux, now that the plugin is shipped with the python-black package (ref: https://bugs.archlinux.org/task/73024) * Fix vim-plug specification to follow stable releases. Moving the same tag is an antipattern that doesn't re-resolve with vim-plug, see this discussion for more detail (https://github.com/junegunn/vim-plug/pull/720\#issuecomment-1105829356). Per vim-plug's maintainer's recommendation, use the 'tag' key instead with a shell wildcard. Wildcard should be '*.*.*' as that follows Black's versioning detailed here (https://black.readthedocs.io/en/latest/contributing/release_process.html\#cutting-a-release) and doesn't include current alpha releases. --- CHANGES.md | 2 ++ docs/integrations/editors.md | 49 +++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1e51f3e9108..e2c5adfda35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -75,6 +75,8 @@ +- Expand `vim-plug` installation instructions to offer more explicit options (#3468) + ## 22.12.0 ### Preview style diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 0778c6a72f1..a8b7978c4d7 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -111,16 +111,51 @@ Configuration: - `g:black_fast` (defaults to `0`) - `g:black_linelength` (defaults to `88`) - `g:black_skip_string_normalization` (defaults to `0`) +- `g:black_skip_magic_trailing_comma` (defaults to `0`) - `g:black_virtualenv` (defaults to `~/.vim/black` or `~/.local/share/nvim/black`) +- `g:black_use_virtualenv` (defaults to `1`) +- `g:black_target_version` (defaults to `""`) - `g:black_quiet` (defaults to `0`) - `g:black_preview` (defaults to `0`) +#### Installation + +This plugin **requires Vim 7.0+ built with Python 3.7+ support**. It needs Python 3.7 to +be able to run _Black_ inside the Vim process which is much faster than calling an +external command. + +##### `vim-plug` + To install with [vim-plug](https://github.com/junegunn/vim-plug): +_Black_'s `stable` branch tracks official version updates, and can be used to simply +follow the most recent stable version. + ``` Plug 'psf/black', { 'branch': 'stable' } ``` +Another option which is a bit more explicit and offers more control is to use +`vim-plug`'s `tag` option with a shell wildcard. This will resolve to the latest tag +which matches the given pattern. + +The following matches all stable versions (see the +[Release Process](../contributing/release_process.md) section for documentation of +version scheme used by Black): + +``` +Plug 'psf/black', { 'tag': '*.*.*' } +``` + +and the following demonstrates pinning to a specific year's stable style (2022 in this +case): + +``` +Plug 'psf/black', { 'tag': '22.*.*' } +``` + +##### Vundle + or with [Vundle](https://github.com/VundleVim/Vundle.vim): ``` @@ -134,6 +169,14 @@ $ cd ~/.vim/bundle/black $ git checkout origin/stable -b stable ``` +##### Arch Linux + +On Arch Linux, the plugin is shipped with the +[`python-black`](https://archlinux.org/packages/community/any/python-black/) package, so +you can start using it in Vim after install with no additional setup. + +##### Vim 8 Native Plugin Management + or you can copy the plugin files from [plugin/black.vim](https://github.com/psf/black/blob/stable/plugin/black.vim) and [autoload/black.vim](https://github.com/psf/black/blob/stable/autoload/black.vim). @@ -148,9 +191,7 @@ curl https://raw.githubusercontent.com/psf/black/stable/autoload/black.vim -o ~/ Let me know if this requires any changes to work with Vim 8's builtin `packadd`, or Pathogen, and so on. -This plugin **requires Vim 7.0+ built with Python 3.7+ support**. It needs Python 3.7 to -be able to run _Black_ inside the Vim process which is much faster than calling an -external command. +#### Usage On first run, the plugin creates its own virtualenv using the right Python version and automatically installs _Black_. You can upgrade it later by calling `:BlackUpgrade` and @@ -187,6 +228,8 @@ To run _Black_ on a key press (e.g. F9 below), add this: nnoremap :Black ``` +#### Troubleshooting + **How to get Vim with Python 3.6?** On Ubuntu 17.10 Vim comes with Python 3.6 by default. On macOS with Homebrew run: `brew install vim`. When building Vim from source, use: `./configure --enable-python3interp=yes`. There's many guides online how to do From 29dd25725303992d36c3a75c3a071080ac06085f Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 20 Dec 2022 17:58:02 -0800 Subject: [PATCH 06/17] Fix an issue where extra empty lines are added. (#3470) --- CHANGES.md | 2 ++ src/black/lines.py | 3 +- tests/data/preview/comments9.py | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e2c5adfda35..c07d81d1320 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,8 @@ regular and f-strings start with an empty span (#3463) - Fix a crash in preview advanced string processing where a standalone comment is placed before a dict's value (#3469) +- Fix an issue where extra empty lines are added when a decorator has `# fmt: skip` + applied or there is a standalone comment between decorators (#3470) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) - Long values in dict literals are now wrapped in parentheses; correspondingly diff --git a/src/black/lines.py b/src/black/lines.py index 08281bcf370..2aa675c3b31 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -520,7 +520,8 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: and (self.semantic_leading_comment is None or before) ): self.semantic_leading_comment = block - elif not current_line.is_decorator: + # `or before` means this decorator already has an empty line before + elif not current_line.is_decorator or before: self.semantic_leading_comment = None self.previous_line = current_line diff --git a/tests/data/preview/comments9.py b/tests/data/preview/comments9.py index 449612c037a..77b25556e74 100644 --- a/tests/data/preview/comments9.py +++ b/tests/data/preview/comments9.py @@ -114,6 +114,31 @@ def first_method(self): pass +# Regression test for https://github.com/psf/black/issues/3454. +def foo(): + pass + # Trailing comment that belongs to this function + + +@decorator1 +@decorator2 # fmt: skip +def bar(): + pass + + +# Regression test for https://github.com/psf/black/issues/3454. +def foo(): + pass + # Trailing comment that belongs to this function. + # NOTE this comment only has one empty line below, and the formatter + # should enforce two blank lines. + +@decorator1 +# A standalone comment +def bar(): + pass + + # output @@ -252,3 +277,29 @@ class MyClass: # More comments. def first_method(self): pass + + +# Regression test for https://github.com/psf/black/issues/3454. +def foo(): + pass + # Trailing comment that belongs to this function + + +@decorator1 +@decorator2 # fmt: skip +def bar(): + pass + + +# Regression test for https://github.com/psf/black/issues/3454. +def foo(): + pass + # Trailing comment that belongs to this function. + # NOTE this comment only has one empty line below, and the formatter + # should enforce two blank lines. + + +@decorator1 +# A standalone comment +def bar(): + pass From 3246df89d6d80fc09357b445630fad87f08f57ce Mon Sep 17 00:00:00 2001 From: Matthew Armand Date: Tue, 20 Dec 2022 22:18:15 -0500 Subject: [PATCH 07/17] Add latest_prerelease Docker Hub tag for following the latest alpha release (#3465) Co-authored-by: Jelle Zijlstra --- .github/workflows/docker.yml | 11 +++++++++++ CHANGES.md | 2 ++ docs/usage_and_configuration/black_docker_image.md | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 855186f9bf1..04e30e727bd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -54,5 +54,16 @@ jobs: push: true tags: pyfound/black:latest_release + - name: Build and push latest_prerelease tag + if: + ${{ github.event_name == 'release' && github.event.action == 'published' && + github.event.release.prerelease }} + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: pyfound/black:latest_prerelease + - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/CHANGES.md b/CHANGES.md index c07d81d1320..c89b10638ed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -71,6 +71,8 @@ - Move 3.11 CI to normal flow now all dependencies support 3.11 (#3446) +- Docker: Add new `latest_prerelease` tag automation to follow latest black alpha + release on docker images (#3465) ### Documentation diff --git a/docs/usage_and_configuration/black_docker_image.md b/docs/usage_and_configuration/black_docker_image.md index 8de566ea270..85aec91ef1c 100644 --- a/docs/usage_and_configuration/black_docker_image.md +++ b/docs/usage_and_configuration/black_docker_image.md @@ -10,6 +10,11 @@ _Black_ images with the following tags are available: - `latest_release` - tag created when a new version of _Black_ is released.\ ℹ Recommended for users who want to use released versions of _Black_. It maps to [the latest release](https://github.com/psf/black/releases/latest) of _Black_. +- `latest_prerelease` - tag created when a new alpha (prerelease) version of _Black_ is + released.\ + ℹ Recommended for users who want to preview or test alpha versions of _Black_. Note that + the most recent release may be newer than any prerelease, because no prereleases are created + before most releases. - `latest` - tag used for the newest image of _Black_.\ ℹ Recommended for users who always want to use the latest version of _Black_, even before it is released. From 3feff21eca0eee4b9fe72d12b506ac273cb5bcd0 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 23 Dec 2022 12:13:45 -0800 Subject: [PATCH 08/17] Significantly speedup ESP on large expressions that contain many strings (#3467) --- CHANGES.md | 1 + src/black/trans.py | 279 +++++++++++++++++++++-------- tests/data/preview/long_strings.py | 31 ++++ 3 files changed, 235 insertions(+), 76 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c89b10638ed..587ca8a2a0d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ +- Improve the performance on large expressions that contain many strings (#3467) - Fix a crash in preview style with assert + parenthesized string (#3415) - Fix crashes in preview style with walrus operators used in function return annotations and except clauses (#3423) diff --git a/src/black/trans.py b/src/black/trans.py index 0eb53e2b098..ec07f5eab74 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -69,7 +69,7 @@ class CannotTransform(Exception): ParserState = int StringID = int TResult = Result[T, CannotTransform] # (T)ransform Result -TMatchResult = TResult[Index] +TMatchResult = TResult[List[Index]] def TErr(err_msg: str) -> Err[CannotTransform]: @@ -198,14 +198,19 @@ def __init__(self, line_length: int, normalize_strings: bool) -> None: def do_match(self, line: Line) -> TMatchResult: """ Returns: - * Ok(string_idx) such that `line.leaves[string_idx]` is our target - string, if a match was able to be made. + * Ok(string_indices) such that for each index, `line.leaves[index]` + is our target string if a match was able to be made. For + transformers that don't result in more lines (e.g. StringMerger, + StringParenStripper), multiple matches and transforms are done at + once to reduce the complexity. OR - * Err(CannotTransform), if a match was not able to be made. + * Err(CannotTransform), if no match could be made. """ @abstractmethod - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: """ Yields: * Ok(new_line) where new_line is the new transformed line. @@ -246,9 +251,9 @@ def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line] " this line as one that it can transform." ) from cant_transform - string_idx = match_result.ok() + string_indices = match_result.ok() - for line_result in self.do_transform(line, string_idx): + for line_result in self.do_transform(line, string_indices): if isinstance(line_result, Err): cant_transform = line_result.err() raise CannotTransform( @@ -371,30 +376,50 @@ def do_match(self, line: Line) -> TMatchResult: is_valid_index = is_valid_index_factory(LL) - for i, leaf in enumerate(LL): + string_indices = [] + idx = 0 + while is_valid_index(idx): + leaf = LL[idx] if ( leaf.type == token.STRING - and is_valid_index(i + 1) - and LL[i + 1].type == token.STRING + and is_valid_index(idx + 1) + and LL[idx + 1].type == token.STRING ): - if is_part_of_annotation(leaf): - return TErr("String is part of type annotation.") - return Ok(i) + if not is_part_of_annotation(leaf): + string_indices.append(idx) - if leaf.type == token.STRING and "\\\n" in leaf.value: - return Ok(i) + # Advance to the next non-STRING leaf. + idx += 2 + while is_valid_index(idx) and LL[idx].type == token.STRING: + idx += 1 - return TErr("This line has no strings that need merging.") + elif leaf.type == token.STRING and "\\\n" in leaf.value: + string_indices.append(idx) + # Advance to the next non-STRING leaf. + idx += 1 + while is_valid_index(idx) and LL[idx].type == token.STRING: + idx += 1 - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + else: + idx += 1 + + if string_indices: + return Ok(string_indices) + else: + return TErr("This line has no strings that need merging.") + + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: new_line = line + rblc_result = self._remove_backslash_line_continuation_chars( - new_line, string_idx + new_line, string_indices ) if isinstance(rblc_result, Ok): new_line = rblc_result.ok() - msg_result = self._merge_string_group(new_line, string_idx) + msg_result = self._merge_string_group(new_line, string_indices) if isinstance(msg_result, Ok): new_line = msg_result.ok() @@ -415,7 +440,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: @staticmethod def _remove_backslash_line_continuation_chars( - line: Line, string_idx: int + line: Line, string_indices: List[int] ) -> TResult[Line]: """ Merge strings that were split across multiple lines using @@ -429,30 +454,40 @@ def _remove_backslash_line_continuation_chars( """ LL = line.leaves - string_leaf = LL[string_idx] - if not ( - string_leaf.type == token.STRING - and "\\\n" in string_leaf.value - and not has_triple_quotes(string_leaf.value) - ): + indices_to_transform = [] + for string_idx in string_indices: + string_leaf = LL[string_idx] + if ( + string_leaf.type == token.STRING + and "\\\n" in string_leaf.value + and not has_triple_quotes(string_leaf.value) + ): + indices_to_transform.append(string_idx) + + if not indices_to_transform: return TErr( - f"String leaf {string_leaf} does not contain any backslash line" - " continuation characters." + "Found no string leaves that contain backslash line continuation" + " characters." ) new_line = line.clone() new_line.comments = line.comments.copy() append_leaves(new_line, line, LL) - new_string_leaf = new_line.leaves[string_idx] - new_string_leaf.value = new_string_leaf.value.replace("\\\n", "") + for string_idx in indices_to_transform: + new_string_leaf = new_line.leaves[string_idx] + new_string_leaf.value = new_string_leaf.value.replace("\\\n", "") return Ok(new_line) - def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: + def _merge_string_group( + self, line: Line, string_indices: List[int] + ) -> TResult[Line]: """ - Merges string group (i.e. set of adjacent strings) where the first - string in the group is `line.leaves[string_idx]`. + Merges string groups (i.e. set of adjacent strings). + + Each index from `string_indices` designates one string group's first + leaf in `line.leaves`. Returns: Ok(new_line), if ALL of the validation checks found in @@ -464,10 +499,54 @@ def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: is_valid_index = is_valid_index_factory(LL) - vresult = self._validate_msg(line, string_idx) - if isinstance(vresult, Err): - return vresult + # A dict of {string_idx: tuple[num_of_strings, string_leaf]}. + merged_string_idx_dict: Dict[int, Tuple[int, Leaf]] = {} + for string_idx in string_indices: + vresult = self._validate_msg(line, string_idx) + if isinstance(vresult, Err): + continue + merged_string_idx_dict[string_idx] = self._merge_one_string_group( + LL, string_idx, is_valid_index + ) + + if not merged_string_idx_dict: + return TErr("No string group is merged") + + # Build the final line ('new_line') that this method will later return. + new_line = line.clone() + previous_merged_string_idx = -1 + previous_merged_num_of_strings = -1 + for i, leaf in enumerate(LL): + if i in merged_string_idx_dict: + previous_merged_string_idx = i + previous_merged_num_of_strings, string_leaf = merged_string_idx_dict[i] + new_line.append(string_leaf) + + if ( + previous_merged_string_idx + <= i + < previous_merged_string_idx + previous_merged_num_of_strings + ): + for comment_leaf in line.comments_after(LL[i]): + new_line.append(comment_leaf, preformatted=True) + continue + + append_leaves(new_line, line, [leaf]) + + return Ok(new_line) + def _merge_one_string_group( + self, LL: List[Leaf], string_idx: int, is_valid_index: Callable[[int], bool] + ) -> Tuple[int, Leaf]: + """ + Merges one string group where the first string in the group is + `LL[string_idx]`. + + Returns: + A tuple of `(num_of_strings, leaf)` where `num_of_strings` is the + number of strings merged and `leaf` is the newly merged string + to be replaced in the new line. + """ # If the string group is wrapped inside an Atom node, we must make sure # to later replace that Atom with our new (merged) string leaf. atom_node = LL[string_idx].parent @@ -590,21 +669,8 @@ def make_naked(string: str, string_prefix: str) -> str: # Else replace the atom node with the new string leaf. replace_child(atom_node, string_leaf) - # Build the final line ('new_line') that this method will later return. - new_line = line.clone() - for i, leaf in enumerate(LL): - if i == string_idx: - new_line.append(string_leaf) - - if string_idx <= i < string_idx + num_of_strings: - for comment_leaf in line.comments_after(LL[i]): - new_line.append(comment_leaf, preformatted=True) - continue - - append_leaves(new_line, line, [leaf]) - self.add_custom_splits(string_leaf.value, custom_splits) - return Ok(new_line) + return num_of_strings, string_leaf @staticmethod def _validate_msg(line: Line, string_idx: int) -> TResult[None]: @@ -718,7 +784,15 @@ def do_match(self, line: Line) -> TMatchResult: is_valid_index = is_valid_index_factory(LL) - for idx, leaf in enumerate(LL): + string_indices = [] + + idx = -1 + while True: + idx += 1 + if idx >= len(LL): + break + leaf = LL[idx] + # Should be a string... if leaf.type != token.STRING: continue @@ -800,39 +874,73 @@ def do_match(self, line: Line) -> TMatchResult: }: continue - return Ok(string_idx) + string_indices.append(string_idx) + idx = string_idx + while idx < len(LL) - 1 and LL[idx + 1].type == token.STRING: + idx += 1 + if string_indices: + return Ok(string_indices) return TErr("This line has no strings wrapped in parens.") - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: LL = line.leaves - string_parser = StringParser() - rpar_idx = string_parser.parse(LL, string_idx) + string_and_rpar_indices: List[int] = [] + for string_idx in string_indices: + string_parser = StringParser() + rpar_idx = string_parser.parse(LL, string_idx) + + should_transform = True + for leaf in (LL[string_idx - 1], LL[rpar_idx]): + if line.comments_after(leaf): + # Should not strip parentheses which have comments attached + # to them. + should_transform = False + break + if should_transform: + string_and_rpar_indices.extend((string_idx, rpar_idx)) - for leaf in (LL[string_idx - 1], LL[rpar_idx]): - if line.comments_after(leaf): - yield TErr( - "Will not strip parentheses which have comments attached to them." - ) - return + if string_and_rpar_indices: + yield Ok(self._transform_to_new_line(line, string_and_rpar_indices)) + else: + yield Err( + CannotTransform("All string groups have comments attached to them.") + ) + + def _transform_to_new_line( + self, line: Line, string_and_rpar_indices: List[int] + ) -> Line: + LL = line.leaves new_line = line.clone() new_line.comments = line.comments.copy() - append_leaves(new_line, line, LL[: string_idx - 1]) - string_leaf = Leaf(token.STRING, LL[string_idx].value) - LL[string_idx - 1].remove() - replace_child(LL[string_idx], string_leaf) - new_line.append(string_leaf) + previous_idx = -1 + # We need to sort the indices, since string_idx and its matching + # rpar_idx may not come in order, e.g. in + # `("outer" % ("inner".join(items)))`, the "inner" string's + # string_idx is smaller than "outer" string's rpar_idx. + for idx in sorted(string_and_rpar_indices): + leaf = LL[idx] + lpar_or_rpar_idx = idx - 1 if leaf.type == token.STRING else idx + append_leaves(new_line, line, LL[previous_idx + 1 : lpar_or_rpar_idx]) + if leaf.type == token.STRING: + string_leaf = Leaf(token.STRING, LL[idx].value) + LL[lpar_or_rpar_idx].remove() # Remove lpar. + replace_child(LL[idx], string_leaf) + new_line.append(string_leaf) + else: + LL[lpar_or_rpar_idx].remove() # This is a rpar. - append_leaves( - new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :] - ) + previous_idx = idx - LL[rpar_idx].remove() + # Append the leaves after the last idx: + append_leaves(new_line, line, LL[idx + 1 :]) - yield Ok(new_line) + return new_line class BaseStringSplitter(StringTransformer): @@ -885,7 +993,12 @@ def do_match(self, line: Line) -> TMatchResult: if isinstance(match_result, Err): return match_result - string_idx = match_result.ok() + string_indices = match_result.ok() + assert len(string_indices) == 1, ( + f"{self.__class__.__name__} should only find one match at a time, found" + f" {len(string_indices)}" + ) + string_idx = string_indices[0] vresult = self._validate(line, string_idx) if isinstance(vresult, Err): return vresult @@ -1219,10 +1332,17 @@ def do_splitter_match(self, line: Line) -> TMatchResult: if is_valid_index(idx): return TErr("This line does not end with a string.") - return Ok(string_idx) + return Ok([string_idx]) - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: LL = line.leaves + assert len(string_indices) == 1, ( + f"{self.__class__.__name__} should only find one match at a time, found" + f" {len(string_indices)}" + ) + string_idx = string_indices[0] QUOTE = LL[string_idx].value[-1] @@ -1710,7 +1830,7 @@ def do_splitter_match(self, line: Line) -> TMatchResult: " resultant line would still be over the specified line" " length and can't be split further by StringSplitter." ) - return Ok(string_idx) + return Ok([string_idx]) return TErr("This line does not contain any non-atomic strings.") @@ -1887,8 +2007,15 @@ def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]: return None - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: LL = line.leaves + assert len(string_indices) == 1, ( + f"{self.__class__.__name__} should only find one match at a time, found" + f" {len(string_indices)}" + ) + string_idx = string_indices[0] is_valid_index = is_valid_index_factory(LL) insert_str_child = insert_str_child_factory(LL[string_idx]) diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 9c78f675b8f..b7a0a42f82a 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -287,6 +287,23 @@ def foo(): ), } +# Complex string concatenations with a method call in the middle. +code = ( + (" return [\n") + + ( + ", \n".join( + " (%r, self.%s, visitor.%s)" + % (attrname, attrname, visit_name) + for attrname, visit_name in names + ) + ) + + ("\n ]\n") +) + + +# Test case of an outer string' parens enclose an inner string's parens. +call(body=("%s %s" % ((",".join(items)), suffix))) + # output @@ -828,3 +845,17 @@ def foo(): f"{some_function_call(j.right)})" ), } + +# Complex string concatenations with a method call in the middle. +code = ( + " return [\n" + + ", \n".join( + " (%r, self.%s, visitor.%s)" % (attrname, attrname, visit_name) + for attrname, visit_name in names + ) + + "\n ]\n" +) + + +# Test case of an outer string' parens enclose an inner string's parens. +call(body="%s %s" % (",".join(items), suffix)) From 9b91638190342cf5a66d4edb11068526f7ebda59 Mon Sep 17 00:00:00 2001 From: Semen Zhydenko Date: Mon, 26 Dec 2022 03:39:51 +0100 Subject: [PATCH 09/17] Fix some typos (#3474) --- docs/contributing/issue_triage.md | 2 +- tests/data/preview/prefer_rhs_split.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/issue_triage.md b/docs/contributing/issue_triage.md index 9b987fb2425..865a47935ed 100644 --- a/docs/contributing/issue_triage.md +++ b/docs/contributing/issue_triage.md @@ -42,7 +42,7 @@ The lifecycle of a bug report or user support issue typically goes something lik 1. _the issue is waiting for triage_ 2. **identified** - has been marked with a type label and other relevant labels, more details or a functional reproduction may be still needed (and therefore should be - marked with `S: needs repro` or `S: awaiting reponse`) + marked with `S: needs repro` or `S: awaiting response`) 3. **confirmed** - the issue can reproduced and necessary details have been provided 4. **discussion** - initial triage has been done and now the general details on how the issue should be best resolved are being hashed out diff --git a/tests/data/preview/prefer_rhs_split.py b/tests/data/preview/prefer_rhs_split.py index 5b89113e618..2f3cf33db41 100644 --- a/tests/data/preview/prefer_rhs_split.py +++ b/tests/data/preview/prefer_rhs_split.py @@ -50,7 +50,7 @@ forth_item, fifth_item, last_item_very_loooooong, -) = everyting = some_loooooog_function_name( +) = everything = some_looooong_function_name( first_argument, second_argument, third_argument ) From 72a3408965f944f39f1080a5b67c25790acdc4e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 06:32:42 -0800 Subject: [PATCH 10/17] Bump pypa/cibuildwheel from 2.11.3 to 2.11.4 (#3475) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.11.3 to 2.11.4. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.11.3...v2.11.4) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 7fd760ef727..ee1c1fa7bcf 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -59,7 +59,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.11.3 + uses: pypa/cibuildwheel@v2.11.4 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From 0abe85eebb94e7640aa5d443aefe5b9bed507bfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:00:23 -0800 Subject: [PATCH 11/17] Bump peter-evans/find-comment from 2.1.0 to 2.2.0 (#3476) Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/peter-evans/find-comment/releases) - [Commits](https://github.com/peter-evans/find-comment/compare/f4499a714d59013c74a08789b48abe4b704364a0...81e2da3af01c92f83cb927cf3ace0e085617c556) --- updated-dependencies: - dependency-name: peter-evans/find-comment dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 1375be9788d..26d06090919 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -33,7 +33,7 @@ jobs: - name: Try to find pre-existing PR comment if: steps.metadata.outputs.needs-comment == 'true' id: find-comment - uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 + uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 with: issue-number: ${{ steps.metadata.outputs.pr-number }} comment-author: "github-actions[bot]" From 4e3303fa08e030722d6fd4d7fe7b8d44ef98991c Mon Sep 17 00:00:00 2001 From: Jordan Ephron Date: Thu, 29 Dec 2022 18:13:15 -0500 Subject: [PATCH 12/17] Parenthesize conditional expressions (#2278) Co-authored-by: Jordan Ephron Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/__init__.py | 16 ++- src/black/linegen.py | 16 +++ src/black/mode.py | 1 + tests/data/conditional_expression.py | 160 +++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 tests/data/conditional_expression.py diff --git a/CHANGES.md b/CHANGES.md index 587ca8a2a0d..2da0fb4720c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ +- Add parentheses around `if`-`else` expressions (#2278) - Improve the performance on large expressions that contain many strings (#3467) - Fix a crash in preview style with assert + parenthesized string (#3415) - Fix crashes in preview style with walrus operators used in function return annotations diff --git a/src/black/__init__.py b/src/black/__init__.py index f00749aaed8..9f44722bfae 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -478,16 +478,20 @@ def main( # noqa: C901 ) 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 ] srcs_string = ", ".join( [ - f'"{_norm}"' - if _norm - else f'\033[31m"{source} (skipping - invalid)"\033[34m' + ( + f'"{_norm}"' + if _norm + else f'\033[31m"{source} (skipping - invalid)"\033[34m' + ) for _norm, source in normalized ] ) diff --git a/src/black/linegen.py b/src/black/linegen.py index 2e75bc94506..4da75b28235 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -140,6 +140,22 @@ def visit_default(self, node: LN) -> Iterator[Line]: self.current_line.append(node) yield from super().visit_default(node) + def visit_test(self, node: Node) -> Iterator[Line]: + """Visit an `x if y else z` test""" + + if Preview.parenthesize_conditional_expressions in self.mode: + already_parenthesized = ( + node.prev_sibling and node.prev_sibling.type == token.LPAR + ) + + if not already_parenthesized: + lpar = Leaf(token.LPAR, "") + rpar = Leaf(token.RPAR, "") + node.insert_child(0, lpar) + node.append_child(rpar) + + yield from self.visit_default(node) + def visit_INDENT(self, node: Leaf) -> Iterator[Line]: """Increase indentation level, maybe yield a line.""" # In blib2to3 INDENT never holds comments. diff --git a/src/black/mode.py b/src/black/mode.py index a104d1b9862..775805ae960 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -161,6 +161,7 @@ class Preview(Enum): # NOTE: string_processing requires wrap_long_dict_values_in_parens # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() + parenthesize_conditional_expressions = auto() skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() diff --git a/tests/data/conditional_expression.py b/tests/data/conditional_expression.py new file mode 100644 index 00000000000..620a12dc986 --- /dev/null +++ b/tests/data/conditional_expression.py @@ -0,0 +1,160 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) + +# output + +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) From 37542e64855ce21bd580f973ae5ce1ed86812a7a Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 31 Dec 2022 01:52:35 -0500 Subject: [PATCH 13/17] Fail lint CI if the PR doesn't target main (#3477) Let's skip the check if we're running on a fork just in case someone opens a PR against a branch on said fork as part of a PR review upstream. --- .github/workflows/lint.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90c48013080..064d4745a53 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,6 +16,13 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Assert PR target is main + if: github.event_name == 'pull_request' && github.repository == 'psf/black' + run: | + if [ "$GITHUB_BASE_REF" != "main" ]; then + echo "::error::PR targeting '$GITHUB_BASE_REF', please refile targeting 'main'." && exit 1 + fi + - name: Set up latest Python uses: actions/setup-python@v4 with: From 5d0d5936db2ed7a01c50a374e32753e1afe9cc71 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Mon, 2 Jan 2023 09:43:48 -0500 Subject: [PATCH 14/17] Add email for Richard Si (#3478) --- AUTHORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index a635e8c3c92..ab3f30b8821 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -10,7 +10,7 @@ Maintained with: - [Mika Naylor](mailto:mail@autophagy.io) - [Zsolt Dollenstein](mailto:zsol.zsol@gmail.com) - [Cooper Lees](mailto:me@cooperlees.com) -- Richard Si +- [Richard Si](mailto:sichard26@gmail.com) - [Felix Hildén](mailto:felix.hilden@gmail.com) - [Batuhan Taskaya](mailto:batuhan@python.org) From 4bee9cca5553c55493203822b5a112ec5216bc74 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 11 Jan 2023 16:19:27 -0300 Subject: [PATCH 15/17] Remove misleading phrase in Usage and Configuration (#3492) The CLI options were already shown in the "Command line options" in the same page. --- docs/usage_and_configuration/the_basics.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 3dab644f2c8..9dc5277c61e 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -40,6 +40,9 @@ so style options are deliberately limited and rarely added. +Note that all command-line options listed above can also be configured using a +`pyproject.toml` file (more on that below). + ### Code input alternatives #### Standard Input @@ -287,9 +290,6 @@ file hierarchy. ## Next steps -You've probably noted that not all of the options you can pass to _Black_ have been -covered. Don't worry, the rest will be covered in a later section. - A good next step would be configuring auto-discovery so `black .` is all you need instead of laborously listing every file or directory. You can get started by heading over to [File collection and discovery](./file_collection_and_discovery.md). From f7580103407743a317e22297793822dd91f8fefe Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Sat, 14 Jan 2023 09:51:59 -0800 Subject: [PATCH 16/17] Documentation: clarify the state of multiple context managers (#3488) Clarify that the backslash & paren-wrapping formatting for multiple context managers aren't yet implemented. --- docs/the_black_code_style/future_style.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 17b7eef092f..9ca260fc0ad 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -19,7 +19,7 @@ with make_context_manager1() as cm1, make_context_manager2() as cm2, make_contex ... # nothing to split on - line too long ``` -So _Black_ will eventually format it like this: +So _Black_ will, when we implement this, format it like this: ```py3 with \ @@ -31,8 +31,8 @@ with \ ... # backslashes and an ugly stranded colon ``` -Although when the target version is Python 3.9 or higher, _Black_ will use parentheses -instead since they're allowed in Python 3.9 and higher. +Although when the target version is Python 3.9 or higher, _Black_ will, when we +implement this, use parentheses instead since they're allowed in Python 3.9 and higher. An alternative to consider if the backslashes in the above formatting are undesirable is to use {external:py:obj}`contextlib.ExitStack` to combine context managers in the From d4ff985853c8d140d73b9d362604deedb41eb20e Mon Sep 17 00:00:00 2001 From: Ruslan <7631314+ruslaniv@users.noreply.github.com> Date: Sun, 15 Jan 2023 01:32:00 +0700 Subject: [PATCH 17/17] Add IntelliJ docs on external tools and file watcher (#3365) Revert deleted documentation on setting up Black using IntelliJ external tool or file watcher utilities. These are still worth keeping because some peole might not want to use a third-party plugin or install Blackd's extra dependencies. Co-authored-by: Richard Si --- docs/integrations/editors.md | 106 +++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index a8b7978c4d7..74c6a283ab8 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -10,6 +10,19 @@ Options include the following: ## PyCharm/IntelliJ IDEA +There are three different ways you can use _Black_ from PyCharm: + +1. As local server using the BlackConnect plugin +1. As external tool +1. As file watcher + +The first option is the simplest to set up and formats the fastest (by spinning up +{doc}`Black's HTTP server `, avoiding the +startup cost on subsequent formats), but if you would prefer to not install a +third-party plugin or blackd's extra dependencies, the other two are also great options. + +### As local server + 1. Install _Black_ with the `d` extra. ```console @@ -46,6 +59,99 @@ Options include the following: - In `Trigger Settings` section of plugin configuration check `Trigger when saving changed files`. +### As external tool + +1. Install `black`. + + ```console + $ pip install black + ``` + +1. Locate your `black` installation folder. + + On macOS / Linux / BSD: + + ```console + $ which black + /usr/local/bin/black # possible location + ``` + + On Windows: + + ```console + $ where black + %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location + ``` + + Note that if you are using a virtual environment detected by PyCharm, this is an + unneeded step. In this case the path to `black` is `$PyInterpreterDirectory$/black`. + +1. Open External tools in PyCharm/IntelliJ IDEA + + On macOS: + + `PyCharm -> Preferences -> Tools -> External Tools` + + On Windows / Linux / BSD: + + `File -> Settings -> Tools -> External Tools` + +1. Click the + icon to add a new external tool with the following values: + + - Name: Black + - Description: Black is the uncompromising Python code formatter. + - Program: \ + - Arguments: `"$FilePath$"` + +1. Format the currently opened file by selecting `Tools -> External Tools -> black`. + + - Alternatively, you can set a keyboard shortcut by navigating to + `Preferences or Settings -> Keymap -> External Tools -> External Tools - Black`. + +### As file watcher + +1. Install `black`. + + ```console + $ pip install black + ``` + +1. Locate your `black` installation folder. + + On macOS / Linux / BSD: + + ```console + $ which black + /usr/local/bin/black # possible location + ``` + + On Windows: + + ```console + $ where black + %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location + ``` + + Note that if you are using a virtual environment detected by PyCharm, this is an + unneeded step. In this case the path to `black` is `$PyInterpreterDirectory$/black`. + +1. Make sure you have the + [File Watchers](https://plugins.jetbrains.com/plugin/7177-file-watchers) plugin + installed. +1. Go to `Preferences or Settings -> Tools -> File Watchers` and click `+` to add a new + watcher: + - Name: Black + - File type: Python + - Scope: Project Files + - Program: \ + - Arguments: `$FilePath$` + - Output paths to refresh: `$FilePath$` + - Working directory: `$ProjectFileDir$` + +- In Advanced Options + - Uncheck "Auto-save edited files to trigger the watcher" + - Uncheck "Trigger the watcher on external changes" + ## Wing IDE Wing IDE supports `black` via **Preference Settings** for system wide settings and