Skip to content

Commit

Permalink
Merge b56f1b4 into 994e801
Browse files Browse the repository at this point in the history
  • Loading branch information
azmeuk committed Apr 20, 2020
2 parents 994e801 + b56f1b4 commit 29af886
Show file tree
Hide file tree
Showing 14 changed files with 180 additions and 36 deletions.
56 changes: 56 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,62 @@ Unreleased
----------

- Updated French translation.
- Modified the changes made in `#286`_: instead of copying the list of
``choices``, :class:`~fields.SelectField` now uses ``list()`` to construct
a new list of choices. (`#475`_)
- Permitted underscores in ``HostnameValidation`` (`#463`_)
- :class:`~validators.URL` validator now allows query parameters in the URL. (`#523`_, `#524`_).
- Updated ``false_values`` param in ``BooleanField`` docs (`#483`_, `#485`_)
- Fixed broken format string in Arabic translation (`#471`_)
- Updated French and Japanese translations. (`#514`_, `#506`_)
- Updated Ukrainian translation (`#433`_)
- ``FieldList`` error list is keeps entries in orders for easier identifcation
of erroring fields (`#257`_, `#407`_)
- :class:`~validators.Length` gives a more helpful error message when
``min`` and ``max`` are the same value (`#266`_)
- :class:`~fields.SelectField` no longer coerces ``None`` to ``"None"``
allowing use of ``"None"`` as an option (`#288`_, `#289`_)
- The :class:`~widgets.TextArea` widget prepends a ``\r\n`` newline
when rendering to account for browsers stripping an initial line for
display. This does not affect the value. (`#238`_, `#395`_)
- HTML5 :class:`~fields.html5.IntegerField` and
:class:`~fields.html5.RangeInput` don't render the ``step="1"``
attribute by default. (`#343`_)
- ``aria_`` args are rendered the same way as ``data_`` args, by
converting underscores to hyphens. ``aria_describedby="name-help"``
becomes ``aria-describedby="name-help"``. (`#239`_, `#389`_)
- Added a ``check_validators`` method to :class:`~fields.Field` which checks
if the given validators are both callable, and not classes (`#298`_, `#410`_)
- form.errors is not cached and will update if an error is appended to a field
after access. (`#568`_)
- :class:`~wtforms.validators.NumberRange` correctly handle *not a number*
values. (`#505`_, `#548`_)

.. _#238: https://github.com/wtforms/wtforms/issues/238
.. _#239: https://github.com/wtforms/wtforms/issues/239
.. _#257: https://github.com/wtforms/wtforms/issues/257
.. _#266: https://github.com/wtforms/wtforms/pull/266
.. _#288: https://github.com/wtforms/wtforms/pull/288
.. _#289: https://github.com/wtforms/wtforms/issues/289
.. _#298: https://github.com/wtforms/wtforms/issues/298
.. _#343: https://github.com/wtforms/wtforms/pull/343
.. _#389: https://github.com/wtforms/wtforms/pull/389
.. _#395: https://github.com/wtforms/wtforms/pull/395
.. _#407: https://github.com/wtforms/wtforms/pull/407
.. _#410: https://github.com/wtforms/wtforms/pull/410
.. _#475: https://github.com/wtforms/wtforms/pull/475
.. _#463: https://github.com/wtforms/wtforms/pull/463
.. _#505: https://github.com/wtforms/wtforms/pull/505
.. _#523: https://github.com/wtforms/wtforms/pull/523
.. _#524: https://github.com/wtforms/wtforms/pull/524
.. _#548: https://github.com/wtforms/wtforms/pull/548
.. _#483: https://github.com/wtforms/wtforms/pull/483
.. _#485: https://github.com/wtforms/wtforms/pull/485
.. _#471: https://github.com/wtforms/wtforms/pull/471
.. _#514: https://github.com/wtforms/wtforms/pull/514
.. _#506: https://github.com/wtforms/wtforms/pull/506
.. _#433: https://github.com/wtforms/wtforms/pull/433
.. _#568: https://github.com/wtforms/wtforms/pull/568

Version 2.2.1
-------------
Expand Down
4 changes: 0 additions & 4 deletions docs/forms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ The Form class
A dict containing a list of errors for each field. Empty if the form
hasn't been validated, or there were no errors.

Note that this is a lazy property, and will only be generated when you
first access it. If you call :meth:`validate` after accessing it, the
cached result will be invalidated and regenerated on next access.

.. attribute:: meta

This is an object which contains various configuration options and also
Expand Down
29 changes: 24 additions & 5 deletions tests/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,19 @@ def test_required_flag(self):
form = self.F()
self.assertEqual(form.b(), u'<input id="b" name="b" required type="text" value="">')

def test_check_validators(self):
v1 = "Not callable"
v2 = validators.DataRequired

with self.assertRaisesRegexp(TypeError, "{} is not a valid validator because "
"it is not callable".format(v1)):
Field(validators=[v1])

with self.assertRaisesRegexp(TypeError, "{} is not a valid validator because "
"it is a class, it should be an "
"instance".format(v2)):
Field(validators=[v2])


class PrePostTestField(TextField):
def pre_validate(self, form):
Expand Down Expand Up @@ -292,7 +305,7 @@ def test_default_coerce(self):
F = make_form(a=SelectField(choices=[('a', 'Foo')]))
form = F(DummyPostData(a=[]))
assert not form.validate()
self.assertEqual(form.a.data, 'None')
self.assertEqual(form.a.data, None)
self.assertEqual(len(form.a.errors), 1)
self.assertEqual(form.a.errors[0], 'Not a valid choice')

Expand Down Expand Up @@ -372,7 +385,7 @@ def test_text_coercion(self):
form.a(),
'''<ul id="a">'''
'''<li><input id="a-0" name="a" type="radio" value="True"> <label for="a-0">yes</label></li>'''
'''<li><input checked id="a-1" name="a" type="radio" value="False"> <label for="a-1">no</label></li></ul>'''
'''<li><input id="a-1" name="a" type="radio" value="False"> <label for="a-1">no</label></li></ul>'''
)


Expand Down Expand Up @@ -407,7 +420,7 @@ class F(Form):

def test(self):
form = self.F()
self.assertEqual(form.a(), """<textarea id="a" name="a">LE DEFAULT</textarea>""")
self.assertEqual(form.a(), """<textarea id="a" name="a">\r\nLE DEFAULT</textarea>""")


class PasswordFieldTest(TestCase):
Expand Down Expand Up @@ -860,6 +873,12 @@ def __init__(self, a):
self.assertEqual(len(form.a.entries), 3)
self.assertEqual(form.a.data, data)

def test_errors(self):
F = make_form(a=FieldList(self.t))
form = F(DummyPostData({'a-0': ['a'], 'a-1': ''}))
assert not form.validate()
self.assertEqual(form.a.errors, [[], ['This field is required.']])


class MyCustomField(TextField):
def process_data(self, data):
Expand Down Expand Up @@ -921,9 +940,9 @@ def test_simple(self):
b('datetime', '2013-09-05 00:23:42', 'type="datetime"', datetime(2013, 9, 5, 0, 23, 42)),
b('date', '2013-09-05', 'type="date"', date(2013, 9, 5)),
b('dt_local', '2013-09-05 00:23:42', 'type="datetime-local"', datetime(2013, 9, 5, 0, 23, 42)),
b('integer', '42', '<input id="integer" name="integer" step="1" type="number" value="42">', 42),
b('integer', '42', '<input id="integer" name="integer" type="number" value="42">', 42),
b('decimal', '43.5', '<input id="decimal" name="decimal" step="any" type="number" value="43.5">', Decimal('43.5')),
b('int_range', '4', '<input id="int_range" name="int_range" step="1" type="range" value="4">', 4),
b('int_range', '4', '<input id="int_range" name="int_range" type="range" value="4">', 4),
b('decimal_range', '58', '<input id="decimal_range" name="decimal_range" step="any" type="range" value="58">', 58),
)
formdata = DummyPostData()
Expand Down
17 changes: 16 additions & 1 deletion tests/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from wtforms.form import BaseForm, Form
from wtforms.meta import DefaultMeta
from wtforms.fields import TextField, IntegerField
from wtforms.validators import ValidationError
from wtforms.validators import ValidationError, DataRequired
from tests.common import DummyPostData


Expand Down Expand Up @@ -211,6 +211,21 @@ def test_empty_formdata(self):
self.assertEqual(self.F(DummyPostData(), test='test').test.data, 'test')
self.assertEqual(self.F(DummyPostData({'test': 'foo'}), test='test').test.data, 'foo')

def test_errors_access_during_validation(self):
class F(Form):
foo = TextField(validators=[DataRequired()])

def validate(self):
super(F, self).validate()
self.errors
self.foo.errors.append("bar")
return True

form = F(foo="whatever")
form.validate()

self.assertEqual({"foo": ["bar"]}, form.errors)


class MetaTest(TestCase):
class F(Form):
Expand Down
19 changes: 19 additions & 0 deletions tests/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)
from functools import partial
from tests.common import DummyField, grab_error_message, grab_stop_message
import decimal


class DummyForm(dict):
Expand Down Expand Up @@ -101,6 +102,7 @@ def test_length(self):
self.assertEqual(length(min=6)(self.form, field), None)
self.assertRaises(ValidationError, length(max=5), self.form, field)
self.assertEqual(length(max=6)(self.form, field), None)
self.assertEqual(length(min=6, max=6)(self.form, field), None)

self.assertRaises(AssertionError, length)
self.assertRaises(AssertionError, length, min=5, max=2)
Expand All @@ -111,6 +113,7 @@ def test_length(self):
self.assertTrue('at least 8' in grab(min=8))
self.assertTrue('longer than 5' in grab(max=5))
self.assertTrue('between 2 and 5' in grab(min=2, max=5))
self.assertTrue('exactly 5' in grab(min=5, max=5))

def test_required(self):
self.assertEqual(required()(self.form, DummyField('foobar')), None)
Expand Down Expand Up @@ -177,12 +180,23 @@ def test_regexp(self):
def test_url(self):
self.assertEqual(url()(self.form, DummyField('http://foobar.dk')), None)
self.assertEqual(url()(self.form, DummyField('http://foobar.dk/')), None)
self.assertEqual(url()(self.form, DummyField('http://foo-bar.dk/')), None)
self.assertEqual(url()(self.form, DummyField('http://foo_bar.dk/')), None)
self.assertEqual(url()(self.form, DummyField('http://foobar.dk?query=param')), None)
self.assertEqual(url()(self.form, DummyField('http://foobar.dk/path?query=param')), None)
self.assertEqual(url()(self.form, DummyField('http://foobar.dk/path?query=param&foo=faa')), None)
self.assertEqual(url()(self.form, DummyField('http://foobar.museum/foobar')), None)
self.assertEqual(url()(self.form, DummyField('http://127.0.0.1/foobar')), None)
self.assertEqual(url()(self.form, DummyField('http://127.0.0.1:9000/fake')), None)
self.assertEqual(url(require_tld=False)(self.form, DummyField('http://localhost/foobar')), None)
self.assertEqual(url(require_tld=False)(self.form, DummyField('http://foobar')), None)
self.assertRaises(ValidationError, url(), self.form, DummyField('http://foobar'))
self.assertRaises(ValidationError, url(), self.form, DummyField('http://foobar?query=param&foo=faa'))
self.assertRaises(ValidationError, url(), self.form, DummyField('http://foobar:5000?query=param&foo=faa'))
self.assertRaises(ValidationError, url(), self.form, DummyField('http://foobar/path?query=param&foo=faa'))
self.assertRaises(ValidationError, url(), self.form, DummyField('http://foobar:1234/path?query=param&foo=faa'))
self.assertRaises(ValidationError, url(), self.form, DummyField('http://-foobar.dk/'))
self.assertRaises(ValidationError, url(), self.form, DummyField('http://foobar-.dk/'))
self.assertRaises(ValidationError, url(), self.form, DummyField('foobar.dk'))
self.assertRaises(ValidationError, url(), self.form, DummyField('http://127.0.0/asdf'))
self.assertRaises(ValidationError, url(), self.form, DummyField('http://foobar.d'))
Expand Down Expand Up @@ -213,6 +227,11 @@ def test_number_range(self):
self.assertEqual(onlymax(self.form, DummyField(30)), None)
self.assertRaises(ValidationError, onlymax, self.form, DummyField(75))

def test_number_range_nan(self):
validator = NumberRange(0, 10)
for nan in (float("Nan"), decimal.Decimal("NaN")):
self.assertRaises(ValidationError, validator, self.form, DummyField(nan))

def test_lazy_proxy(self):
"""Tests that the validators support lazy translation strings for messages."""

Expand Down
6 changes: 5 additions & 1 deletion tests/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def test_data_prefix(self):
self.assertEqual(html_params(data_foo=22), 'data-foo="22"')
self.assertEqual(html_params(data_foo_bar=1), 'data-foo-bar="1"')

def test_aria_prefix(self):
self.assertEqual(html_params(aria_foo='bar'), 'aria-foo="bar"')
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"')

Expand Down Expand Up @@ -117,7 +121,7 @@ def test_radio_input(self):
def test_textarea(self):
# Make sure textareas escape properly and render properly
f = DummyField('hi<>bye')
self.assertEqual(TextArea()(f), '<textarea id="" name="f">hi&lt;&gt;bye</textarea>')
self.assertEqual(TextArea()(f), '<textarea id="" name="f">\r\nhi&lt;&gt;bye</textarea>')

def test_file(self):
self.assertEqual(FileInput()(self.field), '<input id="id" name="bar" type="file">')
Expand Down
39 changes: 34 additions & 5 deletions wtforms/fields/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import datetime
import decimal
import itertools
import inspect

from copy import copy

Expand Down Expand Up @@ -104,7 +105,9 @@ def __init__(self, label=None, validators=None, filters=tuple(),
self.name = _prefix + _name
self.short_name = _name
self.type = type(self).__name__
self.validators = validators or list(self.validators)

self.check_validators(validators)
self.validators = validators or self.validators

self.id = id or self.name
self.label = Label(self.id, label if label is not None else self.gettext(_name.replace('_', ' ').title()))
Expand Down Expand Up @@ -154,6 +157,18 @@ def __call__(self, **kwargs):
"""
return self.meta.render_field(self, kwargs)

@classmethod
def check_validators(cls, validators):
if validators is not None:
for validator in validators:
if not callable(validator):
raise TypeError("{} is not a valid validator because it is not "
"callable".format(validator))

if inspect.isclass(validator):
raise TypeError("{} is not a valid validator because it is a class, "
"it should be an instance".format(validator))

def gettext(self, string):
"""
Get a translation for the given message.
Expand Down Expand Up @@ -190,6 +205,9 @@ def validate(self, form, extra_validators=tuple()):
self.errors = list(self.process_errors)
stop_validation = False

# Check the type of extra_validators
self.check_validators(extra_validators)

# Call pre_validate
try:
self.pre_validate(form)
Expand Down Expand Up @@ -340,6 +358,9 @@ def __init__(self, field_class, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.creation_counter = UnboundField.creation_counter
validators = kwargs.get('validators')
if validators:
self.field_class.check_validators(validators)

def bind(self, form, name, prefix='', translations=None, **kwargs):
kw = dict(
Expand Down Expand Up @@ -448,14 +469,19 @@ class SelectField(SelectFieldBase):
def __init__(self, label=None, validators=None, coerce=text_type, choices=None, **kwargs):
super(SelectField, self).__init__(label, validators, **kwargs)
self.coerce = coerce
self.choices = copy(choices)
self.choices = list(choices) if choices is not None else None

def iter_choices(self):
for value, label in self.choices:
yield (value, label, self.coerce(value) == self.data)

def process_data(self, value):
try:
# protect against coercing None,
# such as in text_type(None) -> "None"
if value is None:
raise ValueError()

self.data = self.coerce(value)
except (ValueError, TypeError):
self.data = None
Expand Down Expand Up @@ -693,7 +719,7 @@ class BooleanField(Field):
:param false_values:
If provided, a sequence of strings each of which is an exact match
string of what is considered a "false" value. Defaults to the tuple
``('false', '')``
``(False, 'false', '',)``
"""
widget = widgets.CheckboxInput()
false_values = (False, 'false', '')
Expand Down Expand Up @@ -938,8 +964,11 @@ def validate(self, form, extra_validators=tuple()):

# Run validators on all entries within
for subfield in self.entries:
if not subfield.validate(form):
self.errors.append(subfield.errors)
subfield.validate(form)
self.errors.append(subfield.errors)

if not any(x for x in self.errors):
self.errors = []

chain = itertools.chain(self.validators, extra_validators)
self._run_validation_chain(form, chain)
Expand Down
4 changes: 2 additions & 2 deletions wtforms/fields/html5.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class IntegerField(core.IntegerField):
"""
Represents an ``<input type="number">``.
"""
widget = widgets.NumberInput(step='1')
widget = widgets.NumberInput()


class DecimalField(core.DecimalField):
Expand All @@ -85,7 +85,7 @@ class IntegerRangeField(core.IntegerField):
"""
Represents an ``<input type="range">``.
"""
widget = widgets.RangeInput(step='1')
widget = widgets.RangeInput()


class DecimalRangeField(core.DecimalField):
Expand Down

0 comments on commit 29af886

Please sign in to comment.