From 1d4c31aa589dc0c8633af7531f8cc09192917b38 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Wed, 25 Oct 2023 18:35:37 +0300 Subject: [PATCH 1/5] [925] Improve multiline dictionary and list indentation for sole function parameter (#3964) --- CHANGES.md | 3 +- docs/the_black_code_style/future_style.md | 26 ++ src/black/linegen.py | 13 + src/black/mode.py | 1 + ..._parens_with_braces_and_square_brackets.py | 273 ++++++++++++++++++ .../cases/preview_long_strings__regression.py | 22 +- 6 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py diff --git a/CHANGES.md b/CHANGES.md index c4ae056b1b9..f7d02af187d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,8 @@ ### Preview style - +- Multiline dictionaries and lists that are the sole argument to a function are now + indented less (#3964) ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 367ff98537c..e73c16ba26e 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -113,6 +113,32 @@ my_dict = { } ``` +### Improved multiline dictionary and list indentation for sole function parameter + +For better readability and less verticality, _Black_ now pairs parantheses ("(", ")") +with braces ("{", "}") and square brackets ("[", "]") on the same line for single +parameter function calls. For example: + +```python +foo( + [ + 1, + 2, + 3, + ] +) +``` + +will be changed to: + +```python +foo([ + 1, + 2, + 3, +]) +``` + ### Improved multiline string handling _Black_ is smarter when formatting multiline strings, especially in function arguments, diff --git a/src/black/linegen.py b/src/black/linegen.py index 2bfe587fa0e..5f5a69152d5 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -815,6 +815,19 @@ def _first_right_hand_split( tail_leaves.reverse() body_leaves.reverse() head_leaves.reverse() + + if Preview.hug_parens_with_braces_and_square_brackets in line.mode: + if ( + tail_leaves[0].type == token.RPAR + and tail_leaves[0].value + and tail_leaves[0].opening_bracket is head_leaves[-1] + and body_leaves[-1].type in [token.RBRACE, token.RSQB] + and body_leaves[-1].opening_bracket is body_leaves[0] + ): + head_leaves = head_leaves + body_leaves[:1] + tail_leaves = body_leaves[-1:] + tail_leaves + body_leaves = body_leaves[1:-1] + head = bracket_split_build_line( head_leaves, line, opening_bracket, component=_BracketSplitComponent.head ) diff --git a/src/black/mode.py b/src/black/mode.py index 4effeef3e7c..99b2a84a63d 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -190,6 +190,7 @@ class Preview(Enum): module_docstring_newlines = auto() accept_raw_docstrings = auto() fix_power_op_line_length = auto() + hug_parens_with_braces_and_square_brackets = auto() allow_empty_first_line_before_new_block_or_comment = auto() diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py new file mode 100644 index 00000000000..98ed342fcbc --- /dev/null +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -0,0 +1,273 @@ +# flags: --preview +def foo_brackets(request): + return JsonResponse( + { + "var_1": foo, + "var_2": bar, + } + ) + +def foo_square_brackets(request): + return JsonResponse( + [ + "var_1", + "var_2", + ] + ) + +func({"a": 37, "b": 42, "c": 927, "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111}) + +func(["random_string_number_one","random_string_number_two","random_string_number_three","random_string_number_four"]) + +func( + { + # expand me + 'a':37, + 'b':42, + 'c':927 + } +) + +func( + [ + 'a', + 'b', + 'c', + ] +) + +func( # a + [ # b + "c", # c + "d", # d + "e", # e + ] # f +) # g + +func( # a + { # b + "c": 1, # c + "d": 2, # d + "e": 3, # e + } # f +) # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func( + [ # preserve me but hug brackets + "c", + "d", + "e", + ] +) + +func( + [ + # preserve me but hug brackets + "c", + "d", + "e", + ] +) + +func( + [ + "c", + # preserve me but hug brackets + "d", + "e", + ] +) + +func( + [ + "c", + "d", + "e", + # preserve me but hug brackets + ] +) + +func( + [ + "c", + "d", + "e", + ] # preserve me but hug brackets +) + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([x for x in "long line long line long line long line long line long line long line"]) +func([x for x in [x for x in "long line long line long line long line long line long line long line"]]) + +func({"short line"}) +func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) +func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) + +# output +def foo_brackets(request): + return JsonResponse({ + "var_1": foo, + "var_2": bar, + }) + + +def foo_square_brackets(request): + return JsonResponse([ + "var_1", + "var_2", + ]) + + +func({ + "a": 37, + "b": 42, + "c": 927, + "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111, +}) + +func([ + "random_string_number_one", + "random_string_number_two", + "random_string_number_three", + "random_string_number_four", +]) + +func({ + # expand me + "a": 37, + "b": 42, + "c": 927, +}) + +func([ + "a", + "b", + "c", +]) + +func([ # a # b + "c", # c + "d", # d + "e", # e +]) # f # g + +func({ # a # b + "c": 1, # c + "d": 2, # d + "e": 3, # e +}) # f # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func([ # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + "c", + # preserve me but hug brackets + "d", + "e", +]) + +func([ + "c", + "d", + "e", + # preserve me but hug brackets +]) + +func([ + "c", + "d", + "e", +]) # preserve me but hug brackets + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([ + x for x in "long line long line long line long line long line long line long line" +]) +func([ + x + for x in [ + x + for x in "long line long line long line long line long line long line long line" + ] +]) + +func({"short line"}) +func({ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}) +func({ + { + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", + } +}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) diff --git a/tests/data/cases/preview_long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py index 436157f4e05..313d898cd83 100644 --- a/tests/data/cases/preview_long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -962,19 +962,17 @@ def who(self): ) -xxxxxxx_xxxxxx_xxxxxxx = xxx( - [ - xxxxxxxxxxxx( - xxxxxx_xxxxxxx=( - '((x.aaaaaaaaa = "xxxxxx.xxxxxxxxxxxxxxxxxxxxx") || (x.xxxxxxxxx =' - ' "xxxxxxxxxxxx")) && ' - # xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx. - "(x.bbbbbbbbbbbb.xxx != " - '"xxx:xxx:xxx::cccccccccccc:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && ' - ) +xxxxxxx_xxxxxx_xxxxxxx = xxx([ + xxxxxxxxxxxx( + xxxxxx_xxxxxxx=( + '((x.aaaaaaaaa = "xxxxxx.xxxxxxxxxxxxxxxxxxxxx") || (x.xxxxxxxxx =' + ' "xxxxxxxxxxxx")) && ' + # xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx. + "(x.bbbbbbbbbbbb.xxx != " + '"xxx:xxx:xxx::cccccccccccc:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && ' ) - ] -) + ) +]) if __name__ == "__main__": for i in range(4, 8): From 878937bcc3282319081057e2f1dbee5e24d69d68 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Wed, 25 Oct 2023 19:47:21 +0300 Subject: [PATCH 2/5] [2213] Add support for single line format skip with other comments on the same line (#3959) --- CHANGES.md | 2 +- docs/the_black_code_style/current_style.md | 12 ++-- src/black/__init__.py | 2 +- src/black/comments.py | 63 +++++++++++++++---- src/black/mode.py | 1 + ...line_format_skip_with_multiple_comments.py | 20 ++++++ 6 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py diff --git a/CHANGES.md b/CHANGES.md index f7d02af187d..c96186c93cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ ### Configuration - +- Add support for single line format skip with other comments on the same line (#3959) ### Packaging diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index ff757a8276b..f59c1853f72 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -8,12 +8,14 @@ deliberately limited and rarely added. Previous formatting is taken into account little as possible, with rare exceptions like the magic trailing comma. The coding style used by _Black_ can be viewed as a strict subset of PEP 8. -_Black_ reformats entire files in place. It doesn't reformat lines that end with +_Black_ reformats entire files in place. It doesn't reformat lines that contain `# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`. -`# fmt: on/off` must be on the same level of indentation and in the same block, meaning -no unindents beyond the initial indentation level between them. It also recognizes -[YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a -courtesy for straddling code. +`# fmt: skip` can be mixed with other pragmas/comments either with multiple comments +(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separeted list (e.g. +`# fmt: skip; pylint; noqa`). `# fmt: on/off` must be on the same level of indentation +and in the same block, meaning no unindents beyond the initial indentation level between +them. It also recognizes [YAPF](https://github.com/google/yapf)'s block comments to the +same effect, as a courtesy for straddling code. The rest of this document describes the current formatting style. If you're interested in trying out where the style is heading, see [future style](./future_style.md) and try diff --git a/src/black/__init__.py b/src/black/__init__.py index 188a4f79f0e..7cf93b89e42 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1099,7 +1099,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS} if supports_feature(versions, feature) } - normalize_fmt_off(src_node) + normalize_fmt_off(src_node, mode) lines = LineGenerator(mode=mode, features=context_manager_features) elt = EmptyLineTracker(mode=mode) split_line_features = { diff --git a/src/black/comments.py b/src/black/comments.py index 226968bff98..862fc7607cc 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -3,6 +3,7 @@ from functools import lru_cache from typing import Final, Iterator, List, Optional, Union +from black.mode import Mode, Preview from black.nodes import ( CLOSING_BRACKETS, STANDALONE_COMMENT, @@ -20,10 +21,11 @@ FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"} FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"} -FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP} FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"} COMMENT_EXCEPTIONS = " !:#'" +_COMMENT_PREFIX = "# " +_COMMENT_LIST_SEPARATOR = ";" @dataclass @@ -130,14 +132,14 @@ def make_comment(content: str) -> str: return "#" + content -def normalize_fmt_off(node: Node) -> None: +def normalize_fmt_off(node: Node, mode: Mode) -> None: """Convert content between `# fmt: off`/`# fmt: on` into standalone comments.""" try_again = True while try_again: - try_again = convert_one_fmt_off_pair(node) + try_again = convert_one_fmt_off_pair(node, mode) -def convert_one_fmt_off_pair(node: Node) -> bool: +def convert_one_fmt_off_pair(node: Node, mode: Mode) -> bool: """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. @@ -145,21 +147,27 @@ def convert_one_fmt_off_pair(node: Node) -> bool: for leaf in node.leaves(): previous_consumed = 0 for comment in list_comments(leaf.prefix, is_endmarker=False): - if comment.value not in FMT_PASS: + should_pass_fmt = comment.value in FMT_OFF or _contains_fmt_skip_comment( + comment.value, mode + ) + if not should_pass_fmt: previous_consumed = comment.consumed continue # We only want standalone comments. If there's no previous leaf or # the previous leaf is indentation, it's a standalone comment in # disguise. - if comment.value in FMT_PASS and comment.type != STANDALONE_COMMENT: + if should_pass_fmt and comment.type != STANDALONE_COMMENT: prev = preceding_leaf(leaf) if prev: if comment.value in FMT_OFF and prev.type not in WHITESPACE: continue - if comment.value in FMT_SKIP and prev.type in WHITESPACE: + if ( + _contains_fmt_skip_comment(comment.value, mode) + and prev.type in WHITESPACE + ): continue - ignored_nodes = list(generate_ignored_nodes(leaf, comment)) + ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode)) if not ignored_nodes: continue @@ -168,7 +176,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool: prefix = first.prefix if comment.value in FMT_OFF: first.prefix = prefix[comment.consumed :] - if comment.value in FMT_SKIP: + if _contains_fmt_skip_comment(comment.value, mode): first.prefix = "" standalone_comment_prefix = prefix else: @@ -178,7 +186,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool: hidden_value = "".join(str(n) for n in ignored_nodes) if comment.value in FMT_OFF: hidden_value = comment.value + "\n" + hidden_value - if comment.value in FMT_SKIP: + if _contains_fmt_skip_comment(comment.value, mode): hidden_value += " " + comment.value if hidden_value.endswith("\n"): # That happens when one of the `ignored_nodes` ended with a NEWLINE @@ -205,13 +213,15 @@ def convert_one_fmt_off_pair(node: Node) -> bool: return False -def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: +def generate_ignored_nodes( + leaf: Leaf, comment: ProtoComment, mode: Mode +) -> Iterator[LN]: """Starting from the container of `leaf`, generate all leaves until `# fmt: on`. If comment is skip, returns leaf only. Stops at the end of the block. """ - if comment.value in FMT_SKIP: + if _contains_fmt_skip_comment(comment.value, mode): yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment) return container: Optional[LN] = container_of(leaf) @@ -327,3 +337,32 @@ def contains_pragma_comment(comment_list: List[Leaf]) -> bool: return True return False + + +def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool: + """ + Checks if the given comment contains FMT_SKIP alone or paired with other comments. + Matching styles: + # fmt:skip <-- single comment + # noqa:XXX # fmt:skip # a nice line <-- multiple comments (Preview) + # pylint:XXX; fmt:skip <-- list of comments (; separated, Preview) + """ + semantic_comment_blocks = ( + [ + comment_line, + *[ + _COMMENT_PREFIX + comment.strip() + for comment in comment_line.split(_COMMENT_PREFIX)[1:] + ], + *[ + _COMMENT_PREFIX + comment.strip() + for comment in comment_line.strip(_COMMENT_PREFIX).split( + _COMMENT_LIST_SEPARATOR + ) + ], + ] + if Preview.single_line_format_skip_with_multiple_comments in mode + else [comment_line] + ) + + return any(comment in FMT_SKIP for comment in semantic_comment_blocks) diff --git a/src/black/mode.py b/src/black/mode.py index 99b2a84a63d..4e4effffb86 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -192,6 +192,7 @@ class Preview(Enum): fix_power_op_line_length = auto() hug_parens_with_braces_and_square_brackets = auto() allow_empty_first_line_before_new_block_or_comment = auto() + single_line_format_skip_with_multiple_comments = auto() class Deprecated(UserWarning): diff --git a/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py b/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py new file mode 100644 index 00000000000..efde662baa8 --- /dev/null +++ b/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py @@ -0,0 +1,20 @@ +# flags: --preview +foo = 123 # fmt: skip # noqa: E501 # pylint +bar = ( + 123 , + ( 1 + 5 ) # pylint # fmt:skip +) +baz = "a" + "b" # pylint; fmt: skip; noqa: E501 +skip_will_not_work = "a" + "b" # pylint fmt:skip +skip_will_not_work2 = "a" + "b" # some text; fmt:skip happens to be part of it + +# output + +foo = 123 # fmt: skip # noqa: E501 # pylint +bar = ( + 123 , + ( 1 + 5 ) # pylint # fmt:skip +) +baz = "a" + "b" # pylint; fmt: skip; noqa: E501 +skip_will_not_work = "a" + "b" # pylint fmt:skip +skip_will_not_work2 = "a" + "b" # some text; fmt:skip happens to be part of it From f7174bfc431e22f38b502579d1234989c3c5ce15 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 27 Oct 2023 01:43:42 +0900 Subject: [PATCH 3/5] Fix typo in future_style.md (#3979) parantheses -> parentheses --- docs/the_black_code_style/future_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index e73c16ba26e..f2534b0f0d0 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -115,7 +115,7 @@ my_dict = { ### Improved multiline dictionary and list indentation for sole function parameter -For better readability and less verticality, _Black_ now pairs parantheses ("(", ")") +For better readability and less verticality, _Black_ now pairs parentheses ("(", ")") with braces ("{", "}") and square brackets ("[", "]") on the same line for single parameter function calls. For example: From de701fe6aa0d61526b806dd31610da5cf8b67ab9 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 26 Oct 2023 21:13:25 -0700 Subject: [PATCH 4/5] Fix CI by running on Python 3.11 (#3984) aiohttp doesn't yet support 3.12 --- .github/workflows/diff_shades.yml | 4 ++-- .github/workflows/doc.yml | 2 +- .github/workflows/lint.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 97db907abc8..6bfc6ca9ed8 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: "*" + python-version: "3.11" - name: Install diff-shades and support dependencies run: | @@ -59,7 +59,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: "*" + python-version: "3.11" - name: Install diff-shades and support dependencies run: | diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index fa3d87c70f5..9a23e19cadd 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -26,7 +26,7 @@ jobs: - name: Set up latest Python uses: actions/setup-python@v4 with: - python-version: "*" + python-version: "3.11" - name: Install dependencies run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3eaf5785f5a..7fe1b04eb02 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,7 +26,7 @@ jobs: - name: Set up latest Python uses: actions/setup-python@v4 with: - python-version: "*" + python-version: "3.11" - name: Install dependencies run: | From 7bfa35cca88a2a6b875fb8564c19164143a46f1d Mon Sep 17 00:00:00 2001 From: Surav Shrestha <148626286+shresthasurav@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:11:47 +0545 Subject: [PATCH 5/5] docs: fix typos in change log and documentations (#3985) --- CHANGES.md | 2 +- docs/the_black_code_style/current_style.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c96186c93cc..7703223a119 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -52,7 +52,7 @@ ### Highlights -- Maintanence release to get a fix out for GitHub Action edge case (#3957) +- Maintenance release to get a fix out for GitHub Action edge case (#3957) ### Preview style diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index f59c1853f72..431bae525f6 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -11,7 +11,7 @@ used by _Black_ can be viewed as a strict subset of PEP 8. _Black_ reformats entire files in place. It doesn't reformat lines that contain `# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`. `# fmt: skip` can be mixed with other pragmas/comments either with multiple comments -(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separeted list (e.g. +(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separated list (e.g. `# fmt: skip; pylint; noqa`). `# fmt: on/off` must be on the same level of indentation and in the same block, meaning no unindents beyond the initial indentation level between them. It also recognizes [YAPF](https://github.com/google/yapf)'s block comments to the