Skip to content

Commit

Permalink
Backported wtforms#400
Browse files Browse the repository at this point in the history
  • Loading branch information
azmeuk committed Apr 20, 2020
1 parent 08345ca commit 47d7b16
Show file tree
Hide file tree
Showing 9 changed files with 33 additions and 72 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Expand Up @@ -42,6 +42,11 @@ Unreleased
(`#434`_, `#493`_)
- Choices which name and data are the same do not need to use tuples. (`#526`_)
- Added more documentation on HTML5 fields (`#326`_, `#409`_)
- HTML is escaped using MarkupSafe instead of the previous internal
implementation. :func:`~widgets.core.escape_html` is removed,
replaced by :func:`markupsafe.escape`.
:class:`~widgets.core.HTMLString` is removed, replaced by
:class:`markupsafe.Markup`. (`#400`_)

.. _#238: https://github.com/wtforms/wtforms/issues/238
.. _#239: https://github.com/wtforms/wtforms/issues/239
Expand All @@ -54,6 +59,7 @@ Unreleased
.. _#343: https://github.com/wtforms/wtforms/pull/343
.. _#389: https://github.com/wtforms/wtforms/pull/389
.. _#395: https://github.com/wtforms/wtforms/pull/395
.. _#400: https://github.com/wtforms/wtforms/pull/400
.. _#407: https://github.com/wtforms/wtforms/pull/407
.. _#409: https://github.com/wtforms/wtforms/pull/409
.. _#410: https://github.com/wtforms/wtforms/pull/410
Expand Down
7 changes: 5 additions & 2 deletions docs/widgets.rst
Expand Up @@ -36,8 +36,11 @@ use in building custom widgets as well.

.. autofunction:: html_params

.. autoclass:: HTMLString
:members: __html__
WTForms uses `MarkupSafe`_ to escape unsafe HTML characters before
rendering. You can mark a string using :class:`markupsafe.Markup` to
indicate that it should not be escaped.

.. _MarkupSafe: https://github.com/pallets/markupsafe

Custom widgets
--------------
Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -66,6 +66,7 @@
'wtforms.ext.sqlalchemy',
],
include_package_data=True,
install_requires=["MarkupSafe"],
extras_require={
'locale': ['Babel>=1.3'],
':python_version=="2.6"': ['ordereddict'],
Expand Down
2 changes: 1 addition & 1 deletion tests/ext_django/tests.py
Expand Up @@ -103,7 +103,7 @@ def test_form_field(self):
)
self.assertEqual(
self._render('''{% form_field a class='foo"bar"' %}'''),
'<input class="foo&quot;bar&quot;" id="a" name="a" type="text" value="">'
'<input class="foo&#34;bar&#34;" id="a" name="a" type="text" value="">'
)

@override_settings(TEMPLATE_STRING_IF_INVALID='__INVALID')
Expand Down
4 changes: 3 additions & 1 deletion tests/fields.py
Expand Up @@ -6,6 +6,8 @@
from decimal import Decimal, ROUND_UP, ROUND_DOWN
from unittest import TestCase

from markupsafe import Markup

from wtforms import validators, widgets, meta
from wtforms.fields import *
from wtforms.fields import Label, Field, SelectFieldBase, html5
Expand Down Expand Up @@ -150,7 +152,7 @@ def test_unbound_field(self):
assert repr(unbound).startswith('<UnboundField(TextField')

def test_htmlstring(self):
self.assertTrue(isinstance(self.field.__html__(), widgets.HTMLString))
self.assertTrue(isinstance(self.field.__html__(), Markup))

def test_str_coerce(self):
self.assertTrue(isinstance(str(self.field), str))
Expand Down
15 changes: 4 additions & 11 deletions tests/widgets.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals

from unittest import TestCase
from markupsafe import Markup
from wtforms.widgets import html_params, Input
from wtforms.widgets import *
from wtforms.widgets import html5
Expand All @@ -22,14 +23,6 @@ def __init__(self, data, name='f', label='', id='', type='TextField'):
iter_choices = lambda x: iter(x.data)


class EscapeHtmlTest(TestCase):
def test(self):
self.assertEqual(core.escape_html('<i class="bar">foo</i>'), '&lt;i class=&quot;bar&quot;&gt;foo&lt;/i&gt;')
self.assertEqual(core.escape_html('<i class="bar">foo</i>', quote=False), '&lt;i class="bar"&gt;foo&lt;/i&gt;')
self.assertEqual(core.escape_html(HTMLString('<i class="bar">foo</i>')), '<i class="bar">foo</i>')
self.assertEqual(core.escape_html(HTMLString('<i class="bar">foo</i>'), quote=False), '<i class="bar">foo</i>')


class HTMLParamsTest(TestCase):
def test_basic(self):
self.assertEqual(html_params(foo=9, k='wuuu'), 'foo="9" k="wuuu"')
Expand All @@ -47,7 +40,7 @@ def test_aria_prefix(self):
self.assertEqual(html_params(aria_foo_bar='foobar'), 'aria-foo-bar="foobar"')

def test_quoting(self):
self.assertEqual(html_params(foo='hi&bye"quot'), 'foo="hi&amp;bye&quot;quot"')
self.assertEqual(html_params(foo='hi&bye"quot'), 'foo="hi&amp;bye&#34;quot"')


class ListWidgetTest(TestCase):
Expand Down Expand Up @@ -153,10 +146,10 @@ def test_render_option(self):
)
self.assertEqual(
Select.render_option('bar', '<i class="bar"></i>foo', False),
'<option value="bar">&lt;i class="bar"&gt;&lt;/i&gt;foo</option>'
'<option value="bar">&lt;i class=&#34;bar&#34;&gt;&lt;/i&gt;foo</option>'
)
self.assertEqual(
Select.render_option('bar', HTMLString('<i class="bar"></i>foo'), False),
Select.render_option('bar', Markup('<i class="bar"></i>foo'), False),
'<option value="bar"><i class="bar"></i>foo</option>'
)

Expand Down
4 changes: 3 additions & 1 deletion wtforms/fields/core.py
Expand Up @@ -7,6 +7,8 @@

from copy import copy

from markupsafe import Markup

from wtforms import widgets
from wtforms.compat import text_type, izip
from wtforms.i18n import DummyTranslations
Expand Down Expand Up @@ -420,7 +422,7 @@ def __call__(self, text=None, **kwargs):
kwargs.setdefault('for', self.field_id)

attributes = widgets.html_params(**kwargs)
return widgets.HTMLString('<label %s>%s</label>' % (attributes, text or self.text))
return Markup('<label %s>%s</label>' % (attributes, text or self.text))

def __repr__(self):
return 'Label(%r, %r)' % (self.field_id, self.text)
Expand Down
2 changes: 1 addition & 1 deletion wtforms/widgets/__init__.py
@@ -1,4 +1,4 @@
from wtforms.widgets.core import *

# Compatibility imports
from wtforms.widgets.core import html_params, Input, HTMLString
from wtforms.widgets.core import html_params, Input
64 changes: 9 additions & 55 deletions wtforms/widgets/core.py
@@ -1,9 +1,6 @@
from __future__ import unicode_literals

try:
from html import escape
except ImportError:
from cgi import escape
from markupsafe import escape, Markup

from wtforms.compat import text_type, iteritems

Expand All @@ -14,24 +11,6 @@
)


def escape_html(s, quote=True):
"""
Replace special characters "&", "<" and ">" to HTML-safe sequences.
If the optional flag quote is true (the default), the quotation mark
characters, both double quote (") and single quote (') characters are also
translated.
If a `HTMLString` is provied, it's assumed that whatever you give to
escape_html is a string with any unsafe values already escaped.
"""
if hasattr(s, '__html__'):
s = s.__html__()
else:
s = escape(text_type(s), quote=quote)
return s


def html_params(**kwargs):
"""
Generate HTML attribute syntax from inputted keyword arguments.
Expand Down Expand Up @@ -69,35 +48,10 @@ def html_params(**kwargs):
elif v is False:
pass
else:
params.append('%s="%s"' % (text_type(k), escape(text_type(v), quote=True)))
params.append('%s="%s"' % (text_type(k), escape(v)))
return ' '.join(params)


class HTMLString(text_type):
"""
This is an "HTML safe string" class that is returned by WTForms widgets.
For the most part, HTMLString acts like a normal unicode string, except
in that it has a `__html__` method. This method is invoked by a compatible
auto-escaping HTML framework to get the HTML-safe version of a string.
Usage::
HTMLString('<input type="text" value="hello">')
"""
def __html__(self):
"""
Give an HTML-safe string.
This method actually returns itself, because it's assumed that
whatever you give to HTMLString is a string with any unsafe values
already escaped. This lets auto-escaping template frameworks
know that this string is safe for HTML rendering.
"""
return self


class ListWidget(object):
"""
Renders a list of fields as a `ul` or `ol` list.
Expand All @@ -124,7 +78,7 @@ def __call__(self, field, **kwargs):
else:
html.append('<li>%s %s</li>' % (subfield(), subfield.label))
html.append('</%s>' % self.html_tag)
return HTMLString(''.join(html))
return Markup(''.join(html))


class TableWidget(object):
Expand Down Expand Up @@ -157,7 +111,7 @@ def __call__(self, field, **kwargs):
html.append('</table>')
if hidden:
html.append(hidden)
return HTMLString(''.join(html))
return Markup(''.join(html))


class Input(object):
Expand All @@ -182,7 +136,7 @@ def __call__(self, field, **kwargs):
kwargs['value'] = field._value()
if 'required' not in kwargs and 'required' in getattr(field, 'flags', []):
kwargs['required'] = True
return HTMLString('<input %s>' % self.html_params(name=field.name, **kwargs))
return Markup('<input %s>' % self.html_params(name=field.name, **kwargs))


class TextInput(Input):
Expand Down Expand Up @@ -294,9 +248,9 @@ def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
if 'required' not in kwargs and 'required' in getattr(field, 'flags', []):
kwargs['required'] = True
return HTMLString('<textarea %s>\r\n%s</textarea>' % (
return Markup('<textarea %s>\r\n%s</textarea>' % (
html_params(name=field.name, **kwargs),
escape(text_type(field._value()), quote=False)
escape(field._value())
))


Expand Down Expand Up @@ -324,7 +278,7 @@ def __call__(self, field, **kwargs):
for val, label, selected in field.iter_choices():
html.append(self.render_option(val, label, selected))
html.append('</select>')
return HTMLString(''.join(html))
return Markup(''.join(html))

@classmethod
def render_option(cls, value, label, selected, **kwargs):
Expand All @@ -335,7 +289,7 @@ def render_option(cls, value, label, selected, **kwargs):
options = dict(kwargs, value=value)
if selected:
options['selected'] = True
return HTMLString('<option %s>%s</option>' % (html_params(**options), escape_html(label, quote=False)))
return Markup('<option %s>%s</option>' % (html_params(**options), escape(label)))


class Option(object):
Expand Down

0 comments on commit 47d7b16

Please sign in to comment.