summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst48
-rw-r--r--markupsafe/__init__.py40
-rw-r--r--markupsafe/tests.py41
3 files changed, 123 insertions, 6 deletions
diff --git a/README.rst b/README.rst
index cc79e28..6d16c09 100644
--- a/README.rst
+++ b/README.rst
@@ -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:
@@ -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`
@@ -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>')
diff --git a/markupsafe/__init__.py b/markupsafe/__init__.py
index 5c5af40..d6c2ef4 100644
--- a/markupsafe/__init__.py
+++ b/markupsafe/__init__.py
@@ -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
@@ -164,7 +165,7 @@ class Markup(text_type):
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)
@@ -178,7 +179,7 @@ class Markup(text_type):
'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'):
@@ -191,13 +192,42 @@ class Markup(text_type):
# 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):
diff --git a/markupsafe/tests.py b/markupsafe/tests.py
index 145fafb..13e8b8c 100644
--- a/markupsafe/tests.py
+++ b/markupsafe/tests.py
@@ -71,9 +71,48 @@ class MarkupTestCase(unittest.TestCase):
(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__: