10

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
Ben
  • 5,952
  • 4
  • 33
  • 44
  • Note: I went down this rabbit-hole trying to make [this](https://stackoverflow.com/a/23816211/2958070) more readable – Ben Jan 06 '19 at 05:24
  • You could just link together normal python strings inside the parenthesis of the `print()`. That way you can add more just by pressing enter. – GeeTransit Jan 06 '19 at 05:27
  • @GeeTransit what I'm really interested in is the slightly more general idea of *formatting* strings. Then I can pass them a round as I please. – Ben Jan 06 '19 at 05:51
  • `'\n'.join(line.lstrip() for line in f'''insert \n multiline \n string \n here \n {variable}'''.split('\n'))` – GeeTransit Jan 06 '19 at 05:55
  • Looks unpythonic inside a comment... It uses triple quoted `f-strings` so you can just add the part at the front, leave the middle, and add the ending `.split('\n')`. – GeeTransit Jan 06 '19 at 05:57
  • Eh, I'll put it inside an answer so you'll understand better. – GeeTransit Jan 06 '19 at 05:58

3 Answers3

4

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)
iz_
  • 15,923
  • 3
  • 25
  • 40
  • 1
    Instead of `s = '\n'.join(l.lstrip() for l in s.splitlines())`, try `s = textwrap.dedent(s)` from the standard library. – DocOc May 04 '21 at 21:57
1

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)
GeeTransit
  • 1,458
  • 9
  • 22
0

OK, so I know this question was posted AGES ago, but I came here cos I had the same question. The answer I've come with is to use the templating module jinja2 - mostly cos I kinda know it :) here's a great start guide. It's also not massively different from f string templates, ish.

Obv, if you don't want the indenting, then just don't use it in the triple-quote string.

Here's an example where I use triple-quotes to define the jinja2 template, set up the I you want in a dictionary, then merge the dictionary into the template. The parameters to render() are similar to the older "".format(), but I like doing it this way.

#! /usr/bin/python3

import jinja2

my_template = """<html>
    <body>
        {{ data.item_one }}
    <P>
        {{ data.item_two }}
    </body>
</html>
"""

my_dict = { "data" : {
    "item_one": "This is line one",
    "item_two": "This is line two"
    }
}

environment = jinja2.Environment()
template = environment.from_string(my_template)
print(template.render(**my_dict))

then run it

$ ./jin.py
<html>
        <body>
                This is line one
        <P>
                This is line two
        </body>
</html>
James Stevens
  • 374
  • 2
  • 8