summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorPablo Galindo Salgado <Pablogsal@gmail.com>2023-10-05 13:26:44 (GMT)
committerGitHub <noreply@github.com>2023-10-05 13:26:44 (GMT)
commitcc389ef627b2a486ab89d9a11245bef48224efb1 (patch)
treef6d4d943438d07fc3d37b905a921cd3f3fd2c3d0 /Lib
parentaf29282fce117cb10f00907fd46d56c2fa6142f5 (diff)
downloadcpython-cc389ef627b2a486ab89d9a11245bef48224efb1.zip
cpython-cc389ef627b2a486ab89d9a11245bef48224efb1.tar.gz
cpython-cc389ef627b2a486ab89d9a11245bef48224efb1.tar.bz2
gh-110259: Fix f-strings with multiline expressions and format specs (#110271)
Signed-off-by: Pablo Galindo <pablogsal@gmail.com>
Diffstat (limited to 'Lib')
-rw-r--r--Lib/ast.py11
-rw-r--r--Lib/test/test_tokenize.py97
-rw-r--r--Lib/test/test_unparse.py3
3 files changed, 107 insertions, 4 deletions
diff --git a/Lib/ast.py b/Lib/ast.py
index 1f54309..f7888d1 100644
--- a/Lib/ast.py
+++ b/Lib/ast.py
@@ -1270,13 +1270,15 @@ class _Unparser(NodeVisitor):
quote_type = quote_types[0]
self.write(f"{quote_type}{value}{quote_type}")
- def _write_fstring_inner(self, node):
+ def _write_fstring_inner(self, node, scape_newlines=False):
if isinstance(node, JoinedStr):
# for both the f-string itself, and format_spec
for value in node.values:
- self._write_fstring_inner(value)
+ self._write_fstring_inner(value, scape_newlines=scape_newlines)
elif isinstance(node, Constant) and isinstance(node.value, str):
value = node.value.replace("{", "{{").replace("}", "}}")
+ if scape_newlines:
+ value = value.replace("\n", "\\n")
self.write(value)
elif isinstance(node, FormattedValue):
self.visit_FormattedValue(node)
@@ -1299,7 +1301,10 @@ class _Unparser(NodeVisitor):
self.write(f"!{chr(node.conversion)}")
if node.format_spec:
self.write(":")
- self._write_fstring_inner(node.format_spec)
+ self._write_fstring_inner(
+ node.format_spec,
+ scape_newlines=True
+ )
def visit_Name(self, node):
self.write(node.id)
diff --git a/Lib/test/test_tokenize.py b/Lib/test/test_tokenize.py
index 94fb6d9..9369560 100644
--- a/Lib/test/test_tokenize.py
+++ b/Lib/test/test_tokenize.py
@@ -567,6 +567,55 @@ f'''{
OP '}' (3, 1) (3, 2)
FSTRING_END "'''" (3, 2) (3, 5)
""")
+ self.check_tokenize("""\
+f'''__{
+ x:a
+}__'''""", """\
+ FSTRING_START "f'''" (1, 0) (1, 4)
+ FSTRING_MIDDLE '__' (1, 4) (1, 6)
+ OP '{' (1, 6) (1, 7)
+ NL '\\n' (1, 7) (1, 8)
+ NAME 'x' (2, 4) (2, 5)
+ OP ':' (2, 5) (2, 6)
+ FSTRING_MIDDLE 'a\\n' (2, 6) (3, 0)
+ OP '}' (3, 0) (3, 1)
+ FSTRING_MIDDLE '__' (3, 1) (3, 3)
+ FSTRING_END "'''" (3, 3) (3, 6)
+ """)
+ self.check_tokenize("""\
+f'''__{
+ x:a
+ b
+ c
+ d
+}__'''""", """\
+ FSTRING_START "f'''" (1, 0) (1, 4)
+ FSTRING_MIDDLE '__' (1, 4) (1, 6)
+ OP '{' (1, 6) (1, 7)
+ NL '\\n' (1, 7) (1, 8)
+ NAME 'x' (2, 4) (2, 5)
+ OP ':' (2, 5) (2, 6)
+ FSTRING_MIDDLE 'a\\n b\\n c\\n d\\n' (2, 6) (6, 0)
+ OP '}' (6, 0) (6, 1)
+ FSTRING_MIDDLE '__' (6, 1) (6, 3)
+ FSTRING_END "'''" (6, 3) (6, 6)
+ """)
+ self.check_tokenize("""\
+f'__{
+ x:d
+}__'""", """\
+ FSTRING_START "f'" (1, 0) (1, 2)
+ FSTRING_MIDDLE '__' (1, 2) (1, 4)
+ OP '{' (1, 4) (1, 5)
+ NL '\\n' (1, 5) (1, 6)
+ NAME 'x' (2, 4) (2, 5)
+ OP ':' (2, 5) (2, 6)
+ FSTRING_MIDDLE 'd' (2, 6) (2, 7)
+ NL '\\n' (2, 7) (2, 8)
+ OP '}' (3, 0) (3, 1)
+ FSTRING_MIDDLE '__' (3, 1) (3, 3)
+ FSTRING_END "'" (3, 3) (3, 4)
+ """)
def test_function(self):
self.check_tokenize("def d22(a, b, c=2, d=2, *k): pass", """\
@@ -2279,6 +2328,54 @@ def"', """\
FSTRING_END \'"\' (1, 16) (1, 17)
""")
+ self.check_tokenize("""\
+f'''__{
+ x:a
+}__'''""", """\
+ FSTRING_START "f'''" (1, 0) (1, 4)
+ FSTRING_MIDDLE '__' (1, 4) (1, 6)
+ LBRACE '{' (1, 6) (1, 7)
+ NAME 'x' (2, 4) (2, 5)
+ COLON ':' (2, 5) (2, 6)
+ FSTRING_MIDDLE 'a\\n' (2, 6) (3, 0)
+ RBRACE '}' (3, 0) (3, 1)
+ FSTRING_MIDDLE '__' (3, 1) (3, 3)
+ FSTRING_END "'''" (3, 3) (3, 6)
+ """)
+
+ self.check_tokenize("""\
+f'''__{
+ x:a
+ b
+ c
+ d
+}__'''""", """\
+ FSTRING_START "f'''" (1, 0) (1, 4)
+ FSTRING_MIDDLE '__' (1, 4) (1, 6)
+ LBRACE '{' (1, 6) (1, 7)
+ NAME 'x' (2, 4) (2, 5)
+ COLON ':' (2, 5) (2, 6)
+ FSTRING_MIDDLE 'a\\n b\\n c\\n d\\n' (2, 6) (6, 0)
+ RBRACE '}' (6, 0) (6, 1)
+ FSTRING_MIDDLE '__' (6, 1) (6, 3)
+ FSTRING_END "'''" (6, 3) (6, 6)
+ """)
+
+ self.check_tokenize("""\
+f'__{
+ x:d
+}__'""", """\
+ FSTRING_START "f'" (1, 0) (1, 2)
+ FSTRING_MIDDLE '__' (1, 2) (1, 4)
+ LBRACE '{' (1, 4) (1, 5)
+ NAME 'x' (2, 4) (2, 5)
+ COLON ':' (2, 5) (2, 6)
+ FSTRING_MIDDLE 'd' (2, 6) (2, 7)
+ RBRACE '}' (3, 0) (3, 1)
+ FSTRING_MIDDLE '__' (3, 1) (3, 3)
+ FSTRING_END "'" (3, 3) (3, 4)
+ """)
+
def test_function(self):
self.check_tokenize('def d22(a, b, c=2, d=2, *k): pass', """\
diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py
index bdf7b05..6f698a8 100644
--- a/Lib/test/test_unparse.py
+++ b/Lib/test/test_unparse.py
@@ -730,7 +730,8 @@ class DirectoryTestCase(ASTTestCase):
test_directories = (lib_dir, lib_dir / "test")
run_always_files = {"test_grammar.py", "test_syntax.py", "test_compile.py",
"test_ast.py", "test_asdl_parser.py", "test_fstring.py",
- "test_patma.py", "test_type_alias.py", "test_type_params.py"}
+ "test_patma.py", "test_type_alias.py", "test_type_params.py",
+ "test_tokenize.py"}
_files_to_test = None