Permalink
Browse files

Simplify the validator API and make it compatible with FormEncode.

It is now always sufficient to call to_python() in order to both convert
and validate a value. For subclassing validators, we use the new API of
FormEncode 1.3, i.e. _validate_python() instead of validate_python().
The old method method name is still supported, but prints a warning.
  • Loading branch information...
Cito committed Feb 3, 2013
1 parent d8378b3 commit 5e5f91afabdef0e54d585acaec2c10f40773f765
@@ -1,11 +1,13 @@
*.pyc
*.pyo
*.egg*
Thumbs.db
docs/.output/
.coverage
.noseids
.project
.pydevproject
.settings
html
dist
build
@@ -429,7 +429,7 @@ If a field has no value, if defaults to ``None``. It is down to that field's val
**Security Consideration**
When a widget is redisplayed after a validation failure, it's value is derived from unvalidated user input. This means widgets must be "safe" for all input values. In practice, this is almost always the case without great care, so widgets are assumed to be safe.
When a widget is redisplayed after a validation failure, it's value is derived from unvalidated user input. This means widgets must be "safe" for all input values. In practice, this is almost always the case without great care, so widgets are assumed to be safe.
.. warning::
If a particular widget is not safe in this way, it must override :meth:`_validate` and set :attr:`value` to *None* in case of error.
@@ -460,7 +460,7 @@ Earlier versions of ToscaWidgets used FormEncode for validation and there are go
However, there are challenges making FormEncode and ToscaWidgets work together. For example, both libraries store the widget hierarchy internally. This makes implementing some features (e.g. ``strip_name`` and :class:`tw2.dynforms.HidingSingleSelectField`) difficult. There are different needs for the handling of unicode, leading ToscaWidgets to override some behaviour. Also, FormEncode just does not support client-side validation, a planned feature of ToscaWidgets 2.
ToscaWidgets 2 does not rely on FormEncode. However, developers can use FormEncode validators for individual fields. The API is compatible in that :meth:`to_python` and :meth:`from_python` are called for conversion, and :class:`formencode.Invalid` is caught. Also, if FormEncode is installed, the :class:`ValidationError` class is a subclass of :class:`formencode.Invalid`.
ToscaWidgets 2 does not rely on FormEncode. However, developers can use FormEncode validators for individual fields. The API is compatible in that :meth:`to_python` and :meth:`from_python` are called for conversion and validation, and :class:`formencode.Invalid` is caught. Also, if FormEncode is installed, the :class:`ValidationError` class is a subclass of :class:`formencode.Invalid`.
Using Validators
@@ -495,15 +495,15 @@ A two-pass approach is used internally, although this is generally hidden from t
.. autofunction:: tw2.core.validation.unflatten_params
If this fails, there is no attempt to determine which parameter failed; the whole submission is considered corrupt. If the root widget has an ``id``, this is stripped from the dictionary, e.g. ``{'myid': {'param':'value', ...}}`` is converted to ``{'param':'value', ...}``. A widget instance is created, and stored in request local storage. This allows compatibility with existing frameworks, e.g. the ``@validate`` decorator in TurboGears. There is a hook in :meth:`display` that detects the request local instance. After creating the instance, validate works recursively, using the :meth:`_validate`.
If this fails, there is no attempt to determine which parameter failed; the whole submission is considered corrupt. If the root widget has an ``id``, this is stripped from the dictionary, e.g. ``{'myid': {'param':'value', ...}}`` is converted to ``{'param':'value', ...}``. A widget instance is created, and stored in request local storage. This allows compatibility with existing frameworks, e.g. the ``@validate`` decorator in TurboGears. There is a hook in :meth:`display` that detects the request local instance. After creating the instance, validate works recursively, using the :meth:`_validate`.
.. automethod:: tw2.core.Widget._validate
.. automethod:: tw2.core.RepeatingWidget._validate
.. automethod:: tw2.core.CompoundWidget._validate
Both :meth:`_validate` and :meth:`validate_python` take an optional state argument. :class:`CompoundWidget` and :class:`RepeatingWidget` pass the partially built dict/list to their child widgets as state. This is useful for creating validators like :class:`MatchValidator` that reference sibling values. If one of the child widgets fails validation, the slot is filled with an :class:`Invalid` instance.
Both :meth:`_validate` and :meth:`to_python` take an optional state argument. :class:`CompoundWidget` and :class:`RepeatingWidget` pass the partially built dict/list to their child widgets as state. This is useful for creating validators like :class:`MatchValidator` that reference sibling values. If one of the child widgets fails validation, the slot is filled with an :class:`Invalid` instance.
General Considerations
@@ -8,7 +8,7 @@
...
>>> A.children[1]._sub_compound
True
>>> a = A.req(value={'a':'1','b':'2','c':'3'})
>>> a = A.req(value={'a':'1', 'b':'2', 'c':'3'})
>>> a.prepare()
>>> a.c.a.value
'1'
@@ -42,7 +42,7 @@ True
True
>>> B.children[1].child._sub_compound
True
>>> b = B.req(value={'a':'1','b':'2','c':'3'})
>>> b = B.req(value={'a':'1', 'b':'2', 'c':'3'})
>>> b.prepare()
>>> b.c.a.child.value
'1'
@@ -158,7 +158,6 @@ def test_inject_head(self):
js.inject()
csssrc.inject()
out = twc.inject_resources(html)
print out
assert eq_xhtml(out, '<html><head><script type="text/javascript" src="paj"></script>\
<style type="text/css">.bob { font-weight: bold; }</style>\
<title>a</title></head><body>hello</body></html>')
@@ -192,7 +191,6 @@ def test_inject_js_twice(self):
js.inject()
js.inject()
out = twc.inject_resources(html)
print out
assert eq_xhtml(out, '<html><head>\
<script type="text/javascript" src="paj"></script>\
<title>a</title></head><body>hello</body></html>')
@@ -223,8 +221,6 @@ def test_detect_clear(self):
rl = twc.core.request_local()
assert(len(rl.get('resources', [])) == 1)
out = twc.inject_resources(html)
print 'after inject_res'
print rl
eq_(rl.get('resources', []), [])
#--
@@ -234,7 +230,6 @@ def test_mw_resourcesapp(self):
testapi.request(1)
mw.resources.register('tw2.core', 'test_templates/simple_genshi.html')
fcont = open(os.path.join(os.path.dirname(twc.__file__), 'test_templates/simple_genshi.html')).read()
# print tst_mw.get('/resources/tw2.core/test_templates/simple_genshi.html').body
assert(tst_mw.get('/resources/tw2.core/test_templates/simple_genshi.html').body == fcont)
def test_mw_clear_rl(self):
@@ -309,13 +304,14 @@ def test_display(self):
}
''')
r = s.req()
displays = []
compare_to = None
for e in self._get_all_possible_engines():
displays.append(r.display(template='%s:%s' % (e, twr.JSSource.template)))
compare_to = str(displays[0]).strip()
equal_displays = filter(lambda x: str(x).strip() == compare_to, displays)
assert len(displays) == len(equal_displays), equal_displays
display = r.display(
template='%s:%s' % (e, twr.JSSource.template)).strip()
if compare_to is None:
compare_to = display
else:
assert display == compare_to, e
class TestCSSSourceEscaping(tb.WidgetTest):
widget = twr.CSSSource
@@ -330,16 +326,17 @@ def test_display(self):
''')
r = s.req()
displays = []
compare_to = None
for e in self._get_all_possible_engines():
#CSSource misses pt template.
# CSSource misses pt template.
if e in ['chameleon']:
continue
displays.append(r.display(template='%s:%s' % (e, twr.CSSSource.template)))
compare_to = str(displays[0]).strip()
equal_displays = filter(lambda x: str(x).strip() == compare_to, displays)
assert len(displays) == len(equal_displays), equal_displays
display = r.display(
template='%s:%s' % (e, twr.CSSSource.template)).strip()
if compare_to is None:
compare_to = display
else:
assert display == compare_to, e
from pkg_resources import Requirement
class TestResourcesApp:
@@ -378,7 +375,7 @@ def testEncoderDefault(self):
res = enc.default(None)
self.assert_(False)
except TypeError, te:
self.assert_(te.message.endswith("is not JSON serializable"))
self.assert_(str(te).endswith("is not JSON serializable"))
def testUnEscapeMarked(self):
enc = twc.encoder
Oops, something went wrong.

2 comments on commit 5e5f91a

@moschlar

This comment has been minimized.

Member

moschlar replied Jun 11, 2013

I know I'm a little late to the party, but I think that this breaks the semantics of BoolValidator.

Previously, an unselected checkbox validated correctly to False, now it validates to None.

@Cito

This comment has been minimized.

Contributor

Cito replied Jun 12, 2013

Unfortunately, that case was not caught by the test suite. I have fixed it in pull request #88.

Please sign in to comment.