From a58d11225c1be59df18afe7a62bc1b1080516b94 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:50:38 -0500 Subject: [PATCH 1/4] fix(preview): Don't remove parenthesis around long dictionary values --- src/black/lines.py | 14 ++ src/black/trans.py | 16 +- .../data/cases/preview_long_dict_values_2.py | 147 ++++++++++++++++++ 3 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 tests/data/cases/preview_long_dict_values_2.py diff --git a/src/black/lines.py b/src/black/lines.py index 6b65372fb3f..1cf4d739e24 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -954,6 +954,20 @@ def can_omit_invisible_parens( ): closing_bracket = leaf + if ( + # Keep parenthesized dictionary values + len(rhs.head.leaves) >= 2 + and rhs.head.leaves[-1].type == token.LPAR + and rhs.head.leaves[-2].type == token.COLON + # Unless key is a function call with trailing comma + and not( + len(rhs.head.leaves) >= 4 + and rhs.head.leaves[-3].type == token.RPAR + and rhs.head.leaves[-4].type == token.COMMA + ) + ): + return False + bt = line.bracket_tracker if not bt.delimiters: # Without delimiters the optional parentheses are useless. diff --git a/src/black/trans.py b/src/black/trans.py index 29a978c6b71..f2c679bdc83 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -886,6 +886,7 @@ class StringParenStripper(StringTransformer): The line contains a string which is surrounded by parentheses and: - The target string is NOT the only argument to a function call. - The target string is NOT a "pointless" string. + - The target string is NOT a dictionary value. - If the target string contains a PERCENT, the brackets are not preceded or followed by an operator with higher precedence than PERCENT. @@ -933,11 +934,14 @@ def do_match(self, line: Line) -> TMatchResult: ): continue - # That LPAR should NOT be preceded by a function name or a closing - # bracket (which could be a function which returns a function or a - # list/dictionary that contains a function)... + # That LPAR should NOT be preceded by a colon (which could be a + # dictionary value), function name, or a closing bracket (which + # could be a function returning a function or a list/dictionary + # containing a function)... if is_valid_index(idx - 2) and ( - LL[idx - 2].type == token.NAME or LL[idx - 2].type in CLOSING_BRACKETS + LL[idx - 2].type == token.COLON + or LL[idx - 2].type == token.NAME + or LL[idx - 2].type in CLOSING_BRACKETS ): continue @@ -2263,12 +2267,12 @@ def do_transform( 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}, + # "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}), + # "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) diff --git a/tests/data/cases/preview_long_dict_values_2.py b/tests/data/cases/preview_long_dict_values_2.py new file mode 100644 index 00000000000..299d57c095b --- /dev/null +++ b/tests/data/cases/preview_long_dict_values_2.py @@ -0,0 +1,147 @@ +# flags: --unstable + +x = { + "xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": ( + "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx" + ) +} +x = { + "xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx", +} +x = { + "foo": (bar), + "foo": bar, + "foo": xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx, +} +x = { + "xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": ( + "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxx" + ) +} + +# Function calls as keys +tasks = { + get_key_name(foo, bar, baz,): src, + loop.run_in_executor(): src, + loop.run_in_executor(xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx): src, + loop.run_in_executor(xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxx): src, + loop.run_in_executor(): xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx +} + +# Dictionary comprehensions +tasks = { + key_name: xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx + for src in sources +} +tasks = {key_name: foobar for src in sources} +tasks = {get_key_name(foo, bar, baz,): src for src in sources} +tasks = { + get_key_name(): xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx + for src in sources +} +tasks = { + get_key_name(): foobar + for src in sources +} + +# Delimiters inside the value +def foo(): + def bar(): + x = { + common.models.DateTimeField: ( + datetime(2020, 1, 31, tzinfo=utc) + timedelta(days=i) + ), + } + x = { + common.models.DateTimeField: datetime(2020, 1, 31, tzinfo=utc) + timedelta( + days=i + ), + } + x = { + "foobar": ( + 123 + 456 + ), + } + + +# output + +x = { + "xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": ( + "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx" + ) +} +x = { + "xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": ( + "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx" + ), +} +x = { + "foo": bar, + "foo": bar, + "foo": ( + xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx + ), +} +x = { + "xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxx" +} + +# Function calls as keys +tasks = { + get_key_name( + foo, + bar, + baz, + ): src, + loop.run_in_executor(): src, + loop.run_in_executor(xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx): src, + loop.run_in_executor( + xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxx + ): src, + loop.run_in_executor(): ( + xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx + ), +} + +# Dictionary comprehensions +tasks = { + key_name: ( + xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx + ) + for src in sources +} +tasks = {key_name: foobar for src in sources} +tasks = { + get_key_name( + foo, + bar, + baz, + ): src + for src in sources +} +tasks = { + get_key_name(): ( + xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx + ) + for src in sources +} +tasks = {get_key_name(): foobar for src in sources} + + +# Delimiters inside the value +def foo(): + def bar(): + x = { + common.models.DateTimeField: ( + datetime(2020, 1, 31, tzinfo=utc) + timedelta(days=i) + ), + } + x = { + common.models.DateTimeField: ( + datetime(2020, 1, 31, tzinfo=utc) + timedelta(days=i) + ), + } + x = { + "foobar": 123 + 456, + } From 43a2bd63b1b42db44ef5bf57699b1628bab49b96 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:45:13 -0500 Subject: [PATCH 2/4] fix: Use node type instead of leaf type Signed-off-by: cobalt <61329810+RedGuy12@users.noreply.github.com> --- src/black/lines.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index 1cf4d739e24..29030dc308c 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -954,13 +954,14 @@ def can_omit_invisible_parens( ): closing_bracket = leaf + bracket = rhs.opening_bracket if ( # Keep parenthesized dictionary values - len(rhs.head.leaves) >= 2 - and rhs.head.leaves[-1].type == token.LPAR - and rhs.head.leaves[-2].type == token.COLON - # Unless key is a function call with trailing comma - and not( + bracket.parent + and bracket.parent.parent + and bracket.parent.parent.type == syms.dictsetmaker + # Unless key is a multiline function call (aka, with trailing comma) + and not ( len(rhs.head.leaves) >= 4 and rhs.head.leaves[-3].type == token.RPAR and rhs.head.leaves[-4].type == token.COMMA From 9140aa2dce538f49ceccece7e999c8226ed2c820 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 20:51:50 +0000 Subject: [PATCH 3/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/contributing/release_process.md | 9 ++++----- docs/usage_and_configuration/black_docker_image.md | 14 +++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index c66ffae8ace..2c904fb95c4 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -29,8 +29,8 @@ frequently than monthly nets rapidly diminishing returns. **You must have `write` permissions for the _Black_ repository to cut a release.** The 10,000 foot view of the release process is that you prepare a release PR and then -publish a [GitHub Release]. This triggers [release automation](#release-workflows) that -builds all release artifacts and publishes them to the various platforms we publish to. +publish a [GitHub Release]. This triggers [release automation](#release-workflows) that builds +all release artifacts and publishes them to the various platforms we publish to. We now have a `scripts/release.py` script to help with cutting the release PRs. @@ -96,9 +96,8 @@ In the end, use your best judgement and ask other maintainers for their thoughts ## Release workflows -All of _Black_'s release automation uses [GitHub Actions]. All workflows are therefore -configured using YAML files in the `.github/workflows` directory of the _Black_ -repository. +All of _Black_'s release automation uses [GitHub Actions]. All workflows are therefore configured +using YAML files in the `.github/workflows` directory of the _Black_ repository. They are triggered by the publication of a [GitHub Release]. diff --git a/docs/usage_and_configuration/black_docker_image.md b/docs/usage_and_configuration/black_docker_image.md index c97c25af328..72969b7b68a 100644 --- a/docs/usage_and_configuration/black_docker_image.md +++ b/docs/usage_and_configuration/black_docker_image.md @@ -8,16 +8,16 @@ _Black_ images with the following tags are available: - release numbers, e.g. `21.5b2`, `21.6b0`, `21.7b0` etc.\ ℹ Recommended for users who want to use a particular version of _Black_. - `latest_release` - tag created when a new version of _Black_ is released.\ - ℹ Recommended for users who want to use released versions of _Black_. It maps to [the latest release](https://github.com/psf/black/releases/latest) - of _Black_. + ℹ Recommended for users who want to use released versions of _Black_. It maps to + [the latest release](https://github.com/psf/black/releases/latest) of _Black_. - `latest_prerelease` - tag created when a new alpha (prerelease) version of _Black_ is released.\ - ℹ Recommended for users who want to preview or test alpha versions of _Black_. Note that - the most recent release may be newer than any prerelease, because no prereleases are created - before most releases. + ℹ Recommended for users who want to preview or test alpha versions of _Black_. Note + that the most recent release may be newer than any prerelease, because no prereleases + are created before most releases. - `latest` - tag used for the newest image of _Black_.\ - ℹ Recommended for users who always want to use the latest version of _Black_, even before - it is released. + ℹ Recommended for users who always want to use the latest version of _Black_, even + before it is released. There is one more tag used for _Black_ Docker images - `latest_non_release`. It is created for all unreleased From a54503d28d8acf9816f8d12a6b971789fe83e95a Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:54:00 -0600 Subject: [PATCH 4/4] add more tests Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- tests/data/cases/preview_long_dict_values_2.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/data/cases/preview_long_dict_values_2.py b/tests/data/cases/preview_long_dict_values_2.py index 299d57c095b..4305461192f 100644 --- a/tests/data/cases/preview_long_dict_values_2.py +++ b/tests/data/cases/preview_long_dict_values_2.py @@ -34,6 +34,7 @@ for src in sources } tasks = {key_name: foobar for src in sources} +tasks = {get_key_name(src,): "foo" for src in sources} tasks = {get_key_name(foo, bar, baz,): src for src in sources} tasks = { get_key_name(): xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx @@ -62,6 +63,11 @@ def bar(): 123 + 456 ), } + x = { + "foobar": ( + 123 + ) + 456, + } # output @@ -97,7 +103,7 @@ def bar(): loop.run_in_executor(): src, loop.run_in_executor(xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx): src, loop.run_in_executor( - xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxx + xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxx, ): src, loop.run_in_executor(): ( xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx @@ -112,6 +118,12 @@ def bar(): for src in sources } tasks = {key_name: foobar for src in sources} +tasks = { + get_key_name( + src, + ): "foo" + for src in sources +} tasks = { get_key_name( foo, @@ -145,3 +157,6 @@ def bar(): x = { "foobar": 123 + 456, } + x = { + "foobar": (123) + 456, + }