Skip to content

unparse() of manually created or modified Interpolation is difficult #138774

@15r10nk

Description

@15r10nk

Bug report

Bug description:

Hi, I generated some code and experienced a difficulty generating the ast of template-strings.

ast.unparse uses the str attribute of the Interpolation to unparse it and ignores the value attribute.

from ast import *

tree = TemplateStr(
    values=[
        Interpolation(value=Name(id="test1", ctx=Load()), str="test2", conversion=-1)
    ]
)

print(unparse(tree))

output (Python 3.14.0rc2+):

t'{test2}'

I know that it is important to preserve the original code because the formatting might be important, but this makes it difficult to generate correct AST.

from ast import *


code=Lambda(args=arguments(args=[arg(arg='a')]), body=Name(id='a', ctx=Load()))

tree = TemplateStr(
    values=[
        Interpolation(value=code, str=unparse(code), conversion=-1)
    ]
)

compile(unparse(tree),"<string>","exec")

output (Python 3.14.0rc2+):

Traceback (most recent call last):
  File "/home/frank/projects/pysource-playground/pysource-codegen/example.py", line 12, in <module>
    compile(unparse(tree),"<string>","exec")
    ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 1
    t'{lambda a: a}'
       ^^^^^^^^^
SyntaxError: t-string: lambda expressions are not allowed without parentheses

The solution here can be to add "()" around all interpolations.

Another problem can be that you use interpolations inside interpolations, which means that you have to process the childs first.

This is my current workaround which fixes these issues for me:

def walk_childs_first(node):
    for e in ast.iter_child_nodes(node):
        yield from walk_childs_first(e)
        yield e

def fix_result(node):
    if sys.version_info >= (3, 14):
        for n in walk_childs_first(node):
            if isinstance(n, ast.Interpolation):
                f_str = ast.JoinedStr(
                    [ast.FormattedValue(value=n.value, conversion=-1, format_spec=None)]
                )
                f_str_repr = ast.unparse(f_str)
                if f_str_repr.startswith(("f'''", 'f"""')):
                    n.str = ast.unparse(f_str)[5:-4]  # strip f"""{...}"""
                else:
                    n.str = ast.unparse(f_str)[3:-2]  # strip f"{...}"

I'm using the unparsing code for f-strings here to generate the correct source code.

I would like to know if there is something which I missed, or if this problem was just overlooked untill now.

I experienced this problem during my work on pysoruce-codegen and pysource-minimize. This might be the reason why this examples could look like some edge cases, but I think it can also be problematic for other people who just want to change the AST and unparse the code, because unparse would use the old code from str and ignore the changed value.

I have no idea how this could be solved but I think it should at least be mentioned in the docs.
Being able to set Interpolation.str to None could help with the unparse use case (it should unparse Interpolation.value in this case), but I don't know if this could cause other problems.

CPython versions tested on:

3.14

Operating systems tested on:

No response

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixes3.15new features, bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions