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 diff --git a/src/black/lines.py b/src/black/lines.py index 6b65372fb3f..29030dc308c 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -954,6 +954,21 @@ def can_omit_invisible_parens( ): closing_bracket = leaf + bracket = rhs.opening_bracket + if ( + # Keep parenthesized dictionary values + 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 + ) + ): + 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..4305461192f --- /dev/null +++ b/tests/data/cases/preview_long_dict_values_2.py @@ -0,0 +1,162 @@ +# 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(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 + 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 + ), + } + 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( + 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 + ) + 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, + } + x = { + "foobar": (123) + 456, + }