简体   繁体   中英

Python string.format with inline pipes

I'm formatting a lot of strings for user messages. One might look like this:

def sms(**kwargs):
  return "Sorry {name}, but your payment was rejects. Please visit {url} and try again.".format(
    name=kwargs.get('name'),
    url=shorten_url(kwargs.get('url'))
  )

If I dont need to reformat any of the keyword args, I could just do this this is sweet:

def sms(**kwargs):
  return "Sorry {name}, but your payment was rejects. Please visit {url} and try again.".format(**kwargs)

So I was hoping maybe it would be possible to do something like this:

def sms(**kwargs):
  return "Sorry {name}, but your payment was rejects. Please visit {url|shorten_url} and try again.".format(**kwargs)

So I could format the string inline using pipes. It may not seem like a big deal, but I'm writing a LOT of these messages.

I noticed python string.vformat function but I'm not sure if thats what I'm looking for. Any ideas?

You can actually implement custom conversion functions if you subclass string.Formatter . Following example is based on this post

import string

class Template(string.Formatter):
    def convert_field(self, value, conversion):
        if conversion == 'u': # has to be a single char
            return value[:3] # replace with your shorten_url function
        # otherwise call the default convert_field method
        return super(Template, self).convert_field(value, conversion)

print(Template().format('{url!u}', url='SOME LONG URL'))

Outputs SOM

Another option is to just modify kwargs before you pass it to format:

>>> def sms(**kwargs):
...     kwargs['shorturl'] = shorten_url(kwargs['url'])
...     print('test {shorturl}'.format(**kwargs))

Edit:

Based on the fact that you want to use globals() , you could use something like

def bold(s):
  return "<strong>" + s + "</strong>"

def span(s):
  return "<span>" + s + "</span>"

class Template(string.Formatter):
    def get_field(self, name, args, kwargs):
        parts = name.split('|')
        # use first part as actual field name ('url' in this case)
        obj, used_key = super(Template, self).get_field(parts.pop(0), args, kwargs)
        for filter in parts:
            obj = globals()[filter](obj) # call remaining parts as filter functions
        return obj, used_key

print(Template().format('{url|bold|span}', url='SOME LONG URL'))
# Outputs: <span><strong>SOME LONG URL</strong></span>

The | char seems to be passed through with the field name, so you can (ab)use this as required. I would recommend adding some error handling and checking the call order on the functions is what you expect. I'm also not sure that using globals() is a great idea, especially if you're going to be processing unsafe format strings.

Pipes, or better "filters", are not implemented in Python stdlib templating.

Standard Python libraries offer various formatting options (justification, padding, number formatting), but it has certainly some limits.

Many templating packages do support custom filters, one of them being jinja2 :

from jinja2 import Environment


def dumb_shorten_url(url):
    # just shortening for fun, implement real shortening
    return url[6:]

env = Environment()
env.filters["shorten_url"] = dumb_shorten_url


templ = env.from_string("Sorry {{name}}, but your payment was rejects. "
                        "Please visit {{url|shorten_url}} and try again.")

kwargs = {"name": "James", "url": "http://acme.com/one/two"}

print templ.render(**kwargs)

There is much more what jinja2 offers (templates read from file system, from directories, loops, conditional expressions, escaping HTML...), but the example above shall demonstrate, it works with "pipes".

So this is more along the line of what I was looking for:

import re

def bold(string):
  return "<strong>" + string + "</strong>"

def format(string, **kwargs):
  # using the global scope, we can pipe kwargs through functions!
  scope = globals()
  def replace(substr):
    pieces = substr.group()[1:-1].split("|")
    value = kwargs.get(pieces[0])
    if len(pieces) > 1:
      pipes = pieces[1:]
      for pipe in pipes:
        value = scope[pipe](value)
    return value
  return re.sub(r"\{\S+\}", replace, string)

format("Hello {name|bold}, {yo}", **{"name":"Joe Schmo", "yo":"gimme more"})

It works, but the whole globals() thing concerns me. What if I define a function in another scope in another file that I want to use?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM