From 9b91638190342cf5a66d4edb11068526f7ebda59 Mon Sep 17 00:00:00 2001 From: Semen Zhydenko Date: Mon, 26 Dec 2022 03:39:51 +0100 Subject: [PATCH 01/16] Fix some typos (#3474) --- docs/contributing/issue_triage.md | 2 +- tests/data/preview/prefer_rhs_split.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/issue_triage.md b/docs/contributing/issue_triage.md index 9b987fb..865a479 100644 --- a/docs/contributing/issue_triage.md +++ b/docs/contributing/issue_triage.md @@ -42,7 +42,7 @@ The lifecycle of a bug report or user support issue typically goes something lik 1. _the issue is waiting for triage_ 2. **identified** - has been marked with a type label and other relevant labels, more details or a functional reproduction may be still needed (and therefore should be - marked with `S: needs repro` or `S: awaiting reponse`) + marked with `S: needs repro` or `S: awaiting response`) 3. **confirmed** - the issue can reproduced and necessary details have been provided 4. **discussion** - initial triage has been done and now the general details on how the issue should be best resolved are being hashed out diff --git a/tests/data/preview/prefer_rhs_split.py b/tests/data/preview/prefer_rhs_split.py index 5b89113..2f3cf33 100644 --- a/tests/data/preview/prefer_rhs_split.py +++ b/tests/data/preview/prefer_rhs_split.py @@ -50,7 +50,7 @@ first_item, second_item = some_looooooooong_module.some_loooooog_function_name( forth_item, fifth_item, last_item_very_loooooong, -) = everyting = some_loooooog_function_name( +) = everything = some_looooong_function_name( first_argument, second_argument, third_argument ) -- 2.39.5 From 72a3408965f944f39f1080a5b67c25790acdc4e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 06:32:42 -0800 Subject: [PATCH 02/16] Bump pypa/cibuildwheel from 2.11.3 to 2.11.4 (#3475) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.11.3 to 2.11.4. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.11.3...v2.11.4) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 7fd760e..ee1c1fa 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -59,7 +59,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.11.3 + uses: pypa/cibuildwheel@v2.11.4 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" -- 2.39.5 From 0abe85eebb94e7640aa5d443aefe5b9bed507bfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:00:23 -0800 Subject: [PATCH 03/16] Bump peter-evans/find-comment from 2.1.0 to 2.2.0 (#3476) Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/peter-evans/find-comment/releases) - [Commits](https://github.com/peter-evans/find-comment/compare/f4499a714d59013c74a08789b48abe4b704364a0...81e2da3af01c92f83cb927cf3ace0e085617c556) --- updated-dependencies: - dependency-name: peter-evans/find-comment dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 1375be9..26d0609 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -33,7 +33,7 @@ jobs: - name: Try to find pre-existing PR comment if: steps.metadata.outputs.needs-comment == 'true' id: find-comment - uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 + uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 with: issue-number: ${{ steps.metadata.outputs.pr-number }} comment-author: "github-actions[bot]" -- 2.39.5 From 4e3303fa08e030722d6fd4d7fe7b8d44ef98991c Mon Sep 17 00:00:00 2001 From: Jordan Ephron Date: Thu, 29 Dec 2022 18:13:15 -0500 Subject: [PATCH 04/16] Parenthesize conditional expressions (#2278) Co-authored-by: Jordan Ephron Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/__init__.py | 16 ++- src/black/linegen.py | 16 +++ src/black/mode.py | 1 + tests/data/conditional_expression.py | 160 +++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 tests/data/conditional_expression.py diff --git a/CHANGES.md b/CHANGES.md index 587ca8a..2da0fb4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ +- Add parentheses around `if`-`else` expressions (#2278) - Improve the performance on large expressions that contain many strings (#3467) - Fix a crash in preview style with assert + parenthesized string (#3415) - Fix crashes in preview style with walrus operators used in function return annotations diff --git a/src/black/__init__.py b/src/black/__init__.py index f00749a..9f44722 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -478,16 +478,20 @@ def main( # noqa: C901 ) normalized = [ - (source, source) - if source == "-" - else (normalize_path_maybe_ignore(Path(source), root), source) + ( + (source, source) + if source == "-" + else (normalize_path_maybe_ignore(Path(source), root), source) + ) for source in src ] srcs_string = ", ".join( [ - f'"{_norm}"' - if _norm - else f'\033[31m"{source} (skipping - invalid)"\033[34m' + ( + f'"{_norm}"' + if _norm + else f'\033[31m"{source} (skipping - invalid)"\033[34m' + ) for _norm, source in normalized ] ) diff --git a/src/black/linegen.py b/src/black/linegen.py index 2e75bc9..4da75b2 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -140,6 +140,22 @@ class LineGenerator(Visitor[Line]): self.current_line.append(node) yield from super().visit_default(node) + def visit_test(self, node: Node) -> Iterator[Line]: + """Visit an `x if y else z` test""" + + if Preview.parenthesize_conditional_expressions in self.mode: + already_parenthesized = ( + node.prev_sibling and node.prev_sibling.type == token.LPAR + ) + + if not already_parenthesized: + lpar = Leaf(token.LPAR, "") + rpar = Leaf(token.RPAR, "") + node.insert_child(0, lpar) + node.append_child(rpar) + + yield from self.visit_default(node) + def visit_INDENT(self, node: Leaf) -> Iterator[Line]: """Increase indentation level, maybe yield a line.""" # In blib2to3 INDENT never holds comments. diff --git a/src/black/mode.py b/src/black/mode.py index a104d1b..775805a 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -161,6 +161,7 @@ class Preview(Enum): # NOTE: string_processing requires wrap_long_dict_values_in_parens # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() + parenthesize_conditional_expressions = auto() skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() diff --git a/tests/data/conditional_expression.py b/tests/data/conditional_expression.py new file mode 100644 index 0000000..620a12d --- /dev/null +++ b/tests/data/conditional_expression.py @@ -0,0 +1,160 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) + +# output + +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) -- 2.39.5 From 37542e64855ce21bd580f973ae5ce1ed86812a7a Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 31 Dec 2022 01:52:35 -0500 Subject: [PATCH 05/16] Fail lint CI if the PR doesn't target main (#3477) Let's skip the check if we're running on a fork just in case someone opens a PR against a branch on said fork as part of a PR review upstream. --- .github/workflows/lint.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90c4801..064d474 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,6 +16,13 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Assert PR target is main + if: github.event_name == 'pull_request' && github.repository == 'psf/black' + run: | + if [ "$GITHUB_BASE_REF" != "main" ]; then + echo "::error::PR targeting '$GITHUB_BASE_REF', please refile targeting 'main'." && exit 1 + fi + - name: Set up latest Python uses: actions/setup-python@v4 with: -- 2.39.5 From 5d0d5936db2ed7a01c50a374e32753e1afe9cc71 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Mon, 2 Jan 2023 09:43:48 -0500 Subject: [PATCH 06/16] Add email for Richard Si (#3478) --- AUTHORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index a635e8c..ab3f30b 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -10,7 +10,7 @@ Maintained with: - [Mika Naylor](mailto:mail@autophagy.io) - [Zsolt Dollenstein](mailto:zsol.zsol@gmail.com) - [Cooper Lees](mailto:me@cooperlees.com) -- Richard Si +- [Richard Si](mailto:sichard26@gmail.com) - [Felix Hildén](mailto:felix.hilden@gmail.com) - [Batuhan Taskaya](mailto:batuhan@python.org) -- 2.39.5 From 4bee9cca5553c55493203822b5a112ec5216bc74 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 11 Jan 2023 16:19:27 -0300 Subject: [PATCH 07/16] Remove misleading phrase in Usage and Configuration (#3492) The CLI options were already shown in the "Command line options" in the same page. --- docs/usage_and_configuration/the_basics.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 3dab644..9dc5277 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -40,6 +40,9 @@ so style options are deliberately limited and rarely added. +Note that all command-line options listed above can also be configured using a +`pyproject.toml` file (more on that below). + ### Code input alternatives #### Standard Input @@ -287,9 +290,6 @@ file hierarchy. ## Next steps -You've probably noted that not all of the options you can pass to _Black_ have been -covered. Don't worry, the rest will be covered in a later section. - A good next step would be configuring auto-discovery so `black .` is all you need instead of laborously listing every file or directory. You can get started by heading over to [File collection and discovery](./file_collection_and_discovery.md). -- 2.39.5 From f7580103407743a317e22297793822dd91f8fefe Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Sat, 14 Jan 2023 09:51:59 -0800 Subject: [PATCH 08/16] Documentation: clarify the state of multiple context managers (#3488) Clarify that the backslash & paren-wrapping formatting for multiple context managers aren't yet implemented. --- docs/the_black_code_style/future_style.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 17b7eef..9ca260f 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -19,7 +19,7 @@ with make_context_manager1() as cm1, make_context_manager2() as cm2, make_contex ... # nothing to split on - line too long ``` -So _Black_ will eventually format it like this: +So _Black_ will, when we implement this, format it like this: ```py3 with \ @@ -31,8 +31,8 @@ with \ ... # backslashes and an ugly stranded colon ``` -Although when the target version is Python 3.9 or higher, _Black_ will use parentheses -instead since they're allowed in Python 3.9 and higher. +Although when the target version is Python 3.9 or higher, _Black_ will, when we +implement this, use parentheses instead since they're allowed in Python 3.9 and higher. An alternative to consider if the backslashes in the above formatting are undesirable is to use {external:py:obj}`contextlib.ExitStack` to combine context managers in the -- 2.39.5 From d4ff985853c8d140d73b9d362604deedb41eb20e Mon Sep 17 00:00:00 2001 From: Ruslan <7631314+ruslaniv@users.noreply.github.com> Date: Sun, 15 Jan 2023 01:32:00 +0700 Subject: [PATCH 09/16] Add IntelliJ docs on external tools and file watcher (#3365) Revert deleted documentation on setting up Black using IntelliJ external tool or file watcher utilities. These are still worth keeping because some peole might not want to use a third-party plugin or install Blackd's extra dependencies. Co-authored-by: Richard Si --- docs/integrations/editors.md | 106 +++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index a8b7978..74c6a28 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -10,6 +10,19 @@ Options include the following: ## PyCharm/IntelliJ IDEA +There are three different ways you can use _Black_ from PyCharm: + +1. As local server using the BlackConnect plugin +1. As external tool +1. As file watcher + +The first option is the simplest to set up and formats the fastest (by spinning up +{doc}`Black's HTTP server `, avoiding the +startup cost on subsequent formats), but if you would prefer to not install a +third-party plugin or blackd's extra dependencies, the other two are also great options. + +### As local server + 1. Install _Black_ with the `d` extra. ```console @@ -46,6 +59,99 @@ Options include the following: - In `Trigger Settings` section of plugin configuration check `Trigger when saving changed files`. +### As external tool + +1. Install `black`. + + ```console + $ pip install black + ``` + +1. Locate your `black` installation folder. + + On macOS / Linux / BSD: + + ```console + $ which black + /usr/local/bin/black # possible location + ``` + + On Windows: + + ```console + $ where black + %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location + ``` + + Note that if you are using a virtual environment detected by PyCharm, this is an + unneeded step. In this case the path to `black` is `$PyInterpreterDirectory$/black`. + +1. Open External tools in PyCharm/IntelliJ IDEA + + On macOS: + + `PyCharm -> Preferences -> Tools -> External Tools` + + On Windows / Linux / BSD: + + `File -> Settings -> Tools -> External Tools` + +1. Click the + icon to add a new external tool with the following values: + + - Name: Black + - Description: Black is the uncompromising Python code formatter. + - Program: \ + - Arguments: `"$FilePath$"` + +1. Format the currently opened file by selecting `Tools -> External Tools -> black`. + + - Alternatively, you can set a keyboard shortcut by navigating to + `Preferences or Settings -> Keymap -> External Tools -> External Tools - Black`. + +### As file watcher + +1. Install `black`. + + ```console + $ pip install black + ``` + +1. Locate your `black` installation folder. + + On macOS / Linux / BSD: + + ```console + $ which black + /usr/local/bin/black # possible location + ``` + + On Windows: + + ```console + $ where black + %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location + ``` + + Note that if you are using a virtual environment detected by PyCharm, this is an + unneeded step. In this case the path to `black` is `$PyInterpreterDirectory$/black`. + +1. Make sure you have the + [File Watchers](https://plugins.jetbrains.com/plugin/7177-file-watchers) plugin + installed. +1. Go to `Preferences or Settings -> Tools -> File Watchers` and click `+` to add a new + watcher: + - Name: Black + - File type: Python + - Scope: Project Files + - Program: \ + - Arguments: `$FilePath$` + - Output paths to refresh: `$FilePath$` + - Working directory: `$ProjectFileDir$` + +- In Advanced Options + - Uncheck "Auto-save edited files to trigger the watcher" + - Uncheck "Trigger the watcher on external changes" + ## Wing IDE Wing IDE supports `black` via **Preference Settings** for system wide settings and -- 2.39.5 From 60a2e8e2c26d6312cd86b40f680c5037571acafc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 16 Jan 2023 12:26:03 -0800 Subject: [PATCH 10/16] Fix two docstring crashes (#3451) --- CHANGES.md | 1 + src/black/linegen.py | 4 ++++ tests/data/miscellaneous/linelength6.py | 5 +++++ tests/data/simple_cases/docstring.py | 11 +++++++++++ tests/test_format.py | 7 +++++++ 5 files changed, 28 insertions(+) create mode 100644 tests/data/miscellaneous/linelength6.py diff --git a/CHANGES.md b/CHANGES.md index 2da0fb4..17dc0d6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,7 @@ - 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) +- Fix two crashes in preview style involving edge cases with docstrings (#3451) - Exclude string type annotations from improved string processing; fix crash when the return type annotation is stringified and spans across multiple lines (#3462) diff --git a/src/black/linegen.py b/src/black/linegen.py index 4da75b2..da41886 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -401,6 +401,7 @@ class LineGenerator(Visitor[Line]): else: docstring = docstring.strip() + has_trailing_backslash = False if docstring: # Add some padding if the docstring starts / ends with a quote mark. if docstring[0] == quote_char: @@ -413,6 +414,7 @@ class LineGenerator(Visitor[Line]): # Odd number of tailing backslashes, add some padding to # avoid escaping the closing string quote. docstring += " " + has_trailing_backslash = True elif not docstring_started_empty: docstring = " " @@ -435,6 +437,8 @@ class LineGenerator(Visitor[Line]): if ( len(lines) > 1 and last_line_length + quote_len > self.mode.line_length + and len(indent) + quote_len <= self.mode.line_length + and not has_trailing_backslash ): leaf.value = prefix + quote + docstring + "\n" + indent + quote else: diff --git a/tests/data/miscellaneous/linelength6.py b/tests/data/miscellaneous/linelength6.py new file mode 100644 index 0000000..4fb3427 --- /dev/null +++ b/tests/data/miscellaneous/linelength6.py @@ -0,0 +1,5 @@ +# Regression test for #3427, which reproes only with line length <= 6 +def f(): + """ + x + """ diff --git a/tests/data/simple_cases/docstring.py b/tests/data/simple_cases/docstring.py index f08bba5..c31d6a6 100644 --- a/tests/data/simple_cases/docstring.py +++ b/tests/data/simple_cases/docstring.py @@ -173,6 +173,11 @@ def multiline_backslash_2(): ''' hey there \ ''' +# Regression test for #3425 +def multiline_backslash_really_long_dont_crash(): + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + def multiline_backslash_3(): ''' @@ -391,6 +396,12 @@ def multiline_backslash_2(): hey there \ """ +# Regression test for #3425 +def multiline_backslash_really_long_dont_crash(): + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + + def multiline_backslash_3(): """ already escaped \\""" diff --git a/tests/test_format.py b/tests/test_format.py index 01cd61e..0816bbd 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -146,6 +146,13 @@ def test_docstring_no_string_normalization() -> None: assert_format(source, expected, mode) +def test_docstring_line_length_6() -> None: + """Like test_docstring but with line length set to 6.""" + source, expected = read_data("miscellaneous", "linelength6") + mode = black.Mode(line_length=6) + assert_format(source, expected, mode) + + def test_preview_docstring_no_string_normalization() -> None: """ Like test_docstring but with string normalization off *and* the preview style -- 2.39.5 From 24469c9bd14c3ddb4739f24c661aeb725f2decd5 Mon Sep 17 00:00:00 2001 From: Bartosz Sokorski Date: Wed, 18 Jan 2023 03:01:03 +0100 Subject: [PATCH 11/16] Add flake8-bugbear B907 to ignore list (#3503) --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index eddaaba..7bc346a 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] # B905 should be enabled when we drop support for 3.9 -ignore = E203, E266, E501, W503, B905 +ignore = E203, E266, E501, W503, B905, B907 # line length is intentionally set to 80 here because black uses Bugbear # See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details max-line-length = 80 -- 2.39.5 From 7e6d3fac197395b0a2b380cc60811536fe23626b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 17 Jan 2023 22:25:05 -0800 Subject: [PATCH 12/16] Fix crash with walrus + await + with (#3473) Fixes #3472 --- CHANGES.md | 2 + src/black/linegen.py | 4 ++ src/black/nodes.py | 11 +++++ .../data/fast/pep_572_do_not_remove_parens.py | 4 ++ tests/data/py_38/pep_572_remove_parens.py | 40 +++++++++++++++++++ 5 files changed, 61 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 17dc0d6..97b68b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,8 @@ - Fix two crashes in preview style involving edge cases with docstrings (#3451) - Exclude string type annotations from improved string processing; fix crash when the return type annotation is stringified and spans across multiple lines (#3462) +- Fix several crashes in preview style with walrus operators used in `with` statements + or tuples (#3473) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index da41886..14f8511 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -46,6 +46,7 @@ from black.nodes import ( is_rpar_token, is_stub_body, is_stub_suite, + is_tuple_containing_walrus, is_vararg, is_walrus_assignment, is_yield, @@ -1279,6 +1280,7 @@ def maybe_make_parens_invisible_in_atom( not remove_brackets_around_comma and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY ) + or is_tuple_containing_walrus(node) ): return False @@ -1290,9 +1292,11 @@ def maybe_make_parens_invisible_in_atom( syms.return_stmt, syms.except_clause, syms.funcdef, + syms.with_stmt, # these ones aren't useful to end users, but they do please fuzzers syms.for_stmt, syms.del_stmt, + syms.for_stmt, ]: return False diff --git a/src/black/nodes.py b/src/black/nodes.py index a11fb7c..a588077 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -563,6 +563,17 @@ def is_one_tuple(node: LN) -> bool: ) +def is_tuple_containing_walrus(node: LN) -> bool: + """Return True if `node` holds a tuple that contains a walrus operator.""" + if node.type != syms.atom: + return False + gexp = unwrap_singleton_parenthesis(node) + if gexp is None or gexp.type != syms.testlist_gexp: + return False + + return any(child.type == syms.namedexpr_test for child in gexp.children) + + def is_one_sequence_between( opening: Leaf, closing: Leaf, diff --git a/tests/data/fast/pep_572_do_not_remove_parens.py b/tests/data/fast/pep_572_do_not_remove_parens.py index 20e80a6..05619dd 100644 --- a/tests/data/fast/pep_572_do_not_remove_parens.py +++ b/tests/data/fast/pep_572_do_not_remove_parens.py @@ -19,3 +19,7 @@ with (y := [3, 2, 1]) as (funfunfun := indeed): @(please := stop) def sigh(): pass + + +for (x := 3, y := 4) in y: + pass diff --git a/tests/data/py_38/pep_572_remove_parens.py b/tests/data/py_38/pep_572_remove_parens.py index 9718d95..4e95fb0 100644 --- a/tests/data/py_38/pep_572_remove_parens.py +++ b/tests/data/py_38/pep_572_remove_parens.py @@ -49,6 +49,26 @@ def a(): def this_is_so_dumb() -> (please := no): pass +async def await_the_walrus(): + with (x := y): + pass + + with (x := y) as z, (a := b) as c: + pass + + with (x := await y): + pass + + with (x := await a, y := await b): + pass + + # Ideally we should remove one set of parentheses + with ((x := await a, y := await b)): + pass + + with (x := await a), (y := await b): + pass + # output if foo := 0: @@ -103,3 +123,23 @@ def a(): def this_is_so_dumb() -> (please := no): pass + +async def await_the_walrus(): + with (x := y): + pass + + with (x := y) as z, (a := b) as c: + pass + + with (x := await y): + pass + + with (x := await a, y := await b): + pass + + # Ideally we should remove one set of parentheses + with ((x := await a, y := await b)): + pass + + with (x := await a), (y := await b): + pass -- 2.39.5 From 18fb88486d434dbde9b2a9c98f008a71cf5d941d Mon Sep 17 00:00:00 2001 From: Antonio Ossa-Guerra Date: Wed, 18 Jan 2023 23:38:27 -0300 Subject: [PATCH 13/16] Fix false symlink detection claims in verbose output (#3385) When trying to format a project from the outside, the verbose output shows says that there are symbolic links that points outside of the project, but displays the wrong project path, meaning that these messages are false positives. This bug is triggered when the command is executed from outside a project on a folder inside it, causing an inconsistency between the path to the detected project root and the relative path to the target contents. The fix is to normalize the target path using the project root before processing the sources, which removes the presence of the incorrect messages. --- The test attemps to emulate the behavior of the CLI as closely as posible by patching some `pathlib.Path` methods and passing certain reference paths to the context object and `black.get_sources`. Before the associated fix was introduced, this test failed because some of the captured files reported the presence of a symlink due to an incorrectly formated path. The test also asserts that only a single file is reported as ignored, which is part of the expected behavior. Signed-off-by: Antonio Ossa Guerra --- CHANGES.md | 2 ++ src/black/__init__.py | 3 ++- tests/test_black.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 97b68b9..313536e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -66,6 +66,8 @@ - Verbose logging now shows the values of `pyproject.toml` configuration variables (#3392) +- Fix false symlink detection messages in verbose output due to using an incorrect + relative path to the project root (#3385) ### _Blackd_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 9f44722..5d35c80 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -673,10 +673,11 @@ def get_sources( sources.add(p) elif p.is_dir(): + p = root / normalize_path_maybe_ignore(p, ctx.obj["root"], report) if using_default_exclude: gitignore = { root: root_gitignore, - root / p: get_gitignore(p), + p: get_gitignore(p), } sources.update( gen_python_files( diff --git a/tests/test_black.py b/tests/test_black.py index dda1055..44d6172 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -475,6 +475,53 @@ class BlackTestCase(BlackBaseTestCase): self.assertFormatEqual(contents_spc, fs(contents_spc)) self.assertFormatEqual(contents_spc, fs(contents_tab)) + def test_false_positive_symlink_output_issue_3384(self) -> None: + # Emulate the behavior when using the CLI (`black ./child --verbose`), which + # involves patching some `pathlib.Path` methods. In particular, `is_dir` is + # patched only on its first call: when checking if "./child" is a directory it + # should return True. The "./child" folder exists relative to the cwd when + # running from CLI, but fails when running the tests because cwd is different + project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests") + working_directory = project_root / "root" + target_abspath = working_directory / "child" + target_contents = ( + src.relative_to(working_directory) for src in target_abspath.iterdir() + ) + + def mock_n_calls(responses: List[bool]) -> Callable[[], bool]: + def _mocked_calls() -> bool: + if responses: + return responses.pop(0) + return False + + return _mocked_calls + + with patch("pathlib.Path.iterdir", return_value=target_contents), patch( + "pathlib.Path.cwd", return_value=working_directory + ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])): + ctx = FakeContext() + ctx.obj["root"] = project_root + report = MagicMock(verbose=True) + black.get_sources( + ctx=ctx, + src=("./child",), + quiet=False, + verbose=True, + include=DEFAULT_INCLUDE, + exclude=None, + report=report, + extend_exclude=None, + force_exclude=None, + stdin_filename=None, + ) + assert not any( + mock_args[1].startswith("is a symbolic link that points outside") + for _, mock_args, _ in report.path_ignored.mock_calls + ), "A symbolic link was reported." + report.path_ignored.assert_called_once_with( + Path("child", "b.py"), "matches a .gitignore file content" + ) + def test_report_verbose(self) -> None: report = Report(verbose=True) out_lines = [] -- 2.39.5 From 91e1e1328aa0a11ef50017316ff97149886e1b05 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 20 Jan 2023 04:14:05 -0800 Subject: [PATCH 14/16] Wrap multiple context managers in parentheses when targeting Python 3.9+ (#3489) --- CHANGES.md | 1 + src/black/__init__.py | 28 ++++- src/black/linegen.py | 101 +++++++++++++---- src/black/mode.py | 5 + .../auto_detect/features_3_10.py | 35 ++++++ .../auto_detect/features_3_11.py | 37 +++++++ .../auto_detect/features_3_8.py | 30 +++++ .../auto_detect/features_3_9.py | 34 ++++++ .../targeting_py38.py | 38 +++++++ .../targeting_py39.py | 104 ++++++++++++++++++ tests/test_format.py | 24 ++++ 11 files changed, 416 insertions(+), 21 deletions(-) create mode 100644 tests/data/preview_context_managers/auto_detect/features_3_10.py create mode 100644 tests/data/preview_context_managers/auto_detect/features_3_11.py create mode 100644 tests/data/preview_context_managers/auto_detect/features_3_8.py create mode 100644 tests/data/preview_context_managers/auto_detect/features_3_9.py create mode 100644 tests/data/preview_context_managers/targeting_py38.py create mode 100644 tests/data/preview_context_managers/targeting_py39.py diff --git a/CHANGES.md b/CHANGES.md index 313536e..1450278 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,7 @@ - Fix two crashes in preview style involving edge cases with docstrings (#3451) - Exclude string type annotations from improved string processing; fix crash when the return type annotation is stringified and spans across multiple lines (#3462) +- Wrap multiple context managers in parentheses when targeting Python 3.9+ (#3489) - Fix several crashes in preview style with walrus operators used in `with` statements or tuples (#3473) diff --git a/src/black/__init__.py b/src/black/__init__.py index 5d35c80..daf6f88 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1096,8 +1096,13 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: future_imports = get_future_imports(src_node) versions = detect_target_versions(src_node, future_imports=future_imports) + context_manager_features = { + feature + for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + if supports_feature(versions, feature) + } normalize_fmt_off(src_node, preview=mode.preview) - lines = LineGenerator(mode=mode) + lines = LineGenerator(mode=mode, features=context_manager_features) elt = EmptyLineTracker(mode=mode) split_line_features = { feature @@ -1159,6 +1164,10 @@ def get_features_used( # noqa: C901 - relaxed decorator syntax; - usage of __future__ flags (annotations); - print / exec statements; + - parenthesized context managers; + - match statements; + - except* clause; + - variadic generics; """ features: Set[Feature] = set() if future_imports: @@ -1234,6 +1243,23 @@ def get_features_used( # noqa: C901 ): features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) + elif ( + n.type == syms.with_stmt + and len(n.children) > 2 + and n.children[1].type == syms.atom + ): + atom_children = n.children[1].children + if ( + len(atom_children) == 3 + and atom_children[0].type == token.LPAR + and atom_children[1].type == syms.testlist_gexp + and atom_children[2].type == token.RPAR + ): + features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) + + elif n.type == syms.match_stmt: + features.add(Feature.PATTERN_MATCHING) + elif ( n.type == syms.except_clause and len(n.children) >= 2 diff --git a/src/black/linegen.py b/src/black/linegen.py index 14f8511..2f50257 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -90,8 +90,9 @@ class LineGenerator(Visitor[Line]): in ways that will no longer stringify to valid Python code on the tree. """ - def __init__(self, mode: Mode) -> None: + def __init__(self, mode: Mode, features: Collection[Feature]) -> None: self.mode = mode + self.features = features self.current_line: Line self.__post_init__() @@ -191,7 +192,9 @@ class LineGenerator(Visitor[Line]): `parens` holds a set of string leaf values immediately after which invisible parens should be put. """ - normalize_invisible_parens(node, parens_after=parens, preview=self.mode.preview) + normalize_invisible_parens( + node, parens_after=parens, mode=self.mode, features=self.features + ) for child in node.children: if is_name_token(child) and child.value in keywords: yield from self.line() @@ -244,7 +247,9 @@ class LineGenerator(Visitor[Line]): def visit_match_case(self, node: Node) -> Iterator[Line]: """Visit either a match or case statement.""" - normalize_invisible_parens(node, parens_after=set(), preview=self.mode.preview) + normalize_invisible_parens( + node, parens_after=set(), mode=self.mode, features=self.features + ) yield from self.line() for child in node.children: @@ -1090,7 +1095,7 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: def normalize_invisible_parens( - node: Node, parens_after: Set[str], *, preview: bool + node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature] ) -> None: """Make existing optional parentheses invisible or create new ones. @@ -1100,17 +1105,24 @@ def normalize_invisible_parens( Standardizes on visible parentheses for single-element tuples, and keeps existing visible parentheses for other tuples and generator expressions. """ - for pc in list_comments(node.prefix, is_endmarker=False, preview=preview): + for pc in list_comments(node.prefix, is_endmarker=False, preview=mode.preview): if pc.value in FMT_OFF: # This `node` has a prefix with `# fmt: off`, don't mess with parens. return + + # The multiple context managers grammar has a different pattern, thus this is + # separate from the for-loop below. This possibly wraps them in invisible parens, + # and later will be removed in remove_with_parens when needed. + if node.type == syms.with_stmt: + _maybe_wrap_cms_in_parens(node, mode, features) + check_lpar = False for index, child in enumerate(list(node.children)): # Fixes a bug where invisible parens are not properly stripped from # assignment statements that contain type annotations. if isinstance(child, Node) and child.type == syms.annassign: normalize_invisible_parens( - child, parens_after=parens_after, preview=preview + child, parens_after=parens_after, mode=mode, features=features ) # Add parentheses around long tuple unpacking in assignments. @@ -1123,7 +1135,7 @@ def normalize_invisible_parens( if check_lpar: if ( - preview + mode.preview and child.type == syms.atom and node.type == syms.for_stmt and isinstance(child.prev_sibling, Leaf) @@ -1136,7 +1148,9 @@ def normalize_invisible_parens( remove_brackets_around_comma=True, ): wrap_in_parentheses(node, child, visible=False) - elif preview and isinstance(child, Node) and node.type == syms.with_stmt: + elif ( + mode.preview and isinstance(child, Node) and node.type == syms.with_stmt + ): remove_with_parens(child, node) elif child.type == syms.atom: if maybe_make_parens_invisible_in_atom( @@ -1147,17 +1161,7 @@ def normalize_invisible_parens( elif is_one_tuple(child): wrap_in_parentheses(node, child, visible=True) elif node.type == syms.import_from: - # "import from" nodes store parentheses directly as part of - # the statement - if is_lpar_token(child): - assert is_rpar_token(node.children[-1]) - # make parentheses invisible - child.value = "" - node.children[-1].value = "" - elif child.type != token.STAR: - # insert invisible parentheses - node.insert_child(index, Leaf(token.LPAR, "")) - node.append_child(Leaf(token.RPAR, "")) + _normalize_import_from(node, child, index) break elif ( index == 1 @@ -1172,13 +1176,27 @@ def normalize_invisible_parens( elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - comma_check = child.type == token.COMMA if preview else False + comma_check = child.type == token.COMMA if mode.preview else False check_lpar = isinstance(child, Leaf) and ( child.value in parens_after or comma_check ) +def _normalize_import_from(parent: Node, child: LN, index: int) -> None: + # "import from" nodes store parentheses directly as part of + # the statement + if is_lpar_token(child): + assert is_rpar_token(parent.children[-1]) + # make parentheses invisible + child.value = "" + parent.children[-1].value = "" + elif child.type != token.STAR: + # insert invisible parentheses + parent.insert_child(index, Leaf(token.LPAR, "")) + parent.append_child(Leaf(token.RPAR, "")) + + def remove_await_parens(node: Node) -> None: if node.children[0].type == token.AWAIT and len(node.children) > 1: if ( @@ -1215,6 +1233,49 @@ def remove_await_parens(node: Node) -> None: remove_await_parens(bracket_contents) +def _maybe_wrap_cms_in_parens( + node: Node, mode: Mode, features: Collection[Feature] +) -> None: + """When enabled and safe, wrap the multiple context managers in invisible parens. + + It is only safe when `features` contain Feature.PARENTHESIZED_CONTEXT_MANAGERS. + """ + 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 + ): + return + colon_index: Optional[int] = None + for i in range(2, len(node.children)): + if node.children[i].type == token.COLON: + colon_index = i + break + if colon_index is not None: + lpar = Leaf(token.LPAR, "") + rpar = Leaf(token.RPAR, "") + context_managers = node.children[1:colon_index] + for child in context_managers: + child.remove() + # After wrapping, the with_stmt will look like this: + # with_stmt + # NAME 'with' + # atom + # LPAR '' + # testlist_gexp + # ... <-- context_managers + # /testlist_gexp + # RPAR '' + # /atom + # COLON ':' + new_child = Node( + syms.atom, [lpar, Node(syms.testlist_gexp, context_managers), rpar] + ) + node.insert_child(1, new_child) + + def remove_with_parens(node: Node, parent: Node) -> None: """Recursively hide optional parens in `with` statements.""" # Removing all unnecessary parentheses in with statements in one pass is a tad diff --git a/src/black/mode.py b/src/black/mode.py index 775805a..af0706e 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -50,6 +50,7 @@ class Feature(Enum): EXCEPT_STAR = 14 VARIADIC_GENERICS = 15 DEBUG_F_STRINGS = 16 + PARENTHESIZED_CONTEXT_MANAGERS = 17 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -106,6 +107,7 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, Feature.ANN_ASSIGN_EXTENDED_RHS, + Feature.PARENTHESIZED_CONTEXT_MANAGERS, }, TargetVersion.PY310: { Feature.F_STRINGS, @@ -120,6 +122,7 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, Feature.ANN_ASSIGN_EXTENDED_RHS, + Feature.PARENTHESIZED_CONTEXT_MANAGERS, Feature.PATTERN_MATCHING, }, TargetVersion.PY311: { @@ -135,6 +138,7 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, Feature.ANN_ASSIGN_EXTENDED_RHS, + Feature.PARENTHESIZED_CONTEXT_MANAGERS, Feature.PATTERN_MATCHING, Feature.EXCEPT_STAR, Feature.VARIADIC_GENERICS, @@ -164,6 +168,7 @@ class Preview(Enum): parenthesize_conditional_expressions = auto() skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() + wrap_multiple_context_managers_in_parens = auto() class Deprecated(UserWarning): diff --git a/tests/data/preview_context_managers/auto_detect/features_3_10.py b/tests/data/preview_context_managers/auto_detect/features_3_10.py new file mode 100644 index 0000000..1458df1 --- /dev/null +++ b/tests/data/preview_context_managers/auto_detect/features_3_10.py @@ -0,0 +1,35 @@ +# This file uses pattern matching introduced in Python 3.10. + + +match http_code: + case 404: + print("Not found") + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +# output + + +# This file uses pattern matching introduced in Python 3.10. + + +match http_code: + case 404: + print("Not found") + + +with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, +): + pass diff --git a/tests/data/preview_context_managers/auto_detect/features_3_11.py b/tests/data/preview_context_managers/auto_detect/features_3_11.py new file mode 100644 index 0000000..f83c533 --- /dev/null +++ b/tests/data/preview_context_managers/auto_detect/features_3_11.py @@ -0,0 +1,37 @@ +# This file uses except* clause in Python 3.11. + + +try: + some_call() +except* Error as e: + pass + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +# output + + +# This file uses except* clause in Python 3.11. + + +try: + some_call() +except* Error as e: + pass + + +with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, +): + pass diff --git a/tests/data/preview_context_managers/auto_detect/features_3_8.py b/tests/data/preview_context_managers/auto_detect/features_3_8.py new file mode 100644 index 0000000..e05094e --- /dev/null +++ b/tests/data/preview_context_managers/auto_detect/features_3_8.py @@ -0,0 +1,30 @@ +# This file doesn't use any Python 3.9+ only grammars. + + +# Make sure parens around a single context manager don't get autodetected as +# Python 3.9+. +with (a): + pass + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +# output +# This file doesn't use any Python 3.9+ only grammars. + + +# Make sure parens around a single context manager don't get autodetected as +# Python 3.9+. +with a: + pass + + +with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: + pass diff --git a/tests/data/preview_context_managers/auto_detect/features_3_9.py b/tests/data/preview_context_managers/auto_detect/features_3_9.py new file mode 100644 index 0000000..0d28f99 --- /dev/null +++ b/tests/data/preview_context_managers/auto_detect/features_3_9.py @@ -0,0 +1,34 @@ +# This file uses parenthesized context managers introduced in Python 3.9. + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +with ( + new_new_new1() as cm1, + new_new_new2() +): + pass + + +# output +# This file uses parenthesized context managers introduced in Python 3.9. + + +with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, +): + pass + + +with new_new_new1() as cm1, new_new_new2(): + pass diff --git a/tests/data/preview_context_managers/targeting_py38.py b/tests/data/preview_context_managers/targeting_py38.py new file mode 100644 index 0000000..6ec4684 --- /dev/null +++ b/tests/data/preview_context_managers/targeting_py38.py @@ -0,0 +1,38 @@ +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2(), \ + make_context_manager3() as cm3, \ + make_context_manager4() \ +: + pass + + +with \ + new_new_new1() as cm1, \ + new_new_new2() \ +: + pass + + +# output + + +with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: + pass + + +with make_context_manager1() as cm1, make_context_manager2(), make_context_manager3() as cm3, make_context_manager4(): + pass + + +with new_new_new1() as cm1, new_new_new2(): + pass diff --git a/tests/data/preview_context_managers/targeting_py39.py b/tests/data/preview_context_managers/targeting_py39.py new file mode 100644 index 0000000..5cb8763 --- /dev/null +++ b/tests/data/preview_context_managers/targeting_py39.py @@ -0,0 +1,104 @@ +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +# Leading comment +with \ + make_context_manager1() as cm1, \ + make_context_manager2(), \ + make_context_manager3() as cm3, \ + make_context_manager4() \ +: + pass + + +with \ + new_new_new1() as cm1, \ + new_new_new2() \ +: + pass + + +with ( + new_new_new1() as cm1, + new_new_new2() +): + pass + + +# Leading comment. +with ( + # First comment. + new_new_new1() as cm1, + # Second comment. + new_new_new2() + # Last comment. +): + pass + + +with \ + this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2) as cm1, \ + this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2, looong_arg3=looong_value3, looong_arg4=looong_value4) as cm2 \ +: + pass + + +# output + + +with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, +): + pass + + +# Leading comment +with ( + make_context_manager1() as cm1, + make_context_manager2(), + make_context_manager3() as cm3, + make_context_manager4(), +): + pass + + +with new_new_new1() as cm1, new_new_new2(): + pass + + +with new_new_new1() as cm1, new_new_new2(): + pass + + +# Leading comment. +with ( + # First comment. + new_new_new1() as cm1, + # Second comment. + new_new_new2() + # Last comment. +): + pass + + +with ( + this_is_a_very_long_call( + looong_arg1=looong_value1, looong_arg2=looong_value2 + ) as cm1, + this_is_a_very_long_call( + looong_arg1=looong_value1, + looong_arg2=looong_value2, + looong_arg3=looong_value3, + looong_arg4=looong_value4, + ) as cm2, +): + pass diff --git a/tests/test_format.py b/tests/test_format.py index 0816bbd..adcbc02 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,3 +1,4 @@ +import re from dataclasses import replace from typing import Any, Iterator from unittest.mock import patch @@ -58,6 +59,29 @@ def test_preview_minimum_python_310_format(filename: str) -> None: assert_format(source, expected, mode, minimum_version=(3, 10)) +def test_preview_context_managers_targeting_py38() -> None: + source, expected = read_data("preview_context_managers", "targeting_py38.py") + mode = black.Mode(preview=True, target_versions={black.TargetVersion.PY38}) + assert_format(source, expected, mode, minimum_version=(3, 8)) + + +def test_preview_context_managers_targeting_py39() -> None: + source, expected = read_data("preview_context_managers", "targeting_py39.py") + mode = black.Mode(preview=True, target_versions={black.TargetVersion.PY39}) + assert_format(source, expected, mode, minimum_version=(3, 9)) + + +@pytest.mark.parametrize( + "filename", all_data_cases("preview_context_managers/auto_detect") +) +def test_preview_context_managers_auto_detect(filename: str) -> None: + match = re.match(r"features_3_(\d+)", filename) + assert match is not None, "Unexpected filename format: %s" % filename + source, expected = read_data("preview_context_managers/auto_detect", filename) + mode = black.Mode(preview=True) + assert_format(source, expected, mode, minimum_version=(3, int(match.group(1)))) + + # =============== # # Complex cases # ============= # -- 2.39.5 From c5df7b7d3ca5b568a81a9ccc1f647c183e8b4075 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 21 Jan 2023 02:50:00 -0500 Subject: [PATCH 15/16] Reenable macOS mypyc wheel build (#3511) Hatchling implemented a workaround for the 'technically right tag but no one understands it, including pip' issue so this should work now. --- .github/workflows/pypi_upload.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index ee1c1fa..6b3eb90 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -47,13 +47,12 @@ jobs: - os: macos-11 name: macos-x86_64 macos_arch: "x86_64" - # Only build x86_64 wheels on macos until #3312 is fixed - # - os: macos-11 - # name: macos-arm64 - # macos_arch: "arm64" - # - os: macos-11 - # name: macos-universal2 - # macos_arch: "universal2" + - os: macos-11 + name: macos-arm64 + macos_arch: "arm64" + - os: macos-11 + name: macos-universal2 + macos_arch: "universal2" steps: - uses: actions/checkout@v3 -- 2.39.5 From 1557f7d3a380ed38801f5fada27550c10f89870f Mon Sep 17 00:00:00 2001 From: Michael Eliachevitch Date: Sun, 22 Jan 2023 06:20:54 +0100 Subject: [PATCH 16/16] Use dashes for pycodestyle max line length config (#3513) The option is `max-line-length` with dashes, not underscores. The config option name is given in the output of `pycodestyle -h`, which can also be checked on https://pep8.readthedocs.io/en/stable/intro.html#example-usage-and-output: ``` Configuration: The project options are read from the [pycodestyle] section of the tox.ini file or the setup.cfg file located in any parent folder of the path(s) being processed. Allowed options are: exclude, filename, select, ignore, max-line-length, max-doc-length, hang-closing, count, format, quiet, show-pep8, show-source, statistics, verbose ``` --- docs/guides/using_black_with_other_tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 7bc0726..9356caa 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -260,7 +260,7 @@ max-line-length = "88" #### Configuration ``` -max_line_length = 88 +max-line-length = 88 ignore = E203 ``` -- 2.39.5