diff options
-rw-r--r-- | README.rst | 48 | ||||
-rw-r--r-- | markupsafe/__init__.py | 40 | ||||
-rw-r--r-- | markupsafe/tests.py | 41 |
3 files changed, 123 insertions, 6 deletions
@@ -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, '>'), '< 123 >'), (Markup('<em>{awesome}</em>').format(awesome='<awesome>'), - '<em><awesome></em>')): + '<em><awesome></em>'), + (Markup('{0[1][bar]}').format([0, {'bar': '<bar/>'}]), + '<bar/>'), + (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__: |