From 1aa5f82615e20a13efea152d9c3ea3c533d088c2 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Tue, 31 Oct 2023 21:18:08 +0200 Subject: [PATCH 01/12] Add test cases --- tests/data/cases/preview_long_strings.py | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index 5519f098774..9daf8221ba8 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -305,6 +305,13 @@ def foo(): # Test case of an outer string' parens enclose an inner string's parens. call(body=("%s %s" % ((",".join(items)), suffix))) +log.info(f'Skipping: {desc["db_id"]=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]=} {desc["exposure_max"]=}') + +log.info(f"Skipping: {desc['db_id']=} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']=} {desc['exposure_max']=}") + +log.info(f'Skipping: {desc["db_id"]} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') + +log.info(f'Skipping: { longer_longer_longer_longer_longer_longer_name [ "db_id" ] [ "another_key" ] = : .3f }') # output @@ -846,3 +853,23 @@ def foo(): # Test case of an outer string' parens enclose an inner string's parens. call(body="%s %s" % (",".join(items), suffix)) + +log.info( + "Skipping:" + f' {desc["db_id"]=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]=} {desc["exposure_max"]=}' +) + +log.info( + "Skipping:" + f" {desc['db_id']=} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']=} {desc['exposure_max']=}" +) + +log.info( + "Skipping:" + f" {desc['db_id']} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" +) + +log.info( + "Skipping:" + f' { longer_longer_longer_longer_longer_longer_name [ "db_id" ] [ "another_key" ] = : .3f }' +) From 46919f80bb3cb7b396901b611ca0c0ea773d3663 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Tue, 31 Oct 2023 21:18:52 +0200 Subject: [PATCH 02/12] Don't toggle printables quotes in debug f-strings --- src/black/trans.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/black/trans.py b/src/black/trans.py index a3f6467cc9e..71ffa3ed4d6 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -590,11 +590,20 @@ def make_naked(string: str, string_prefix: str) -> str: """ assert_is_leaf_string(string) if "f" in string_prefix: - string = _toggle_fexpr_quotes(string, QUOTE) - # After quotes toggling, quotes in expressions won't be escaped - # because quotes can't be reused in f-strings. So we can simply - # let the escaping logic below run without knowing f-string - # expressions. + RE_QUOTES_IN_DEBUG_F_STRING = ( + r"{[^[]*\[\s*[\"\']\w*[\"\']\s*\][^=}]*=[^}]*}" + ) + is_debug_f_string_with_quotes = re.search( + RE_QUOTES_IN_DEBUG_F_STRING, string + ) + if not is_debug_f_string_with_quotes: + # We don't want to toggle debug f-string quotes in the printable + # part, because that would modify the AST + string = _toggle_fexpr_quotes(string, QUOTE) + # After quotes toggling, quotes in expressions won't be escaped + # because quotes can't be reused in f-strings. So we can simply + # let the escaping logic below run without knowing f-string + # expressions. RE_EVEN_BACKSLASHES = r"(?:(? Date: Tue, 31 Oct 2023 21:19:39 +0200 Subject: [PATCH 03/12] Add changelog entry --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 60231468bdf..e06789e3b67 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ - Multiline dictionaries and lists that are the sole argument to a function are now indented less (#3964) +- Preserve printable quote types for debug f-strings (#4005) ### Configuration From b139419d3d5cb1c7d580e9887d23cf5f0953f851 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Tue, 31 Oct 2023 23:09:23 +0200 Subject: [PATCH 04/12] Add test case --- tests/data/cases/preview_long_strings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index 9daf8221ba8..c69ec0a3e1d 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -311,6 +311,8 @@ def foo(): log.info(f'Skipping: {desc["db_id"]} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') +log.info(f'Skipping: {foo("asdf")=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') + log.info(f'Skipping: { longer_longer_longer_longer_longer_longer_name [ "db_id" ] [ "another_key" ] = : .3f }') # output @@ -869,6 +871,11 @@ def foo(): f" {desc['db_id']} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" ) +log.info( + "Skipping:" + f' {foo("asdf")=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}' +) + log.info( "Skipping:" f' { longer_longer_longer_longer_longer_longer_name [ "db_id" ] [ "another_key" ] = : .3f }' From bb71648c12e02d391beab0dc61c5919cd40343d3 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Tue, 31 Oct 2023 23:17:15 +0200 Subject: [PATCH 05/12] Replace silly regex with iter_fexpr_spans --- src/black/trans.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/black/trans.py b/src/black/trans.py index 71ffa3ed4d6..b9555f356d5 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -590,15 +590,16 @@ def make_naked(string: str, string_prefix: str) -> str: """ assert_is_leaf_string(string) if "f" in string_prefix: - RE_QUOTES_IN_DEBUG_F_STRING = ( - r"{[^[]*\[\s*[\"\']\w*[\"\']\s*\][^=}]*=[^}]*}" + f_expressions = ( + string[span[0] : span[1]] for span in iter_fexpr_spans(string) ) - is_debug_f_string_with_quotes = re.search( - RE_QUOTES_IN_DEBUG_F_STRING, string + expressions_contain_printable_quotes = any( + re.search(r".*[\'\"].*=", expression) + for expression in f_expressions ) - if not is_debug_f_string_with_quotes: - # We don't want to toggle debug f-string quotes in the printable - # part, because that would modify the AST + if not expressions_contain_printable_quotes: + # We don't want to toggle printable quotes in debug f-strings, as + # that would modify the AST string = _toggle_fexpr_quotes(string, QUOTE) # After quotes toggling, quotes in expressions won't be escaped # because quotes can't be reused in f-strings. So we can simply From 004fa948bc404bdb65f3f19d988fad078202fbda Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Fri, 3 Nov 2023 10:18:44 +0200 Subject: [PATCH 06/12] Add test cases --- tests/data/cases/preview_long_strings.py | 25 ++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index c69ec0a3e1d..789efad819f 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -309,10 +309,16 @@ def foo(): log.info(f"Skipping: {desc['db_id']=} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']=} {desc['exposure_max']=}") -log.info(f'Skipping: {desc["db_id"]} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') +log.info(f'Skipping: {desc["db_id"]} {foo("bar",x=123)} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') + +log.info(f'Skipping: {desc["db_id"]} {foo("bar",x=123)=} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') log.info(f'Skipping: {foo("asdf")=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') +log.info(f'Skipping: {"a" == "b" == "c" == "d"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') + +log.info(f'Skipping: {"a" == "b" == "c" == "d"=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') + log.info(f'Skipping: { longer_longer_longer_longer_longer_longer_name [ "db_id" ] [ "another_key" ] = : .3f }') # output @@ -868,7 +874,12 @@ def foo(): log.info( "Skipping:" - f" {desc['db_id']} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" + f" {desc['db_id']} {foo('bar',x=123)} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" +) + +log.info( + "Skipping:" + f' {desc["db_id"]} {foo("bar",x=123)=} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}' ) log.info( @@ -876,6 +887,16 @@ def foo(): f' {foo("asdf")=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}' ) +log.info( + "Skipping:" + f" {'a' == 'b' == 'c' == 'd'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" +) + +log.info( + "Skipping:" + f' {"a" == "b" == "c" == "d"=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}' +) + log.info( "Skipping:" f' { longer_longer_longer_longer_longer_longer_name [ "db_id" ] [ "another_key" ] = : .3f }' From f85df4232b9d0d7198bc59c85a4c639f788211e6 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Fri, 3 Nov 2023 10:29:41 +0200 Subject: [PATCH 07/12] Skip curly braces from f_expressions --- src/black/trans.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/trans.py b/src/black/trans.py index b9555f356d5..a92460992f3 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -591,7 +591,8 @@ def make_naked(string: str, string_prefix: str) -> str: assert_is_leaf_string(string) if "f" in string_prefix: f_expressions = ( - string[span[0] : span[1]] for span in iter_fexpr_spans(string) + string[span[0] + 1 : span[1] - 1] # +-1 to get rid of curly braces + for span in iter_fexpr_spans(string) ) expressions_contain_printable_quotes = any( re.search(r".*[\'\"].*=", expression) From 54f38e56a388458fd3951d3addf2b0d91d2828b6 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Fri, 3 Nov 2023 10:43:09 +0200 Subject: [PATCH 08/12] Add test cases --- tests/data/cases/preview_long_strings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index 789efad819f..85fd61f166b 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -309,7 +309,7 @@ def foo(): log.info(f"Skipping: {desc['db_id']=} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']=} {desc['exposure_max']=}") -log.info(f'Skipping: {desc["db_id"]} {foo("bar",x=123)} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') +log.info(f'Skipping: {desc["db_id"]} {foo("bar",x=123)} {"foo" != "bar"} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') log.info(f'Skipping: {desc["db_id"]} {foo("bar",x=123)=} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') @@ -874,7 +874,7 @@ def foo(): log.info( "Skipping:" - f" {desc['db_id']} {foo('bar',x=123)} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" + f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" ) log.info( From a489eca3fdd0e99e3258882c85c698acb36dd762 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Fri, 3 Nov 2023 10:46:18 +0200 Subject: [PATCH 09/12] Revisit regex to expect a single equal sign followed by an optional colon --- src/black/trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/trans.py b/src/black/trans.py index a92460992f3..d54e7d3d530 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -595,7 +595,7 @@ def make_naked(string: str, string_prefix: str) -> str: for span in iter_fexpr_spans(string) ) expressions_contain_printable_quotes = any( - re.search(r".*[\'\"].*=", expression) + re.search(r".*[\'\"].*(? Date: Fri, 3 Nov 2023 11:08:32 +0200 Subject: [PATCH 10/12] Add triple quote test cases --- tests/data/cases/preview_long_strings.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index 85fd61f166b..20f0e6b14a0 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -321,6 +321,12 @@ def foo(): log.info(f'Skipping: { longer_longer_longer_longer_longer_longer_name [ "db_id" ] [ "another_key" ] = : .3f }') +log.info(f'''Skipping: {"a" == 'b'} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}''') + +log.info(f'''Skipping: {'a' == "b"=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}''') + +log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}""") + # output @@ -901,3 +907,15 @@ def foo(): "Skipping:" f' { longer_longer_longer_longer_longer_longer_name [ "db_id" ] [ "another_key" ] = : .3f }' ) + +log.info( + f"""Skipping: {"a" == 'b'} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}""" +) + +log.info( + f"""Skipping: {'a' == "b"=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}""" +) + +log.info( + f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}""" +) From 85f78a3cfe87b8db4ff41d4dc8a4bcecf6afc747 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Fri, 3 Nov 2023 11:13:06 +0200 Subject: [PATCH 11/12] Wording --- CHANGES.md | 3 ++- src/black/trans.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c53b77d03e9..5220c9e20ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,8 @@ indented less (#3964) - Multiline list and dict unpacking as the sole argument to a function is now also indented less (#3992) -- Preserve printable quote types for debug f-strings (#4005) +- In f-string debug expressions preserve quote types that are visible in the final + string (#4005) ### Configuration diff --git a/src/black/trans.py b/src/black/trans.py index d54e7d3d530..ab3197fa6df 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -594,12 +594,12 @@ def make_naked(string: str, string_prefix: str) -> str: string[span[0] + 1 : span[1] - 1] # +-1 to get rid of curly braces for span in iter_fexpr_spans(string) ) - expressions_contain_printable_quotes = any( + debug_expressions_contain_visible_quotes = any( re.search(r".*[\'\"].*(? Date: Tue, 7 Nov 2023 10:01:53 +0200 Subject: [PATCH 12/12] Add test cases involving := --- tests/data/cases/preview_long_strings.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index 20f0e6b14a0..19ac47a7032 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -309,7 +309,9 @@ def foo(): log.info(f"Skipping: {desc['db_id']=} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']=} {desc['exposure_max']=}") -log.info(f'Skipping: {desc["db_id"]} {foo("bar",x=123)} {"foo" != "bar"} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') +log.info(f'Skipping: {desc["db_id"]} {foo("bar",x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}') + +log.info(f'Skipping: {desc["db_id"]} {desc["ms_name"]} {money=} {(x := "abc=")=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') log.info(f'Skipping: {desc["db_id"]} {foo("bar",x=123)=} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}') @@ -880,7 +882,12 @@ def foo(): log.info( "Skipping:" - f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" + f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}" +) + +log.info( + "Skipping:" + f' {desc["db_id"]} {desc["ms_name"]} {money=} {(x := "abc=")=} {pos_share=} {desc["status"]} {desc["exposure_max"]}' ) log.info(