From 77c01a3521ca56d3009c77ec27933652ba934a55 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 9 Aug 2022 20:46:30 -0700 Subject: [PATCH 1/5] Use debug f-strings for feature detection Fixes #2907 --- src/black/__init__.py | 7 +++++++ src/black/mode.py | 5 +++++ tests/test_black.py | 17 +++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/src/black/__init__.py b/src/black/__init__.py index 2a5c750a583..b8a9d031896 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -87,6 +87,7 @@ from black.parsing import InvalidInput # noqa F401 from black.parsing import lib2to3_parse, parse_ast, stringify_ast from black.report import Changed, NothingChanged, Report +from black.trans import iter_fexpr_spans from blib2to3.pgen2 import token from blib2to3.pytree import Leaf, Node @@ -1240,6 +1241,7 @@ def get_features_used( # noqa: C901 Currently looking for: - f-strings; + - self-documenting expressions in f-strings (f"{x=}"); - underscores in numeric literals; - trailing commas after * or ** in function signatures and calls; - positional only arguments in function signatures and lambdas; @@ -1261,6 +1263,11 @@ def get_features_used( # noqa: C901 value_head = n.value[:2] if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}: features.add(Feature.F_STRINGS) + if Feature.DEBUG_F_STRINGS not in features: + for span_beg, span_end in iter_fexpr_spans(n.value): + if n.value[span_beg : span_end - 1].rstrip().endswith("="): + features.add(Feature.DEBUG_F_STRINGS) + break elif is_number_token(n): if "_" in n.value: diff --git a/src/black/mode.py b/src/black/mode.py index 32b65d16ca5..f6d0cbf62bd 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -49,6 +49,7 @@ class Feature(Enum): ANN_ASSIGN_EXTENDED_RHS = 13 EXCEPT_STAR = 14 VARIADIC_GENERICS = 15 + DEBUG_F_STRINGS = 16 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -81,6 +82,7 @@ class Feature(Enum): }, TargetVersion.PY38: { Feature.F_STRINGS, + Feature.DEBUG_F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, @@ -93,6 +95,7 @@ class Feature(Enum): }, TargetVersion.PY39: { Feature.F_STRINGS, + Feature.DEBUG_F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, @@ -106,6 +109,7 @@ class Feature(Enum): }, TargetVersion.PY310: { Feature.F_STRINGS, + Feature.DEBUG_F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, @@ -120,6 +124,7 @@ class Feature(Enum): }, TargetVersion.PY311: { Feature.F_STRINGS, + Feature.DEBUG_F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, diff --git a/tests/test_black.py b/tests/test_black.py index bb7784d5478..c7706d5f113 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -310,6 +310,23 @@ def test_detect_pos_only_arguments(self) -> None: versions = black.detect_target_versions(root) self.assertIn(black.TargetVersion.PY38, versions) + def test_detect_debug_f_strings(self) -> None: + root = black.lib2to3_parse("""f"{x=}" """) + features = black.get_features_used(root) + self.assertIn(black.Feature.DEBUG_F_STRINGS, features) + versions = black.detect_target_versions(root) + self.assertIn(black.TargetVersion.PY38, versions) + + root = black.lib2to3_parse("""f"{x}" """) + features = black.get_features_used(root) + self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features) + + root = black.lib2to3_parse( + """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """ + ) + features = black.get_features_used(root) + self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features) + @patch("black.dump_to_file", dump_to_stderr) def test_string_quotes(self) -> None: source, expected = read_data("miscellaneous", "string_quotes") From 566efb58385d5cc2fd2fb836b515fbf488e71133 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 9 Aug 2022 20:47:32 -0700 Subject: [PATCH 2/5] comment --- tests/test_black.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_black.py b/tests/test_black.py index c7706d5f113..13f8d7627bb 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -321,6 +321,7 @@ def test_detect_debug_f_strings(self) -> None: features = black.get_features_used(root) self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features) + # We don't yet support feature version detection in nested f-strings root = black.lib2to3_parse( """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """ ) From f3b26aa32a7e6e88acc6a58d2c383052244fb541 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 9 Aug 2022 20:48:47 -0700 Subject: [PATCH 3/5] changelog entry --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 5b29f20bfff..72e9079d3d6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,7 @@ ### Configuration +- Black now uses the presence of debug f-strings to detect target version. (#3215) ### Documentation From 96d2b779642f3e4931dec37df9be3d776b7b22a0 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 9 Aug 2022 20:52:30 -0700 Subject: [PATCH 4/5] mawr bot --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 72e9079d3d6..1fc8c65d6d8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,7 @@ ### Configuration + - Black now uses the presence of debug f-strings to detect target version. (#3215) ### Documentation From 87e279b916db9c74e0379a397268fffa1316b212 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 10 Aug 2022 11:35:30 -0700 Subject: [PATCH 5/5] more tests --- tests/test_black.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index 13f8d7627bb..81e7a9a7d0d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -317,7 +317,9 @@ def test_detect_debug_f_strings(self) -> None: versions = black.detect_target_versions(root) self.assertIn(black.TargetVersion.PY38, versions) - root = black.lib2to3_parse("""f"{x}" """) + root = black.lib2to3_parse( + """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n""" + ) features = black.get_features_used(root) self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)