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?

4
  • 2
    Maybe you can use a real templating engine like jinja2? Commented Jan 27, 2016 at 0:12
  • Yes, lots of the Python templating systems I've seen have features like this. jinja2 has this in the form of custom filters Commented Jan 27, 2016 at 0:14
  • yeah, I'm actually moving away from jinja in favor of functional composition and string concatenation. here's a snippet of some actual code I'm writing -- I like it a lot more this way. Commented Jan 27, 2016 at 3:06
  • * See also: stackoverflow.com/q/35574349/42223 for another example of subclassing string.Formatter Commented Dec 23, 2016 at 4:16

3 Answers 3

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.

Sign up to request clarification or add additional context in comments.

3 Comments

nice to see that even python stdlib provides custom formatters.
You can also override format_field for more complex scenarios to even extend the formatting syntax, and allow more than one character for the conversion token.
hmm. for there is a built in "conversion" using !<letter> 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?
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".

4 Comments

yeah, I'm actually moving away from jinja -- I mentioned that in another comment. thanks for the suggestion though!
@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.
@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?
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?

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.