From 16b98abca94343770aad561ea659380c97d473b4 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Sat, 10 Dec 2022 19:49:33 +0000 Subject: [PATCH 01/11] make black[jupyter] installation cross-shell (#3394) --- README.md | 2 +- docs/getting_started.md | 2 +- src/black/handle_ipynb_magics.py | 2 +- tests/test_no_ipynb.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c2dd9e6b17d..b12ddfb1290 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the ### Installation _Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run. -If you want to format Jupyter Notebooks, install with `pip install 'black[jupyter]'`. +If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/docs/getting_started.md b/docs/getting_started.md index 1825f3b5aa3..33fb2f978bb 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -17,7 +17,7 @@ Also, you can try out _Black_ online for minimal fuss on the ## Installation _Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run. -If you want to format Jupyter Notebooks, install with `pip install 'black[jupyter]'`. +If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 693f1a68bd4..9e1af757c32 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -64,7 +64,7 @@ def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool: if verbose or not quiet: msg = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" - "You can fix this by running ``pip install black[jupyter]``" + 'You can fix this by running ``pip install "black[jupyter]"``' ) out(msg) return False diff --git a/tests/test_no_ipynb.py b/tests/test_no_ipynb.py index 3e0b1593bf0..b63ecde8896 100644 --- a/tests/test_no_ipynb.py +++ b/tests/test_no_ipynb.py @@ -17,7 +17,7 @@ def test_ipynb_diff_with_no_change_single() -> None: result = runner.invoke(main, [str(path)]) expected_output = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" - "You can fix this by running ``pip install black[jupyter]``\n" + 'You can fix this by running ``pip install "black[jupyter]"``\n' ) assert expected_output in result.output @@ -32,6 +32,6 @@ def test_ipynb_diff_with_no_change_dir(tmp_path: pathlib.Path) -> None: result = runner.invoke(main, [str(tmp_path)]) expected_output = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" - "You can fix this by running ``pip install black[jupyter]``\n" + 'You can fix this by running ``pip install "black[jupyter]"``\n' ) assert expected_output in result.output From 5f0dc862f5bbfc8abed3f29d76325404eb4f99c0 Mon Sep 17 00:00:00 2001 From: mainj12 <118842653+mainj12@users.noreply.github.com> Date: Sat, 10 Dec 2022 20:56:14 +0000 Subject: [PATCH 02/11] Adding pyproject.toml configuration output to verbose logging (#3392) --- CHANGES.md | 3 +++ src/black/__init__.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index c84feb04934..e781cbd523e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,9 @@ +- Verbose logging now shows the values of `pyproject.toml` configuration variables + (#3392) + ### _Blackd_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 39d12968c4b..f00749aaed8 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -511,6 +511,9 @@ def main( # noqa: C901 out("Using configuration from project root.", fg="blue") else: out(f"Using configuration in '{config}'.", fg="blue") + if ctx.default_map: + for param, value in ctx.default_map.items(): + out(f"{param}: {value}") error_msg = "Oh no! 💥 💔 💥" if ( From 80de2372e4ec385c082c38f365414ac3622a4010 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 10 Dec 2022 15:56:34 -0500 Subject: [PATCH 03/11] Bump mypy[c] from 0.971 to 0.991 (#3380) --- .pre-commit-config.yaml | 2 +- CHANGES.md | 3 +++ pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0be8dc42890..2d5ac26b629 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - flake8-simplify - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v0.991 hooks: - id: mypy exclude: ^docs/conf.py diff --git a/CHANGES.md b/CHANGES.md index e781cbd523e..86d44f033b8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,9 @@ +- Upgrade mypyc from `0.971` to `0.991` so mypycified _Black_ can be built on armv7 + (#3380) + ### Parser diff --git a/pyproject.toml b/pyproject.toml index 329adaa65d8..aede497e2af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,7 +119,7 @@ sources = ["src"] enable-by-default = false dependencies = [ "hatch-mypyc>=0.13.0", - "mypy==0.971", + "mypy==0.991", # Required stubs to be removed when the packages support PEP 561 themselves "types-typed-ast>=1.4.2", ] From 9bbe11dd7baf7aea23a5b51d7daf9e12acb3b28c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 06:27:18 -0800 Subject: [PATCH 04/11] Bump pypa/cibuildwheel from 2.11.2 to 2.11.3 (#3434) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.11.2 to 2.11.3. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.11.2...v2.11.3) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 16cf90bd206..7fd760ef727 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -59,7 +59,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.11.2 + uses: pypa/cibuildwheel@v2.11.3 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From abd2b2556a8f9d8efe8914c23520eef7223e2a99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 06:28:12 -0800 Subject: [PATCH 05/11] Bump furo from 2022.9.29 to 2022.12.7 in /docs (#3433) Bumps [furo](https://github.com/pradyunsg/furo) from 2022.9.29 to 2022.12.7. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2022.09.29...2022.12.07) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 426a78a7abf..9a269d02a75 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==5.3.0 docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.1 -furo==2022.9.29 +furo==2022.12.7 From 7d062ecd5f14124a99daf452c46054ada656ad8b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 12 Dec 2022 20:56:38 -0800 Subject: [PATCH 06/11] Do not put the closing quotes in a docstring on a separate line (#3430) Fixes #3320. Followup from #3044. --- CHANGES.md | 2 ++ src/black/linegen.py | 13 ++++++------- tests/data/preview/docstring_preview.py | 6 ++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 86d44f033b8..f6040359623 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ - Fix a crash in preview style with assert + parenthesized string (#3415) +- Do not put the closing quotes in a docstring on a separate line, even if the line is + too long (#3430) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 219495e9a5e..644824a3c86 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -389,19 +389,18 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: # We need to find the length of the last line of the docstring # to find if we can add the closing quotes to the line without # exceeding the maximum line length. - # If docstring is one line, then we need to add the length - # of the indent, prefix, and starting quotes. Ending quotes are - # handled later. + # If docstring is one line, we don't put the closing quotes on a + # separate line because it looks ugly (#3320). lines = docstring.splitlines() last_line_length = len(lines[-1]) if docstring else 0 - if len(lines) == 1: - last_line_length += len(indent) + len(prefix) + quote_len - # If adding closing quotes would cause the last line to exceed # the maximum line length then put a line break before the # closing quotes - if last_line_length + quote_len > self.mode.line_length: + if ( + len(lines) > 1 + and last_line_length + quote_len > self.mode.line_length + ): leaf.value = prefix + quote + docstring + "\n" + indent + quote else: leaf.value = prefix + quote + docstring + quote diff --git a/tests/data/preview/docstring_preview.py b/tests/data/preview/docstring_preview.py index 292352c82f3..ff4819acb67 100644 --- a/tests/data/preview/docstring_preview.py +++ b/tests/data/preview/docstring_preview.py @@ -54,13 +54,11 @@ def single_quote_docstring_over_line_limit2(): def docstring_almost_at_line_limit(): - """long docstring................................................................. - """ + """long docstring.................................................................""" def docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................ - """ + f"""long docstring................................................................""" def mulitline_docstring_almost_at_line_limit(): From a2821815af5f5a706c673279d6405e286d6e95b8 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Wed, 14 Dec 2022 17:56:14 -0800 Subject: [PATCH 07/11] Fix a crash when a colon line is marked between `# fmt: off` and `# fmt: on` (#3439) --- CHANGES.md | 2 ++ src/black/comments.py | 12 +++++++++++- tests/data/simple_cases/fmtonoff5.py | 22 ++++++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f6040359623..1a7c320baf8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ +- Fix a crash when a colon line is marked between `# fmt: off` and `# fmt: on` (#3439) + ### Preview style diff --git a/src/black/comments.py b/src/black/comments.py index dce83abf1bb..e733dccd844 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -232,7 +232,7 @@ def generate_ignored_nodes( # fix for fmt: on in children if children_contains_fmt_on(container, preview=preview): - for child in container.children: + for index, child in enumerate(container.children): if isinstance(child, Leaf) and is_fmt_on(child, preview=preview): if child.type in CLOSING_BRACKETS: # This means `# fmt: on` is placed at a different bracket level @@ -241,6 +241,16 @@ def generate_ignored_nodes( # The alternative is to fail the formatting. yield child return + if ( + child.type == token.INDENT + and index < len(container.children) - 1 + and children_contains_fmt_on( + container.children[index + 1], preview=preview + ) + ): + # This means `# fmt: on` is placed right after an indentation + # level, and we shouldn't swallow the previous INDENT token. + return if children_contains_fmt_on(child, preview=preview): return yield child diff --git a/tests/data/simple_cases/fmtonoff5.py b/tests/data/simple_cases/fmtonoff5.py index 71b1381ed0d..181151b6bd6 100644 --- a/tests/data/simple_cases/fmtonoff5.py +++ b/tests/data/simple_cases/fmtonoff5.py @@ -64,7 +64,7 @@ async def call(param): print ( "This will be formatted" ) -# Regression test for https://github.com/psf/black/issues/2985 +# Regression test for https://github.com/psf/black/issues/2985. class Named(t.Protocol): # fmt: off @property @@ -75,6 +75,15 @@ def this_will_be_formatted ( self, **kwargs ) -> Named: ... # fmt: on +# Regression test for https://github.com/psf/black/issues/3436. +if x: + return x +# fmt: off +elif unformatted: +# fmt: on + will_be_formatted () + + # output @@ -144,7 +153,7 @@ async def call(param): print("This will be formatted") -# Regression test for https://github.com/psf/black/issues/2985 +# Regression test for https://github.com/psf/black/issues/2985. class Named(t.Protocol): # fmt: off @property @@ -156,3 +165,12 @@ def this_will_be_formatted(self, **kwargs) -> Named: ... # fmt: on + + +# Regression test for https://github.com/psf/black/issues/3436. +if x: + return x +# fmt: off +elif unformatted: + # fmt: on + will_be_formatted() From 658c8d8d96047c5ba77be4aecc2545a22d5e35b9 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 15 Dec 2022 08:25:28 -0800 Subject: [PATCH 08/11] Improve long values in dict literals (#3440) --- CHANGES.md | 3 ++ src/black/linegen.py | 17 ++++++ src/black/mode.py | 3 ++ src/black/trans.py | 36 ++++++++++--- tests/data/preview/long_dict_values.py | 53 +++++++++++++++++++ tests/data/preview/long_strings.py | 28 ++++++++-- .../data/preview/long_strings__regression.py | 15 ++++++ 7 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 tests/data/preview/long_dict_values.py diff --git a/CHANGES.md b/CHANGES.md index 1a7c320baf8..03c7a286771 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,9 @@ - Fix a crash in preview style with assert + parenthesized string (#3415) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) +- Long values in dict literals are now wrapped in parentheses; correspondingly + unnecessary parentheses around short values in dict literals are now removed; long + string lambda values are now wrapped in parentheses (#3440) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 644824a3c86..244dbe77eb5 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -179,6 +179,23 @@ def visit_stmt( yield from self.visit(child) + 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: + wrap_in_parentheses(node, child, visible=False) + yield from self.visit_default(node) + def visit_funcdef(self, node: Node) -> Iterator[Line]: """Visit function definition.""" if Preview.annotation_parens not in self.mode: diff --git a/src/black/mode.py b/src/black/mode.py index a3ce20b8619..bcd35b4d4be 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -157,8 +157,11 @@ class Preview(Enum): one_element_subscript = auto() remove_block_trailing_newline = auto() remove_redundant_parens = 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() skip_magic_trailing_comma_in_subscript = auto() + wrap_long_dict_values_in_parens = auto() class Deprecated(UserWarning): diff --git a/src/black/trans.py b/src/black/trans.py index 8893ab02aab..b08a6d243d8 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1638,6 +1638,8 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): * The line is a dictionary key assignment where some valid key is being assigned the value of some string. OR + * The line is an lambda expression and the value is a string. + OR * The line starts with an "atom" string that prefers to be wrapped in parens. It's preferred to be wrapped when the string is surrounded by commas (or is the first/last child). @@ -1683,7 +1685,7 @@ def do_splitter_match(self, line: Line) -> TMatchResult: or self._else_match(LL) or self._assert_match(LL) or self._assign_match(LL) - or self._dict_match(LL) + or self._dict_or_lambda_match(LL) or self._prefer_paren_wrap_match(LL) ) @@ -1841,22 +1843,23 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]: return None @staticmethod - def _dict_match(LL: List[Leaf]) -> Optional[int]: + def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]: """ Returns: string_idx such that @LL[string_idx] is equal to our target (i.e. matched) string, if this line matches the dictionary key assignment - statement requirements listed in the 'Requirements' section of this - classes' docstring. + statement or lambda expression requirements listed in the + 'Requirements' section of this classes' docstring. OR None, otherwise. """ - # If this line is apart of a dictionary key assignment... - if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]: + # If this line is a part of a dictionary key assignment or lambda expression... + parent_types = [parent_type(LL[0]), parent_type(LL[0].parent)] + if syms.dictsetmaker in parent_types or syms.lambdef in parent_types: is_valid_index = is_valid_index_factory(LL) for i, leaf in enumerate(LL): - # We MUST find a colon... + # We MUST find a colon, it can either be dict's or lambda's colon... if leaf.type == token.COLON: idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 @@ -1951,6 +1954,25 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: f" (left_leaves={left_leaves}, right_leaves={right_leaves})" ) old_rpar_leaf = right_leaves.pop() + elif right_leaves and right_leaves[-1].type == token.RPAR: + # Special case for lambda expressions as dict's value, e.g.: + # my_dict = { + # "key": lambda x: f"formatted: {x}, + # } + # After wrapping the dict's value with parentheses, the string is + # followed by a RPAR but its opening bracket is lambda's, not + # the string's: + # "key": (lambda x: f"formatted: {x}), + opening_bracket = right_leaves[-1].opening_bracket + if opening_bracket is not None and opening_bracket in left_leaves: + index = left_leaves.index(opening_bracket) + if ( + index > 0 + and index < len(left_leaves) - 1 + and left_leaves[index - 1].type == token.COLON + and left_leaves[index + 1].value == "lambda" + ): + right_leaves.pop() append_leaves(string_line, line, right_leaves) diff --git a/tests/data/preview/long_dict_values.py b/tests/data/preview/long_dict_values.py new file mode 100644 index 00000000000..f23c5d3dad1 --- /dev/null +++ b/tests/data/preview/long_dict_values.py @@ -0,0 +1,53 @@ +my_dict = { + "something_something": + r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t", +} + +my_dict = { + "a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0 +} + +my_dict = { + "a key in my dict": a_very_long_variable * and_a_very_long_function_call() * and_another_long_func() / 100000.0 +} + +my_dict = { + "a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value") +} + + +# output + + +my_dict = { + "something_something": ( + r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" + ), +} + +my_dict = { + "a key in my dict": ( + a_very_long_variable * and_a_very_long_function_call() / 100000.0 + ) +} + +my_dict = { + "a key in my dict": ( + a_very_long_variable + * and_a_very_long_function_call() + * and_another_long_func() + / 100000.0 + ) +} + +my_dict = { + "a key in my dict": ( + MyClass.some_attribute.first_call() + .second_call() + .third_call(some_args="some value") + ) +} diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 9288b253b60..9c78f675b8f 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -278,6 +278,15 @@ def foo(): "........................................................................... \\N{LAO KO LA}" ) +msg = lambda x: f"this is a very very very long lambda value {x} that doesn't fit on a single line" + +dict_with_lambda_values = { + "join": lambda j: ( + f"{j.__class__.__name__}({some_function_call(j.left)}, " + f"{some_function_call(j.right)})" + ), +} + # output @@ -362,9 +371,8 @@ def foo(): "A %s %s" % ("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), + " dictionary. It is %s bad (#%d)." % ("soooo", 2) + ), } D5 = { # Test for https://github.com/psf/black/issues/3261 @@ -806,3 +814,17 @@ def foo(): "..........................................................................." " \\N{LAO KO LA}" ) + +msg = ( + lambda x: ( + f"this is a very very very long lambda value {x} that doesn't fit on a single" + " line" + ) +) + +dict_with_lambda_values = { + "join": lambda j: ( + f"{j.__class__.__name__}({some_function_call(j.left)}, " + f"{some_function_call(j.right)})" + ), +} diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 8b00e76f40e..6d56dcc635d 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -524,6 +524,13 @@ async def foo(self): }, ) +# Regression test for https://github.com/psf/black/issues/3117. +some_dict = { + "something_something": + r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t", +} + # output @@ -1178,3 +1185,11 @@ async def foo(self): ), }, ) + +# Regression test for https://github.com/psf/black/issues/3117. +some_dict = { + "something_something": ( + r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" + ), +} From aafc21aa77f5c4d2ebcb833aa60faba6c2138b94 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 15 Dec 2022 15:58:51 -0800 Subject: [PATCH 09/11] Prefer splitting right hand side of assignment statements. (#3368) --- CHANGES.md | 2 + src/black/linegen.py | 122 +++++++++++++++--- src/black/mode.py | 1 + .../data/preview/long_strings__regression.py | 6 +- tests/data/preview/prefer_rhs_split.py | 85 ++++++++++++ .../preview/prefer_rhs_split_reformatted.py | 38 ++++++ 6 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 tests/data/preview/prefer_rhs_split.py create mode 100644 tests/data/preview/prefer_rhs_split_reformatted.py diff --git a/CHANGES.md b/CHANGES.md index 03c7a286771..e1ad5e1f1cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -73,6 +73,8 @@ present) or as a single newline character (if a newline is present) (#3348) - Implicitly concatenated strings used as function args are now wrapped inside parentheses (#3307) +- For assignment statements, prefer splitting the right hand side if the left hand side + fits on a single line (#3368) - Correctly handle trailing commas that are inside a line's leading non-nested parens (#3370) diff --git a/src/black/linegen.py b/src/black/linegen.py index 244dbe77eb5..91223747618 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -2,6 +2,7 @@ Generating lines of code. """ import sys +from dataclasses import dataclass from enum import Enum, auto from functools import partial, wraps from typing import Collection, Iterator, List, Optional, Set, Union, cast @@ -24,6 +25,7 @@ from black.mode import Feature, Mode, Preview from black.nodes import ( ASSIGNMENTS, + BRACKETS, CLOSING_BRACKETS, OPENING_BRACKETS, RARROW, @@ -634,6 +636,17 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator yield result +@dataclass +class _RHSResult: + """Intermediate split result from a right hand split.""" + + head: Line + body: Line + tail: Line + opening_bracket: Leaf + closing_bracket: Leaf + + def right_hand_split( line: Line, line_length: int, @@ -648,6 +661,22 @@ def right_hand_split( Note: running this function modifies `bracket_depth` on the leaves of `line`. """ + rhs_result = _first_right_hand_split(line, omit=omit) + yield from _maybe_split_omitting_optional_parens( + rhs_result, line, line_length, features=features, omit=omit + ) + + +def _first_right_hand_split( + line: Line, + omit: Collection[LeafID] = (), +) -> _RHSResult: + """Split the line into head, body, tail starting with the last bracket pair. + + Note: this function should not have side effects. It's relied upon by + _maybe_split_omitting_optional_parens to get an opinion whether to prefer + splitting on the right side of an assignment statement. + """ tail_leaves: List[Leaf] = [] body_leaves: List[Leaf] = [] head_leaves: List[Leaf] = [] @@ -683,37 +712,71 @@ def right_hand_split( tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail ) bracket_split_succeeded_or_raise(head, body, tail) + return _RHSResult(head, body, tail, opening_bracket, closing_bracket) + + +def _maybe_split_omitting_optional_parens( + rhs: _RHSResult, + line: Line, + line_length: int, + features: Collection[Feature] = (), + omit: Collection[LeafID] = (), +) -> Iterator[Line]: if ( Feature.FORCE_OPTIONAL_PARENTHESES not in features # the opening bracket is an optional paren - and opening_bracket.type == token.LPAR - and not opening_bracket.value + and rhs.opening_bracket.type == token.LPAR + and not rhs.opening_bracket.value # the closing bracket is an optional paren - and closing_bracket.type == token.RPAR - and not closing_bracket.value + and rhs.closing_bracket.type == token.RPAR + and not rhs.closing_bracket.value # it's not an import (optional parens are the only thing we can split on # in this case; attempting a split without them is a waste of time) and not line.is_import # there are no standalone comments in the body - and not body.contains_standalone_comments(0) + and not rhs.body.contains_standalone_comments(0) # and we can actually remove the parens - and can_omit_invisible_parens(body, line_length) + and can_omit_invisible_parens(rhs.body, line_length) ): - omit = {id(closing_bracket), *omit} + omit = {id(rhs.closing_bracket), *omit} try: - yield from right_hand_split(line, line_length, features=features, omit=omit) - return + # 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 + and rhs.head.leaves[-2].type == token.EQUAL + # the left side of assignement contains brackets + and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1]) + # the left side of assignment is short enough (the -1 is for the ending + # optional paren) + and is_line_short_enough(rhs.head, line_length=line_length - 1) + # the left side of assignment won't explode further because of magic + # trailing comma + and rhs.head.magic_trailing_comma is None + # the split by omitting optional parens isn't preferred by some other + # reason + and not _prefer_split_rhs_oop(rhs_oop, line_length=line_length) + ): + yield from _maybe_split_omitting_optional_parens( + rhs_oop, line, line_length, features=features, omit=omit + ) + return except CannotSplit as e: if not ( - can_be_split(body) - or is_line_short_enough(body, line_length=line_length) + can_be_split(rhs.body) + or is_line_short_enough(rhs.body, line_length=line_length) ): raise CannotSplit( "Splitting failed, body is still too long and can't be split." ) from e - elif head.contains_multiline_strings() or tail.contains_multiline_strings(): + elif ( + rhs.head.contains_multiline_strings() + or rhs.tail.contains_multiline_strings() + ): raise CannotSplit( "The current optional pair of parentheses is bound to fail to" " satisfy the splitting algorithm because the head or the tail" @@ -721,13 +784,42 @@ def right_hand_split( " line." ) from e - ensure_visible(opening_bracket) - ensure_visible(closing_bracket) - for result in (head, body, tail): + ensure_visible(rhs.opening_bracket) + ensure_visible(rhs.closing_bracket) + for result in (rhs.head, rhs.body, rhs.tail): if result: yield result +def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool: + """ + Returns whether we should prefer the result from a split omitting optional parens. + """ + has_closing_bracket_after_assign = False + for leaf in reversed(rhs_oop.head.leaves): + if leaf.type == token.EQUAL: + break + if leaf.type in CLOSING_BRACKETS: + has_closing_bracket_after_assign = True + break + return ( + # contains matching brackets after the `=` (done by checking there is a + # closing bracket) + has_closing_bracket_after_assign + or ( + # the split is actually from inside the optional parens (done by checking + # the first line still contains the `=`) + any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves) + # the first line is short enough + and is_line_short_enough(rhs_oop.head, line_length=line_length) + ) + # contains unsplittable type ignore + or rhs_oop.head.contains_unsplittable_type_ignore() + or rhs_oop.body.contains_unsplittable_type_ignore() + or rhs_oop.tail.contains_unsplittable_type_ignore() + ) + + def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None: """Raise :exc:`CannotSplit` if the last left- or right-hand split failed. diff --git a/src/black/mode.py b/src/black/mode.py index bcd35b4d4be..a104d1b9862 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -155,6 +155,7 @@ class Preview(Enum): long_docstring_quotes_on_newline = auto() normalize_docstring_quotes_and_prefixes_properly = auto() one_element_subscript = auto() + prefer_splitting_right_hand_side_of_assignments = auto() remove_block_trailing_newline = auto() remove_redundant_parens = auto() # NOTE: string_processing requires wrap_long_dict_values_in_parens diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 6d56dcc635d..8b8fc179147 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -983,9 +983,9 @@ def xxxxxxx_xxxxxx(xxxx): ) -value.__dict__[ - key -] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +value.__dict__[key] = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) RE_ONE_BACKSLASH = { "asdf_hjkl_jkl": re.compile( diff --git a/tests/data/preview/prefer_rhs_split.py b/tests/data/preview/prefer_rhs_split.py new file mode 100644 index 00000000000..5b89113e618 --- /dev/null +++ b/tests/data/preview/prefer_rhs_split.py @@ -0,0 +1,85 @@ +first_item, second_item = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) +) + +some_dict["with_a_long_key"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) +) + +# Make sure it works when the RHS only has one pair of (optional) parens. +first_item, second_item = ( + some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +) + +some_dict["with_a_long_key"] = ( + some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +) + +# Make sure chaining assignments work. +first_item, second_item, third_item, forth_item = m["everything"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) +) + +# Make sure when the RHS's first split at the non-optional paren fits, +# we split there instead of the outer RHS optional paren. +first_item, second_item = some_looooooooong_module.some_loooooog_function_name( + first_argument, second_argument, third_argument +) + +( + first_item, + second_item, + third_item, + forth_item, + fifth_item, + last_item_very_loooooong, +) = some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument +) + +( + first_item, + second_item, + third_item, + forth_item, + fifth_item, + last_item_very_loooooong, +) = everyting = some_loooooog_function_name( + first_argument, second_argument, third_argument +) + + +# Make sure unsplittable type ignore won't be moved. +some_kind_of_table[some_key] = util.some_function( # type: ignore # noqa: E501 + some_arg +).intersection(pk_cols) + +some_kind_of_table[ + some_key +] = lambda obj: obj.some_long_named_method() # type: ignore # noqa: E501 + +some_kind_of_table[ + some_key # type: ignore # noqa: E501 +] = lambda obj: obj.some_long_named_method() + + +# Make when when the left side of assignement plus the opening paren "... = (" is +# exactly line length limit + 1, it won't be split like that. +xxxxxxxxx_yyy_zzzzzzzz[ + xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) +] = 1 + + +# Right side of assignment contains un-nested pairs of inner parens. +some_kind_of_instance.some_kind_of_map[a_key] = ( + isinstance(some_var, SomeClass) + and table.something_and_something != table.something_else +) or ( + isinstance(some_other_var, BaseClass) and table.something != table.some_other_thing +) diff --git a/tests/data/preview/prefer_rhs_split_reformatted.py b/tests/data/preview/prefer_rhs_split_reformatted.py new file mode 100644 index 00000000000..781e75be0aa --- /dev/null +++ b/tests/data/preview/prefer_rhs_split_reformatted.py @@ -0,0 +1,38 @@ +# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. + +# Left hand side fits in a single line but will still be exploded by the +# magic trailing comma. +first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( + arg1, + arg2, +) + +# Make when when the left side of assignement plus the opening paren "... = (" is +# exactly line length limit + 1, it won't be split like that. +xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1 + + +# output + + +# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. + +# Left hand side fits in a single line but will still be exploded by the +# magic trailing comma. +( + first_value, + ( + m1, + m2, + ), + third_value, +) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( + arg1, + arg2, +) + +# Make when when the left side of assignement plus the opening paren "... = (" is +# exactly line length limit + 1, it won't be split like that. +xxxxxxxxx_yyy_zzzzzzzz[ + xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) +] = 1 From 78163939f157d9e18a8c0528fc5e1c58b1c1e69c Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 16 Dec 2022 05:02:41 -0800 Subject: [PATCH 10/11] Fix an infinite recursion error exposed by #3440 (#3444) --- src/black/linegen.py | 6 +++-- tests/data/preview/long_dict_values.py | 37 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 91223747618..fe6ea11c501 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1417,8 +1417,10 @@ def run_transformer( result.extend(transform_line(transformed_line, mode=mode, features=features)) + features_set = set(features) if ( - transform.__class__.__name__ != "rhs" + Feature.FORCE_OPTIONAL_PARENTHESES in features_set + or transform.__class__.__name__ != "rhs" or not line.bracket_tracker.invisible or any(bracket.value for bracket in line.bracket_tracker.invisible) or line.contains_multiline_strings() @@ -1435,7 +1437,7 @@ def run_transformer( line_copy = line.clone() append_leaves(line_copy, line, line.leaves) - features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES} + features_fop = features_set | {Feature.FORCE_OPTIONAL_PARENTHESES} second_opinion = run_transformer( line_copy, transform, mode, features_fop, line_str=line_str ) diff --git a/tests/data/preview/long_dict_values.py b/tests/data/preview/long_dict_values.py index f23c5d3dad1..4c515180028 100644 --- a/tests/data/preview/long_dict_values.py +++ b/tests/data/preview/long_dict_values.py @@ -17,6 +17,24 @@ "a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value") } +{ + 'xxxxxx': + xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx( + xxxxxxxxxxxxxx={ + 'x': + xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + .xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + .xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={ + 'x': x.xx, + 'x': x.x, + })))) + }), +} + # output @@ -51,3 +69,22 @@ .third_call(some_args="some value") ) } + +{ + "xxxxxx": xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx( + xxxxxxxxxxxxxx={ + "x": xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={ + "x": x.xx, + "x": x.x, + } + ) + ) + ) + ) + } + ), +} From c0089ef19dd12f872c581f106b1236c46d609955 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 17 Dec 2022 09:19:45 -0800 Subject: [PATCH 11/11] Remove separate 3.11 CI now deps support 3.11 (#3446) * Remove separate 3.11 CI now deps support 3.11 - We can run everything now like all other stable versions of Python - test in a 3.11 vent: `/tmp/tb/bin/tox -e py311,ci-py311` ``` py311: OK (28.99=setup[7.90]+cmd[5.29,0.66,6.94,6.08,1.89,0.24] seconds) ci-py311: OK (30.33=setup[3.20]+cmd[3.66,0.31,17.43,4.60,0.90,0.23] seconds) congratulations :) (59.35 seconds) ``` * Add to CHANGES.md * Add fuzz run in 3.11 --- .github/workflows/fuzz.yml | 2 +- .github/workflows/test-311.yml | 57 ---------------------------------- .github/workflows/test.yml | 2 +- CHANGES.md | 2 ++ 4 files changed, 4 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/test-311.yml diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index ebb8a9fda9e..373e1500ee9 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test-311.yml b/.github/workflows/test-311.yml deleted file mode 100644 index c2da2465ad5..00000000000 --- a/.github/workflows/test-311.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Test 3.11 without aiohttp extensions - -on: - push: - paths-ignore: - - "docs/**" - - "*.md" - - pull_request: - paths-ignore: - - "docs/**" - - "*.md" - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -jobs: - main: - # We want to run on external PRs, but not on our own internal PRs as they'll be run - # by the push to the branch. Without this if check, checks are duplicated since - # internal PRs match both the push and pull_request events. - if: - github.event_name == 'push' || github.event.pull_request.head.repo.full_name != - github.repository - - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.11.0-rc - 3.11"] - os: [ubuntu-latest, macOS-latest, windows-latest] - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install tox - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade tox - - - name: Run tests via tox - run: | - python -m tox -e 311 - - - name: Format ourselves - run: | - python -m pip install . - python -m black --check src/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 372d1fd5d38..3ca2a469147 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.7", "pypy-3.8"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/CHANGES.md b/CHANGES.md index e1ad5e1f1cc..ba71ee6c99d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -57,6 +57,8 @@ +- Move 3.11 CI to normal flow now all dependencies support 3.11 (#3446) + ### Documentation