From cca7e709fd09c54a4cc32f339749ad2fb173e912 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 17 Apr 2014 11:49:29 +0200 Subject: Added docs and more tests for new string formatting --- README.rst | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ markupsafe/__init__.py | 40 +++++++++++++++++++++++++++++++++++----- markupsafe/tests.py | 41 ++++++++++++++++++++++++++++++++++++++++- 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'Nice') >>> Markup(Foo()) Markup(u'Nice') +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('{1}').format( + self.id, + self.__html__(), + ) + elif format_spec: + raise ValueError('Invalid format spec') + return self.__html__() + + def __html__(self): + return Markup('{0}').format(self.username) + +And to format that user: + +>>> user = User(1, 'foo') +>>> Markup('

User: {0:link}').format(user) +Markup(u'

User: foo') 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('{awesome}').format(awesome=''), - '<awesome>')): + '<awesome>'), + (Markup('{0[1][bar]}').format([0, {'bar': ''}]), + '<bar/>'), + (Markup('{0[1][bar]}').format([0, {'bar': Markup('')}]), + '')): assert actual == expected, "%r should be %r!" % (actual, expected) + def test_custom_formatting(self): + class HasHTMLOnly(object): + def __html__(self): + return Markup('') + + class HasHTMLAndFormat(object): + def __html__(self): + return Markup('') + def __html_format__(self, spec): + return Markup('') + + assert Markup('{0}').format(HasHTMLOnly()) == Markup('') + assert Markup('{0}').format(HasHTMLAndFormat()) == Markup('') + + 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('{1}').format( + self.id, + self.__html__(), + ) + elif format_spec: + raise ValueError('Invalid format spec') + return self.__html__() + def __html__(self): + return Markup('{0}').format(self.username) + + user = User(1, 'foo') + assert Markup('

User: {0:link}').format(user) == \ + Markup('

User: foo') + def test_all_set(self): import markupsafe as markup for item in markup.__all__: -- cgit v1.2.1