From 80ba597ae4c3f88faad6613ed2b7af242e256766 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 10 Dec 2021 17:50:42 -0500 Subject: [PATCH 1/7] Hug power operators with "simple" operands Co-authored-by: Diego --- src/black/linegen.py | 22 +++++++--- src/black/trans.py | 73 +++++++++++++++++++++++++++++++++- tests/data/power_op_spacing.py | 65 ++++++++++++++++++++++++++++++ tests/test_format.py | 1 + 4 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 tests/data/power_op_spacing.py diff --git a/src/black/linegen.py b/src/black/linegen.py index f234913a161..a4160c01af7 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -21,7 +21,12 @@ from black.strings import get_string_prefix, fix_docstring from black.strings import normalize_string_prefix, normalize_string_quotes from black.trans import Transformer, CannotTransform, StringMerger -from black.trans import StringSplitter, StringParenWrapper, StringParenStripper +from black.trans import ( + StringSplitter, + StringParenWrapper, + StringParenStripper, + hug_power_op, +) from black.mode import Mode from black.mode import Feature @@ -341,9 +346,9 @@ def transform_line( ): # Only apply basic string preprocessing, since lines shouldn't be split here. if mode.experimental_string_processing: - transformers = [string_merge, string_paren_strip] + transformers = [string_merge, string_paren_strip, hug_power_op] else: - transformers = [] + transformers = [hug_power_op] elif line.is_def: transformers = [left_hand_split] else: @@ -393,6 +398,7 @@ def _rhs( standalone_comment_split, string_paren_wrap, rhs, + hug_power_op, ] else: transformers = [ @@ -401,12 +407,18 @@ def _rhs( string_split, string_paren_wrap, rhs, + hug_power_op, ] else: if line.inside_brackets: - transformers = [delimiter_split, standalone_comment_split, rhs] + transformers = [ + delimiter_split, + standalone_comment_split, + rhs, + hug_power_op, + ] else: - transformers = [rhs] + transformers = [rhs, hug_power_op] for transform in transformers: # We are accumulating lines in `result` because we might want to abort diff --git a/src/black/trans.py b/src/black/trans.py index cb41c1be487..34064d16923 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -24,9 +24,9 @@ import sys if sys.version_info < (3, 8): - from typing_extensions import Final + from typing_extensions import Literal, Final else: - from typing import Final + from typing import Literal, Final from mypy_extensions import trait @@ -71,6 +71,75 @@ def TErr(err_msg: str) -> Err[CannotTransform]: return Err(cant_transform) +def hug_power_op(line: Line, features: Collection[Feature]) -> Iterator[Line]: + """A transformer which normalizes spacing around power operators.""" + + for leaf in line.leaves: + if leaf.type == token.DOUBLESTAR: + break + else: + raise CannotTransform("No doublestar token was found in the line.") + + def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool: + if step == -1: + disallowed = {token.RPAR, token.RSQB} + else: + disallowed = {token.LPAR, token.LSQB} + + while 0 <= index < len(line.leaves): + current = line.leaves[index] + if current.type in disallowed: + return False + if current.type not in {token.NAME, token.DOT} or current.value == "for": + return True + + index += step + + return True + + def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool: + start = line.leaves[index] + if start.type in {token.NAME, token.NUMBER}: + return is_simple_lookup(index, step=(1 if kind == "exponent" else -1)) + + if start.type in {token.PLUS, token.MINUS, token.TILDE}: + if line.leaves[index + 1].type in {token.NAME, token.NUMBER}: + return is_simple_lookup(index + 1, step=1) + + return False + + leaves: List[Leaf] = [] + should_hug = False + for idx, leaf in enumerate(line.leaves): + new_leaf = leaf.clone() + if should_hug: + new_leaf.prefix = "" + should_hug = False + + should_hug = ( + (0 < idx < len(line.leaves) - 1) + and leaf.type == token.DOUBLESTAR + and is_simple_operand(idx - 1, kind="base") + and line.leaves[idx - 1].value != "lambda" + and is_simple_operand(idx + 1, kind="exponent") + ) + if should_hug: + new_leaf.prefix = "" + + leaves.append(new_leaf) + + yield Line( + mode=line.mode, + depth=line.depth, + leaves=leaves, + comments=line.comments, + bracket_tracker=line.bracket_tracker, + inside_brackets=line.inside_brackets, + should_split_rhs=line.should_split_rhs, + magic_trailing_comma=line.magic_trailing_comma, + ) + + class StringTransformer(ABC): """ An implementation of the Transformer protocol that relies on its diff --git a/tests/data/power_op_spacing.py b/tests/data/power_op_spacing.py new file mode 100644 index 00000000000..7d4ffc3e31d --- /dev/null +++ b/tests/data/power_op_spacing.py @@ -0,0 +1,65 @@ +def function(**kwargs): + t = a**2 + b**3 + return t ** 2 + + +def function_replace_spaces(**kwargs): + t = a **2 + b** 3 + c ** 4 + + +def function_dont_replace_spaces(): + {**a, **b, **c} + + +a = 5**~4 +b = 5 ** f() +c = -(5**2) +d = 5 ** f["hi"] +e = lazy(lambda **kwargs: 5) +f = f() ** 5 +g = a.b**c.d +h = 5 ** funcs.f() +i = funcs.f() ** 5 +j = super().name ** 5 +k = [(2**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2**63], [1, 2**63])] +n = count <= 10**5 +o = settings(max_examples=10**6) +p = {(k, k**2): v**2 for k, v in pairs} +q = [10**i for i in range(6)] + + +# output + + +def function(**kwargs): + t = a**2 + b**3 + return t**2 + + +def function_replace_spaces(**kwargs): + t = a**2 + b**3 + c**4 + + +def function_dont_replace_spaces(): + {**a, **b, **c} + + +a = 5**~4 +b = 5 ** f() +c = -(5**2) +d = 5 ** f["hi"] +e = lazy(lambda **kwargs: 5) +f = f() ** 5 +g = a.b**c.d +h = 5 ** funcs.f() +i = funcs.f() ** 5 +j = super().name ** 5 +k = [(2**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2**63], [1, 2**63])] +n = count <= 10**5 +o = settings(max_examples=10**6) +p = {(k, k**2): v**2 for k, v in pairs} +q = [10**i for i in range(6)] diff --git a/tests/test_format.py b/tests/test_format.py index 30099aaf1bc..fedc786c1b1 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -48,6 +48,7 @@ "function2", "function_trailing_comma", "import_spacing", + "power_op_spacing", "remove_parens", "slices", "string_prefixes", From cd84e5c6d23b0f9645ec9011cf195a9dc0df83d7 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 19 Dec 2021 14:12:27 -0500 Subject: [PATCH 2/7] Update outdated test data --- tests/data/expression.diff | 44 +++++++++++++------ tests/data/expression.py | 30 ++++++------- .../expression_skip_magic_trailing_comma.diff | 44 +++++++++++++------ tests/data/pep_572.py | 2 +- tests/data/pep_572_py39.py | 2 +- tests/data/slices.py | 2 +- 6 files changed, 78 insertions(+), 46 deletions(-) diff --git a/tests/data/expression.diff b/tests/data/expression.diff index 721a07d2141..5f29a18dc7f 100644 --- a/tests/data/expression.diff +++ b/tests/data/expression.diff @@ -11,7 +11,17 @@ True False 1 -@@ -29,63 +29,96 @@ +@@ -21,71 +21,104 @@ + Name1 or (Name2 and Name3) or Name4 + Name1 or Name2 and Name3 or Name4 + v1 << 2 + 1 >> v2 + 1 % finished +-1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8 +-((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8) ++1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 ++((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) + not great ~great +value -1 @@ -19,7 +29,7 @@ (~int) and (not ((v1 ^ (123 + v2)) | True)) -+really ** -confusing ** ~operator ** -precedence -flags & ~ select.EPOLLIN and waiters.write_task is not None -++(really ** -(confusing ** ~(operator ** -precedence))) +++(really ** -(confusing ** ~(operator**-precedence))) +flags & ~select.EPOLLIN and waiters.write_task is not None lambda arg: None lambda a=True: a @@ -88,15 +98,19 @@ + *more, +] {i for i in (1, 2, 3)} - {(i ** 2) for i in (1, 2, 3)} +-{(i ** 2) for i in (1, 2, 3)} -{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))} -+{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} - {((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} +-{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} ++{(i**2) for i in (1, 2, 3)} ++{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} ++{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} [i for i in (1, 2, 3)] - [(i ** 2) for i in (1, 2, 3)] +-[(i ** 2) for i in (1, 2, 3)] -[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))] -+[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] - [((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] +-[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] ++[(i**2) for i in (1, 2, 3)] ++[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] ++[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] {i: 0 for i in (1, 2, 3)} -{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))} +{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} @@ -181,10 +195,12 @@ SomeName (Good, Bad, Ugly) (i for i in (1, 2, 3)) - ((i ** 2) for i in (1, 2, 3)) +-((i ** 2) for i in (1, 2, 3)) -((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))) -+((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) - (((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) +-(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) ++((i**2) for i in (1, 2, 3)) ++((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) ++(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) (*starred,) -{"id": "1","type": "type","started_at": now(),"ended_at": now() + timedelta(days=10),"priority": 1,"import_session_id": 1,**kwargs} +{ @@ -403,13 +419,13 @@ + return True +if ( + ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e -+ | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n ++ | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n +): + return True +if ( + ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e + | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h -+ ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n ++ ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n +): + return True +if ( @@ -419,7 +435,7 @@ + | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k -+ >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ++ >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n +): + return True +( diff --git a/tests/data/expression.py b/tests/data/expression.py index d13450cda68..b056841027d 100644 --- a/tests/data/expression.py +++ b/tests/data/expression.py @@ -282,15 +282,15 @@ async def f(): v1 << 2 1 >> v2 1 % finished -1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8 -((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8) +1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 +((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) not great ~great +value -1 ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) -+(really ** -(confusing ** ~(operator ** -precedence))) ++(really ** -(confusing ** ~(operator**-precedence))) flags & ~select.EPOLLIN and waiters.write_task is not None lambda arg: None lambda a=True: a @@ -347,13 +347,13 @@ async def f(): *more, ] {i for i in (1, 2, 3)} -{(i ** 2) for i in (1, 2, 3)} -{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} -{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} +{(i**2) for i in (1, 2, 3)} +{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} +{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} [i for i in (1, 2, 3)] -[(i ** 2) for i in (1, 2, 3)] -[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] -[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] +[(i**2) for i in (1, 2, 3)] +[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] +[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] {i: 0 for i in (1, 2, 3)} {i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} {a: b * 2 for a, b in dictionary.items()} @@ -441,9 +441,9 @@ async def f(): SomeName (Good, Bad, Ugly) (i for i in (1, 2, 3)) -((i ** 2) for i in (1, 2, 3)) -((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) -(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) +((i**2) for i in (1, 2, 3)) +((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) +(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) (*starred,) { "id": "1", @@ -588,13 +588,13 @@ async def f(): return True if ( ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e - | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n + | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n ): return True if ( ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h - ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n + ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True if ( @@ -604,7 +604,7 @@ async def f(): | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k - >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n + >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ): return True ( diff --git a/tests/data/expression_skip_magic_trailing_comma.diff b/tests/data/expression_skip_magic_trailing_comma.diff index 4a8a95c7237..5b722c91352 100644 --- a/tests/data/expression_skip_magic_trailing_comma.diff +++ b/tests/data/expression_skip_magic_trailing_comma.diff @@ -11,7 +11,17 @@ True False 1 -@@ -29,63 +29,84 @@ +@@ -21,71 +21,92 @@ + Name1 or (Name2 and Name3) or Name4 + Name1 or Name2 and Name3 or Name4 + v1 << 2 + 1 >> v2 + 1 % finished +-1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8 +-((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8) ++1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 ++((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) + not great ~great +value -1 @@ -19,7 +29,7 @@ (~int) and (not ((v1 ^ (123 + v2)) | True)) -+really ** -confusing ** ~operator ** -precedence -flags & ~ select.EPOLLIN and waiters.write_task is not None -++(really ** -(confusing ** ~(operator ** -precedence))) +++(really ** -(confusing ** ~(operator**-precedence))) +flags & ~select.EPOLLIN and waiters.write_task is not None lambda arg: None lambda a=True: a @@ -76,15 +86,19 @@ + *more, +] {i for i in (1, 2, 3)} - {(i ** 2) for i in (1, 2, 3)} +-{(i ** 2) for i in (1, 2, 3)} -{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))} -+{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} - {((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} +-{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} ++{(i**2) for i in (1, 2, 3)} ++{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} ++{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} [i for i in (1, 2, 3)] - [(i ** 2) for i in (1, 2, 3)] +-[(i ** 2) for i in (1, 2, 3)] -[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))] -+[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] - [((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] +-[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] ++[(i**2) for i in (1, 2, 3)] ++[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] ++[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] {i: 0 for i in (1, 2, 3)} -{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))} +{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} @@ -164,10 +178,12 @@ SomeName (Good, Bad, Ugly) (i for i in (1, 2, 3)) - ((i ** 2) for i in (1, 2, 3)) +-((i ** 2) for i in (1, 2, 3)) -((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))) -+((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) - (((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) +-(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) ++((i**2) for i in (1, 2, 3)) ++((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) ++(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) (*starred,) -{"id": "1","type": "type","started_at": now(),"ended_at": now() + timedelta(days=10),"priority": 1,"import_session_id": 1,**kwargs} +{ @@ -384,13 +400,13 @@ + return True +if ( + ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e -+ | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n ++ | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n +): + return True +if ( + ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e + | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h -+ ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n ++ ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n +): + return True +if ( @@ -400,7 +416,7 @@ + | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k -+ >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ++ >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n +): + return True +( diff --git a/tests/data/pep_572.py b/tests/data/pep_572.py index c6867f26258..d41805f1cb1 100644 --- a/tests/data/pep_572.py +++ b/tests/data/pep_572.py @@ -4,7 +4,7 @@ pass if match := pattern.search(data): pass -[y := f(x), y ** 2, y ** 3] +[y := f(x), y**2, y**3] filtered_data = [y for x in data if (y := f(x)) is None] (y := f(x)) y0 = (y1 := f(x)) diff --git a/tests/data/pep_572_py39.py b/tests/data/pep_572_py39.py index 7bbd5091197..b8b081b8c45 100644 --- a/tests/data/pep_572_py39.py +++ b/tests/data/pep_572_py39.py @@ -1,7 +1,7 @@ # Unparenthesized walruses are now allowed in set literals & set comprehensions # since Python 3.9 {x := 1, 2, 3} -{x4 := x ** 5 for x in range(7)} +{x4 := x**5 for x in range(7)} # We better not remove the parentheses here (since it's a 3.10 feature) x[(a := 1)] x[(a := 1), (b := 3)] diff --git a/tests/data/slices.py b/tests/data/slices.py index 7a42678f646..165117cdcb4 100644 --- a/tests/data/slices.py +++ b/tests/data/slices.py @@ -9,7 +9,7 @@ slice[:c, c - 1] slice[c, c + 1, d::] slice[ham[c::d] :: 1] -slice[ham[cheese ** 2 : -1] : 1 : 1, ham[1:2]] +slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] slice[:-1:] slice[lambda: None : lambda: None] slice[lambda x, y, *args, really=2, **kwargs: None :, None::] From 45b62ee1a2936f460c3c1b2d9c115c04660af03e Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 25 Dec 2021 20:53:38 -0500 Subject: [PATCH 3/7] Add changelog and documentation --- CHANGES.md | 1 + docs/the_black_code_style/current_style.md | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 0452f1820c4..101792c15f2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ - Tuple unpacking on `return` and `yield` constructs now implies 3.8+ (#2700) - Unparenthesized tuples on annotated assignments (e.g `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) +- Power operators now "hug" their operands if they're both "simple" (#2726) ## 21.12b0 diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index b9ab350cd12..a3dfc77da99 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -291,6 +291,12 @@ multiple lines. This is so that _Black_ is compliant with the recent changes in [PEP 8](https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator) style guide, which emphasizes that this approach improves readability. +Almost all operators will be surrounded by single spaces, the only exceptions are unary +operators (`+`, `-`, and `~`), and power operators when both its operands are considered +"simple". For power ops, an operand is considered "simple" if it's only a NAME, numeric +CONSTANT, or attribute access (chained attribute access is allowed), with or without a +preceding unary operator. + ### Slices PEP 8 From 83e941682dc854887d1a1878c5dff1ec33a2134e Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 30 Dec 2021 20:35:28 -0500 Subject: [PATCH 4/7] Add some comments --- src/black/trans.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/black/trans.py b/src/black/trans.py index 34064d16923..fadffe91f97 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -74,6 +74,7 @@ def TErr(err_msg: str) -> Err[CannotTransform]: def hug_power_op(line: Line, features: Collection[Feature]) -> Iterator[Line]: """A transformer which normalizes spacing around power operators.""" + # Performance optimization to avoid unnecessary Leaf clones and other ops. for leaf in line.leaves: if leaf.type == token.DOUBLESTAR: break @@ -81,6 +82,9 @@ def hug_power_op(line: Line, features: Collection[Feature]) -> Iterator[Line]: raise CannotTransform("No doublestar token was found in the line.") def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool: + # Brackets and parenthesises indicate calls, subscripts, etc. ... + # basically stuff that doesn't count as "simple". Only a NAME lookup + # or dotted lookup (eg. NAME.NAME) is OK. if step == -1: disallowed = {token.RPAR, token.RSQB} else: @@ -91,6 +95,10 @@ def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool: if current.type in disallowed: return False if current.type not in {token.NAME, token.DOT} or current.value == "for": + # If the current token isn't disallowed, we'll assume this is simple as + # only the disallowed tokens are semantically attached to this lookup + # expression we're checking. Also, stop early if we hit the 'for' bit + # of a comprehension. return True index += step @@ -98,12 +106,17 @@ def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool: return True def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool: + # An operand is considered "simple" if's a NAME, a numeric CONSTANT, a simple + # lookup (see above), with or without a preceding unary operator. start = line.leaves[index] if start.type in {token.NAME, token.NUMBER}: return is_simple_lookup(index, step=(1 if kind == "exponent" else -1)) if start.type in {token.PLUS, token.MINUS, token.TILDE}: if line.leaves[index + 1].type in {token.NAME, token.NUMBER}: + # step is always one as bases with a preceding unary op will be checked + # for simplicity starting from the next token (so it'll hit the check + # above). return is_simple_lookup(index + 1, step=1) return False From a4fdf424d6632d8f24c54e28e95809d352fe9997 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 30 Dec 2021 20:49:29 -0500 Subject: [PATCH 5/7] I literally added this code to bugbear :p --- src/black_primer/primer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 8c966e346d9..b3796c7683d 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -81,7 +81,7 @@ }, "flake8-bugbear": { "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/PyCQA/flake8-bugbear.git", "long_checkout": false, "py_versions": ["all"] From 56cb9006562539cebc347a9dd101a3680fc7c288 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 6 Jan 2022 18:31:34 -0500 Subject: [PATCH 6/7] Significantly clean up wording in docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Hildén --- CHANGES.md | 2 +- docs/the_black_code_style/current_style.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 02195302d89..0d1291520c8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ - Tuple unpacking on `return` and `yield` constructs now implies 3.8+ (#2700) - Unparenthesized tuples on annotated assignments (e.g `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) -- Power operators now "hug" their operands if they're both "simple" (#2726) +- Remove spaces around power operators if both operands are simple (#2726) ### Packaging diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index a3dfc77da99..f863c996422 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -292,8 +292,8 @@ multiple lines. This is so that _Black_ is compliant with the recent changes in style guide, which emphasizes that this approach improves readability. Almost all operators will be surrounded by single spaces, the only exceptions are unary -operators (`+`, `-`, and `~`), and power operators when both its operands are considered -"simple". For power ops, an operand is considered "simple" if it's only a NAME, numeric +operators (`+`, `-`, and `~`), and power operators when both operands are simple. +For power ops, an operand is considered simple if it's only a NAME, numeric CONSTANT, or attribute access (chained attribute access is allowed), with or without a preceding unary operator. From b6e8758c461933393dc29b27e86f43178a8b8d54 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 8 Jan 2022 14:07:01 -0500 Subject: [PATCH 7/7] Improve tests and docs, also resolve linting --- docs/the_black_code_style/current_style.md | 22 ++++++++++--- src/black/linegen.py | 27 +++++---------- src/black/trans.py | 2 +- tests/data/power_op_spacing.py | 38 ++++++++++++++++++++++ 4 files changed, 66 insertions(+), 23 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 01d99aa6909..5be7ba6dbdb 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -285,10 +285,24 @@ multiple lines. This is so that _Black_ is compliant with the recent changes in style guide, which emphasizes that this approach improves readability. Almost all operators will be surrounded by single spaces, the only exceptions are unary -operators (`+`, `-`, and `~`), and power operators when both operands are simple. -For power ops, an operand is considered simple if it's only a NAME, numeric -CONSTANT, or attribute access (chained attribute access is allowed), with or without a -preceding unary operator. +operators (`+`, `-`, and `~`), and power operators when both operands are simple. For +powers, an operand is considered simple if it's only a NAME, numeric CONSTANT, or +attribute access (chained attribute access is allowed), with or without a preceding +unary operator. + +```python +# For example, these won't be surrounded by whitespace +a = x**y +b = config.base**5.2 +c = config.base**runtime.config.exponent +d = 2**5 +e = 2**~5 + +# ... but these will be surrounded by whitespace +f = 2 ** get_exponent() +g = get_x() ** get_y() +h = config['base'] ** 2 +``` ### Slices diff --git a/src/black/linegen.py b/src/black/linegen.py index 5bcde88e82e..9fbdfadba6a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -21,13 +21,8 @@ from black.numerics import normalize_numeric_literal from black.strings import get_string_prefix, fix_docstring from black.strings import normalize_string_prefix, normalize_string_quotes -from black.trans import Transformer, CannotTransform, StringMerger -from black.trans import ( - StringSplitter, - StringParenWrapper, - StringParenStripper, - hug_power_op, -) +from black.trans import Transformer, CannotTransform, StringMerger, StringSplitter +from black.trans import StringParenWrapper, StringParenStripper, hug_power_op from black.mode import Mode, Feature, Preview from blib2to3.pytree import Node, Leaf @@ -343,9 +338,9 @@ def transform_line( ): # Only apply basic string preprocessing, since lines shouldn't be split here. if Preview.string_processing in mode: - transformers = [string_merge, string_paren_strip, hug_power_op] + transformers = [string_merge, string_paren_strip] else: - transformers = [hug_power_op] + transformers = [] elif line.is_def: transformers = [left_hand_split] else: @@ -395,7 +390,6 @@ def _rhs( standalone_comment_split, string_paren_wrap, rhs, - hug_power_op, ] else: transformers = [ @@ -404,18 +398,15 @@ def _rhs( string_split, string_paren_wrap, rhs, - hug_power_op, ] else: if line.inside_brackets: - transformers = [ - delimiter_split, - standalone_comment_split, - rhs, - hug_power_op, - ] + transformers = [delimiter_split, standalone_comment_split, rhs] else: - transformers = [rhs, hug_power_op] + transformers = [rhs] + # It's always safe to attempt hugging of power operations and pretty much every line + # could match. + transformers.append(hug_power_op) for transform in transformers: # We are accumulating lines in `result` because we might want to abort diff --git a/src/black/trans.py b/src/black/trans.py index fadffe91f97..74d052fe2dc 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -82,7 +82,7 @@ def hug_power_op(line: Line, features: Collection[Feature]) -> Iterator[Line]: raise CannotTransform("No doublestar token was found in the line.") def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool: - # Brackets and parenthesises indicate calls, subscripts, etc. ... + # Brackets and parentheses indicate calls, subscripts, etc. ... # basically stuff that doesn't count as "simple". Only a NAME lookup # or dotted lookup (eg. NAME.NAME) is OK. if step == -1: diff --git a/tests/data/power_op_spacing.py b/tests/data/power_op_spacing.py index 7d4ffc3e31d..87dde7f39dc 100644 --- a/tests/data/power_op_spacing.py +++ b/tests/data/power_op_spacing.py @@ -28,6 +28,25 @@ def function_dont_replace_spaces(): o = settings(max_examples=10**6) p = {(k, k**2): v**2 for k, v in pairs} q = [10**i for i in range(6)] +r = x**y + +a = 5.0**~4.0 +b = 5.0 ** f() +c = -(5.0**2.0) +d = 5.0 ** f["hi"] +e = lazy(lambda **kwargs: 5) +f = f() ** 5.0 +g = a.b**c.d +h = 5.0 ** funcs.f() +i = funcs.f() ** 5.0 +j = super().name ** 5.0 +k = [(2.0**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2.0**63.0], [1.0, 2**63.0])] +n = count <= 10**5.0 +o = settings(max_examples=10**6.0) +p = {(k, k**2): v**2.0 for k, v in pairs} +q = [10.5**i for i in range(6)] # output @@ -63,3 +82,22 @@ def function_dont_replace_spaces(): o = settings(max_examples=10**6) p = {(k, k**2): v**2 for k, v in pairs} q = [10**i for i in range(6)] +r = x**y + +a = 5.0**~4.0 +b = 5.0 ** f() +c = -(5.0**2.0) +d = 5.0 ** f["hi"] +e = lazy(lambda **kwargs: 5) +f = f() ** 5.0 +g = a.b**c.d +h = 5.0 ** funcs.f() +i = funcs.f() ** 5.0 +j = super().name ** 5.0 +k = [(2.0**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2.0**63.0], [1.0, 2**63.0])] +n = count <= 10**5.0 +o = settings(max_examples=10**6.0) +p = {(k, k**2): v**2.0 for k, v in pairs} +q = [10.5**i for i in range(6)]