Search code examples
pythonpython-3.xprintingnested-loopsf-string

Nested f-strings in Python (Third nested f-string)


I need to convert json to given hcl structure. There is this code:

user_dicts = { 'users': [ {'name': 'test1', 'pass': 'password1', 'permissions': [ {'access': 'yes', 'role': 'admin'}, {'access': 'yes', 'role': 'user'} ] }, {'name': 'test2', 'pass': 'password2', 'permissions': [ {'access': 'yes', 'role': 'admin'} ] } ] }

double_q = '"'

result = f"""
users = [
    {''.join([
    f'''{{
        name        = {double_q}{ d['name'] }{double_q},
        pass        = {double_q}{ d['pass'] }{double_q},
        permissions = [
            {''.join([f"{{ access = {double_q}{ d['permissions'][index]['access'] }{double_q}, role = {double_q}{ d['permissions'][index]['role'] }{double_q} }}," for index in range(len(d['permissions'])) ])}
        ]
    }},
    ''' for d in user_dicts['users']
    ])}
]"""

print(result)

which returns the following line:

users = [
    {
        name        = "test1",
        pass        = "password1",
        permissions = [
            { access = "yes", role = "admin" },{ access = "yes", role = "user" },
        ]
    },
    {
        name        = "test2",
        pass        = "password2",
        permissions = [
            { access = "yes", role = "admin" },
        ]
    },
    
]

What needs to be done so that there is a new line between the dictionaries in permissions while maintaining the number of spaces? That is:

users = [
    {
        name        = "test1",
        pass        = "password1",
        permissions = [
            { access = "yes", role = "admin" },
            { access = "yes", role = "user" },
        ]
    },
    {
        name        = "test2",
        pass        = "password2",
        permissions = [
            { access = "yes", role = "admin" },
        ]
    },
    
]

I tried adding f-string to new_line variable:

user_dicts = { 'users': [ {'name': 'test1', 'pass': 'password1', 'permissions': [ {'access': 'yes', 'role': 'admin'}, {'access': 'yes', 'role': 'user'} ] }, {'name': 'test2', 'pass': 'password2', 'permissions': [ {'access': 'yes', 'role': 'admin'} ] } ] }

double_q = '"'
new_line = '\n'

result = f"""
users = [
    {''.join([
    f'''{{
        name        = {double_q}{ d['name'] }{double_q},
        pass        = {double_q}{ d['pass'] }{double_q},
        permissions = [
            {f'{new_line}'.join([f"{{ access = {double_q}{ d['permissions'][index]['access'] }{double_q}, role = {double_q}{ d['permissions'][index]['role'] }{double_q} }}," for index in range(len(d['permissions'])) ])}
        ]
    }},
    ''' for d in user_dicts['users']
    ])}
]"""

print(result)

But the next line becomes from the beginning of the line:

users = [
    {
        name        = "test1",
        pass        = "password1",
        permissions = [
            { access = "yes", role = "admin" },
{ access = "yes", role = "user" },
        ]
    },
    {
        name        = "test2",
        pass        = "password2",
        permissions = [
            { access = "yes", role = "admin" },
        ]
    },
    
]

Solution

  • Quick fix

    You were on the right track, using the new_line variable together with str.join(). To maintain the number of spaces, just include them in new_line:

    new_line = '            \n'
    

    Further thinking

    You may want to start using a templating engine, like Jinja.

    Pros

    • You don't need to concern yourself with (nested) f-strings
    • The template string looks like your desired output (kinda WYSIWYG)
    • Whitespace control is easy

    Cons

    • You'll probably have to spend some time learning how to use the templating engine

    Your example, implemented with Jinja

    To run the following code, you need to install the jinja2 package

    import jinja2
    
    user_dicts = { 'users': [ {'name': 'test1', 'pass': 'password1', 'permissions': [ {'access': 'yes', 'role': 'admin'}, {'access': 'yes', 'role': 'user'} ] }, {'name': 'test2', 'pass': 'password2', 'permissions': [ {'access': 'yes', 'role': 'admin'} ] } ] }
    
    template_str = """
    users = [
    {% for user in users %}
        {
            name        = "{{ user['name'] }}",
            pass        = "{{ user['pass'] }}",
            permissions = [
    {% for p in user['permissions'] %}
                { access = "{{ p['access'] }}", role = "{{ p['role'] }}" },
    {% endfor %}
            ]
        },
    {% endfor %}
    
    ]
    """.strip()
    
    template = jinja2.Template(template_str, trim_blocks=True, lstrip_blocks=True)
    users = template.render(user_dicts).replace("'", '"')
    
    

    The template string template_str is basically your desired output, plus some special tokens ({% for ... %}, {% endfor %}, {{ ... }}). These tokens tell the engine how to "fill in the blanks".