Skip to content

Commit

Permalink
Added docs and more tests for new string formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Apr 17, 2014
1 parent 026f317 commit cca7e70
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 6 deletions.
48 changes: 48 additions & 0 deletions README.rst
Expand Up @@ -21,6 +21,9 @@ u'42'
>>> soft_unicode(Markup('foo'))
Markup(u'foo')

HTML Representations
--------------------

Objects can customize their HTML markup equivalent by overriding
the `__html__` function:

Expand All @@ -33,6 +36,9 @@ Markup(u'<strong>Nice</strong>')
>>> Markup(Foo())
Markup(u'<strong>Nice</strong>')

Silent Escapes
--------------

Since MarkupSafe 0.10 there is now also a separate escape function
called `escape_silent` that returns an empty string for `None` for
consistency with other systems that return empty strings for `None`
Expand All @@ -49,3 +55,45 @@ object, you can create your own subclass that does that::
@classmethod
def escape(cls, s):
return cls(escape(s))

New-Style String Formatting
---------------------------

Starting with MarkupSafe 0.21 new style string formats from Python 2.6 and
3.x are now fully supported. Previously the escape behavior of those
functions was spotty at best. The new implementations operates under the
following algorithm:

1. if an object has an ``__html_format__`` method it is called as
replacement for ``__format__`` with the format specifier. It either
has to return a string or markup object.
2. if an object has an ``__html__`` method it is called.
3. otherwise the default format system of Python kicks in and the result
is HTML escaped.

Here is how you can implement your own formatting:

class User(object):

def __init__(self, id, username):
self.id = id
self.username = username

def __html_format__(self, format_spec):
if format_spec == 'link':
return Markup('<a href="/user/{0}">{1}</a>').format(
self.id,
self.__html__(),
)
elif format_spec:
raise ValueError('Invalid format spec')
return self.__html__()

def __html__(self):
return Markup('<span class=user>{0}</span>').format(self.username)

And to format that user:

>>> user = User(1, 'foo')
>>> Markup('<p>User: {0:link}').format(user)
Markup(u'<p>User: <a href="/user/1"><span class=user>foo</span></a>')
40 changes: 35 additions & 5 deletions markupsafe/__init__.py
Expand Up @@ -9,6 +9,7 @@
:license: BSD, see LICENSE for more details.
"""
import re
import string
from markupsafe._compat import text_type, string_types, int_types, \
unichr, iteritems, PY2

Expand Down Expand Up @@ -164,7 +165,7 @@ def escape(cls, s):
return cls(rv)
return rv

def make_wrapper(name):
def make_simple_escaping_wrapper(name):
orig = getattr(text_type, name)
def func(self, *args, **kwargs):
args = _escape_argspec(list(args), enumerate(args), self.escape)
Expand All @@ -178,7 +179,7 @@ def func(self, *args, **kwargs):
'title', 'lower', 'upper', 'replace', 'ljust', \
'rjust', 'lstrip', 'rstrip', 'center', 'strip', \
'translate', 'expandtabs', 'swapcase', 'zfill':
locals()[method] = make_wrapper(method)
locals()[method] = make_simple_escaping_wrapper(method)

# new in python 2.5
if hasattr(text_type, 'partition'):
Expand All @@ -191,13 +192,42 @@ def rpartition(self, sep):

# new in python 2.6
if hasattr(text_type, 'format'):
format = make_wrapper('format')
def format(*args, **kwargs):
self, args = args[0], args[1:]
formatter = EscapeFormatter(self.escape)
return self.__class__(formatter.format(self, *args, **kwargs))

def __html_format__(self, format_spec):
if format_spec:
raise ValueError('Unsupported format specification '
'for Markup.')
return self

# not in python 3
if hasattr(text_type, '__getslice__'):
__getslice__ = make_wrapper('__getslice__')
__getslice__ = make_simple_escaping_wrapper('__getslice__')

del method, make_simple_escaping_wrapper


if hasattr(text_type, 'format'):
class EscapeFormatter(string.Formatter):

def __init__(self, escape):
self.escape = escape

del method, make_wrapper
def format_field(self, value, format_spec):
if hasattr(value, '__html_format__'):
rv = value.__html_format__(format_spec)
elif hasattr(value, '__html__'):
if format_spec:
raise ValueError('No format specification allowed '
'when formatting an object with '
'its __html__ method.')
rv = value.__html__()
else:
rv = string.Formatter.format_field(self, value, format_spec)
return text_type(self.escape(rv))


def _escape_argspec(obj, iterable, escape):
Expand Down
41 changes: 40 additions & 1 deletion markupsafe/tests.py
Expand Up @@ -71,9 +71,48 @@ def test_formatting(self):
(Markup('%.2f') % 3.14159, '3.14'),
(Markup('%s %s %s') % ('<', 123, '>'), '&lt; 123 &gt;'),
(Markup('<em>{awesome}</em>').format(awesome='<awesome>'),
'<em>&lt;awesome&gt;</em>')):
'<em>&lt;awesome&gt;</em>'),
(Markup('{0[1][bar]}').format([0, {'bar': '<bar/>'}]),
'&lt;bar/&gt;'),
(Markup('{0[1][bar]}').format([0, {'bar': Markup('<bar/>')}]),
'<bar/>')):
assert actual == expected, "%r should be %r!" % (actual, expected)

def test_custom_formatting(self):
class HasHTMLOnly(object):
def __html__(self):
return Markup('<foo>')

class HasHTMLAndFormat(object):
def __html__(self):
return Markup('<foo>')
def __html_format__(self, spec):
return Markup('<FORMAT>')

assert Markup('{0}').format(HasHTMLOnly()) == Markup('<foo>')
assert Markup('{0}').format(HasHTMLAndFormat()) == Markup('<FORMAT>')

def test_complex_custom_formatting(self):
class User(object):
def __init__(self, id, username):
self.id = id
self.username = username
def __html_format__(self, format_spec):
if format_spec == 'link':
return Markup('<a href="/user/{0}">{1}</a>').format(
self.id,
self.__html__(),
)
elif format_spec:
raise ValueError('Invalid format spec')
return self.__html__()
def __html__(self):
return Markup('<span class=user>{0}</span>').format(self.username)

user = User(1, 'foo')
assert Markup('<p>User: {0:link}').format(user) == \
Markup('<p>User: <a href="/user/1"><span class=user>foo</span></a>')

def test_all_set(self):
import markupsafe as markup
for item in markup.__all__:
Expand Down

0 comments on commit cca7e70

Please sign in to comment.