From 18e711f7b46f76aa5ada2f54f639866631f83122 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 21:04:50 -0800 Subject: [PATCH 01/43] Allow empty lines at beginning of blocks (again) --- CHANGES.md | 3 ++- src/black/lines.py | 10 +++------- src/black/mode.py | 2 +- ...cial_cases.py => preview_allow_empty_first_line.py} | 9 +++++++++ tests/data/cases/preview_form_feeds.py | 1 + 5 files changed, 16 insertions(+), 9 deletions(-) rename tests/data/cases/{preview_allow_empty_first_line_in_special_cases.py => preview_allow_empty_first_line.py} (94%) diff --git a/CHANGES.md b/CHANGES.md index 18bab5131e6..41a4f86c19a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,9 +13,10 @@ ### Preview style - Standalone form feed characters at the module level are no longer removed (#4021) - - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) +- Allow empty lines at the beginning of all blocks, except immediately before a + docstring (#4060) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index ec6145ff848..cf89d8b6b50 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -685,17 +685,13 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: return before, 1 is_empty_first_line_ok = ( - Preview.allow_empty_first_line_before_new_block_or_comment + Preview.allow_empty_first_line_in_block in current_line.mode and ( - # If it's a standalone comment - current_line.leaves[0].type == STANDALONE_COMMENT - # If it opens a new block - or current_line.opens_block + not is_docstring(current_line.leaves[0]) # If it's a triple quote comment (but not at the start of a funcdef) or ( - is_docstring(current_line.leaves[0]) - and self.previous_line + self.previous_line and self.previous_line.leaves[0] and self.previous_line.leaves[0].parent and not is_funcdef(self.previous_line.leaves[0].parent) diff --git a/src/black/mode.py b/src/black/mode.py index 04038f49627..9df19618363 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -191,7 +191,7 @@ class Preview(Enum): 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() + allow_empty_first_line_in_block = auto() single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() diff --git a/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py b/tests/data/cases/preview_allow_empty_first_line.py similarity index 94% rename from tests/data/cases/preview_allow_empty_first_line_in_special_cases.py rename to tests/data/cases/preview_allow_empty_first_line.py index 96c1433c110..600e737449e 100644 --- a/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py +++ b/tests/data/cases/preview_allow_empty_first_line.py @@ -51,6 +51,10 @@ def baz(): if x: a = 123 +def quux(): + + new_line = here + # output def foo(): @@ -104,3 +108,8 @@ def baz(): # OK if x: a = 123 + + +def quux(): + + new_line = here diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/preview_form_feeds.py index 2d8653a1f04..c236f177a95 100644 --- a/tests/data/cases/preview_form_feeds.py +++ b/tests/data/cases/preview_form_feeds.py @@ -198,6 +198,7 @@ def foo(): # form feeds are prohibited inside blocks, or on a line with nonwhitespace def bar(a=1, b: bool = False): + pass From d7537030053edf07a2ed2fd420544bfa1520db5b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 21:10:50 -0800 Subject: [PATCH 02/43] reformat --- src/black/lines.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index cf89d8b6b50..8895f5b2d1b 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -684,18 +684,14 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: return 0, 1 return before, 1 - is_empty_first_line_ok = ( - Preview.allow_empty_first_line_in_block - in current_line.mode - and ( - not is_docstring(current_line.leaves[0]) - # If it's a triple quote comment (but not at the start of a funcdef) - or ( - self.previous_line - and self.previous_line.leaves[0] - and self.previous_line.leaves[0].parent - and not is_funcdef(self.previous_line.leaves[0].parent) - ) + is_empty_first_line_ok = Preview.allow_empty_first_line_in_block in current_line.mode and ( + not is_docstring(current_line.leaves[0]) + # If it's a triple quote comment (but not at the start of a funcdef) + or ( + self.previous_line + and self.previous_line.leaves[0] + and self.previous_line.leaves[0].parent + and not is_funcdef(self.previous_line.leaves[0].parent) ) ) From 129349ce664e514c70fa93023332277a5fd27c95 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 21:11:44 -0800 Subject: [PATCH 03/43] update comments --- src/black/lines.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index 8895f5b2d1b..37615c473fa 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -684,14 +684,17 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: return 0, 1 return before, 1 - is_empty_first_line_ok = Preview.allow_empty_first_line_in_block in current_line.mode and ( - not is_docstring(current_line.leaves[0]) - # If it's a triple quote comment (but not at the start of a funcdef) - or ( - self.previous_line - and self.previous_line.leaves[0] - and self.previous_line.leaves[0].parent - and not is_funcdef(self.previous_line.leaves[0].parent) + # In preview mode, always allow blank lines, except right before a function docstring + is_empty_first_line_ok = ( + Preview.allow_empty_first_line_in_block in current_line.mode + and ( + not is_docstring(current_line.leaves[0]) + or ( + self.previous_line + and self.previous_line.leaves[0] + and self.previous_line.leaves[0].parent + and not is_funcdef(self.previous_line.leaves[0].parent) + ) ) ) From 51bb901a8a640e2f587a18b3fc4424187f48d3ee Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 21:24:26 -0800 Subject: [PATCH 04/43] Many uncontroverisal preview changes --- src/black/comments.py | 30 ++++++-------- src/black/linegen.py | 33 +++------------ src/black/lines.py | 40 +++++-------------- src/black/mode.py | 12 ------ src/black/nodes.py | 4 +- ...g.py => no_blank_line_before_docstring.py} | 1 - 6 files changed, 30 insertions(+), 90 deletions(-) rename tests/data/cases/{preview_no_blank_line_before_docstring.py => no_blank_line_before_docstring.py} (97%) diff --git a/src/black/comments.py b/src/black/comments.py index 8a0e925fdc0..8155f23f824 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -376,22 +376,18 @@ def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool: # 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] - ) + 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 + ) + ], + ] return any(comment in FMT_SKIP for comment in semantic_comment_blocks) diff --git a/src/black/linegen.py b/src/black/linegen.py index 7fbbe290d7e..d603694a791 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -114,10 +114,8 @@ def line(self, indent: int = 0) -> Iterator[Line]: self.current_line.depth += indent return # Line is empty, don't emit. Creating a new one unnecessary. - if ( - Preview.improved_async_statements_handling in self.mode - and len(self.current_line.leaves) == 1 - and is_async_stmt_or_funcdef(self.current_line.leaves[0]) + if len(self.current_line.leaves) == 1 and is_async_stmt_or_funcdef( + self.current_line.leaves[0] ): # Special case for async def/for/with statements. `visit_async_stmt` # adds an `ASYNC` leaf then visits the child def/for/with statement @@ -333,11 +331,7 @@ def visit_async_stmt(self, node: Node) -> Iterator[Line]: break internal_stmt = next(children) - if Preview.improved_async_statements_handling in self.mode: - yield from self.visit(internal_stmt) - else: - for child in internal_stmt.children: - yield from self.visit(child) + yield from self.visit(internal_stmt) def visit_decorators(self, node: Node) -> Iterator[Line]: """Visit decorators.""" @@ -567,9 +561,7 @@ def transform_line( # We need the line string when power operators are hugging to determine if we should # split the line. Default to line_str, if no power operator are present on the line. line_str_hugging_power_ops = ( - (_hugging_power_ops_line_to_string(line, features, mode) or line_str) - if Preview.fix_power_op_line_length in mode - else line_str + _hugging_power_ops_line_to_string(line, features, mode) or line_str ) ll = mode.line_length @@ -679,9 +671,6 @@ def should_split_funcdef_with_rhs(line: Line, mode: Mode) -> bool: """If a funcdef has a magic trailing comma in the return type, then we should first split the line with rhs to respect the comma. """ - if Preview.respect_magic_trailing_comma_in_return_type not in mode: - return False - return_type_leaves: List[Leaf] = [] in_return_type = False @@ -1191,11 +1180,7 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features ) - if ( - Preview.add_trailing_comma_consistently in mode - and last_leaf.type == STANDALONE_COMMENT - and leaf_idx == last_non_comment_leaf - ): + if last_leaf.type == STANDALONE_COMMENT and leaf_idx == last_non_comment_leaf: current_line = _safe_add_trailing_comma( trailing_comma_safe, delimiter_priority, current_line ) @@ -1282,11 +1267,7 @@ def normalize_invisible_parens( # noqa: C901 # Fixes a bug where invisible parens are not properly wrapped around # case blocks. - if ( - isinstance(child, Node) - and child.type == syms.case_block - and Preview.long_case_block_line_splitting in mode - ): + if isinstance(child, Node) and child.type == syms.case_block: normalize_invisible_parens( child, parens_after={"case"}, mode=mode, features=features ) @@ -1341,7 +1322,6 @@ def normalize_invisible_parens( # noqa: C901 and child.next_sibling is not None and child.next_sibling.type == token.COLON and child.value == "case" - and Preview.long_case_block_line_splitting in mode ): # A special patch for "case case:" scenario, the second occurrence # of case will be not parsed as a Python keyword. @@ -1415,7 +1395,6 @@ def _maybe_wrap_cms_in_parens( """ if ( Feature.PARENTHESIZED_CONTEXT_MANAGERS not in features - or Preview.wrap_multiple_context_managers_in_parens not in mode or len(node.children) <= 2 # If it's an atom, it's already wrapped in parens. or node.children[1].type == syms.atom diff --git a/src/black/lines.py b/src/black/lines.py index 37615c473fa..c7d59178139 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -203,9 +203,7 @@ def is_triple_quoted_string(self) -> bool: value = self.leaves[0].value if value.startswith(('"""', "'''")): return True - if Preview.accept_raw_docstrings in self.mode and value.startswith( - ("r'''", 'r"""', "R'''", 'R"""') - ): + if value.startswith(("r'''", 'r"""', "R'''", 'R"""')): return True return False @@ -628,11 +626,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if depth and not current_line.is_def and self.previous_line.is_def: # Empty lines between attributes and methods should be preserved. before = 1 if user_had_newline else 0 - elif ( - Preview.blank_line_after_nested_stub_class in self.mode - and previous_def.is_class - and not previous_def.is_stub_class - ): + elif previous_def.is_class and not previous_def.is_stub_class: before = 1 elif depth: before = 0 @@ -680,14 +674,13 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: and self.previous_line.is_class and current_line.is_triple_quoted_string ): - if Preview.no_blank_line_before_class_docstring in current_line.mode: - return 0, 1 - return before, 1 + return 0, 1 - # In preview mode, always allow blank lines, except right before a function docstring - is_empty_first_line_ok = ( - Preview.allow_empty_first_line_in_block in current_line.mode - and ( + if ( + self.previous_line + and self.previous_line.opens_block + # Always allow blank lines, except right before a function docstring + and not ( not is_docstring(current_line.leaves[0]) or ( self.previous_line @@ -696,12 +689,6 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: and not is_funcdef(self.previous_line.leaves[0].parent) ) ) - ) - - if ( - self.previous_line - and self.previous_line.opens_block - and not is_empty_first_line_ok ): return 0, 0 return before, 0 @@ -762,10 +749,7 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 # Don't inspect the previous line if it's part of the body of the previous # statement in the same level, we always want a blank line if there's # something with a body preceding. - elif ( - Preview.blank_line_between_nested_and_def_stub_file in current_line.mode - and self.previous_line.depth > current_line.depth - ): + elif self.previous_line.depth > current_line.depth: newlines = 1 elif ( current_line.is_def or current_line.is_decorator @@ -1001,11 +985,7 @@ def can_omit_invisible_parens( return False if delimiter_count == 1: - if ( - Preview.wrap_multiple_context_managers_in_parens in line.mode - and max_priority == COMMA_PRIORITY - and rhs.head.is_with_or_async_with_stmt - ): + if max_priority == COMMA_PRIORITY and rhs.head.is_with_or_async_with_stmt: # For two context manager with statements, the optional parentheses read # better. In this case, `rhs.body` is the context managers part of # the with statement. `rhs.head` is the `with (` part on the previous diff --git a/src/black/mode.py b/src/black/mode.py index 9df19618363..f6f614f67c1 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -168,32 +168,20 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b class Preview(Enum): """Individual preview style features.""" - add_trailing_comma_consistently = auto() blank_line_after_nested_stub_class = auto() - blank_line_between_nested_and_def_stub_file = auto() hex_codes_in_unicode_sequences = auto() - improved_async_statements_handling = auto() multiline_string_handling = auto() - no_blank_line_before_class_docstring = auto() prefer_splitting_right_hand_side_of_assignments = auto() # 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() parenthesize_long_type_hints = auto() - respect_magic_trailing_comma_in_return_type = auto() - skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() wrap_multiple_context_managers_in_parens = auto() dummy_implementations = auto() - walrus_subscript = auto() 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_in_block = auto() - single_line_format_skip_with_multiple_comments = auto() - long_case_block_line_splitting = auto() allow_form_feeds = auto() diff --git a/src/black/nodes.py b/src/black/nodes.py index de53f8e36a3..c1bc728ce83 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -346,9 +346,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no return NO - elif Preview.walrus_subscript in mode and ( - t == token.COLONEQUAL or prev.type == token.COLONEQUAL - ): + elif t == token.COLONEQUAL or prev.type == token.COLONEQUAL: return SPACE elif not complex_subscript: diff --git a/tests/data/cases/preview_no_blank_line_before_docstring.py b/tests/data/cases/no_blank_line_before_docstring.py similarity index 97% rename from tests/data/cases/preview_no_blank_line_before_docstring.py rename to tests/data/cases/no_blank_line_before_docstring.py index 303035a7efb..a37362de100 100644 --- a/tests/data/cases/preview_no_blank_line_before_docstring.py +++ b/tests/data/cases/no_blank_line_before_docstring.py @@ -1,4 +1,3 @@ -# flags: --preview def line_before_docstring(): """Please move me up""" From c71b5e23bdf47d23122afc0a71ed90b587f389e2 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 21:44:26 -0800 Subject: [PATCH 05/43] more relatively noncontroversial features --- src/black/linegen.py | 32 ++++++++------------------------ src/black/lines.py | 19 +++---------------- src/black/mode.py | 8 -------- src/black/nodes.py | 1 + 4 files changed, 12 insertions(+), 48 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index d603694a791..3fb190e9dc6 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -282,9 +282,7 @@ def visit_match_case(self, node: Node) -> Iterator[Line]: def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" - if ( - self.mode.is_pyi or Preview.dummy_implementations in self.mode - ) and is_stub_suite(node): + if is_stub_suite(node): yield from self.visit(node.children[2]) else: yield from self.visit_default(node) @@ -299,9 +297,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: is_suite_like = node.parent and node.parent.type in STATEMENT if is_suite_like: - if ( - self.mode.is_pyi or Preview.dummy_implementations in self.mode - ) and is_stub_body(node): + if is_stub_body(node): yield from self.visit_default(node) else: yield from self.line(+1) @@ -309,11 +305,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: yield from self.line(-1) else: - if ( - not (self.mode.is_pyi or Preview.dummy_implementations in self.mode) - or not node.parent - or not is_stub_suite(node.parent) - ): + if not node.parent or not is_stub_suite(node.parent): yield from self.line() yield from self.visit_default(node) @@ -405,10 +397,9 @@ def foo(a: int, b: float = 7): ... def foo(a: (int), b: (float) = 7): ... """ - if Preview.parenthesize_long_type_hints in self.mode: - assert len(node.children) == 3 - if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): - wrap_in_parentheses(node, node.children[2], visible=False) + assert len(node.children) == 3 + if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): + wrap_in_parentheses(node, node.children[2], visible=False) yield from self.visit_default(node) @@ -514,13 +505,7 @@ def __post_init__(self) -> None: self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) - # When this is moved out of preview, add ":" directly to ASSIGNMENTS in nodes.py - if Preview.parenthesize_long_type_hints in self.mode: - assignments = ASSIGNMENTS | {":"} - else: - assignments = ASSIGNMENTS - self.visit_expr_stmt = partial(v, keywords=Ø, parens=assignments) - + self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"}) self.visit_import_from = partial(v, keywords=Ø, parens={"import"}) self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"}) @@ -900,9 +885,8 @@ def _maybe_split_omitting_optional_parens( # The RHSResult Omitting Optional Parens. rhs_oop = _first_right_hand_split(line, omit=omit) if not ( - Preview.prefer_splitting_right_hand_side_of_assignments in line.mode # the split is right after `=` - and len(rhs.head.leaves) >= 2 + len(rhs.head.leaves) >= 2 and rhs.head.leaves[-2].type == token.EQUAL # the left side of assignment contains brackets and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1]) diff --git a/src/black/lines.py b/src/black/lines.py index c7d59178139..48c557dfa55 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -550,8 +550,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: lines (two on module-level). """ form_feed = ( - Preview.allow_form_feeds in self.mode - and current_line.depth == 0 + current_line.depth == 0 and bool(current_line.leaves) and "\f\n" in current_line.leaves[0].prefix ) @@ -565,8 +564,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: else before - previous_after ) if ( - Preview.module_docstring_newlines in current_line.mode - and self.previous_block + self.previous_block and self.previous_block.previous_block is None and len(self.previous_block.original_line.leaves) == 1 and self.previous_block.original_line.is_triple_quoted_string @@ -770,11 +768,7 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 newlines = 1 if current_line.depth else 2 # If a user has left no space after a dummy implementation, don't insert # new lines. This is useful for instance for @overload or Protocols. - if ( - Preview.dummy_implementations in self.mode - and self.previous_line.is_stub_def - and not user_had_newline - ): + if self.previous_line.is_stub_def and not user_had_newline: newlines = 0 if comment_to_add_newlines is not None: previous_block = comment_to_add_newlines.previous_block @@ -831,13 +825,6 @@ def is_line_short_enough( # noqa: C901 width = str_width if mode.preview else len - if Preview.multiline_string_handling not in mode: - return ( - width(line_str) <= mode.line_length - and "\n" not in line_str # multiline strings - and not line.contains_standalone_comments() - ) - if line.contains_standalone_comments(): return False if "\n" not in line_str: diff --git a/src/black/mode.py b/src/black/mode.py index f6f614f67c1..124926a9cc3 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -168,21 +168,13 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b class Preview(Enum): """Individual preview style features.""" - blank_line_after_nested_stub_class = auto() hex_codes_in_unicode_sequences = auto() - multiline_string_handling = auto() - prefer_splitting_right_hand_side_of_assignments = auto() # 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() - parenthesize_long_type_hints = auto() wrap_long_dict_values_in_parens = auto() - wrap_multiple_context_managers_in_parens = auto() - dummy_implementations = auto() - module_docstring_newlines = auto() hug_parens_with_braces_and_square_brackets = auto() - allow_form_feeds = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index c1bc728ce83..a7ec9126402 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -121,6 +121,7 @@ ">>=", "**=", "//=", + ":", } IMPLICIT_TUPLE: Final = {syms.testlist, syms.testlist_star_expr, syms.exprlist} From cc3780fd9f8932d7240c51fc042bd1841fc44a0e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 22:26:08 -0800 Subject: [PATCH 06/43] Update some tests --- .../attribute_access_on_number_literals.py | 3 +- tests/data/cases/comments5.py | 9 +-- tests/data/cases/composition.py | 4 +- .../cases/composition_no_trailing_comma.py | 4 +- tests/data/cases/empty_lines.py | 1 + tests/data/cases/expression.py | 18 ++--- tests/data/cases/fmtonoff.py | 8 +- tests/data/cases/fmtonoff5.py | 3 +- tests/data/cases/function.py | 8 +- ...r_match.py => keep_newline_after_match.py} | 6 ++ .../data/cases/long_strings_flag_disabled.py | 10 ++- .../cases/parenthesized_context_managers.py | 18 ++--- tests/data/cases/pattern_matching_extras.py | 6 +- tests/data/cases/pep_572_py310.py | 6 +- tests/data/cases/pep_572_remove_parens.py | 6 +- tests/data/cases/python39.py | 9 +-- .../remove_newline_after_code_block_open.py | 79 ++++++++++++------- tests/data/cases/remove_with_brackets.py | 12 +-- .../data/cases/return_annotation_brackets.py | 34 ++++---- 19 files changed, 116 insertions(+), 128 deletions(-) rename tests/data/cases/{remove_newline_after_match.py => keep_newline_after_match.py} (98%) diff --git a/tests/data/cases/attribute_access_on_number_literals.py b/tests/data/cases/attribute_access_on_number_literals.py index 7c16bdfb3a5..1b4dbbc2907 100644 --- a/tests/data/cases/attribute_access_on_number_literals.py +++ b/tests/data/cases/attribute_access_on_number_literals.py @@ -40,8 +40,7 @@ x = (0.000000006).hex() x = -100.0000j -if (10).real: - ... +if (10).real: ... y = 100[no] y = 100(no) diff --git a/tests/data/cases/comments5.py b/tests/data/cases/comments5.py index bda40619f62..4270d3a09a2 100644 --- a/tests/data/cases/comments5.py +++ b/tests/data/cases/comments5.py @@ -45,8 +45,7 @@ def wat(): @deco2(with_args=True) # leading 3 @deco3 -def decorated1(): - ... +def decorated1(): ... # leading 1 @@ -54,8 +53,7 @@ def decorated1(): # leading 2 @deco2(with_args=True) # leading function comment -def decorated1(): - ... +def decorated1(): ... # Note: this is fixed in @@ -65,8 +63,7 @@ def decorated1(): # This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... +def g(): ... if __name__ == "__main__": diff --git a/tests/data/cases/composition.py b/tests/data/cases/composition.py index e429f15e669..0798d3f3b29 100644 --- a/tests/data/cases/composition.py +++ b/tests/data/cases/composition.py @@ -161,9 +161,7 @@ def tricky_asserts(self) -> None: 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE - """ % ( - _C.__init__.__code__.co_firstlineno + 1, - ) + """ % (_C.__init__.__code__.co_firstlineno + 1,) assert ( expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect diff --git a/tests/data/cases/composition_no_trailing_comma.py b/tests/data/cases/composition_no_trailing_comma.py index f17b89dea8d..88d17b743de 100644 --- a/tests/data/cases/composition_no_trailing_comma.py +++ b/tests/data/cases/composition_no_trailing_comma.py @@ -347,9 +347,7 @@ def tricky_asserts(self) -> None: 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE - """ % ( - _C.__init__.__code__.co_firstlineno + 1, - ) + """ % (_C.__init__.__code__.co_firstlineno + 1,) assert ( expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect diff --git a/tests/data/cases/empty_lines.py b/tests/data/cases/empty_lines.py index 4fd47b93dca..4c03e432383 100644 --- a/tests/data/cases/empty_lines.py +++ b/tests/data/cases/empty_lines.py @@ -119,6 +119,7 @@ def f(): if not prev: prevp = preceding_leaf(p) if not prevp or prevp.type in OPENING_BRACKETS: + return NO if prevp.type == token.EQUAL: diff --git a/tests/data/cases/expression.py b/tests/data/cases/expression.py index 06096c589f1..8e13726210b 100644 --- a/tests/data/cases/expression.py +++ b/tests/data/cases/expression.py @@ -514,18 +514,12 @@ async def f(): force=False ), "Short message" assert parens is TooMany -for (x,) in (1,), (2,), (3,): - ... -for y in (): - ... -for z in (i for i in (1, 2, 3)): - ... -for i in call(): - ... -for j in 1 + (2 + 3): - ... -while this and that: - ... +for (x,) in (1,), (2,), (3,): ... +for y in (): ... +for z in (i for i in (1, 2, 3)): ... +for i in call(): ... +for j in 1 + (2 + 3): ... +while this and that: ... for ( addr_family, addr_type, diff --git a/tests/data/cases/fmtonoff.py b/tests/data/cases/fmtonoff.py index d1f15cd5c8b..8af94563af8 100644 --- a/tests/data/cases/fmtonoff.py +++ b/tests/data/cases/fmtonoff.py @@ -243,12 +243,8 @@ def spaces_types( g: int = 1 if False else 2, h: str = "", i: str = r"", -): - ... - - -def spaces2(result=_core.Value(None)): - ... +): ... +def spaces2(result=_core.Value(None)): ... something = { diff --git a/tests/data/cases/fmtonoff5.py b/tests/data/cases/fmtonoff5.py index 181151b6bd6..4c134a9eea3 100644 --- a/tests/data/cases/fmtonoff5.py +++ b/tests/data/cases/fmtonoff5.py @@ -161,8 +161,7 @@ def this_wont_be_formatted ( self ) -> str: ... class Factory(t.Protocol): - def this_will_be_formatted(self, **kwargs) -> Named: - ... + def this_will_be_formatted(self, **kwargs) -> Named: ... # fmt: on diff --git a/tests/data/cases/function.py b/tests/data/cases/function.py index 2d642c8731b..8aba756a919 100644 --- a/tests/data/cases/function.py +++ b/tests/data/cases/function.py @@ -114,8 +114,7 @@ def func_no_args(): c if True: raise RuntimeError - if False: - ... + if False: ... for i in range(10): print(i) continue @@ -158,10 +157,7 @@ def spaces_types( g: int = 1 if False else 2, h: str = "", i: str = r"", -): - ... - - +): ... def spaces2(result=_core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) diff --git a/tests/data/cases/remove_newline_after_match.py b/tests/data/cases/keep_newline_after_match.py similarity index 98% rename from tests/data/cases/remove_newline_after_match.py rename to tests/data/cases/keep_newline_after_match.py index fe6592b664d..dbeccce6264 100644 --- a/tests/data/cases/remove_newline_after_match.py +++ b/tests/data/cases/keep_newline_after_match.py @@ -21,15 +21,21 @@ def http_status(status): # output def http_status(status): + match status: + case 400: + return "Bad request" case 401: + return "Unauthorized" case 403: + return "Forbidden" case 404: + return "Not found" \ No newline at end of file diff --git a/tests/data/cases/long_strings_flag_disabled.py b/tests/data/cases/long_strings_flag_disabled.py index db3954e3abd..ce60b16a3ff 100644 --- a/tests/data/cases/long_strings_flag_disabled.py +++ b/tests/data/cases/long_strings_flag_disabled.py @@ -254,10 +254,12 @@ + CONCATENATED + "using the '+' operator." ) -annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." -annotated_variable: Literal[ - "fakse_literal" -] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Final = ( + "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +) +annotated_variable: Literal["fakse_literal"] = ( + "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +) backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" diff --git a/tests/data/cases/parenthesized_context_managers.py b/tests/data/cases/parenthesized_context_managers.py index 16645a18baa..538eff7e8aa 100644 --- a/tests/data/cases/parenthesized_context_managers.py +++ b/tests/data/cases/parenthesized_context_managers.py @@ -23,24 +23,18 @@ # output -with CtxManager() as example: - ... +with CtxManager() as example: ... -with CtxManager1(), CtxManager2(): - ... +with CtxManager1(), CtxManager2(): ... -with CtxManager1() as example, CtxManager2(): - ... +with CtxManager1() as example, CtxManager2(): ... -with CtxManager1(), CtxManager2() as example: - ... +with CtxManager1(), CtxManager2() as example: ... -with CtxManager1() as example1, CtxManager2() as example2: - ... +with CtxManager1() as example1, CtxManager2() as example2: ... with ( CtxManager1() as example1, CtxManager2() as example2, CtxManager3() as example3, -): - ... +): ... diff --git a/tests/data/cases/pattern_matching_extras.py b/tests/data/cases/pattern_matching_extras.py index 1aef8f16b5a..df6ef4b1ab1 100644 --- a/tests/data/cases/pattern_matching_extras.py +++ b/tests/data/cases/pattern_matching_extras.py @@ -24,10 +24,8 @@ def func(match: case, case: match) -> case: match Something(): - case func(match, case): - ... - case another: - ... + case func(match, case): ... + case another: ... match a, *b, c: diff --git a/tests/data/cases/pep_572_py310.py b/tests/data/cases/pep_572_py310.py index 9f999deeb89..ba488d4741c 100644 --- a/tests/data/cases/pep_572_py310.py +++ b/tests/data/cases/pep_572_py310.py @@ -1,8 +1,8 @@ # flags: --minimum-version=3.10 # Unparenthesized walruses are now allowed in indices since Python 3.10. -x[a:=0] -x[a:=0, b:=1] -x[5, b:=0] +x[a := 0] +x[a := 0, b := 1] +x[5, b := 0] # Walruses are allowed inside generator expressions on function calls since 3.10. if any(match := pattern_error.match(s) for s in buffer): diff --git a/tests/data/cases/pep_572_remove_parens.py b/tests/data/cases/pep_572_remove_parens.py index 24f1ac29168..5e30e710f79 100644 --- a/tests/data/cases/pep_572_remove_parens.py +++ b/tests/data/cases/pep_572_remove_parens.py @@ -96,12 +96,10 @@ async def await_the_walrus(): foo(x=(y := f(x))) -def foo(answer=(p := 42)): - ... +def foo(answer=(p := 42)): ... -def foo2(answer: (p := 42) = 5): - ... +def foo2(answer: (p := 42) = 5): ... lambda: (x := 1) diff --git a/tests/data/cases/python39.py b/tests/data/cases/python39.py index 1b9536c1529..85eddc38e00 100644 --- a/tests/data/cases/python39.py +++ b/tests/data/cases/python39.py @@ -15,19 +15,16 @@ def f(): # output @relaxed_decorator[0] -def f(): - ... +def f(): ... @relaxed_decorator[ extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length ] -def f(): - ... +def f(): ... @extremely_long_variable_name_that_doesnt_fit := complex.expression( with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" ) -def f(): - ... \ No newline at end of file +def f(): ... \ No newline at end of file diff --git a/tests/data/cases/remove_newline_after_code_block_open.py b/tests/data/cases/remove_newline_after_code_block_open.py index ef2e5c2f6f5..6622e8afb7d 100644 --- a/tests/data/cases/remove_newline_after_code_block_open.py +++ b/tests/data/cases/remove_newline_after_code_block_open.py @@ -3,14 +3,14 @@ def foo1(): - print("The newline above me should be deleted!") + print("The newline above me should be kept!") def foo2(): - print("All the newlines above me should be deleted!") + print("All the newlines above me should be kept!") def foo3(): @@ -30,31 +30,31 @@ def foo4(): class Foo: def bar(self): - print("The newline above me should be deleted!") + print("The newline above me should be kept!") for i in range(5): - print(f"{i}) The line above me should be removed!") + print(f"{i}) The line above me should be kept!") for i in range(5): - print(f"{i}) The lines above me should be removed!") + print(f"{i}) The lines above me should be kept!") for i in range(5): for j in range(7): - print(f"{i}) The lines above me should be removed!") + print(f"{i}) The lines above me should be kept!") if random.randint(0, 3) == 0: - print("The new line above me is about to be removed!") + print("The new line above me will be kept!") if random.randint(0, 3) == 0: @@ -62,43 +62,45 @@ def bar(self): - print("The new lines above me is about to be removed!") + print("The new lines above me will be kept!") if random.randint(0, 3) == 0: + if random.uniform(0, 1) > 0.5: - print("Two lines above me are about to be removed!") + + print("Two lines above me will be kept!") while True: - print("The newline above me should be deleted!") + print("The newline above me should be kept!") while True: - print("The newlines above me should be deleted!") + print("The newlines above me should be kept!") while True: while False: - print("The newlines above me should be deleted!") + print("The newlines above me should be kept!") with open("/path/to/file.txt", mode="w") as file: - file.write("The new line above me is about to be removed!") + file.write("The new line above me will be kept!") with open("/path/to/file.txt", mode="w") as file: - file.write("The new lines above me is about to be removed!") + file.write("The new lines above me will be kept!") with open("/path/to/file.txt", mode="r") as read_file: @@ -113,20 +115,24 @@ def bar(self): def foo1(): - print("The newline above me should be deleted!") + + print("The newline above me should be kept!") def foo2(): - print("All the newlines above me should be deleted!") + + print("All the newlines above me should be kept!") def foo3(): + print("No newline above me!") print("There is a newline above me, and that's OK!") def foo4(): + # There is a comment here print("The newline above me should not be deleted!") @@ -134,56 +140,73 @@ def foo4(): class Foo: def bar(self): - print("The newline above me should be deleted!") + + print("The newline above me should be kept!") for i in range(5): - print(f"{i}) The line above me should be removed!") + + print(f"{i}) The line above me should be kept!") for i in range(5): - print(f"{i}) The lines above me should be removed!") + + print(f"{i}) The lines above me should be kept!") for i in range(5): + for j in range(7): - print(f"{i}) The lines above me should be removed!") + + print(f"{i}) The lines above me should be kept!") if random.randint(0, 3) == 0: - print("The new line above me is about to be removed!") + + print("The new line above me will be kept!") if random.randint(0, 3) == 0: - print("The new lines above me is about to be removed!") + + print("The new lines above me will be kept!") if random.randint(0, 3) == 0: + if random.uniform(0, 1) > 0.5: - print("Two lines above me are about to be removed!") + + print("Two lines above me will be kept!") while True: - print("The newline above me should be deleted!") + + print("The newline above me should be kept!") while True: - print("The newlines above me should be deleted!") + + print("The newlines above me should be kept!") while True: + while False: - print("The newlines above me should be deleted!") + + print("The newlines above me should be kept!") with open("/path/to/file.txt", mode="w") as file: - file.write("The new line above me is about to be removed!") + + file.write("The new line above me will be kept!") with open("/path/to/file.txt", mode="w") as file: - file.write("The new lines above me is about to be removed!") + + file.write("The new lines above me will be kept!") with open("/path/to/file.txt", mode="r") as read_file: + with open("/path/to/output_file.txt", mode="w") as write_file: + write_file.writelines(read_file.readlines()) diff --git a/tests/data/cases/remove_with_brackets.py b/tests/data/cases/remove_with_brackets.py index 3ee64902a30..f90b158a769 100644 --- a/tests/data/cases/remove_with_brackets.py +++ b/tests/data/cases/remove_with_brackets.py @@ -75,15 +75,13 @@ with open("bla.txt") as f, open("x"): pass -with CtxManager1() as example1, CtxManager2() as example2: - ... +with CtxManager1() as example1, CtxManager2() as example2: ... # Brackets remain when using magic comma with ( CtxManager1() as example1, CtxManager2() as example2, -): - ... +): ... # Brackets remain for multi-line context managers with ( @@ -92,8 +90,7 @@ CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, -): - ... +): ... # Don't touch assignment expressions with (y := open("./test.py")) as f: @@ -116,5 +113,4 @@ with open("bla.txt") as f: pass -with CtxManager1() as example1, CtxManager2() as example2: - ... +with CtxManager1() as example1, CtxManager2() as example2: ... diff --git a/tests/data/cases/return_annotation_brackets.py b/tests/data/cases/return_annotation_brackets.py index 8509ecdb92c..ed05bed61f4 100644 --- a/tests/data/cases/return_annotation_brackets.py +++ b/tests/data/cases/return_annotation_brackets.py @@ -88,7 +88,6 @@ def foo() -> tuple[int, int, int,]: return 2 # Magic trailing comma example, with params -# this is broken - the trailing comma is transferred to the param list. Fixed in preview def foo(a,b) -> tuple[int, int, int,]: return 2 @@ -194,30 +193,27 @@ def foo() -> tuple[int, int, int]: return 2 -def foo() -> ( - tuple[ - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - ] -): +def foo() -> tuple[ + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, +]: return 2 # Magic trailing comma example -def foo() -> ( - tuple[ - int, - int, - int, - ] -): +def foo() -> tuple[ + int, + int, + int, +]: return 2 # Magic trailing comma example, with params -# this is broken - the trailing comma is transferred to the param list. Fixed in preview -def foo( - a, b -) -> tuple[int, int, int,]: +def foo(a, b) -> tuple[ + int, + int, + int, +]: return 2 From bb31678201e1bebac1c1b7cea038d58db2171b0d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 22:29:44 -0800 Subject: [PATCH 07/43] Enable two more --- src/black/linegen.py | 42 +++++++++---------- src/black/mode.py | 4 -- tests/data/cases/expression.py | 4 +- .../data/cases/long_strings_flag_disabled.py | 6 ++- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 3fb190e9dc6..b53f8c95a56 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -161,16 +161,15 @@ def visit_default(self, node: LN) -> Iterator[Line]: 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 - ) + 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) + 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) @@ -230,20 +229,19 @@ def visit_paramspec(self, node: Node) -> Iterator[Line]: node.children[1].prefix = "" def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: - if Preview.wrap_long_dict_values_in_parens in self.mode: - for i, child in enumerate(node.children): - if i == 0: - continue - if node.children[i - 1].type == token.COLON: - if child.type == syms.atom and child.children[0].type == token.LPAR: - if maybe_make_parens_invisible_in_atom( - child, - parent=node, - remove_brackets_around_comma=False, - ): - wrap_in_parentheses(node, child, visible=False) - else: + for i, child in enumerate(node.children): + if i == 0: + continue + if node.children[i - 1].type == token.COLON: + if child.type == syms.atom and child.children[0].type == token.LPAR: + if maybe_make_parens_invisible_in_atom( + child, + parent=node, + remove_brackets_around_comma=False, + ): wrap_in_parentheses(node, child, visible=False) + else: + wrap_in_parentheses(node, child, visible=False) yield from self.visit_default(node) def visit_funcdef(self, node: Node) -> Iterator[Line]: diff --git a/src/black/mode.py b/src/black/mode.py index 124926a9cc3..13d8551c91e 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -169,11 +169,7 @@ class Preview(Enum): """Individual preview style features.""" hex_codes_in_unicode_sequences = auto() - # 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() - wrap_long_dict_values_in_parens = auto() hug_parens_with_braces_and_square_brackets = auto() diff --git a/tests/data/cases/expression.py b/tests/data/cases/expression.py index 8e13726210b..613d2d36605 100644 --- a/tests/data/cases/expression.py +++ b/tests/data/cases/expression.py @@ -312,8 +312,8 @@ async def f(): if (1 if super_long_test_name else 2) else (str or bytes or None) ) -{"2.7": dead, "3.7": (long_live or die_hard)} -{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} +{"2.7": dead, "3.7": long_live or die_hard} +{"2.7": dead, "3.7": long_live or die_hard, **{"3.6": verygood}} {**a, **b, **c} {"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} ({"a": "b"}, (True or False), (+value), "string", b"bytes") or None diff --git a/tests/data/cases/long_strings_flag_disabled.py b/tests/data/cases/long_strings_flag_disabled.py index ce60b16a3ff..d81c331cab2 100644 --- a/tests/data/cases/long_strings_flag_disabled.py +++ b/tests/data/cases/long_strings_flag_disabled.py @@ -43,8 +43,10 @@ % ( "formatted", "string", - ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." - % ("soooo", 2), + ): ( + "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2) + ), } func_with_keywords( From 37f8ed0a3fb97cc99554673efe22a12684f543db Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 19:36:35 -0800 Subject: [PATCH 08/43] Remove obsolete features --- src/black/mode.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/black/mode.py b/src/black/mode.py index dd2c447d36c..13d8551c91e 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -171,10 +171,6 @@ class Preview(Enum): hex_codes_in_unicode_sequences = auto() string_processing = auto() hug_parens_with_braces_and_square_brackets = auto() - allow_empty_first_line_in_block = auto() - single_line_format_skip_with_multiple_comments = auto() - long_case_block_line_splitting = auto() - allow_form_feeds = auto() class Deprecated(UserWarning): From be46470e55224c01d65bf6ac09e5e991afc099ad Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 19:37:47 -0800 Subject: [PATCH 09/43] Fix up merge --- src/black/lines.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index bd86390b26d..02bd8e2a1b9 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -681,14 +681,17 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: # In preview mode, always allow blank lines, except right before a function # docstring - is_empty_first_line_ok = ( - not is_docstring(current_line.leaves[0]) - or ( - self.previous_line - and self.previous_line.leaves[0] - and self.previous_line.leaves[0].parent - and not is_funcdef(self.previous_line.leaves[0].parent) - ) + is_empty_first_line_ok = not is_docstring(current_line.leaves[0]) or ( + self.previous_line + and self.previous_line.leaves[0] + and self.previous_line.leaves[0].parent + and not is_funcdef(self.previous_line.leaves[0].parent) + ) + + if ( + self.previous_line + and self.previous_line.opens_block + and not is_empty_first_line_ok ): return 0, 0 return before, 0 From 7712a7426abc8190b1c82a4d606c5c0a4d9f33b8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 20:14:21 -0800 Subject: [PATCH 10/43] Add --unstable flag --- src/black/__init__.py | 11 +++++++++++ src/black/lines.py | 2 +- src/black/mode.py | 19 ++++++++++++++----- tests/data/cases/preview_cantfit.py | 14 -------------- tests/data/cases/preview_cantfit_string.py | 18 ++++++++++++++++++ tests/data/cases/preview_comments7.py | 2 +- tests/data/cases/preview_long_strings.py | 2 +- .../preview_long_strings__east_asian_width.py | 2 +- .../cases/preview_long_strings__edge_case.py | 2 +- .../cases/preview_long_strings__regression.py | 2 +- tests/data/cases/preview_multiline_strings.py | 2 +- .../data/cases/preview_percent_precedence.py | 2 +- ...eview_return_annotation_brackets_string.py | 2 +- tests/util.py | 12 +++++++++--- 14 files changed, 61 insertions(+), 31 deletions(-) create mode 100644 tests/data/cases/preview_cantfit_string.py diff --git a/src/black/__init__.py b/src/black/__init__.py index 5073fa748d5..30e5df8b620 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -299,6 +299,15 @@ def validate_regex( " functionality in the next major release." ), ) +@click.option( + "--unstable", + is_flag=True, + help=( + "Enable potentially disruptive style changes that have known bugs or are not" + " currently expected to make it into the stable style Black's next major" + " release. Implies --preview." + ), +) @click.option( "--check", is_flag=True, @@ -491,6 +500,7 @@ def main( # noqa: C901 skip_magic_trailing_comma: bool, experimental_string_processing: bool, preview: bool, + unstable: bool, quiet: bool, verbose: bool, required_version: Optional[str], @@ -579,6 +589,7 @@ def main( # noqa: C901 magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, preview=preview, + unstable=unstable, python_cell_magics=set(python_cell_magics), ) diff --git a/src/black/lines.py b/src/black/lines.py index 4050f819757..48a70a74025 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -851,7 +851,7 @@ def is_line_short_enough( # noqa: C901 if not line_str: line_str = line_to_string(line) - width = str_width if mode.preview else len + width = str_width if Preview.string_processing in mode else len if Preview.multiline_string_handling not in mode: return ( diff --git a/src/black/mode.py b/src/black/mode.py index 9df19618363..35fa481eb70 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -197,6 +197,11 @@ class Preview(Enum): allow_form_feeds = auto() +UNSTABLE_FEATURES: Set[Preview] = { + Preview.string_processing, +} + + class Deprecated(UserWarning): """Visible deprecation warning.""" @@ -213,6 +218,7 @@ class Mode: experimental_string_processing: bool = False python_cell_magics: Set[str] = field(default_factory=set) preview: bool = False + unstable: bool = False def __post_init__(self) -> None: if self.experimental_string_processing: @@ -226,12 +232,15 @@ def __contains__(self, feature: Preview) -> bool: """ Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag. - The argument is not checked and features are not differentiated. - They only exist to make development easier by clarifying intent. + In unstable mode, all features are enabled. In preview mode, all features + except those in UNSTABLE_FEATURES are enabled. For legacy reasons, the + string_processing feature has its own flag, which is deprecated. """ - if feature is Preview.string_processing: - return self.preview or self.experimental_string_processing - return self.preview + if self.unstable: + return True + if feature is Preview.string_processing and self.experimental_string_processing: + return True + return self.preview and feature not in UNSTABLE_FEATURES def get_cache_key(self) -> str: if self.target_versions: diff --git a/tests/data/cases/preview_cantfit.py b/tests/data/cases/preview_cantfit.py index d5da6654f0c..29789c7e653 100644 --- a/tests/data/cases/preview_cantfit.py +++ b/tests/data/cases/preview_cantfit.py @@ -20,12 +20,6 @@ normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying( [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 ) -# long arguments -normal_name = normal_function_name( - "but with super long string arguments that on their own exceed the line limit so there's no way it can ever fit", - "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs", - this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, -) string_variable_name = ( "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa ) @@ -78,14 +72,6 @@ [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 ) ) -# long arguments -normal_name = normal_function_name( - "but with super long string arguments that on their own exceed the line limit so" - " there's no way it can ever fit", - "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs" - " with spam and eggs and spam with eggs", - this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, -) string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa for key in """ hostname diff --git a/tests/data/cases/preview_cantfit_string.py b/tests/data/cases/preview_cantfit_string.py new file mode 100644 index 00000000000..3b48e318ade --- /dev/null +++ b/tests/data/cases/preview_cantfit_string.py @@ -0,0 +1,18 @@ +# flags: --unstable +# long arguments +normal_name = normal_function_name( + "but with super long string arguments that on their own exceed the line limit so there's no way it can ever fit", + "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs", + this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, +) + +# output + +# long arguments +normal_name = normal_function_name( + "but with super long string arguments that on their own exceed the line limit so" + " there's no way it can ever fit", + "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs" + " with spam and eggs and spam with eggs", + this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, +) diff --git a/tests/data/cases/preview_comments7.py b/tests/data/cases/preview_comments7.py index 006d4f7266f..e4d547138db 100644 --- a/tests/data/cases/preview_comments7.py +++ b/tests/data/cases/preview_comments7.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable from .config import ( Any, Bool, diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index 19ac47a7032..86fa1b0c7e1 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." diff --git a/tests/data/cases/preview_long_strings__east_asian_width.py b/tests/data/cases/preview_long_strings__east_asian_width.py index d190f422a60..022b0452522 100644 --- a/tests/data/cases/preview_long_strings__east_asian_width.py +++ b/tests/data/cases/preview_long_strings__east_asian_width.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable # The following strings do not have not-so-many chars, but are long enough # when these are rendered in a monospace font (if the renderer respects # Unicode East Asian Width properties). diff --git a/tests/data/cases/preview_long_strings__edge_case.py b/tests/data/cases/preview_long_strings__edge_case.py index a8e8971968c..28497e731bc 100644 --- a/tests/data/cases/preview_long_strings__edge_case.py +++ b/tests/data/cases/preview_long_strings__edge_case.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable some_variable = "This string is long but not so long that it needs to be split just yet" some_variable = 'This string is long but not so long that it needs to be split just yet' some_variable = "This string is long, just long enough that it needs to be split, u get?" diff --git a/tests/data/cases/preview_long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py index 5e76a8cf61c..afe2b311cf4 100644 --- a/tests/data/cases/preview_long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable class A: def foo(): result = type(message)("") diff --git a/tests/data/cases/preview_multiline_strings.py b/tests/data/cases/preview_multiline_strings.py index 3ff643610b7..9288f6991bd 100644 --- a/tests/data/cases/preview_multiline_strings.py +++ b/tests/data/cases/preview_multiline_strings.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable """cow say""", call(3, "dogsay", textwrap.dedent("""dove diff --git a/tests/data/cases/preview_percent_precedence.py b/tests/data/cases/preview_percent_precedence.py index aeaf450ff5e..8fca16d415b 100644 --- a/tests/data/cases/preview_percent_precedence.py +++ b/tests/data/cases/preview_percent_precedence.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable ("" % a) ** 2 ("" % a)[0] ("" % a)() diff --git a/tests/data/cases/preview_return_annotation_brackets_string.py b/tests/data/cases/preview_return_annotation_brackets_string.py index fea0ea6839a..2f937cf54ef 100644 --- a/tests/data/cases/preview_return_annotation_brackets_string.py +++ b/tests/data/cases/preview_return_annotation_brackets_string.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable # Long string example def frobnicate() -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": pass diff --git a/tests/util.py b/tests/util.py index 9ea30e62fe3..39837de20e4 100644 --- a/tests/util.py +++ b/tests/util.py @@ -112,16 +112,20 @@ def assert_format( # For both preview and non-preview tests, ensure that Black doesn't crash on # this code, but don't pass "expected" because the precise output may differ. try: + if mode.unstable: + new_mode = replace(mode, unstable=False, preview=False) + else: + new_mode = replace(mode, preview=not mode.preview) _assert_format_inner( source, None, - replace(mode, preview=not mode.preview), + new_mode, fast=fast, minimum_version=minimum_version, lines=lines, ) except Exception as e: - text = "non-preview" if mode.preview else "preview" + text = "unstable" if mode.unstable else "non-preview" if mode.preview else "preview" raise FormatFailure( f"Black crashed formatting this case in {text} mode." ) from e @@ -138,7 +142,7 @@ def assert_format( _assert_format_inner( source, None, - replace(mode, preview=preview_mode, line_length=1), + replace(mode, preview=preview_mode, line_length=1, unstable=False), fast=fast, minimum_version=minimum_version, lines=lines, @@ -241,6 +245,7 @@ def get_flags_parser() -> argparse.ArgumentParser: "--skip-magic-trailing-comma", default=False, action="store_true" ) parser.add_argument("--preview", default=False, action="store_true") + parser.add_argument("--unstable", default=False, action="store_true") parser.add_argument("--fast", default=False, action="store_true") parser.add_argument( "--minimum-version", @@ -278,6 +283,7 @@ def parse_mode(flags_line: str) -> TestCaseArgs: is_ipynb=args.ipynb, magic_trailing_comma=not args.skip_magic_trailing_comma, preview=args.preview, + unstable=args.unstable, ) if args.line_ranges: lines = parse_line_ranges(args.line_ranges) From 6238e38228eb7b161dec05011bfc796c5cf1be42 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 20:23:58 -0800 Subject: [PATCH 11/43] self --- tests/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/util.py b/tests/util.py index 39837de20e4..d5425f1f743 100644 --- a/tests/util.py +++ b/tests/util.py @@ -125,7 +125,11 @@ def assert_format( lines=lines, ) except Exception as e: - text = "unstable" if mode.unstable else "non-preview" if mode.preview else "preview" + text = ( + "unstable" + if mode.unstable + else "non-preview" if mode.preview else "preview" + ) raise FormatFailure( f"Black crashed formatting this case in {text} mode." ) from e From 47221c9f0f12757c7a36e9c5d493949530f95550 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 21:13:33 -0800 Subject: [PATCH 12/43] fine --- src/black/mode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/black/mode.py b/src/black/mode.py index 35fa481eb70..401f2344e7e 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -236,6 +236,7 @@ def __contains__(self, feature: Preview) -> bool: except those in UNSTABLE_FEATURES are enabled. For legacy reasons, the string_processing feature has its own flag, which is deprecated. """ + return False if self.unstable: return True if feature is Preview.string_processing and self.experimental_string_processing: From 6e8871b0ce271cabb7de717926973c14ac541eaf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 13:23:26 -0800 Subject: [PATCH 13/43] Fix the worst issues --- src/black/linegen.py | 4 ++-- src/black/lines.py | 6 ++---- src/black/nodes.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index c2be30497ed..1745b957731 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -280,7 +280,7 @@ def visit_match_case(self, node: Node) -> Iterator[Line]: def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" - if is_stub_suite(node, self.mode): + if is_stub_suite(node): yield from self.visit(node.children[2]) else: yield from self.visit_default(node) @@ -303,7 +303,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: yield from self.line(-1) else: - if not node.parent or not is_stub_suite(node.parent, self.mode): + if not node.parent or not is_stub_suite(node.parent): yield from self.line() yield from self.visit_default(node) diff --git a/src/black/lines.py b/src/black/lines.py index 1046ff18f06..4bed73ccc78 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -828,16 +828,14 @@ def is_line_short_enough( # noqa: C901 if not line_str: line_str = line_to_string(line) - width = str_width if Preview.respect_east_asian_width in mode else len - if line.contains_standalone_comments(): return False if "\n" not in line_str: # No multiline strings (MLS) present - return width(line_str) <= mode.line_length + return str_width(line_str) <= mode.line_length first, *_, last = line_str.split("\n") - if width(first) > mode.line_length or width(last) > mode.line_length: + if str_width(first) > mode.line_length or str_width(last) > mode.line_length: return False # Traverse the AST to examine the context of the multiline string (MLS), diff --git a/src/black/nodes.py b/src/black/nodes.py index 388d1eea045..da53fa218c6 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -735,10 +735,10 @@ def is_funcdef(node: Node) -> bool: return node.type == syms.funcdef -def is_stub_suite(node: Node, mode: Mode) -> bool: +def is_stub_suite(node: Node) -> bool: """Return True if `node` is a suite with a stub body.""" if node.parent is not None: - if Preview.dummy_implementations in mode and node.parent.type not in ( + if node.parent.type not in ( syms.funcdef, syms.async_funcdef, syms.classdef, From 10f6449e3d514eb2037f9d5cadddc6eb049477aa Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 13:24:35 -0800 Subject: [PATCH 14/43] fix some tests --- .../attribute_access_on_number_literals.py | 3 ++- .../cases/parenthesized_context_managers.py | 18 ++++++++++++------ tests/data/cases/remove_with_brackets.py | 12 ++++++++---- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/data/cases/attribute_access_on_number_literals.py b/tests/data/cases/attribute_access_on_number_literals.py index 1b4dbbc2907..7c16bdfb3a5 100644 --- a/tests/data/cases/attribute_access_on_number_literals.py +++ b/tests/data/cases/attribute_access_on_number_literals.py @@ -40,7 +40,8 @@ x = (0.000000006).hex() x = -100.0000j -if (10).real: ... +if (10).real: + ... y = 100[no] y = 100(no) diff --git a/tests/data/cases/parenthesized_context_managers.py b/tests/data/cases/parenthesized_context_managers.py index 538eff7e8aa..16645a18baa 100644 --- a/tests/data/cases/parenthesized_context_managers.py +++ b/tests/data/cases/parenthesized_context_managers.py @@ -23,18 +23,24 @@ # output -with CtxManager() as example: ... +with CtxManager() as example: + ... -with CtxManager1(), CtxManager2(): ... +with CtxManager1(), CtxManager2(): + ... -with CtxManager1() as example, CtxManager2(): ... +with CtxManager1() as example, CtxManager2(): + ... -with CtxManager1(), CtxManager2() as example: ... +with CtxManager1(), CtxManager2() as example: + ... -with CtxManager1() as example1, CtxManager2() as example2: ... +with CtxManager1() as example1, CtxManager2() as example2: + ... with ( CtxManager1() as example1, CtxManager2() as example2, CtxManager3() as example3, -): ... +): + ... diff --git a/tests/data/cases/remove_with_brackets.py b/tests/data/cases/remove_with_brackets.py index f90b158a769..3ee64902a30 100644 --- a/tests/data/cases/remove_with_brackets.py +++ b/tests/data/cases/remove_with_brackets.py @@ -75,13 +75,15 @@ with open("bla.txt") as f, open("x"): pass -with CtxManager1() as example1, CtxManager2() as example2: ... +with CtxManager1() as example1, CtxManager2() as example2: + ... # Brackets remain when using magic comma with ( CtxManager1() as example1, CtxManager2() as example2, -): ... +): + ... # Brackets remain for multi-line context managers with ( @@ -90,7 +92,8 @@ CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, -): ... +): + ... # Don't touch assignment expressions with (y := open("./test.py")) as f: @@ -113,4 +116,5 @@ with open("bla.txt") as f: pass -with CtxManager1() as example1, CtxManager2() as example2: ... +with CtxManager1() as example1, CtxManager2() as example2: + ... From c27daf6a8ff27aafc8ae037b85711a695ffc9926 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 13:27:23 -0800 Subject: [PATCH 15/43] Update tests --- tests/data/cases/expression.diff | 34 +++++++------------ .../expression_skip_magic_trailing_comma.diff | 34 +++++++------------ 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/tests/data/cases/expression.diff b/tests/data/cases/expression.diff index 2eaaeb479f8..28a8d5ea74b 100644 --- a/tests/data/cases/expression.diff +++ b/tests/data/cases/expression.diff @@ -57,8 +57,8 @@ + if (1 if super_long_test_name else 2) + else (str or bytes or None) +) -+{"2.7": dead, "3.7": (long_live or die_hard)} -+{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} ++{"2.7": dead, "3.7": long_live or die_hard} ++{"2.7": dead, "3.7": long_live or die_hard, **{"3.6": verygood}} {**a, **b, **c} -{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')} -({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None @@ -183,7 +183,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -137,118 +173,199 @@ +@@ -137,118 +173,193 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -283,30 +283,22 @@ -assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message" -assert(((parens is TooMany))) -for x, in (1,), (2,), (3,): ... --for y in (): ... --for z in (i for i in (1, 2, 3)): ... --for i in (call()): ... --for j in (1 + (2 + 3)): ... --while(this and that): ... --for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): +print(*lambda x: x) +assert not Test, "Short message" +assert this is ComplexTest and not requirements.fit_in_a_single_line( + force=False +), "Short message" +assert parens is TooMany -+for (x,) in (1,), (2,), (3,): -+ ... -+for y in (): -+ ... -+for z in (i for i in (1, 2, 3)): -+ ... -+for i in call(): -+ ... -+for j in 1 + (2 + 3): -+ ... -+while this and that: -+ ... ++for (x,) in (1,), (2,), (3,): ... + for y in (): ... + for z in (i for i in (1, 2, 3)): ... +-for i in (call()): ... +-for j in (1 + (2 + 3)): ... +-while(this and that): ... +-for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): ++for i in call(): ... ++for j in 1 + (2 + 3): ... ++while this and that: ... +for ( + addr_family, + addr_type, diff --git a/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff index d17467b15c7..d20ad0da503 100644 --- a/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff +++ b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff @@ -57,8 +57,8 @@ + if (1 if super_long_test_name else 2) + else (str or bytes or None) +) -+{"2.7": dead, "3.7": (long_live or die_hard)} -+{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} ++{"2.7": dead, "3.7": long_live or die_hard} ++{"2.7": dead, "3.7": long_live or die_hard, **{"3.6": verygood}} {**a, **b, **c} -{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')} -({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None @@ -167,7 +167,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -137,118 +156,197 @@ +@@ -137,118 +156,191 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -265,30 +265,22 @@ -assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message" -assert(((parens is TooMany))) -for x, in (1,), (2,), (3,): ... --for y in (): ... --for z in (i for i in (1, 2, 3)): ... --for i in (call()): ... --for j in (1 + (2 + 3)): ... --while(this and that): ... --for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): +print(*lambda x: x) +assert not Test, "Short message" +assert this is ComplexTest and not requirements.fit_in_a_single_line( + force=False +), "Short message" +assert parens is TooMany -+for (x,) in (1,), (2,), (3,): -+ ... -+for y in (): -+ ... -+for z in (i for i in (1, 2, 3)): -+ ... -+for i in call(): -+ ... -+for j in 1 + (2 + 3): -+ ... -+while this and that: -+ ... ++for (x,) in (1,), (2,), (3,): ... + for y in (): ... + for z in (i for i in (1, 2, 3)): ... +-for i in (call()): ... +-for j in (1 + (2 + 3)): ... +-while(this and that): ... +-for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): ++for i in call(): ... ++for j in 1 + (2 + 3): ... ++while this and that: ... +for ( + addr_family, + addr_type, From dc2d1046eecefa34bccad6ec116d021064ba4324 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 13:39:36 -0800 Subject: [PATCH 16/43] Fix another dummy impl case --- src/black/linegen.py | 6 ++-- src/black/nodes.py | 13 ++++---- tests/data/cases/expression.py | 18 +++++++---- tests/data/cases/function.py | 3 +- tests/data/cases/pattern_matching_extras.py | 6 ++-- .../expression_skip_magic_trailing_comma.diff | 30 ++++++++++++------- 6 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 1745b957731..4760c4decba 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -42,6 +42,7 @@ is_atom_with_invisible_parens, is_docstring, is_empty_tuple, + is_function_or_class, is_lpar_token, is_multiline_string, is_name_token, @@ -293,9 +294,8 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: wrap_in_parentheses(node, child, visible=False) prev_type = child.type - is_suite_like = node.parent and node.parent.type in STATEMENT - if is_suite_like: - if is_stub_body(node): + if node.parent and node.parent.type in STATEMENT: + if is_stub_body(node) and is_function_or_class(node.parent): yield from self.visit_default(node) else: yield from self.line(+1) diff --git a/src/black/nodes.py b/src/black/nodes.py index da53fa218c6..36b2c5bffac 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -735,15 +735,14 @@ def is_funcdef(node: Node) -> bool: return node.type == syms.funcdef +def is_function_or_class(node: Node) -> bool: + return node.type in {syms.funcdef, syms.classdef, syms.async_funcdef} + + def is_stub_suite(node: Node) -> bool: """Return True if `node` is a suite with a stub body.""" - if node.parent is not None: - if node.parent.type not in ( - syms.funcdef, - syms.async_funcdef, - syms.classdef, - ): - return False + if node.parent is not None and not is_function_or_class(node.parent): + return False # If there is a comment, we want to keep it. if node.prefix.strip(): diff --git a/tests/data/cases/expression.py b/tests/data/cases/expression.py index 613d2d36605..761b33cfd9f 100644 --- a/tests/data/cases/expression.py +++ b/tests/data/cases/expression.py @@ -514,12 +514,18 @@ async def f(): force=False ), "Short message" assert parens is TooMany -for (x,) in (1,), (2,), (3,): ... -for y in (): ... -for z in (i for i in (1, 2, 3)): ... -for i in call(): ... -for j in 1 + (2 + 3): ... -while this and that: ... +for (x,) in (1,), (2,), (3,): + ... +for y in (): + ... +for z in (i for i in (1, 2, 3)): + ... +for i in call(): + ... +for j in 1 + (2 + 3): + ... +while this and that: + ... for ( addr_family, addr_type, diff --git a/tests/data/cases/function.py b/tests/data/cases/function.py index 8aba756a919..4e3f91fd8b1 100644 --- a/tests/data/cases/function.py +++ b/tests/data/cases/function.py @@ -114,7 +114,8 @@ def func_no_args(): c if True: raise RuntimeError - if False: ... + if False: + ... for i in range(10): print(i) continue diff --git a/tests/data/cases/pattern_matching_extras.py b/tests/data/cases/pattern_matching_extras.py index df6ef4b1ab1..1aef8f16b5a 100644 --- a/tests/data/cases/pattern_matching_extras.py +++ b/tests/data/cases/pattern_matching_extras.py @@ -24,8 +24,10 @@ def func(match: case, case: match) -> case: match Something(): - case func(match, case): ... - case another: ... + case func(match, case): + ... + case another: + ... match a, *b, c: diff --git a/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff index d20ad0da503..8d0f1cee146 100644 --- a/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff +++ b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff @@ -167,7 +167,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -137,118 +156,191 @@ +@@ -137,118 +156,197 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -265,22 +265,30 @@ -assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message" -assert(((parens is TooMany))) -for x, in (1,), (2,), (3,): ... +-for y in (): ... +-for z in (i for i in (1, 2, 3)): ... +-for i in (call()): ... +-for j in (1 + (2 + 3)): ... +-while(this and that): ... +-for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): +print(*lambda x: x) +assert not Test, "Short message" +assert this is ComplexTest and not requirements.fit_in_a_single_line( + force=False +), "Short message" +assert parens is TooMany -+for (x,) in (1,), (2,), (3,): ... - for y in (): ... - for z in (i for i in (1, 2, 3)): ... --for i in (call()): ... --for j in (1 + (2 + 3)): ... --while(this and that): ... --for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): -+for i in call(): ... -+for j in 1 + (2 + 3): ... -+while this and that: ... ++for (x,) in (1,), (2,), (3,): ++ ... ++for y in (): ++ ... ++for z in (i for i in (1, 2, 3)): ++ ... ++for i in call(): ++ ... ++for j in 1 + (2 + 3): ++ ... ++while this and that: ++ ... +for ( + addr_family, + addr_type, From b073cbd37b2a380408a62edf8760ebee4a42be3d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 13:40:48 -0800 Subject: [PATCH 17/43] fix some more --- tests/data/cases/expression.diff | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/data/cases/expression.diff b/tests/data/cases/expression.diff index 28a8d5ea74b..ec110a9c9ad 100644 --- a/tests/data/cases/expression.diff +++ b/tests/data/cases/expression.diff @@ -183,7 +183,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -137,118 +173,193 @@ +@@ -137,118 +173,199 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -283,22 +283,30 @@ -assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message" -assert(((parens is TooMany))) -for x, in (1,), (2,), (3,): ... +-for y in (): ... +-for z in (i for i in (1, 2, 3)): ... +-for i in (call()): ... +-for j in (1 + (2 + 3)): ... +-while(this and that): ... +-for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): +print(*lambda x: x) +assert not Test, "Short message" +assert this is ComplexTest and not requirements.fit_in_a_single_line( + force=False +), "Short message" +assert parens is TooMany -+for (x,) in (1,), (2,), (3,): ... - for y in (): ... - for z in (i for i in (1, 2, 3)): ... --for i in (call()): ... --for j in (1 + (2 + 3)): ... --while(this and that): ... --for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): -+for i in call(): ... -+for j in 1 + (2 + 3): ... -+while this and that: ... ++for (x,) in (1,), (2,), (3,): ++ ... ++for y in (): ++ ... ++for z in (i for i in (1, 2, 3)): ++ ... ++for i in call(): ++ ... ++for j in 1 + (2 + 3): ++ ... ++while this and that: ++ ... +for ( + addr_family, + addr_type, From acb8c7c09d4a7bfd8d237666660d446e73f6e6e0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 14:45:32 -0800 Subject: [PATCH 18/43] unused imports --- src/black/comments.py | 2 +- src/black/lines.py | 2 +- src/black/nodes.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/black/comments.py b/src/black/comments.py index e0cd31acad8..ffd97055c13 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -3,7 +3,7 @@ from functools import lru_cache from typing import Collection, Final, Iterator, List, Optional, Tuple, Union -from black.mode import Mode, Preview +from black.mode import Mode from black.nodes import ( CLOSING_BRACKETS, STANDALONE_COMMENT, diff --git a/src/black/lines.py b/src/black/lines.py index 4bed73ccc78..cb8a708121b 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -15,7 +15,7 @@ ) from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, BracketTracker -from black.mode import Mode, Preview +from black.mode import Mode from black.nodes import ( BRACKETS, CLOSING_BRACKETS, diff --git a/src/black/nodes.py b/src/black/nodes.py index 36b2c5bffac..60a46118ecc 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -13,7 +13,7 @@ from mypy_extensions import mypyc_attr from black.cache import CACHE_DIR -from black.mode import Mode, Preview +from black.mode import Mode from black.strings import get_string_prefix, has_triple_quotes from blib2to3 import pygram from blib2to3.pgen2 import token From 27c8a3440d7518f4f0e144f673114124bee20fba Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 20:58:56 -0800 Subject: [PATCH 19/43] Fix feature detection for parenthesized CMs --- src/black/__init__.py | 18 +++++- tests/test_black.py | 130 ++++++++++++++++++++++++------------------ 2 files changed, 91 insertions(+), 57 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 5073fa748d5..735ba713b8f 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1351,7 +1351,7 @@ def get_features_used( # noqa: C901 if ( len(atom_children) == 3 and atom_children[0].type == token.LPAR - and atom_children[1].type == syms.testlist_gexp + and _contains_asexpr(atom_children[1]) and atom_children[2].type == token.RPAR ): features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) @@ -1384,6 +1384,22 @@ def get_features_used( # noqa: C901 return features +def _contains_asexpr(node: Union[Node, Leaf]) -> bool: + """Return True if `node` contains an as-pattern.""" + if node.type == syms.asexpr_test: + return True + elif node.type == syms.atom: + if ( + len(node.children) == 3 + and node.children[0].type == token.LPAR + and node.children[2].type == token.RPAR + ): + return _contains_asexpr(node.children[1]) + elif node.type == syms.testlist_gexp: + return any(_contains_asexpr(child) for child in node.children) + return False + + def detect_target_versions( node: Node, *, future_imports: Optional[Set[str]] = None ) -> Set[TargetVersion]: diff --git a/tests/test_black.py b/tests/test_black.py index 23815da9042..0af5fd2a1f4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -25,6 +25,7 @@ List, Optional, Sequence, + Set, Type, TypeVar, Union, @@ -874,71 +875,88 @@ def test_get_features_used_decorator(self) -> None: ) def test_get_features_used(self) -> None: - node = black.lib2to3_parse("def f(*, arg): ...\n") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def f(*, arg,): ...\n") - self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF}) - node = black.lib2to3_parse("f(*arg,)\n") - self.assertEqual( - black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL} + self.check_features_used("def f(*, arg): ...\n", set()) + self.check_features_used( + "def f(*, arg,): ...\n", {Feature.TRAILING_COMMA_IN_DEF} ) - node = black.lib2to3_parse("def f(*, arg): f'string'\n") - self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS}) - node = black.lib2to3_parse("123_456\n") - self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES}) - node = black.lib2to3_parse("123456\n") - self.assertEqual(black.get_features_used(node), set()) + self.check_features_used("f(*arg,)\n", {Feature.TRAILING_COMMA_IN_CALL}) + self.check_features_used("def f(*, arg): f'string'\n", {Feature.F_STRINGS}) + self.check_features_used("123_456\n", {Feature.NUMERIC_UNDERSCORES}) + self.check_features_used("123456\n", set()) + source, expected = read_data("cases", "function") - node = black.lib2to3_parse(source) expected_features = { Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, Feature.F_STRINGS, } - self.assertEqual(black.get_features_used(node), expected_features) - node = black.lib2to3_parse(expected) - self.assertEqual(black.get_features_used(node), expected_features) + self.check_features_used(source, expected_features) + self.check_features_used(expected, expected_features) + source, expected = read_data("cases", "expression") - node = black.lib2to3_parse(source) - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse(expected) - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("lambda a, /, b: ...") - self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) - node = black.lib2to3_parse("def fn(a, /, b): ...") - self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) - node = black.lib2to3_parse("def fn(): yield a, b") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def fn(): return a, b") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def fn(): yield *b, c") - self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) - node = black.lib2to3_parse("def fn(): return a, *b, c") - self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) - node = black.lib2to3_parse("x = a, *b, c") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = regular") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = (regular, regular)") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c") - self.assertEqual( - black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS} + self.check_features_used(source, set()) + self.check_features_used(expected, set()) + + self.check_features_used("lambda a, /, b: ...\n", {Feature.POS_ONLY_ARGUMENTS}) + self.check_features_used("def fn(a, /, b): ...", {Feature.POS_ONLY_ARGUMENTS}) + + self.check_features_used("def fn(): yield a, b", set()) + self.check_features_used("def fn(): return a, b", set()) + self.check_features_used("def fn(): yield *b, c", {Feature.UNPACKING_ON_FLOW}) + self.check_features_used( + "def fn(): return a, *b, c", {Feature.UNPACKING_ON_FLOW} ) - node = black.lib2to3_parse("try: pass\nexcept Something: pass") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("try: pass\nexcept *Group: pass") - self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR}) - node = black.lib2to3_parse("a[*b]") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) - node = black.lib2to3_parse("a[x, *y(), z] = t") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) - node = black.lib2to3_parse("def fn(*args: *T): pass") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) + self.check_features_used("x = a, *b, c", set()) + + self.check_features_used("x: Any = regular", set()) + self.check_features_used("x: Any = (regular, regular)", set()) + self.check_features_used("x: Any = Complex(Type(1))[something]", set()) + self.check_features_used( + "x: Tuple[int, ...] = a, b, c", {Feature.ANN_ASSIGN_EXTENDED_RHS} + ) + + self.check_features_used("try: pass\nexcept Something: pass", set()) + self.check_features_used("try: pass\nexcept (*Something,): pass", set()) + self.check_features_used( + "try: pass\nexcept *Group: pass", {Feature.EXCEPT_STAR} + ) + + self.check_features_used("a[*b]", {Feature.VARIADIC_GENERICS}) + self.check_features_used("a[x, *y(), z] = t", {Feature.VARIADIC_GENERICS}) + self.check_features_used("def fn(*args: *T): pass", {Feature.VARIADIC_GENERICS}) + + self.check_features_used("with a: pass", set()) + self.check_features_used("with a, b: pass", set()) + self.check_features_used("with a as b: pass", set()) + self.check_features_used("with a as b, c as d: pass", set()) + self.check_features_used("with (a): pass", set()) + self.check_features_used("with (a, b): pass", set()) + self.check_features_used("with (a, b) as (c, d): pass", set()) + self.check_features_used( + "with (a as b): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with ((a as b)): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with (a, b as c): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with (a, (b as c)): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with ((a, ((b as c)))): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + + def check_features_used(self, source: str, expected: Set[Feature]) -> None: + node = black.lib2to3_parse(source) + actual = black.get_features_used(node) + msg = f"Expected {expected} but got {actual} for {source!r}" + try: + self.assertEqual(actual, expected, msg=msg) + except AssertionError: + DebugVisitor.show(node) + raise def test_get_features_used_for_future_flags(self) -> None: for src, features in [ From 0302e8dd68fefec5f862e0aaf75eb254d8fbcc42 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 21:33:56 -0800 Subject: [PATCH 20/43] changelog --- CHANGES.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index dcf6613b70c..8352615659e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,42 @@ +A draft for the 2024 stable style (#4064), stabilizing the following changes: + +- Multiline strings passed as the sole function arguments are formatted more compactly + (#1879) +- Dummy class and function implementations consisting only of `...` are formatted more + compactly (#3796) +- If an assignment statement is too long, we now prefer splitting on the right-hand side + (#3368) +- Hex codes in Unicode escape sequences are now standardized to lowercase (#2916) +- Allow empty first lines at the beginning of most blocks (#3967, #4061) +- Add parentheses around long type annotations (#3899) +- Standardize on a single newline after module docstrings (#3932) +- Fix incorrect magic trailing comma handling in return types (#3916) +- Remove blank lines before class docstrings (#3692) +- Wrap multiple context managers in parentheses if combined in a single `with` statement + (#3489) +- Fix bug in line length calculations for power operations (#3942) +- Add trailing commas to collection literals even if there's a comment after the last + entry (#3393) +- When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from + subscript expressions with more than 1 element (#3209) +- Add extra blank lines in stubs in a few cases (#3564, #3862) +- Accept raw strings as docstrings (#3947) +- Split long lines in case blocks (#4024) +- Stop removing spaces from walrus operators within subscripts (#3823) +- Fix incorrect formatting of certain async statements (#3609) +- Allow combining `# fmt: skip` with other comments (#3959) + +The following two changes may be included, but have outstanding issues that will need to +be resolved: + +- 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) +- Add parentheses around `if`-`else` expressions (#2278) + ### Stable style - Fix bug where `# fmt: off` automatically dedents when used with the `--line-ranges` From 7f7eb72432e3bfed29cd9421a4817a7b6fa60e42 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 4 Jan 2024 03:42:06 -0800 Subject: [PATCH 21/43] Fix up --- src/black/lines.py | 20 +++++-------------- src/black/nodes.py | 8 +++----- .../cases/no_blank_line_before_docstring.py | 1 + tests/data/cases/pep_572_remove_parens.py | 2 +- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index ed1af219f90..4a3d25ea16e 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -15,7 +15,7 @@ ) from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, BracketTracker -from black.mode import Mode +from black.mode import Mode, Preview from black.nodes import ( BRACKETS, CLOSING_BRACKETS, @@ -448,14 +448,8 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: if subscript_start.type == syms.subscriptlist: subscript_start = child_towards(subscript_start, leaf) - # When this is moved out of preview, add syms.namedexpr_test directly to - # TEST_DESCENDANTS in nodes.py - if Preview.walrus_subscript in self.mode: - test_decendants = TEST_DESCENDANTS | {syms.namedexpr_test} - else: - test_decendants = TEST_DESCENDANTS return subscript_start is not None and any( - n.type in test_decendants for n in subscript_start.pre_order() + n.type in TEST_DESCENDANTS for n in subscript_start.pre_order() ) def enumerate_with_length( @@ -636,10 +630,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if previous_def is not None: assert self.previous_line is not None if self.mode.is_pyi: - if ( - previous_def.is_class - and not previous_def.is_stub_class - ): + if previous_def.is_class and not previous_def.is_stub_class: before = 1 elif depth and not current_line.is_def and self.previous_line.is_def: # Empty lines between attributes and methods should be preserved. @@ -694,9 +685,8 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: # In preview mode, always allow blank lines, except right before a function # docstring - is_empty_first_line_ok = ( - not current_line.is_docstring - or (self.previous_line and not self.previous_line.is_def) + is_empty_first_line_ok = not current_line.is_docstring or ( + self.previous_line and not self.previous_line.is_def ) if ( diff --git a/src/black/nodes.py b/src/black/nodes.py index a62fc768159..a8869cba234 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -13,7 +13,7 @@ from mypy_extensions import mypyc_attr from black.cache import CACHE_DIR -from black.mode import Mode +from black.mode import Mode, Preview from black.strings import get_string_prefix, has_triple_quotes from blib2to3 import pygram from blib2to3.pgen2 import token @@ -104,6 +104,7 @@ syms.trailer, syms.term, syms.power, + syms.namedexpr_test, } TYPED_NAMES: Final = {syms.tname, syms.tname_star} ASSIGNMENTS: Final = { @@ -754,10 +755,7 @@ def is_function_or_class(node: Node) -> bool: def is_stub_suite(node: Node) -> bool: """Return True if `node` is a suite with a stub body.""" - if ( - node.parent is not None - and not is_parent_function_or_class(node) - ): + if node.parent is not None and not is_parent_function_or_class(node): return False # If there is a comment, we want to keep it. diff --git a/tests/data/cases/no_blank_line_before_docstring.py b/tests/data/cases/no_blank_line_before_docstring.py index 74d43cd7eaf..ced125fef78 100644 --- a/tests/data/cases/no_blank_line_before_docstring.py +++ b/tests/data/cases/no_blank_line_before_docstring.py @@ -62,4 +62,5 @@ class MultilineDocstringsAsWell: class SingleQuotedDocstring: + "I'm a docstring but I don't even get triple quotes." diff --git a/tests/data/cases/pep_572_remove_parens.py b/tests/data/cases/pep_572_remove_parens.py index 5e30e710f79..f0026ceb032 100644 --- a/tests/data/cases/pep_572_remove_parens.py +++ b/tests/data/cases/pep_572_remove_parens.py @@ -105,7 +105,7 @@ def foo2(answer: (p := 42) = 5): ... lambda: (x := 1) a[(x := 12)] -a[:(x := 13)] +a[: (x := 13)] # we don't touch expressions in f-strings but if we do one day, don't break 'em f"{(x:=10)}" From 975e7f1917adc5fc7e3d041977f1654a1d54a707 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 16:57:46 -0800 Subject: [PATCH 22/43] wrap_long_dict_values_in_parens and multiline_string_handling back to preview --- src/black/lines.py | 7 +++++++ src/black/mode.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/black/lines.py b/src/black/lines.py index 4a3d25ea16e..29f87137614 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -832,6 +832,13 @@ def is_line_short_enough( # noqa: C901 if not line_str: line_str = line_to_string(line) + if Preview.multiline_string_handling not in mode: + return ( + str_width(line_str) <= mode.line_length + and "\n" not in line_str # multiline strings + and not line.contains_standalone_comments() + ) + if line.contains_standalone_comments(): return False if "\n" not in line_str: diff --git a/src/black/mode.py b/src/black/mode.py index ead8198619e..1b97f3508ee 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -169,9 +169,13 @@ class Preview(Enum): """Individual preview style features.""" hex_codes_in_unicode_sequences = auto() + # NOTE: string_processing requires wrap_long_dict_values_in_parens + # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() hug_parens_with_braces_and_square_brackets = auto() unify_docstring_detection = auto() + wrap_long_dict_values_in_parens = auto() + multiline_string_handling = auto() class Deprecated(UserWarning): From 073ee3f7ee8d3fb7a4c897bdd006de8ffeb55e90 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 17:00:33 -0800 Subject: [PATCH 23/43] bad merge --- src/black/linegen.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 39e47be9d7d..a276805f2fe 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -251,8 +251,6 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: ) else: wrap_in_parentheses(node, child, visible=False) - else: - wrap_in_parentheses(node, child, visible=False) yield from self.visit_default(node) def visit_funcdef(self, node: Node) -> Iterator[Line]: From 8758f3c8c939fe6cf5283a06096d7d00baa63dd0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 17:04:07 -0800 Subject: [PATCH 24/43] fix tests --- tests/data/cases/composition.py | 4 +++- tests/data/cases/composition_no_trailing_comma.py | 4 +++- tests/data/cases/expression.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/data/cases/composition.py b/tests/data/cases/composition.py index 0798d3f3b29..e429f15e669 100644 --- a/tests/data/cases/composition.py +++ b/tests/data/cases/composition.py @@ -161,7 +161,9 @@ def tricky_asserts(self) -> None: 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE - """ % (_C.__init__.__code__.co_firstlineno + 1,) + """ % ( + _C.__init__.__code__.co_firstlineno + 1, + ) assert ( expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect diff --git a/tests/data/cases/composition_no_trailing_comma.py b/tests/data/cases/composition_no_trailing_comma.py index 88d17b743de..f17b89dea8d 100644 --- a/tests/data/cases/composition_no_trailing_comma.py +++ b/tests/data/cases/composition_no_trailing_comma.py @@ -347,7 +347,9 @@ def tricky_asserts(self) -> None: 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE - """ % (_C.__init__.__code__.co_firstlineno + 1,) + """ % ( + _C.__init__.__code__.co_firstlineno + 1, + ) assert ( expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect diff --git a/tests/data/cases/expression.py b/tests/data/cases/expression.py index 761b33cfd9f..06096c589f1 100644 --- a/tests/data/cases/expression.py +++ b/tests/data/cases/expression.py @@ -312,8 +312,8 @@ async def f(): if (1 if super_long_test_name else 2) else (str or bytes or None) ) -{"2.7": dead, "3.7": long_live or die_hard} -{"2.7": dead, "3.7": long_live or die_hard, **{"3.6": verygood}} +{"2.7": dead, "3.7": (long_live or die_hard)} +{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} {**a, **b, **c} {"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} ({"a": "b"}, (True or False), (+value), "string", b"bytes") or None From a9ca9a496cbc524072c8bd792265b3debb120599 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 17:09:30 -0800 Subject: [PATCH 25/43] fix more tests --- tests/data/cases/expression.diff | 4 ++-- .../miscellaneous/expression_skip_magic_trailing_comma.diff | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/data/cases/expression.diff b/tests/data/cases/expression.diff index ec110a9c9ad..2eaaeb479f8 100644 --- a/tests/data/cases/expression.diff +++ b/tests/data/cases/expression.diff @@ -57,8 +57,8 @@ + if (1 if super_long_test_name else 2) + else (str or bytes or None) +) -+{"2.7": dead, "3.7": long_live or die_hard} -+{"2.7": dead, "3.7": long_live or die_hard, **{"3.6": verygood}} ++{"2.7": dead, "3.7": (long_live or die_hard)} ++{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} {**a, **b, **c} -{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')} -({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None diff --git a/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff index 8d0f1cee146..d17467b15c7 100644 --- a/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff +++ b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff @@ -57,8 +57,8 @@ + if (1 if super_long_test_name else 2) + else (str or bytes or None) +) -+{"2.7": dead, "3.7": long_live or die_hard} -+{"2.7": dead, "3.7": long_live or die_hard, **{"3.6": verygood}} ++{"2.7": dead, "3.7": (long_live or die_hard)} ++{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} {**a, **b, **c} -{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')} -({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None From 47449ff135a2f28c131f38b017a19689e18c3b7e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 17:22:50 -0800 Subject: [PATCH 26/43] Remove --preview from some tests --- ..._empty_first_line.py => allow_empty_first_line.py} | 1 - .../cases/{preview_async_stmts.py => async_stmts.py} | 1 - tests/data/cases/conditional_expression.py | 11 ++++++----- ..._context_managers_38.py => context_managers_38.py} | 2 +- ..._context_managers_39.py => context_managers_39.py} | 2 +- ...tect_310.py => context_managers_autodetect_310.py} | 2 +- ...tect_311.py => context_managers_autodetect_311.py} | 2 +- ...detect_38.py => context_managers_autodetect_38.py} | 1 - ...detect_39.py => context_managers_autodetect_39.py} | 2 +- ...my_implementations.py => dummy_implementations.py} | 1 - .../cases/{preview_form_feeds.py => form_feeds.py} | 1 - tests/data/cases/module_docstring_1.py | 1 - tests/data/cases/module_docstring_2.py | 4 ++-- tests/data/cases/module_docstring_3.py | 1 - tests/data/cases/module_docstring_4.py | 1 - .../data/cases/module_docstring_followed_by_class.py | 1 - .../cases/module_docstring_followed_by_function.py | 1 - tests/data/cases/nested_stub.py | 2 +- ...tern_matching_long.py => pattern_matching_long.py} | 2 +- ...ng_comma.py => pattern_matching_trailing_comma.py} | 2 +- tests/data/cases/pep604_union_types_line_breaks.py | 2 +- .../cases/{preview_pep_572.py => pep_572_slices.py} | 1 - ...ew_percent_precedence.py => percent_precedence.py} | 5 ++--- ...w_power_op_spacing.py => power_op_spacing_long.py} | 1 - ...review_prefer_rhs_split.py => prefer_rhs_split.py} | 1 - tests/data/cases/py310_pep572.py | 2 +- tests/data/cases/raw_docstring.py | 2 +- ...on.py => raw_docstring_no_string_normalization.py} | 2 +- ...single_line_format_skip_with_multiple_comments.py} | 1 - .../{preview_trailing_comma.py => trailing_comma.py} | 1 - tests/data/cases/walrus_in_dict.py | 2 ++ 31 files changed, 24 insertions(+), 37 deletions(-) rename tests/data/cases/{preview_allow_empty_first_line.py => allow_empty_first_line.py} (98%) rename tests/data/cases/{preview_async_stmts.py => async_stmts.py} (93%) rename tests/data/cases/{preview_context_managers_38.py => context_managers_38.py} (96%) rename tests/data/cases/{preview_context_managers_39.py => context_managers_39.py} (98%) rename tests/data/cases/{preview_context_managers_autodetect_310.py => context_managers_autodetect_310.py} (93%) rename tests/data/cases/{preview_context_managers_autodetect_311.py => context_managers_autodetect_311.py} (92%) rename tests/data/cases/{preview_context_managers_autodetect_38.py => context_managers_autodetect_38.py} (98%) rename tests/data/cases/{preview_context_managers_autodetect_39.py => context_managers_autodetect_39.py} (93%) rename tests/data/cases/{preview_dummy_implementations.py => dummy_implementations.py} (99%) rename tests/data/cases/{preview_form_feeds.py => form_feeds.py} (99%) rename tests/data/cases/{preview_pattern_matching_long.py => pattern_matching_long.py} (94%) rename tests/data/cases/{preview_pattern_matching_trailing_comma.py => pattern_matching_trailing_comma.py} (92%) rename tests/data/cases/{preview_pep_572.py => pep_572_slices.py} (75%) rename tests/data/cases/{preview_percent_precedence.py => percent_precedence.py} (91%) rename tests/data/cases/{preview_power_op_spacing.py => power_op_spacing_long.py} (99%) rename tests/data/cases/{preview_prefer_rhs_split.py => prefer_rhs_split.py} (99%) rename tests/data/cases/{preview_docstring_no_string_normalization.py => raw_docstring_no_string_normalization.py} (88%) rename tests/data/cases/{preview_single_line_format_skip_with_multiple_comments.py => single_line_format_skip_with_multiple_comments.py} (97%) rename tests/data/cases/{preview_trailing_comma.py => trailing_comma.py} (97%) diff --git a/tests/data/cases/preview_allow_empty_first_line.py b/tests/data/cases/allow_empty_first_line.py similarity index 98% rename from tests/data/cases/preview_allow_empty_first_line.py rename to tests/data/cases/allow_empty_first_line.py index 4269987305d..32a170a97d0 100644 --- a/tests/data/cases/preview_allow_empty_first_line.py +++ b/tests/data/cases/allow_empty_first_line.py @@ -1,4 +1,3 @@ -# flags: --preview def foo(): """ Docstring diff --git a/tests/data/cases/preview_async_stmts.py b/tests/data/cases/async_stmts.py similarity index 93% rename from tests/data/cases/preview_async_stmts.py rename to tests/data/cases/async_stmts.py index 0a7671be5a6..fe9594b2164 100644 --- a/tests/data/cases/preview_async_stmts.py +++ b/tests/data/cases/async_stmts.py @@ -1,4 +1,3 @@ -# flags: --preview async def func() -> (int): return 0 diff --git a/tests/data/cases/conditional_expression.py b/tests/data/cases/conditional_expression.py index 76251bd9318..f65d6fb00e7 100644 --- a/tests/data/cases/conditional_expression.py +++ b/tests/data/cases/conditional_expression.py @@ -1,4 +1,3 @@ -# flags: --preview 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, @@ -197,7 +196,9 @@ def foo(wait: bool = True): time.sleep(1) if wait else None -a = "".join(( - "", # comment - "" if True else "", -)) +a = "".join( + ( + "", # comment + "" if True else "", + ) +) diff --git a/tests/data/cases/preview_context_managers_38.py b/tests/data/cases/context_managers_38.py similarity index 96% rename from tests/data/cases/preview_context_managers_38.py rename to tests/data/cases/context_managers_38.py index 719d94fdcc5..54fb97c708b 100644 --- a/tests/data/cases/preview_context_managers_38.py +++ b/tests/data/cases/context_managers_38.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.8 +# flags: --minimum-version=3.8 with \ make_context_manager1() as cm1, \ make_context_manager2() as cm2, \ diff --git a/tests/data/cases/preview_context_managers_39.py b/tests/data/cases/context_managers_39.py similarity index 98% rename from tests/data/cases/preview_context_managers_39.py rename to tests/data/cases/context_managers_39.py index 589e00ad187..60fd1a56409 100644 --- a/tests/data/cases/preview_context_managers_39.py +++ b/tests/data/cases/context_managers_39.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.9 +# flags: --minimum-version=3.9 with \ make_context_manager1() as cm1, \ make_context_manager2() as cm2, \ diff --git a/tests/data/cases/preview_context_managers_autodetect_310.py b/tests/data/cases/context_managers_autodetect_310.py similarity index 93% rename from tests/data/cases/preview_context_managers_autodetect_310.py rename to tests/data/cases/context_managers_autodetect_310.py index a9e31076f03..80f211032e5 100644 --- a/tests/data/cases/preview_context_managers_autodetect_310.py +++ b/tests/data/cases/context_managers_autodetect_310.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 # This file uses pattern matching introduced in Python 3.10. diff --git a/tests/data/cases/preview_context_managers_autodetect_311.py b/tests/data/cases/context_managers_autodetect_311.py similarity index 92% rename from tests/data/cases/preview_context_managers_autodetect_311.py rename to tests/data/cases/context_managers_autodetect_311.py index af1e83fe74c..020c4cea967 100644 --- a/tests/data/cases/preview_context_managers_autodetect_311.py +++ b/tests/data/cases/context_managers_autodetect_311.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.11 +# flags: --minimum-version=3.11 # This file uses except* clause in Python 3.11. diff --git a/tests/data/cases/preview_context_managers_autodetect_38.py b/tests/data/cases/context_managers_autodetect_38.py similarity index 98% rename from tests/data/cases/preview_context_managers_autodetect_38.py rename to tests/data/cases/context_managers_autodetect_38.py index 25217a40604..79e438b995e 100644 --- a/tests/data/cases/preview_context_managers_autodetect_38.py +++ b/tests/data/cases/context_managers_autodetect_38.py @@ -1,4 +1,3 @@ -# flags: --preview # This file doesn't use any Python 3.9+ only grammars. diff --git a/tests/data/cases/preview_context_managers_autodetect_39.py b/tests/data/cases/context_managers_autodetect_39.py similarity index 93% rename from tests/data/cases/preview_context_managers_autodetect_39.py rename to tests/data/cases/context_managers_autodetect_39.py index 3f72e48db9d..98e674b2f9d 100644 --- a/tests/data/cases/preview_context_managers_autodetect_39.py +++ b/tests/data/cases/context_managers_autodetect_39.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.9 +# flags: --minimum-version=3.9 # This file uses parenthesized context managers introduced in Python 3.9. diff --git a/tests/data/cases/preview_dummy_implementations.py b/tests/data/cases/dummy_implementations.py similarity index 99% rename from tests/data/cases/preview_dummy_implementations.py rename to tests/data/cases/dummy_implementations.py index 3cd392c9587..0a52c081bcc 100644 --- a/tests/data/cases/preview_dummy_implementations.py +++ b/tests/data/cases/dummy_implementations.py @@ -1,4 +1,3 @@ -# flags: --preview from typing import NoReturn, Protocol, Union, overload class Empty: diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/form_feeds.py similarity index 99% rename from tests/data/cases/preview_form_feeds.py rename to tests/data/cases/form_feeds.py index dc3bd6cfe2e..48ffc98106b 100644 --- a/tests/data/cases/preview_form_feeds.py +++ b/tests/data/cases/form_feeds.py @@ -1,4 +1,3 @@ -# flags: --preview # Warning! This file contains form feeds (ASCII 0x0C, often represented by \f or ^L). diff --git a/tests/data/cases/module_docstring_1.py b/tests/data/cases/module_docstring_1.py index d5897b4db60..5751154f7f0 100644 --- a/tests/data/cases/module_docstring_1.py +++ b/tests/data/cases/module_docstring_1.py @@ -1,4 +1,3 @@ -# flags: --preview """Single line module-level docstring should be followed by single newline.""" diff --git a/tests/data/cases/module_docstring_2.py b/tests/data/cases/module_docstring_2.py index 1cc9aea9aea..ac486096c02 100644 --- a/tests/data/cases/module_docstring_2.py +++ b/tests/data/cases/module_docstring_2.py @@ -1,7 +1,7 @@ # flags: --preview """I am a very helpful module docstring. -With trailing spaces: +With trailing spaces (only removed with unify_docstring_detection on): Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, @@ -39,7 +39,7 @@ # output """I am a very helpful module docstring. -With trailing spaces: +With trailing spaces (only removed with unify_docstring_detection on): Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, diff --git a/tests/data/cases/module_docstring_3.py b/tests/data/cases/module_docstring_3.py index 0631e136a3d..3d0058dd554 100644 --- a/tests/data/cases/module_docstring_3.py +++ b/tests/data/cases/module_docstring_3.py @@ -1,4 +1,3 @@ -# flags: --preview """Single line module-level docstring should be followed by single newline.""" a = 1 diff --git a/tests/data/cases/module_docstring_4.py b/tests/data/cases/module_docstring_4.py index 515174dcc04..b1720078f71 100644 --- a/tests/data/cases/module_docstring_4.py +++ b/tests/data/cases/module_docstring_4.py @@ -1,4 +1,3 @@ -# flags: --preview """Single line module-level docstring should be followed by single newline.""" a = 1 diff --git a/tests/data/cases/module_docstring_followed_by_class.py b/tests/data/cases/module_docstring_followed_by_class.py index 6fdbfc8c240..c291e61b960 100644 --- a/tests/data/cases/module_docstring_followed_by_class.py +++ b/tests/data/cases/module_docstring_followed_by_class.py @@ -1,4 +1,3 @@ -# flags: --preview """Two blank lines between module docstring and a class.""" class MyClass: pass diff --git a/tests/data/cases/module_docstring_followed_by_function.py b/tests/data/cases/module_docstring_followed_by_function.py index 5913a59e1fe..fd29b98da8e 100644 --- a/tests/data/cases/module_docstring_followed_by_function.py +++ b/tests/data/cases/module_docstring_followed_by_function.py @@ -1,4 +1,3 @@ -# flags: --preview """Two blank lines between module docstring and a function def.""" def function(): pass diff --git a/tests/data/cases/nested_stub.py b/tests/data/cases/nested_stub.py index ef13c588ce6..40ca11e9330 100644 --- a/tests/data/cases/nested_stub.py +++ b/tests/data/cases/nested_stub.py @@ -1,4 +1,4 @@ -# flags: --pyi --preview +# flags: --pyi import sys class Outer: diff --git a/tests/data/cases/preview_pattern_matching_long.py b/tests/data/cases/pattern_matching_long.py similarity index 94% rename from tests/data/cases/preview_pattern_matching_long.py rename to tests/data/cases/pattern_matching_long.py index df849fdc4f2..9a944c9d0c9 100644 --- a/tests/data/cases/preview_pattern_matching_long.py +++ b/tests/data/cases/pattern_matching_long.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 match x: case "abcd" | "abcd" | "abcd" : pass diff --git a/tests/data/cases/preview_pattern_matching_trailing_comma.py b/tests/data/cases/pattern_matching_trailing_comma.py similarity index 92% rename from tests/data/cases/preview_pattern_matching_trailing_comma.py rename to tests/data/cases/pattern_matching_trailing_comma.py index e6c0d88bb80..5660b0f6a14 100644 --- a/tests/data/cases/preview_pattern_matching_trailing_comma.py +++ b/tests/data/cases/pattern_matching_trailing_comma.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 match maybe, multiple: case perhaps, 5: pass diff --git a/tests/data/cases/pep604_union_types_line_breaks.py b/tests/data/cases/pep604_union_types_line_breaks.py index fee2b840494..745bc9e8b02 100644 --- a/tests/data/cases/pep604_union_types_line_breaks.py +++ b/tests/data/cases/pep604_union_types_line_breaks.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 # This has always worked z= Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong diff --git a/tests/data/cases/preview_pep_572.py b/tests/data/cases/pep_572_slices.py similarity index 75% rename from tests/data/cases/preview_pep_572.py rename to tests/data/cases/pep_572_slices.py index 75ad0cc4176..aa772b1f1f5 100644 --- a/tests/data/cases/preview_pep_572.py +++ b/tests/data/cases/pep_572_slices.py @@ -1,4 +1,3 @@ -# flags: --preview x[(a:=0):] x[:(a:=0)] diff --git a/tests/data/cases/preview_percent_precedence.py b/tests/data/cases/percent_precedence.py similarity index 91% rename from tests/data/cases/preview_percent_precedence.py rename to tests/data/cases/percent_precedence.py index aeaf450ff5e..7822e42c69d 100644 --- a/tests/data/cases/preview_percent_precedence.py +++ b/tests/data/cases/percent_precedence.py @@ -1,4 +1,3 @@ -# flags: --preview ("" % a) ** 2 ("" % a)[0] ("" % a)() @@ -31,9 +30,9 @@ 2 // ("" % a) 2 % ("" % a) +("" % a) -b + "" % a +b + ("" % a) -("" % a) -b - "" % a +b - ("" % a) b + -("" % a) ~("" % a) 2 ** ("" % a) diff --git a/tests/data/cases/preview_power_op_spacing.py b/tests/data/cases/power_op_spacing_long.py similarity index 99% rename from tests/data/cases/preview_power_op_spacing.py rename to tests/data/cases/power_op_spacing_long.py index 650c6fecb20..30e6eb788b3 100644 --- a/tests/data/cases/preview_power_op_spacing.py +++ b/tests/data/cases/power_op_spacing_long.py @@ -1,4 +1,3 @@ -# flags: --preview a = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 b = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 c = 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 diff --git a/tests/data/cases/preview_prefer_rhs_split.py b/tests/data/cases/prefer_rhs_split.py similarity index 99% rename from tests/data/cases/preview_prefer_rhs_split.py rename to tests/data/cases/prefer_rhs_split.py index 28d89c368c0..f3d9fd67251 100644 --- a/tests/data/cases/preview_prefer_rhs_split.py +++ b/tests/data/cases/prefer_rhs_split.py @@ -1,4 +1,3 @@ -# flags: --preview first_item, second_item = ( some_looooooooong_module.some_looooooooooooooong_function_name( first_argument, second_argument, third_argument diff --git a/tests/data/cases/py310_pep572.py b/tests/data/cases/py310_pep572.py index 172be3898d6..73fbe44d42c 100644 --- a/tests/data/cases/py310_pep572.py +++ b/tests/data/cases/py310_pep572.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 x[a:=0] x[a := 0] x[a := 0, b := 1] diff --git a/tests/data/cases/raw_docstring.py b/tests/data/cases/raw_docstring.py index 751fd3201df..7f88bb2de86 100644 --- a/tests/data/cases/raw_docstring.py +++ b/tests/data/cases/raw_docstring.py @@ -1,4 +1,4 @@ -# flags: --preview --skip-string-normalization +# flags: --skip-string-normalization class C: r"""Raw""" diff --git a/tests/data/cases/preview_docstring_no_string_normalization.py b/tests/data/cases/raw_docstring_no_string_normalization.py similarity index 88% rename from tests/data/cases/preview_docstring_no_string_normalization.py rename to tests/data/cases/raw_docstring_no_string_normalization.py index 712c7364f51..a201c1e8fae 100644 --- a/tests/data/cases/preview_docstring_no_string_normalization.py +++ b/tests/data/cases/raw_docstring_no_string_normalization.py @@ -1,4 +1,4 @@ -# flags: --preview --skip-string-normalization +# flags: --skip-string-normalization def do_not_touch_this_prefix(): R"""There was a bug where docstring prefixes would be normalized even with -S.""" diff --git a/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py b/tests/data/cases/single_line_format_skip_with_multiple_comments.py similarity index 97% rename from tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py rename to tests/data/cases/single_line_format_skip_with_multiple_comments.py index efde662baa8..7212740fc42 100644 --- a/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py +++ b/tests/data/cases/single_line_format_skip_with_multiple_comments.py @@ -1,4 +1,3 @@ -# flags: --preview foo = 123 # fmt: skip # noqa: E501 # pylint bar = ( 123 , diff --git a/tests/data/cases/preview_trailing_comma.py b/tests/data/cases/trailing_comma.py similarity index 97% rename from tests/data/cases/preview_trailing_comma.py rename to tests/data/cases/trailing_comma.py index bba7e7ad16d..5b09c664606 100644 --- a/tests/data/cases/preview_trailing_comma.py +++ b/tests/data/cases/trailing_comma.py @@ -1,4 +1,3 @@ -# flags: --preview e = { "a": fun(msg, "ts"), "longggggggggggggggid": ..., diff --git a/tests/data/cases/walrus_in_dict.py b/tests/data/cases/walrus_in_dict.py index c33eecd84a6..c91ad9e8611 100644 --- a/tests/data/cases/walrus_in_dict.py +++ b/tests/data/cases/walrus_in_dict.py @@ -1,7 +1,9 @@ # flags: --preview +# This is testing an issue that is specific to the preview style { "is_update": (up := commit.hash in update_hashes) } # output +# This is testing an issue that is specific to the preview style {"is_update": (up := commit.hash in update_hashes)} From a646358f6f2ef7d4fe460751bf6d2bb808d45db6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 17:55:42 -0800 Subject: [PATCH 27/43] changelog --- CHANGES.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a6d2f46562e..0e2974d706e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,10 +6,10 @@ -A draft for the 2024 stable style (#4064), stabilizing the following changes: +This release introduces the new 2024 stable style (#4106), stabilizing the following +changes: -- Multiline strings passed as the sole function arguments are formatted more compactly - (#1879) +- Add parentheses around `if`-`else` expressions (#2278) - Dummy class and function implementations consisting only of `...` are formatted more compactly (#3796) - If an assignment statement is too long, we now prefer splitting on the right-hand side @@ -34,30 +34,26 @@ A draft for the 2024 stable style (#4064), stabilizing the following changes: - Fix incorrect formatting of certain async statements (#3609) - Allow combining `# fmt: skip` with other comments (#3959) -The following two changes may be included, but have outstanding issues that will need to -be resolved: - -- 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) -- Add parentheses around `if`-`else` expressions (#2278) - ### Stable style -### Preview style - - +Several bug fixes were made in features that are moved to the stable style in this +release: - Fix comment handling when parenthesising conditional expressions (#4134) -- Format module docstrings the same as class and function docstrings (#4095) - Fix bug where spaces were not added around parenthesized walruses in subscripts, unlike other binary operators (#4109) - Remove empty lines before docstrings in async functions (#4132) - Address a missing case in the change to allow empty lines at the beginning of all blocks, except immediately before a docstring (#4130) - For stubs, fix logic to enforce empty line after nested classes with bodies (#4141) + +### Preview style + + + +- Format module docstrings the same as class and function docstrings (#4095) - Fix crash when using a walrus in a dictionary (#4155) - Fix unnecessary parentheses when wrapping long dicts (#4135) From 7836fb9186e1d940d4118cce832ce1407c351792 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 18:57:24 -0800 Subject: [PATCH 28/43] docs, blackd --- CHANGES.md | 9 + docs/faq.md | 7 +- docs/the_black_code_style/current_style.md | 6 + docs/the_black_code_style/future_style.md | 213 +++++++++--------- docs/the_black_code_style/index.md | 8 +- .../black_as_a_server.md | 3 + docs/usage_and_configuration/the_basics.md | 15 +- src/black/__init__.py | 2 +- src/black/mode.py | 6 +- src/blackd/__init__.py | 4 + tests/data/cases/preview_long_dict_values.py | 2 +- 11 files changed, 156 insertions(+), 119 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0e2974d706e..965ae8f6242 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,14 @@ changes: - Fix incorrect formatting of certain async statements (#3609) - Allow combining `# fmt: skip` with other comments (#3959) +There are already a few improvements in the `--preview` style, which are slated for the +2025 stable style. Try them out and +[share your feedback](https://github.com/psf/black/issues). In the past, the preview +style has included some features that we were not able to stabilize. This year, we're +adding a separate `--unstable` style for features with known problems. Now, the +`--preview` style only includes features that we actually expect to make it into next +year's stable style. + ### Stable style @@ -53,6 +61,7 @@ release: +- Add `--unstable` style (#4096) - Format module docstrings the same as class and function docstrings (#4095) - Fix crash when using a walrus in a dictionary (#4155) - Fix unnecessary parentheses when wrapping long dicts (#4135) diff --git a/docs/faq.md b/docs/faq.md index c62e1b504b5..124a096efac 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -41,9 +41,10 @@ other tools, such as `# noqa`, may be moved by _Black_. See below for more detai Stable. _Black_ aims to enforce one style and one style only, with some room for pragmatism. See [The Black Code Style](the_black_code_style/index.md) for more details. -Starting in 2022, the formatting output will be stable for the releases made in the same -year (other than unintentional bugs). It is possible to opt-in to the latest formatting -styles, using the `--preview` flag. +Starting in 2022, the formatting output is stable for the releases made in the same year +(other than unintentional bugs). At the beginning of every year, the first release will +make changes to the stable style. It is possible to opt in to the latest formatting +styles using the `--preview` flag. ## Why is my file not formatted? diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 00bd81416dc..ca5d1d4a701 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -449,6 +449,12 @@ file that are not enforced yet but might be in a future version of the formatter _Black_ will normalize line endings (`\n` or `\r\n`) based on the first line ending of the file. +### Form feed characters + +_Black_ will retain form feed characters on otherwise empty lines at the module level. +Only one form feed is retained for a group of consecutive empty lines. Where there are +two empty lines in a row, the form feed is placed on the second line. + ## Pragmatism Early versions of _Black_ used to be absolutist in some respects. They took after its diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index f55ea5f60a9..daf3b4b7369 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -1,54 +1,5 @@ # The (future of the) Black code style -```{warning} -Changes to this document often aren't tied and don't relate to releases of -_Black_. It's recommended that you read the latest version available. -``` - -## Using backslashes for with statements - -[Backslashes are bad and should be never be used](labels/why-no-backslashes) however -there is one exception: `with` statements using multiple context managers. Before Python -3.9 Python's grammar does not allow organizing parentheses around the series of context -managers. - -We don't want formatting like: - -```py3 -with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - ... # nothing to split on - line too long -``` - -So _Black_ will, when we implement this, format it like this: - -```py3 -with \ - make_context_manager1() as cm1, \ - make_context_manager2() as cm2, \ - make_context_manager3() as cm3, \ - make_context_manager4() as cm4 \ -: - ... # backslashes and an ugly stranded colon -``` - -Although when the target version is Python 3.9 or higher, _Black_ uses parentheses -instead in `--preview` mode (see below) 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 -following way: - -```python -with contextlib.ExitStack() as exit_stack: - cm1 = exit_stack.enter_context(make_context_manager1()) - cm2 = exit_stack.enter_context(make_context_manager2()) - cm3 = exit_stack.enter_context(make_context_manager3()) - cm4 = exit_stack.enter_context(make_context_manager4()) - ... -``` - -(labels/preview-style)= - ## Preview style Experimental, potentially disruptive style changes are gathered under the `--preview` @@ -56,62 +7,32 @@ CLI flag. At the end of each year, these changes may be adopted into the default as described in [The Black Code Style](index.md). Because the functionality is experimental, feedback and issue reports are highly encouraged! -### Improved string processing - -_Black_ will split long string literals and merge short ones. Parentheses are used where -appropriate. When split, parts of f-strings that don't need formatting are converted to -plain strings. User-made splits are respected when they do not exceed the line length -limit. Line continuation backslashes are converted into parenthesized strings. -Unnecessary parentheses are stripped. The stability and status of this feature is -tracked in [this issue](https://github.com/psf/black/issues/2188). - -### Improved line breaks - -For assignment expressions, _Black_ now prefers to split and wrap the right side of the -assignment instead of left side. For example: - -```python -some_dict[ - "with_a_long_key" -] = some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument -) -``` - -will be changed to: +In the past, the preview style included some features with known bugs, so that we were +unable to move these features to the stable style. Therefore, such features are now +moved to the `--unstable` style. All features in the `--preview` style are expected to +make it to next year's stable style; features in the `--unstable` style will be +stabilized only if issues with them are fixed. If bugs are discovered in a `--preview` +feature, it is demoted to the `--unstable` style. -```python -some_dict["with_a_long_key"] = ( - some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument - ) -) -``` +Currently, the following features are included in the preview style: -### Improved parentheses management +- `hex_codes_in_unicode_sequences`: normalize casing of Unicode escape characters in + strings +- `unify_docstring_detection`: fix inconsistencies in whether certain strings are + detected as docstrings +- `hug_parens_with_braces_and_square_brackets`: more compact formatting of nested + brackets ([see below](labels/hug-parens)) -For dict literals with long values, they are now wrapped in parentheses. Unnecessary -parentheses are now removed. For example: +The unstable style additionally includes the following features: -```python -my_dict = { - "a key in my dict": a_very_long_variable - * and_a_very_long_function_call() - / 100000.0, - "another key": (short_value), -} -``` +- `string_processing`: split long string literals and related changes + ([see below](labels/string-processing)) +- `wrap_long_dict_values_in_parens`: add parentheses to long values in dictionaries + ([see below](labels/wrap-long-dict-values)) +- `multiline_string_handling`: more compact formatting of expressions involving + multiline strings ([see below](labels/multiline-string-handling)) -will be changed to: - -```python -my_dict = { - "a key in my dict": ( - a_very_long_variable * and_a_very_long_function_call() / 100000.0 - ), - "another key": short_value, -} -``` +(labels/hug-parens)= ### Improved multiline dictionary and list indentation for sole function parameter @@ -185,6 +106,46 @@ foo( ) ``` +(labels/string-processing)= + +### Improved string processing + +_Black_ will split long string literals and merge short ones. Parentheses are used where +appropriate. When split, parts of f-strings that don't need formatting are converted to +plain strings. User-made splits are respected when they do not exceed the line length +limit. Line continuation backslashes are converted into parenthesized strings. +Unnecessary parentheses are stripped. The stability and status of this feature is +tracked in [this issue](https://github.com/psf/black/issues/2188). + +(labels/wrap-long-dict-values)= + +### Improved parentheses management in dicts + +For dict literals with long values, they are now wrapped in parentheses. Unnecessary +parentheses are now removed. For example: + +```python +my_dict = { + "a key in my dict": a_very_long_variable + * and_a_very_long_function_call() + / 100000.0, + "another key": (short_value), +} +``` + +will be changed to: + +```python +my_dict = { + "a key in my dict": ( + a_very_long_variable * and_a_very_long_function_call() / 100000.0 + ), + "another key": short_value, +} +``` + +(labels/multiline-string-handling)= + ### Improved multiline string handling _Black_ is smarter when formatting multiline strings, especially in function arguments, @@ -297,13 +258,51 @@ s = ( # Top comment ) ``` -======= +## Potential future changes + +This section lists changes that we may want to make in the future, but that aren't +implemented yet. + +### Using backslashes for with statements + +[Backslashes are bad and should be never be used](labels/why-no-backslashes) however +there is one exception: `with` statements using multiple context managers. Before Python +3.9 Python's grammar does not allow organizing parentheses around the series of context +managers. + +We don't want formatting like: + +```py3 +with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: + ... # nothing to split on - line too long +``` + +So _Black_ will, when we implement this, format it like this: + +```py3 +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + ... # backslashes and an ugly stranded colon +``` + +Although when the target version is Python 3.9 or higher, _Black_ uses parentheses +instead in `--preview` mode (see below) since they're allowed in Python 3.9 and higher. -### Form feed characters +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 +following way: -_Black_ will now retain form feed characters on otherwise empty lines at the module -level. Only one form feed is retained for a group of consecutive empty lines. Where -there are two empty lines in a row, the form feed will be placed on the second line. +```python +with contextlib.ExitStack() as exit_stack: + cm1 = exit_stack.enter_context(make_context_manager1()) + cm2 = exit_stack.enter_context(make_context_manager2()) + cm3 = exit_stack.enter_context(make_context_manager3()) + cm4 = exit_stack.enter_context(make_context_manager4()) + ... +``` -_Black_ already retained form feed literals inside a comment or inside a string. This -remains the case. +(labels/preview-style)= diff --git a/docs/the_black_code_style/index.md b/docs/the_black_code_style/index.md index 1719347eec8..58f28673022 100644 --- a/docs/the_black_code_style/index.md +++ b/docs/the_black_code_style/index.md @@ -42,9 +42,11 @@ _Black_: enabled by newer Python language syntax as well as due to improvements in the formatting logic. -- The `--preview` flag is exempt from this policy. There are no guarantees around the - stability of the output with that flag passed into _Black_. This flag is intended for - allowing experimentation with the proposed changes to the _Black_ code style. +- The `--preview` and `--unstable` flags are exempt from this policy. There are no + guarantees around the stability of the output with these flags passed into _Black_. + They are intended for allowing experimentation with proposed changes to the _Black_ + code style. The `--preview` style at the end of a year should closely match the stable + style for the next year, but we may always make changes. Documentation for both the current and future styles can be found: diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index f24fb34d915..6b9acb443ef 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -62,6 +62,9 @@ The headers controlling how source code is formatted are: - `X-Preview`: corresponds to the `--preview` command line flag. If present and its value is not an empty string, experimental and potentially disruptive style changes will be used. +- `X-Unstable`: corresponds to the `--unstable` command line flag. If present and its + value is not an empty string, experimental style changes that are known to be buggy + will be used. - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the `--fast` command line flag. - `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index b541f07907c..eebcf20995d 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -144,9 +144,18 @@ magic trailing comma is ignored. #### `--preview` -Enable potentially disruptive style changes that may be added to Black's main -functionality in the next major release. Read more about -[our preview style](labels/preview-style). +Enable potentially disruptive style changes that we expect to add to Black's main +functionality in the next major release. Use this if you want a taste of what next +year's style will look like. + +Read more about [our preview style](labels/preview-style). + +#### `--unstable` + +Enable all style changes in `--preview`, plus additional changes that we would like to +make eventually, but that have known issues that need to be fixed before they can move +back to the `--preview` style. Use this if you want to experiment with these changes and +help fix issues with them. (labels/exit-code)= diff --git a/src/black/__init__.py b/src/black/__init__.py index 064cb81e829..6e9f9953fee 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -290,7 +290,7 @@ def validate_regex( "--experimental-string-processing", is_flag=True, hidden=True, - help="(DEPRECATED and now included in --preview) Normalize string literals.", + help="(DEPRECATED and now included in --unstable) Normalize string literals.", ) @click.option( "--preview", diff --git a/src/black/mode.py b/src/black/mode.py index 6fad91b2d0c..0ad77cd7f60 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -179,7 +179,12 @@ class Preview(Enum): UNSTABLE_FEATURES: Set[Preview] = { + # Many issues, see summary in https://github.com/psf/black/issues/4042 Preview.string_processing, + # See issues #3452 and #4158 + Preview.wrap_long_dict_values_in_parens, + # See issue #4159 + Preview.multiline_string_handling, } @@ -217,7 +222,6 @@ def __contains__(self, feature: Preview) -> bool: except those in UNSTABLE_FEATURES are enabled. For legacy reasons, the string_processing feature has its own flag, which is deprecated. """ - return False if self.unstable: return True if feature is Preview.string_processing and self.experimental_string_processing: diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 6b0f3d33295..4c7d35a7e08 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -34,6 +34,7 @@ SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization" SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma" PREVIEW = "X-Preview" +UNSTABLE = "X-Unstable" FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe" DIFF_HEADER = "X-Diff" @@ -45,6 +46,7 @@ SKIP_STRING_NORMALIZATION_HEADER, SKIP_MAGIC_TRAILING_COMMA, PREVIEW, + UNSTABLE, FAST_OR_SAFE_HEADER, DIFF_HEADER, ] @@ -123,6 +125,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: request.headers.get(SKIP_SOURCE_FIRST_LINE, False) ) preview = bool(request.headers.get(PREVIEW, False)) + unstable = bool(request.headers.get(UNSTABLE, False)) fast = False if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast": fast = True @@ -134,6 +137,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, preview=preview, + unstable=unstable, ) req_bytes = await request.content.read() charset = request.charset if request.charset is not None else "utf8" diff --git a/tests/data/cases/preview_long_dict_values.py b/tests/data/cases/preview_long_dict_values.py index 54da76038dc..a19210605f6 100644 --- a/tests/data/cases/preview_long_dict_values.py +++ b/tests/data/cases/preview_long_dict_values.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable my_dict = { "something_something": r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" From 5eaba4a9d9afc2366c13c1e07515cb5a863c69ca Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 19:00:48 -0800 Subject: [PATCH 29/43] Format ourselves unstably --- pyproject.toml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 24b9c07674d..fa3654b8d67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,10 +16,11 @@ extend-exclude = ''' | profiling )/ ''' -# We use preview style for formatting Black itself. If you -# want stable formatting across releases, you should keep -# this off. -preview = true +# We use the unstable style for formatting Black itself. If you +# want bug-free formatting, you should keep this off. If you want +# stable formatting across releases, you should also keep `preview = true` +# (which is implied by this flag) off. +unstable = true # Build system information and other project-specific configuration below. # NOTE: You don't need this in your own Black configuration. From 718ce47e0b5929fd4c19a002fb9971fc47f17c6a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jan 2024 19:06:50 -0800 Subject: [PATCH 30/43] fix test --- tests/test_black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index 2b5fab5d28d..3a985632e8a 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -373,7 +373,7 @@ def test_detect_debug_f_strings(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_string_quotes(self) -> None: source, expected = read_data("miscellaneous", "string_quotes") - mode = black.Mode(preview=True) + mode = black.Mode(unstable=True) assert_format(source, expected, mode) mode = replace(mode, string_normalization=False) not_normalized = fs(source, mode=mode) From cb9bc00d5420f3d18bfa59aa5747a4344dab5815 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 05:27:38 -0800 Subject: [PATCH 31/43] one more --- docs/the_black_code_style/future_style.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index daf3b4b7369..9c9c3f6f133 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -22,6 +22,8 @@ Currently, the following features are included in the preview style: detected as docstrings - `hug_parens_with_braces_and_square_brackets`: more compact formatting of nested brackets ([see below](labels/hug-parens)) +- `no_normalize_fmt_skip_whitespace`: whitespace before `# fmt: skip` comments is + no longer normalized The unstable style additionally includes the following features: From 9bfeae28bf05d3660db18e9fe5d7b4b4f5557b41 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 05:40:26 -0800 Subject: [PATCH 32/43] Add separate flags for --unstable features --- src/black/__init__.py | 43 ++++++++++++++++++++++++++++++++++--------- src/black/mode.py | 20 +++++++------------- tests/test_black.py | 8 ++------ 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 6e9f9953fee..ddab3203c92 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -66,7 +66,13 @@ ) from black.linegen import LN, LineGenerator, transform_line from black.lines import EmptyLineTracker, LinesBlock -from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature +from black.mode import ( + FUTURE_FLAG_TO_FEATURE, + UNSTABLE_FEATURES, + VERSION_TO_FEATURES, + Feature, + Preview, +) from black.mode import Mode as Mode # re-exported from black.mode import TargetVersion, supports_feature from black.nodes import ( @@ -192,6 +198,13 @@ def target_version_option_callback( return [TargetVersion[val.upper()] for val in v] +def enable_unstable_feature_callback( + c: click.Context, p: Union[click.Option, click.Parameter], v: Tuple[str, ...] +) -> List[Preview]: + """Compute the features from an --enable-unstable-feature flag.""" + return [Preview[val] for val in v] + + def re_compile_maybe_verbose(regex: str) -> Pattern[str]: """Compile a regular expression string in `regex`. @@ -286,12 +299,6 @@ def validate_regex( is_flag=True, help="Don't use trailing commas as a reason to split lines.", ) -@click.option( - "--experimental-string-processing", - is_flag=True, - hidden=True, - help="(DEPRECATED and now included in --unstable) Normalize string literals.", -) @click.option( "--preview", is_flag=True, @@ -309,6 +316,17 @@ def validate_regex( " release. Implies --preview." ), ) +@click.option( + "--enable-unstable-feature", + type=click.Choice([v.name for v in UNSTABLE_FEATURES]), + callback=enable_unstable_feature_callback, + multiple=True, + help=( + "Enable specific features included in the `--unstable` style. Requires" + " `--preview`. No compatibility guarantees are provided on the behavior" + " or existence of any unstable features." + ), +) @click.option( "--check", is_flag=True, @@ -499,9 +517,9 @@ def main( # noqa: C901 skip_source_first_line: bool, skip_string_normalization: bool, skip_magic_trailing_comma: bool, - experimental_string_processing: bool, preview: bool, unstable: bool, + enable_unstable_feature: List[Preview], quiet: bool, verbose: bool, required_version: Optional[str], @@ -527,6 +545,13 @@ def main( # noqa: C901 out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.") ctx.exit(1) + if enable_unstable_feature and not preview: + out( + main.get_usage(ctx) + + "\n\n'--enable-unstable-feature' requires '--preview'." + ) + ctx.exit(1) + root, method = ( find_project_root(src, stdin_filename) if code is None else (None, None) ) @@ -588,10 +613,10 @@ def main( # noqa: C901 skip_source_first_line=skip_source_first_line, string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, - experimental_string_processing=experimental_string_processing, preview=preview, unstable=unstable, python_cell_magics=set(python_cell_magics), + enabled_features=set(enable_unstable_feature), ) lines: List[Tuple[int, int]] = [] diff --git a/src/black/mode.py b/src/black/mode.py index b0bb980b7d4..c0d2aa42316 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -202,30 +202,22 @@ class Mode: is_ipynb: bool = False skip_source_first_line: bool = False magic_trailing_comma: bool = True - experimental_string_processing: bool = False python_cell_magics: Set[str] = field(default_factory=set) preview: bool = False unstable: bool = False - - def __post_init__(self) -> None: - if self.experimental_string_processing: - warn( - "`experimental string processing` has been included in `preview`" - " and deprecated. Use `preview` instead.", - Deprecated, - ) + enabled_features: set[Preview] = field(default_factory=set) def __contains__(self, feature: Preview) -> bool: """ Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag. In unstable mode, all features are enabled. In preview mode, all features - except those in UNSTABLE_FEATURES are enabled. For legacy reasons, the - string_processing feature has its own flag, which is deprecated. + except those in UNSTABLE_FEATURES are enabled. Any features in + `self.enabled_features` are also enabled. """ if self.unstable: return True - if feature is Preview.string_processing and self.experimental_string_processing: + if feature in self.enabled_features: return True return self.preview and feature not in UNSTABLE_FEATURES @@ -245,7 +237,9 @@ def get_cache_key(self) -> str: str(int(self.is_ipynb)), str(int(self.skip_source_first_line)), str(int(self.magic_trailing_comma)), - str(int(self.experimental_string_processing)), + sha256( + (",".join(sorted(f.name for f in self.enabled_features))).encode() + ).hexdigest(), str(int(self.preview)), sha256((",".join(sorted(self.python_cell_magics))).encode()).hexdigest(), ] diff --git a/tests/test_black.py b/tests/test_black.py index 3a985632e8a..49dd2c43b64 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -44,6 +44,7 @@ from black import re_compile_maybe_verbose as compile_pattern from black.cache import FileData, get_cache_dir, get_cache_file from black.debug import DebugVisitor +from black.mode import Preview from black.output import color_diff, diff from black.report import Report @@ -183,11 +184,6 @@ def test_one_empty_line_ff(self) -> None: os.unlink(tmp_file) self.assertFormatEqual(expected, actual) - def test_experimental_string_processing_warns(self) -> None: - self.assertWarns( - black.mode.Deprecated, black.Mode, experimental_string_processing=True - ) - def test_piping(self) -> None: _, source, expected = read_data_from_file( PROJECT_ROOT / "src/black/__init__.py" @@ -257,7 +253,7 @@ def _test_wip(self) -> None: sys.settrace(tracefunc) mode = replace( DEFAULT_MODE, - experimental_string_processing=False, + enable_features={Preview.string_processing}, target_versions={black.TargetVersion.PY38}, ) actual = fs(source, mode=mode) From 93893905048f0551253dfd7ebebfd694f1957cb9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 05:41:17 -0800 Subject: [PATCH 33/43] Clean up obsolete preview tests These pass in stable, I think since last year --- tests/test_black.py | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index 49dd2c43b64..cfa28abae42 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -158,13 +158,11 @@ def test_empty_ff(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_one_empty_line(self) -> None: - mode = black.Mode(preview=True) for nl in ["\n", "\r\n"]: source = expected = nl - assert_format(source, expected, mode=mode) + assert_format(source, expected) def test_one_empty_line_ff(self) -> None: - mode = black.Mode(preview=True) for nl in ["\n", "\r\n"]: expected = nl tmp_file = Path(black.dump_to_file(nl)) @@ -175,9 +173,7 @@ def test_one_empty_line_ff(self) -> None: with open(tmp_file, "wb") as f: f.write(nl.encode("utf-8")) try: - self.assertFalse( - ff(tmp_file, mode=mode, write_back=black.WriteBack.YES) - ) + self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES)) with open(tmp_file, "rb") as f: actual = f.read().decode("utf-8") finally: @@ -1047,7 +1043,6 @@ def test_format_file_contents(self) -> None: black.format_file_contents(invalid, mode=mode, fast=False) self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can") - mode = black.Mode(preview=True) just_crlf = "\r\n" with self.assertRaises(black.NothingChanged): black.format_file_contents(just_crlf, mode=mode, fast=False) @@ -1391,7 +1386,6 @@ def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper: return get_output - mode = black.Mode(preview=True) for content, expected in cases: output = io.StringIO() io_TextIOWrapper = io.TextIOWrapper @@ -1402,27 +1396,12 @@ def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper: fast=True, content=content, write_back=black.WriteBack.YES, - mode=mode, + mode=DEFAULT_MODE, ) except io.UnsupportedOperation: pass # StringIO does not support detach assert output.getvalue() == expected - # An empty string is the only test case for `preview=False` - output = io.StringIO() - io_TextIOWrapper = io.TextIOWrapper - with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)): - try: - black.format_stdin_to_stdout( - fast=True, - content="", - write_back=black.WriteBack.YES, - mode=DEFAULT_MODE, - ) - except io.UnsupportedOperation: - pass # StringIO does not support detach - assert output.getvalue() == "" - def test_invalid_cli_regex(self) -> None: for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]: self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2) From 07eccbe20ba08330962b33a5a07cd76903dda08e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 05:55:43 -0800 Subject: [PATCH 34/43] docs, blackd --- CHANGES.md | 1 + docs/the_black_code_style/future_style.md | 6 +++++- .../black_as_a_server.md | 3 +++ docs/usage_and_configuration/the_basics.md | 15 +++++++++++++++ src/blackd/__init__.py | 17 +++++++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2a951713f0a..372485f6360 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -74,6 +74,7 @@ release: - Fix symlink handling, properly catch and ignore symlinks that point outside of root (#4161) - Fix cache mtime logic that resulted in false positive cache hits (#4128) +- Remove the long-deprecated `--experimental-string-processing` flag (#4096) ### Packaging diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 9c9c3f6f133..ffafe114837 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -12,7 +12,9 @@ unable to move these features to the stable style. Therefore, such features are moved to the `--unstable` style. All features in the `--preview` style are expected to make it to next year's stable style; features in the `--unstable` style will be stabilized only if issues with them are fixed. If bugs are discovered in a `--preview` -feature, it is demoted to the `--unstable` style. +feature, it is demoted to the `--unstable` style. To avoid thrash when a feature is +demoted from the `--preview` to the `--unstable` style, users can use the +`--enable-unstable-feature` flag to enable specific unstable features. Currently, the following features are included in the preview style: @@ -25,6 +27,8 @@ Currently, the following features are included in the preview style: - `no_normalize_fmt_skip_whitespace`: whitespace before `# fmt: skip` comments is no longer normalized +(labels/unstable-features)= + The unstable style additionally includes the following features: - `string_processing`: split long string literals and related changes diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index 6b9acb443ef..feff4b85e06 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -65,6 +65,9 @@ The headers controlling how source code is formatted are: - `X-Unstable`: corresponds to the `--unstable` command line flag. If present and its value is not an empty string, experimental style changes that are known to be buggy will be used. +- `X-Enable-Unstable-Feature`: corresponds to the `--enable-unstable-feature` flag. + The contents of the flag must be a comma-separated list of unstable features to + be enabled. Example: `X-Enable-Unstable-Feature: feature1, feature2`. - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the `--fast` command line flag. - `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index eebcf20995d..06d901c0540 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -150,6 +150,8 @@ year's style will look like. Read more about [our preview style](labels/preview-style). +There is no guarantee on the code style produced by this flag across releases. + #### `--unstable` Enable all style changes in `--preview`, plus additional changes that we would like to @@ -157,6 +159,19 @@ make eventually, but that have known issues that need to be fixed before they ca back to the `--preview` style. Use this if you want to experiment with these changes and help fix issues with them. +There is no guarantee on the code style produced by this flag across releases. + +#### `--enable-unstable-feature` + +Enable specific features from the `--unstable` style. See [the preview style documentation](labels/unstable-features) for the list of supported features. +This flag can only be used when `--preview` is enabled. Users are encouraged to use this +flag if they use `--preview` style and a feature that affects their code is moved +from the `--preview` to the `--unstable` style, but they want to avoid the thrash from +undoing this change. + +There are no guarantees on the behavior of these features, or even their existence, +across releases. + (labels/exit-code)= #### `--check` diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 4c7d35a7e08..e83fc6996ad 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -35,6 +35,7 @@ SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma" PREVIEW = "X-Preview" UNSTABLE = "X-Unstable" +ENABLE_UNSTABLE_FEATURE = "X-Enable-Unstable-Feature" FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe" DIFF_HEADER = "X-Diff" @@ -47,6 +48,7 @@ SKIP_MAGIC_TRAILING_COMMA, PREVIEW, UNSTABLE, + ENABLE_UNSTABLE_FEATURE, FAST_OR_SAFE_HEADER, DIFF_HEADER, ] @@ -124,8 +126,22 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: skip_source_first_line = bool( request.headers.get(SKIP_SOURCE_FIRST_LINE, False) ) + preview = bool(request.headers.get(PREVIEW, False)) unstable = bool(request.headers.get(UNSTABLE, False)) + enable_features: Set[black.Preview] = set() + enable_unstable_features = request.headers.get(ENABLE_UNSTABLE_FEATURE, "").split(",") + for piece in enable_unstable_features: + piece = piece.strip() + if piece: + try: + enable_features.add(black.Preview[piece]) + except KeyError: + return web.Response( + status=400, + text=f"Invalid value for {ENABLE_UNSTABLE_FEATURE}: {piece}", + ) + fast = False if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast": fast = True @@ -138,6 +154,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: magic_trailing_comma=not skip_magic_trailing_comma, preview=preview, unstable=unstable, + enabled_features=enable_features, ) req_bytes = await request.content.read() charset = request.charset if request.charset is not None else "utf8" From 81ab9cc62c93558c2dd503d653fc97328f48a25f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 06:00:56 -0800 Subject: [PATCH 35/43] some tests --- src/black/__init__.py | 3 ++- tests/test_black.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index ddab3203c92..652bee3dc0e 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -545,7 +545,8 @@ def main( # noqa: C901 out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.") ctx.exit(1) - if enable_unstable_feature and not preview: + # It doesn't do anything if --unstable is also passed, so just allow it. + if enable_unstable_feature and not (preview or unstable): out( main.get_usage(ctx) + "\n\n'--enable-unstable-feature' requires '--preview'." diff --git a/tests/test_black.py b/tests/test_black.py index cfa28abae42..3ecda3f0270 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1402,6 +1402,22 @@ def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper: pass # StringIO does not support detach assert output.getvalue() == expected + def test_cli_unstable(self) -> None: + self.invokeBlack(["--unstable", "-c", "0"], exit_code=0) + self.invokeBlack(["--preview", "-c", "0"], exit_code=0) + # Must also pass --preview + self.invokeBlack( + ["--enable-unstable-feature", "string_processing", "-c", "0"], exit_code=1 + ) + self.invokeBlack( + ["--preview", "--enable-unstable-feature", "string_processing", "-c", "0"], + exit_code=0, + ) + self.invokeBlack( + ["--unstable", "--enable-unstable-feature", "string_processing", "-c", "0"], + exit_code=0, + ) + def test_invalid_cli_regex(self) -> None: for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]: self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2) From 926bbde4564b0a54f94ea671932f55c4a446a0f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:02:22 +0000 Subject: [PATCH 36/43] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/the_black_code_style/future_style.md | 4 ++-- docs/usage_and_configuration/black_as_a_server.md | 6 +++--- docs/usage_and_configuration/the_basics.md | 11 ++++++----- src/black/__init__.py | 3 +-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index ffafe114837..1cdd25fdb7c 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -24,8 +24,8 @@ Currently, the following features are included in the preview style: detected as docstrings - `hug_parens_with_braces_and_square_brackets`: more compact formatting of nested brackets ([see below](labels/hug-parens)) -- `no_normalize_fmt_skip_whitespace`: whitespace before `# fmt: skip` comments is - no longer normalized +- `no_normalize_fmt_skip_whitespace`: whitespace before `# fmt: skip` comments is no + longer normalized (labels/unstable-features)= diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index feff4b85e06..0a7edb57fd7 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -65,9 +65,9 @@ The headers controlling how source code is formatted are: - `X-Unstable`: corresponds to the `--unstable` command line flag. If present and its value is not an empty string, experimental style changes that are known to be buggy will be used. -- `X-Enable-Unstable-Feature`: corresponds to the `--enable-unstable-feature` flag. - The contents of the flag must be a comma-separated list of unstable features to - be enabled. Example: `X-Enable-Unstable-Feature: feature1, feature2`. +- `X-Enable-Unstable-Feature`: corresponds to the `--enable-unstable-feature` flag. The + contents of the flag must be a comma-separated list of unstable features to be + enabled. Example: `X-Enable-Unstable-Feature: feature1, feature2`. - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the `--fast` command line flag. - `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 06d901c0540..a42e093155b 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -163,11 +163,12 @@ There is no guarantee on the code style produced by this flag across releases. #### `--enable-unstable-feature` -Enable specific features from the `--unstable` style. See [the preview style documentation](labels/unstable-features) for the list of supported features. -This flag can only be used when `--preview` is enabled. Users are encouraged to use this -flag if they use `--preview` style and a feature that affects their code is moved -from the `--preview` to the `--unstable` style, but they want to avoid the thrash from -undoing this change. +Enable specific features from the `--unstable` style. See +[the preview style documentation](labels/unstable-features) for the list of supported +features. This flag can only be used when `--preview` is enabled. Users are encouraged +to use this flag if they use `--preview` style and a feature that affects their code is +moved from the `--preview` to the `--unstable` style, but they want to avoid the thrash +from undoing this change. There are no guarantees on the behavior of these features, or even their existence, across releases. diff --git a/src/black/__init__.py b/src/black/__init__.py index 652bee3dc0e..248a8dc34c7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -71,10 +71,9 @@ UNSTABLE_FEATURES, VERSION_TO_FEATURES, Feature, - Preview, ) from black.mode import Mode as Mode # re-exported -from black.mode import TargetVersion, supports_feature +from black.mode import Preview, TargetVersion, supports_feature from black.nodes import ( STARS, is_number_token, From 17558970c00d189cda920cd92089b6e9127c6146 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 06:05:31 -0800 Subject: [PATCH 37/43] ugh 3.8 --- src/black/mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/mode.py b/src/black/mode.py index c0d2aa42316..aab6d25100c 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -205,7 +205,7 @@ class Mode: python_cell_magics: Set[str] = field(default_factory=set) preview: bool = False unstable: bool = False - enabled_features: set[Preview] = field(default_factory=set) + enabled_features: Set[Preview] = field(default_factory=set) def __contains__(self, feature: Preview) -> bool: """ From 3a7310f9b606cf7338c62be3880134065f7bd79d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 06:06:33 -0800 Subject: [PATCH 38/43] note replacement --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 372485f6360..6497a98b2c4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -74,7 +74,9 @@ release: - Fix symlink handling, properly catch and ignore symlinks that point outside of root (#4161) - Fix cache mtime logic that resulted in false positive cache hits (#4128) -- Remove the long-deprecated `--experimental-string-processing` flag (#4096) +- Remove the long-deprecated `--experimental-string-processing` flag. This feature can + currently be enabled with `--preview --enable-unstable-feature string_processing`. + (#4096) ### Packaging From a1b4dca840330432ebfcb017612c750a3871255e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 06:15:57 -0800 Subject: [PATCH 39/43] lint --- src/black/mode.py | 1 - src/blackd/__init__.py | 117 +++++++++++++++++++++-------------------- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/src/black/mode.py b/src/black/mode.py index aab6d25100c..68919fb4901 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -9,7 +9,6 @@ from hashlib import sha256 from operator import attrgetter from typing import Dict, Final, Set -from warnings import warn from black.const import DEFAULT_LINE_LENGTH diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index e83fc6996ad..e08089d7681 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -8,6 +8,7 @@ try: from aiohttp import web + from multidict import CIMultiDictProxy from .middlewares import cors except ImportError as ie: @@ -57,6 +58,10 @@ BLACK_VERSION_HEADER = "X-Black-Version" +class HeaderError(Exception): + pass + + class InvalidVariantHeader(Exception): pass @@ -97,72 +102,21 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: return web.Response( status=501, text="This server only supports protocol version 1" ) - try: - line_length = int( - request.headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH) - ) - except ValueError: - return web.Response(status=400, text="Invalid line length header value") - - if PYTHON_VARIANT_HEADER in request.headers: - value = request.headers[PYTHON_VARIANT_HEADER] - try: - pyi, versions = parse_python_variant_header(value) - except InvalidVariantHeader as e: - return web.Response( - status=400, - text=f"Invalid value for {PYTHON_VARIANT_HEADER}: {e.args[0]}", - ) - else: - pyi = False - versions = set() - - skip_string_normalization = bool( - request.headers.get(SKIP_STRING_NORMALIZATION_HEADER, False) - ) - skip_magic_trailing_comma = bool( - request.headers.get(SKIP_MAGIC_TRAILING_COMMA, False) - ) - skip_source_first_line = bool( - request.headers.get(SKIP_SOURCE_FIRST_LINE, False) - ) - - preview = bool(request.headers.get(PREVIEW, False)) - unstable = bool(request.headers.get(UNSTABLE, False)) - enable_features: Set[black.Preview] = set() - enable_unstable_features = request.headers.get(ENABLE_UNSTABLE_FEATURE, "").split(",") - for piece in enable_unstable_features: - piece = piece.strip() - if piece: - try: - enable_features.add(black.Preview[piece]) - except KeyError: - return web.Response( - status=400, - text=f"Invalid value for {ENABLE_UNSTABLE_FEATURE}: {piece}", - ) fast = False if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast": fast = True - mode = black.FileMode( - target_versions=versions, - is_pyi=pyi, - line_length=line_length, - skip_source_first_line=skip_source_first_line, - string_normalization=not skip_string_normalization, - magic_trailing_comma=not skip_magic_trailing_comma, - preview=preview, - unstable=unstable, - enabled_features=enable_features, - ) + try: + mode = parse_mode(request.headers) + except HeaderError as e: + return web.Response(status=400, text=e.args[0]) req_bytes = await request.content.read() charset = request.charset if request.charset is not None else "utf8" req_str = req_bytes.decode(charset) then = datetime.now(timezone.utc) header = "" - if skip_source_first_line: + if mode.skip_source_first_line: first_newline_position: int = req_str.find("\n") + 1 header = req_str[:first_newline_position] req_str = req_str[first_newline_position:] @@ -211,6 +165,57 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: return web.Response(status=500, headers=headers, text=str(e)) +def parse_mode(headers: CIMultiDictProxy[str]) -> black.Mode: + try: + line_length = int(headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH)) + except ValueError: + raise HeaderError("Invalid line length header value") from None + + if PYTHON_VARIANT_HEADER in headers: + value = headers[PYTHON_VARIANT_HEADER] + try: + pyi, versions = parse_python_variant_header(value) + except InvalidVariantHeader as e: + raise HeaderError( + f"Invalid value for {PYTHON_VARIANT_HEADER}: {e.args[0]}", + ) from None + else: + pyi = False + versions = set() + + skip_string_normalization = bool( + headers.get(SKIP_STRING_NORMALIZATION_HEADER, False) + ) + skip_magic_trailing_comma = bool(headers.get(SKIP_MAGIC_TRAILING_COMMA, False)) + skip_source_first_line = bool(headers.get(SKIP_SOURCE_FIRST_LINE, False)) + + preview = bool(headers.get(PREVIEW, False)) + unstable = bool(headers.get(UNSTABLE, False)) + enable_features: Set[black.Preview] = set() + enable_unstable_features = headers.get(ENABLE_UNSTABLE_FEATURE, "").split(",") + for piece in enable_unstable_features: + piece = piece.strip() + if piece: + try: + enable_features.add(black.Preview[piece]) + except KeyError: + raise HeaderError( + f"Invalid value for {ENABLE_UNSTABLE_FEATURE}: {piece}", + ) from None + + return black.FileMode( + target_versions=versions, + is_pyi=pyi, + line_length=line_length, + skip_source_first_line=skip_source_first_line, + string_normalization=not skip_string_normalization, + magic_trailing_comma=not skip_magic_trailing_comma, + preview=preview, + unstable=unstable, + enabled_features=enable_features, + ) + + def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersion]]: if value == "pyi": return True, set() From 47ea1d2dcff473a9f73df0f11bad2d4c12c2c3f5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 06:17:09 -0800 Subject: [PATCH 40/43] Remove broken and disabled test There is no "wip.py" --- tests/test_black.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index 3ecda3f0270..678183e7c8d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -243,21 +243,6 @@ def test_piping_diff_with_color(self) -> None: self.assertIn("\033[31m", actual) self.assertIn("\033[0m", actual) - @patch("black.dump_to_file", dump_to_stderr) - def _test_wip(self) -> None: - source, expected = read_data("miscellaneous", "wip") - sys.settrace(tracefunc) - mode = replace( - DEFAULT_MODE, - enable_features={Preview.string_processing}, - target_versions={black.TargetVersion.PY38}, - ) - actual = fs(source, mode=mode) - sys.settrace(None) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) - def test_pep_572_version_detection(self) -> None: source, _ = read_data("cases", "pep_572") root = black.lib2to3_parse(source) From e21787fa38e50b856391aef7477fb0ab5324c162 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 06:19:17 -0800 Subject: [PATCH 41/43] lint the lint --- src/blackd/__init__.py | 4 ++-- tests/test_black.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index e08089d7681..7041671f596 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -8,7 +8,7 @@ try: from aiohttp import web - from multidict import CIMultiDictProxy + from multidict import MultiMapping from .middlewares import cors except ImportError as ie: @@ -165,7 +165,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: return web.Response(status=500, headers=headers, text=str(e)) -def parse_mode(headers: CIMultiDictProxy[str]) -> black.Mode: +def parse_mode(headers: MultiMapping[str]) -> black.Mode: try: line_length = int(headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH)) except ValueError: diff --git a/tests/test_black.py b/tests/test_black.py index 678183e7c8d..42d0cea2297 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -44,7 +44,6 @@ from black import re_compile_maybe_verbose as compile_pattern from black.cache import FileData, get_cache_dir, get_cache_file from black.debug import DebugVisitor -from black.mode import Preview from black.output import color_diff, diff from black.report import Report From 09c1fb54bcd0e2a753769787c5c7cd6e17340027 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 16:11:06 -0800 Subject: [PATCH 42/43] Allow --enable-unstable-feature for preview features --- src/black/__init__.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 248a8dc34c7..2fd3cd17f43 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -66,12 +66,7 @@ ) from black.linegen import LN, LineGenerator, transform_line from black.lines import EmptyLineTracker, LinesBlock -from black.mode import ( - FUTURE_FLAG_TO_FEATURE, - UNSTABLE_FEATURES, - VERSION_TO_FEATURES, - Feature, -) +from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature from black.mode import Mode as Mode # re-exported from black.mode import Preview, TargetVersion, supports_feature from black.nodes import ( @@ -317,7 +312,7 @@ def validate_regex( ) @click.option( "--enable-unstable-feature", - type=click.Choice([v.name for v in UNSTABLE_FEATURES]), + type=click.Choice([v.name for v in Preview]), callback=enable_unstable_feature_callback, multiple=True, help=( From 9cc105bd52a92355025aaf07adca3a173a00d1c0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 16:19:57 -0800 Subject: [PATCH 43/43] Expand changelog --- CHANGES.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6497a98b2c4..62f56f1efd9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -61,7 +61,12 @@ release: -- Add `--unstable` style (#4096) +- Add `--unstable` style, covering preview features that have known problems that would + block them from going into the stable style. Also add the `--enable-unstable-feature` + flag; for example, use + `--enable-unstable-feature hug_parens_with_braces_and_square_brackets` to apply this + preview style throughout 2024, even if a later Black release downgrades the feature to + unstable (#4096) - Format module docstrings the same as class and function docstrings (#4095) - Fix crash when using a walrus in a dictionary (#4155) - Fix unnecessary parentheses when wrapping long dicts (#4135)