2

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?

Chet
  • 18,421
  • 15
  • 69
  • 113
  • 2
    Maybe you can use a real templating engine like [jinja2](http://jinja.pocoo.org/docs/dev/)? – univerio Jan 27 '16 at 00:12
  • Yes, lots of the Python templating systems I've seen have features like this. jinja2 has this in the form of [custom filters](http://jinja.pocoo.org/docs/dev/api/#custom-filters) – Marius Jan 27 '16 at 00:14
  • yeah, I'm actually moving away from jinja in favor of functional composition and string concatenation. [here's a snippet](http://pastebin.com/DbNptvTQ) of some actual code I'm writing -- I like it a lot more this way. – Chet Jan 27 '16 at 03:06
  • * **See also**: http://stackoverflow.com/q/35574349/42223 for another example of subclassing string.Formatter – dreftymac Dec 23 '16 at 04:16

3 Answers3

4

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.

Community
  • 1
  • 1
Peter Gibson
  • 19,086
  • 7
  • 60
  • 64
  • nice to see that even python stdlib provides custom formatters. – Jan Vlcinsky Jan 27 '16 at 01:27
  • 1
    You can also override [`format_field`](https://docs.python.org/2/library/string.html#string.Formatter.format_field) for more complex scenarios to even extend the formatting syntax, and allow more than one character for the conversion token. – Brendan Abel Jan 27 '16 at 01:36
  • hmm. for there is a built in "conversion" using `!` syntax? I didnt know that. Weird that it only support one letter and one conversion. I was hoping to have access to all the functions in the namespace. I forgot that python lets you add object methods. I'm not sure how to do this, but could I potentially make my own `string.custrom_format` function? – Chet Jan 27 '16 at 03:12
3

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".

Jan Vlcinsky
  • 42,725
  • 12
  • 101
  • 98
  • yeah, I'm actually moving away from jinja -- I mentioned that in another comment. thanks for the suggestion though! – Chet Jan 27 '16 at 03:12
  • @Chet I am interested to know the reasons, can you provide some link to your comment? We are using jinja2 often are very happy with that. – Jan Vlcinsky Jan 27 '16 at 08:12
  • [I metioned it here](http://stackoverflow.com/questions/35026423/python-string-format-with-inline-pipes/35027016?noredirect=1#comment57782497_35026423) – Chet Jan 27 '16 at 22:23
  • @Chet I have seen your comment with link to pastebin - but it shows Jinja2 code and does not illustrate any reason you are moving away from it.What do I miss? – Jan Vlcinsky Jan 29 '16 at 04:35
0

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?

Chet
  • 18,421
  • 15
  • 69
  • 113