Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Define a triple-quoted f-string with newline-containing substrings inside a function without outputted indents

I'm trying to pretty-print a HTTP request (that I've mocked here).

from typing import NamedTuple

class RequestMock(NamedTuple):
    method = 'POST'
    url = 'https://bob.com'
    body = 'body1\nbody2'
    headers = {'a': '1', 'b': '2'}

I have a function that does this:

req = RequestMock()

def print1(req):
    headers = '\n'.join(f'{k}: {v}' for k, v in req.headers.items())
    s = '\n'.join([
        f'{req.method} {req.url}',
        headers,
        req.body
    ])
    print(s)

print1(req)
# POST https://bob.com
# a: 1
# b: 2
# body1
# body2

But when I've tried to rewrite it with f-strings for clarity and ease of modification, I get some bad indents:

# what I want the code to look like
def print2(req):
    headers = '\n'.join(f'{k}: {v}' for k, v in req.headers.items())
    s = f"""
    {req.method} {req.url}
    {headers}
    {req.body}
    """
    print(s)

print2(req)
#     POST https://bob.com
#     a: 1
# b: 2
#     body1
# body2

I know this is because I'm defining strings with newlines and putting them in a triple-quoted string. Is there a simple way to get the output I'm looking with a triple-quoted f-string defined in a function and without having to know the indentation level of its definition? I've played with textwrap.indent, textwrap.dedent, str.lstrip, re, etc., but the code stops being simple and pythonic fast. The closest thing I've come up with is the following, but the length is awkward and I feel like I'm repeating myself.

def print3(req):
    headers = '\n'.join(f'{k}: {v}' for k, v in req.headers.items())
    s = textwrap.dedent("""
    {method} {url}
    {headers}
    {body}
    """).strip()
    s = s.format(
        method=req.method,
        url=req.url,
        headers=headers,
        body=req.body,
    )
    print(s)
print3(req)
# POST https://bob.com
# a: 1
# b: 2
# body1
# body2
like image 890
Ben Avatar asked Oct 17 '22 08:10

Ben


2 Answers

I think you can try to take advantage of implicit string concatenation for a semi-nice looking solution:

def print4(req):
    headers = '\n'.join(f'{k}: {v}' for k, v in req.headers.items())
    s = (f'{req.method} {req.url}\n'
         f'{headers}\n'
         f'{req.body}')
    print(s)

print4(req)

Output:

POST https://bob.com
a: 1
b: 2
body1
body2

Note that, if you want, you can take out the parentheses and use backslashes:

s = f'{req.method} {req.url}\n' \
    f'{headers}\n'              \
    f'{req.body}'

However, the style guide prefers parentheses over backslashes.


Another option:

def print5(req):
    headers = '\n'.join(f'{k}: {v}' for k, v in req.headers.items())
    s = f"""
    {req.method} {req.url}
    {headers}
    {req.body}
    """
    s = '\n'.join(l.lstrip() for l in s.splitlines())
    print(s)
like image 109
iz_ Avatar answered Oct 21 '22 06:10

iz_


You can fix it with 2 tiny changes:

def print6(req, **w):
    headers = '\n'.join(f'{k}: {v}' for k, v in req.headers.items())
    method, url, body = \
        w['method'], w['url'], w['body']
    #   < note the changes belowwwwwwwwwwww >
    s = '\n'.join(line.lstrip() for line in f"""
    {method} {url}
    {headers}
    {body}
    """.split('\n')) # and note this .split('\n') over here
    print(s)
print6(req)
like image 40
GeeTransit Avatar answered Oct 21 '22 08:10

GeeTransit