From cf717197d1779b8d746fa436bd88a225eb75f1be Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sat, 3 Jul 2004 12:01:42 +0000 Subject: [PATCH] Backported test fixes from trunk, see revision 26091. --- tests/test_utility.py | 905 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 905 insertions(+) create mode 100644 tests/test_utility.py diff --git a/tests/test_utility.py b/tests/test_utility.py new file mode 100644 index 0000000..09869fb --- /dev/null +++ b/tests/test_utility.py @@ -0,0 +1,905 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Form Utilities Tests + +$Id$ +""" +import doctest + +from zope.interface import Interface, implements +from zope.component.interfaces import IViewFactory +from zope.component.exceptions import ComponentLookupError +from zope.publisher.browser import TestRequest + +from zope.schema import Field +from zope.schema.interfaces import IField + +from zope.app.tests import ztapi +from zope.app.publisher.browser import BrowserView +from zope.app.form import Widget +from zope.app.form.interfaces import IWidget, IInputWidget +from zope.app.form.interfaces import IDisplayWidget, WidgetsError + +from zope.app.form.utility import no_value, setUpWidget, setUpWidgets +from zope.app.form.utility import setUpEditWidgets, setUpDisplayWidgets +from zope.app.form.utility import getWidgetsData, viewHasInput +from zope.app.form.utility import applyWidgetsChanges + +from zope.app.tests import placelesssetup + +request = TestRequest() + +class IFoo(IField): + pass + +class Foo(Field): + implements(IFoo) + +class IBar(IField): + pass + +class Bar(Field): + implements(IBar) + +class IContent(Interface): + foo = Foo() + bar = Bar() + +class Content: + implements(IContent) + foo = 'Foo' + +class IFooWidget(IWidget): + pass + +class IBarWidget(IWidget): + pass + +class FooWidget(Widget): + implements(IFooWidget) + def getPrefix(self): return self._prefix # exposes _prefix for testing + def getRenderedValue(self): return self._data # exposes _data for testing + def renderedValueSet(self): return self._renderedValueSet() # for testing + +class BarWidget(Widget): + implements(IBarWidget) + def getRenderedValue(self): return self._data # exposes _data for testing + def renderedValueSet(self): return self._renderedValueSet() # for testing + +def setUp(): + """Setup for tests.""" + placelesssetup.setUp() + ztapi.browserView(IFoo, '', FooWidget, providing=IFooWidget) + ztapi.browserView(IBar, '', BarWidget, providing=IBarWidget) + +def tearDown(): + placelesssetup.tearDown() + +def assertRaises(exceptionType, callable, *args): + try: + callable(*args) + return False + except Exception, e: + return isinstance(e, exceptionType) + +class TestSetUpWidget: + + def test_typical(self): + """Documents and tests the typical uses of setUpWidget. + + >>> setUp() + + setUpWidget ensures that the appropriate widget exists as an + attribute of a view. There are four required arguments to the + function: + + >>> view = BrowserView(Content(), request) + >>> name = 'foo' + >>> field = IContent['foo'] + >>> typeView = IFooWidget + + setUpWidget will add the appropriate widget as attribute to view + named 'foo_widget'. + + >>> hasattr(view, 'foo_widget') + False + >>> setUpWidget(view, name, field, typeView) + >>> hasattr(view, 'foo_widget') + True + >>> IFooWidget.providedBy(view.foo_widget) + True + + If the view already has an attribute with that name, it attempts to + use the existing value to create a widget. Two types are supported: + + - IViewFactory + - IWidget + + If the existing attribute value implements IViewFactory, it is used + to create a widget: + + >>> widget = FooWidget(IContent['foo'], request) + >>> class Factory: + ... implements(IViewFactory) + ... def __call__(self, request, context): + ... return widget + >>> setattr(view, 'foo_widget', Factory()) + >>> view.foo_widget is widget + False + >>> setUpWidget(view, name, field, typeView) + >>> view.foo_widget is widget + True + + If the existing attribute value implements IWidget, it is used without + modification: + + >>> setattr(view, 'foo_widget', widget) + >>> IWidget.providedBy(widget) + True + >>> setUpWidget(view, name, field, typeView) + >>> view.foo_widget is widget + True + + We now have to cleanup, so that these tests can be run in a loop. We + modified the 'IContent' interface saying that 'foo' is not mandatory, + so we have to change that back. + + >>> IContent['foo'].required = True + + >>> tearDown() + """ + + def test_validation(self): + """Documents and tests validation performed by setUpWidget. + + >>> setUp() + + setUpWidget ensures that the the view has an attribute that implements + IWidget. If setUpWidget cannot configure a widget, it raises a + TypeError. + + E.g., if a view already has a widget attribute of the name + + '_widget' that does not implement IViewFactory or + IWidget, setUpWidget raises a TypeError: + + >>> view = BrowserView(Content(), request) + >>> setattr(view, 'foo_widget', 'not a widget') + >>> assertRaises(TypeError, setUpWidget, + ... view, 'foo', IContent['foo'], IFooWidget) + True + + Similarly, if a view has a widget attribute that implements + IViewFactory, the object created by the factory must implement IWidget. + + >>> class Factory: + ... implements(IViewFactory) + ... def __call__(self, request, context): + ... return 'not a widget' + >>> setattr(view, 'foo_widget', Factory()) + >>> assertRaises(TypeError, setUpWidget, + ... view, 'foo', IContent['foo'], IFooWidget) + True + + >>> tearDown() + """ + + def test_context(self): + """Documents and tests the role of context in setUpWidget. + + >>> setUp() + + setUpWidget configures a widget by associating it to a bound field, + which is a copy of a schema field that is bound to an object. The + object the field is bound to can be explicitly specified in the + setUpWidget 'context' argument. + + By default, the context used by setUpWidget is the view context: + + >>> context = Content() + >>> view = BrowserView(context, request) + >>> setUpWidget(view, 'foo', IContent['foo'], IFooWidget) + >>> view.foo_widget.context.context is context + True + + Alternatively, you can specify a context explicitly: + + >>> view = BrowserView(context, request) + >>> altContext = Content() + >>> setUpWidget(view, 'foo', IContent['foo'], IFooWidget, + ... context=altContext) + >>> view.foo_widget.context.context is context + False + >>> view.foo_widget.context.context is altContext + True + + >>> tearDown() + """ + + def test_widgetLookup(self): + """Documents and tests how widgets are looked up by type. + + >>> setUp() + + If the view does not already have a widget configured for the + specified field name, setUpWidget will look up a widget using + an interface specified for the widgetType argument. + + Widgets are typically looked up for IInputWidget and IDisplayWidget + types. To illustrate this, we'll create two widgets, one for editing + and another for displaying IFoo attributes. Each widget is registered + as a view providing the appropriate widget type. + + >>> class EditFooWidget(Widget): + ... implements(IInputWidget) + ... def hasInput(self): + ... return False + >>> ztapi.browserViewProviding(IFoo, EditFooWidget, IInputWidget) + >>> class DisplayFooWidget(Widget): + ... implements(IDisplayWidget) + >>> ztapi.browserViewProviding(IFoo, DisplayFooWidget, + ... IDisplayWidget) + + A call to setUpWidget will lookup the widgets based on the specified + type. + + >>> view = BrowserView(Content(), request) + >>> setUpWidget(view, 'foo', IContent['foo'], IInputWidget) + >>> IInputWidget.providedBy(view.foo_widget) + True + >>> delattr(view, 'foo_widget') + >>> setUpWidget(view, 'foo', IContent['foo'], IDisplayWidget) + >>> IDisplayWidget.providedBy(view.foo_widget) + True + + A ComponentError is raised if a widget is not registered for the + specified type: + + >>> class IUnregisteredWidget(IWidget): + ... pass + >>> delattr(view, 'foo_widget') + >>> assertRaises(ComponentLookupError, setUpWidget, + ... view, 'foo', IContent['foo'], IUnregisteredWidget) + True + + >>> tearDown() + """ + + def test_prefix(self): + """Documents and tests the specification of widget prefixes. + + >>> setUp() + + Widgets support a prefix that can be used to group related widgets + on a view. To specify the prefix for a widget, specify in the call to + setUpWidget: + + >>> view = BrowserView(Content(), request) + >>> setUpWidget(view, 'foo', IContent['foo'], IFooWidget, + ... prefix='mygroup') + >>> view.foo_widget.getPrefix() + 'mygroup.' + + >>> tearDown() + """ + + def test_value(self): + """Documents and tests values and setUpWidget. + + >>> setUp() + + setUpWidget configures the widget with the value specified in the + 'value' argument: + + >>> view = BrowserView(Content(), request) + >>> setUpWidget(view, 'foo', IContent['foo'], IFooWidget, + ... value='Explicit Widget Value') + >>> view.foo_widget.renderedValueSet() + True + >>> view.foo_widget.getRenderedValue() + 'Explicit Widget Value' + + The utility module provides a marker object 'no_value' that can be + used as setUpWidget's 'value' argument to indicate that a value + doesn't exist for the bound field. This may seem a bit unusual since + None is typically used for this purpose. However, None is a valid + value for many fields and does not indicate 'no value'. + + When no_value is specified in a call to setUpWidget, the effected + widget is not configured with a value: + + >>> delattr(view, 'foo_widget') + >>> setUpWidget(view, 'foo', IContent['foo'], IFooWidget, + ... value=no_value) + >>> view.foo_widget.renderedValueSet() + False + + This is the also default behavior when the value argument is omitted: + + >>> delattr(view, 'foo_widget') + >>> setUpWidget(view, 'foo', IContent['foo'], IFooWidget) + >>> view.foo_widget.renderedValueSet() + False + + Note that when None is specified for 'value', the widget is configured + with None: + + >>> delattr(view, 'foo_widget') + >>> setUpWidget(view, 'foo', IContent['foo'], IFooWidget, + ... value=None) + >>> view.foo_widget.renderedValueSet() + True + >>> view.foo_widget.getRenderedValue() is None + True + + >>> tearDown() + """ + + def test_stickyValues(self): + """Documents and tests setUpWidget's handling of sticky values. + + >>> setUp() + + setUpWidget supports the concept of 'sticky values'. A sticky value + is a value displayed by a widget that should persist across multiple + across multiple object edit sessions. Sticky values ensure that invalid + user is available for the user to modify rather than being replaced + by some other value. + + setUpWidget inferst that a widget has a sticky value if: + + - The widget implements IInputWidget + - The widget returns True for its hasInput method + + To illustrate, we'll create and register an edit widget for foo that + has input: + + >>> class EditFooWidget(Widget): + ... implements(IInputWidget) + ... _data = "Original Value" + ... def hasInput(self): return True + ... def getRenderedValue(self): return self._data + >>> ztapi.browserView(IFoo, '', EditFooWidget, + ... providing=IInputWidget) + + Specifying a value to setUpWidget would typically cause that value + to be set for the widget: + + >>> view = BrowserView(Content(), request) + >>> setUpWidget(view, 'foo', IContent['foo'], IInputWidget, + ... value="A New Value") + + However, because EditFooWidget has input (i.e. has a 'sticky' value), + setUpWidget will not overwrite its value: + + >>> view.foo_widget._data + 'Original Value' + + You can use setUpWidget's 'ignoreStickyValues' argument to override + this behavior and force the widget's value to be overwritten with + the 'value' argument: + + >>> delattr(view, 'foo_widget') + >>> setUpWidget(view, 'foo', IContent['foo'], IInputWidget, + ... value="A New Value", ignoreStickyValues=True) + >>> view.foo_widget.getRenderedValue() + 'A New Value' + + >>> tearDown() + """ + +class TestSetUpWidgets: + + def test_typical(self): + """Tests the typical use of setUpWidgets. + + >>> setUp() + + The simplest use of setUpWidget configures a view with widgets of a + particular type for a schema: + + >>> view = BrowserView(Content(), request) + >>> setUpWidgets(view, IContent, IWidget) + + The view now has two widgets, one for each field in the specified + schema: + + >>> IWidget.providedBy(view.foo_widget) + True + >>> IWidget.providedBy(view.bar_widget) + True + + Because we did not provide initial values, the widget values are not + configured: + + >>> view.foo_widget.renderedValueSet() + False + >>> view.bar_widget.renderedValueSet() + False + + To specify initial values for the widgets, we can use the 'initial' + argument: + + >>> view = BrowserView(Content(), request) + >>> initial = {'foo': 'Value of Foo', 'bar': 'Value of Bar'} + >>> setUpWidgets(view, IContent, IWidget, initial=initial) + >>> view.foo_widget.getRenderedValue() + 'Value of Foo' + >>> view.bar_widget.getRenderedValue() + 'Value of Bar' + + >>> tearDown() + """ + + def test_names(self): + """Documents and tests the use of names in setUpWidgets. + + >>> setUp() + + The names argument can be used to configure a specific set of widgets + for a view: + + >>> view = BrowserView(Content(), request) + >>> IContent.names() + ['foo', 'bar'] + >>> setUpWidgets(view, IContent, IWidget, names=('bar',)) + >>> hasattr(view, 'foo_widget') + False + >>> hasattr(view, 'bar_widget') + True + + >>> tearDown() + """ + + def test_delegation(self): + """Tests setUpWidgets' use of setUpWidget. + + >>> setUp() + + setUpWidgets delegates several of its arguments to multiple calls to + setUpWidget - one call for each widget being configured. The arguments + passed directly through to calls to setUpWidget are: + + view + viewType + prefix + ignoreStickyValues + context + + To illustrate this, we'll replace setUpWidget in the utility module + and capture arguments passed to it when setUpWidgets is called. + + >>> def setUpWidget(view, name, field, viewType, value=None, + ... prefix=None, ignoreStickyValues=False, + ... context=None): + ... print "view: %s" % view.__class__ + ... print "name: %s" % name + ... print "field: %s" % field.__class__ + ... print "viewType: %s" % viewType.__class__ + ... if value is no_value: + ... print "value: not specified" + ... else: + ... print "value: %s" % value + ... print "prefix %s" % prefix + ... print "ignoreStickyValues: %s" % ignoreStickyValues + ... print "context: %s" % context + ... print '---' + >>> import zope.app.form.utility + >>> setUpWidgetsSave = zope.app.form.utility.setUpWidget + >>> zope.app.form.utility.setUpWidget = setUpWidget + + When we call setUpWidgets, we should see that setUpWidget is called + for each field in the specified schema: + + >>> view = BrowserView(Content(), request) + >>> setUpWidgets(view, IContent, IWidget, 'prefix', True, + ... initial={ "bar":"Bar" }, + ... context="Alt Context") + view: + name: foo + field: + viewType: + value: not specified + prefix prefix + ignoreStickyValues: True + context: Alt Context + --- + view: + name: bar + field: + viewType: + value: Bar + prefix prefix + ignoreStickyValues: True + context: Alt Context + --- + >>> zope.app.form.utility.setUpWidget = setUpWidgetsSave + + >>> tearDown() + """ + +class TestFormSetUp: + + def test_setUpEditWidgets(self): + """Documents and tests setUpEditWidgets. + + >>> setUp() + + setUpEditWidgets configures a view to collect field values from a + user. The function looks up widgets of type IInputWidget for the + specified schema. + + We'll first create and register widgets for the schema fields for + which we want input: + + >>> class InputWidget(Widget): + ... implements(IInputWidget) + ... def hasInput(self): + ... return False + ... def getRenderedValue(self): return self._data + >>> ztapi.browserViewProviding(IFoo, InputWidget, IInputWidget) + >>> ztapi.browserViewProviding(IBar, InputWidget, IInputWidget) + + Next we'll configure a view with a context object: + + >>> context = Content() + >>> context.foo = 'abc' + >>> context.bar = 'def' + >>> view = BrowserView(context, request) + + A call to setUpEditWidgets with the view: + + >>> setUpEditWidgets(view, IContent) + + configures the view with widgets that accept input for the context + field values: + + >>> isinstance(view.foo_widget, InputWidget) + True + >>> view.foo_widget.getRenderedValue() + 'abc' + >>> isinstance(view.bar_widget, InputWidget) + True + >>> view.bar_widget.getRenderedValue() + 'def' + + setUpEditWidgets provides a 'source' argument that provides an + alternate source of values to be edited: + + >>> view = BrowserView(context, request) + >>> source = Content() + >>> source.foo = 'abc2' + >>> source.bar = 'def2' + >>> setUpEditWidgets(view, IContent, source=source) + >>> view.foo_widget.getRenderedValue() + 'abc2' + >>> view.bar_widget.getRenderedValue() + 'def2' + + If a field is read only, setUpEditWidgets will use a display widget + (IDisplayWidget) intead of an input widget to display the field value. + + >>> class DisplayWidget(Widget): + ... implements(IDisplayWidget) + >>> ztapi.browserViewProviding(IFoo, DisplayWidget, IDisplayWidget) + >>> save = IContent['foo'].readonly # save readonly value + >>> IContent['foo'].readonly = True + >>> delattr(view, 'foo_widget') + >>> setUpEditWidgets(view, IContent) + >>> isinstance(view.foo_widget, DisplayWidget) + True + >>> IContent['foo'].readonly = save # restore readonly value + + >>> tearDown() + """ + + def test_setUpDisplayWidgets(self): + """Documents and tests setUpDisplayWidgets. + + >>> setUp() + + setUpDisplayWidgets configures a view for use as a display only form. + The function looks up widgets of type IDisplayWidget for the specified + schema. + + We'll first create and register widgets for the schame fields + we want to edit: + + >>> class DisplayWidget(Widget): + ... implements(IDisplayWidget) + ... def getRenderedValue(self): return self._data + >>> ztapi.browserViewProviding(IFoo, DisplayWidget, IDisplayWidget) + >>> ztapi.browserViewProviding(IBar, DisplayWidget, IDisplayWidget) + + Next we'll configure a view with a context object: + + >>> context = Content() + >>> context.foo = 'abc' + >>> context.bar = 'def' + >>> view = BrowserView(context, request) + + A call to setUpEditWidgets with the view: + + >>> setUpDisplayWidgets(view, IContent) + + configures the view with widgets that display the context fields: + + >>> isinstance(view.foo_widget, DisplayWidget) + True + >>> view.foo_widget.getRenderedValue() + 'abc' + >>> isinstance(view.bar_widget, DisplayWidget) + True + >>> view.bar_widget.getRenderedValue() + 'def' + + Like setUpEditWidgets, setUpDisplayWidgets accepts a 'source' + argument that provides an alternate source of values to be edited: + + >>> view = BrowserView(context, request) + >>> source = Content() + >>> source.foo = 'abc2' + >>> source.bar = 'def2' + >>> setUpDisplayWidgets(view, IContent, source=source) + >>> view.foo_widget.getRenderedValue() + 'abc2' + >>> view.bar_widget.getRenderedValue() + 'def2' + + >>> tearDown() + """ + +class TestForms: + + def test_viewHasInput(self): + """Tests viewHasInput. + + >>> setUp() + + viewHasInput returns True if any of the widgets for a set of fields + have user input. + + This method is typically invoked on a view that has been configured + with one setUpEditWidgets. + + >>> class InputWidget(Widget): + ... implements(IInputWidget) + ... input = None + ... def hasInput(self): + ... return self.input is not None + >>> ztapi.browserViewProviding(IFoo, InputWidget, IInputWidget) + >>> ztapi.browserViewProviding(IBar, InputWidget, IInputWidget) + >>> view = BrowserView(Content(), request) + >>> setUpEditWidgets(view, IContent) + + Because InputWidget is configured to not have input by default, the + view does not have input: + + >>> viewHasInput(view, IContent) + False + + But if we specify input for at least one widget: + + >>> view.foo_widget.input = 'Some Value' + >>> viewHasInput(view, IContent) + True + + >>> tearDown() + """ + + def test_applyWidgetsChanges(self): + """Documents and tests applyWidgetsChanges. + + >>> setUp() + + applyWidgetsChanges updates the view context, or an optional alternate + context, with widget values. This is typically called when a form + is submitted. + + We'll first create a simple edit widget that can be used to update + an object: + + >>> class InputWidget(Widget): + ... implements(IInputWidget) + ... input = None + ... def hasInput(self): + ... return input is not None + ... def applyChanges(self, object): + ... field = self.context + ... field.set(object, self.input) + ... return True + >>> ztapi.browserViewProviding(IFoo, InputWidget, IInputWidget) + + Before calling applyWidgetsUpdate, we need to configure a context and + a view with edit widgets: + + >>> context = Content() + >>> view = BrowserView(context, request) + >>> setUpEditWidgets(view, IContent, names=('foo',)) + + We now specify new widget input and apply the changes: + + >>> view.foo_widget.input = 'The quick brown fox...' + >>> context.foo + 'Foo' + >>> applyWidgetsChanges(view, IContent, names=('foo',)) + True + >>> context.foo + 'The quick brown fox...' + + By default, applyWidgetsChanges applies the new widget values to the + view context. Alternatively, we can provide a 'target' argument to + be updated: + + >>> target = Content() + >>> target.foo + 'Foo' + >>> applyWidgetsChanges(view, IContent, target=target, + ... names=('foo',)) + True + >>> target.foo + 'The quick brown fox...' + + applyWidgetsChanges is typically used in conjunction with one of the + setUp utility functions. If applyWidgetsChanges is called using a + view that was not previously configured with a setUp function, or + was not otherwise configured with widgets for each of the applicable + fields, an AttributeError will be raised: + + >>> view = BrowserView(context, request) + >>> applyWidgetsChanges(view, IContent, names=('foo',)) + Traceback (most recent call last): + AttributeError: 'BrowserView' object has no attribute 'foo_widget' + + >>> tearDown() + """ + +class TestGetWidgetsData: + + def test_typical(self): + """Documents and tests the typical use of getWidgetsData. + + >>> setUp() + + getWidgetsData retrieves the current values from widgets on a view. + For this test, we'll create a simple edit widget and register it + for the schema field types: + + >>> class InputWidget(Widget): + ... implements(IInputWidget) + ... input = None + ... def hasInput(self): + ... return self.input is not None + ... def getInputValue(self): + ... return self.input + >>> ztapi.browserViewProviding(IFoo, InputWidget, IInputWidget) + >>> ztapi.browserViewProviding(IBar, InputWidget, IInputWidget) + + We use setUpEditWidgets to configure a view with widgets for the + IContent schema: + + >>> view = BrowserView(Content(), request) + >>> setUpEditWidgets(view, IContent) + + The simplest form of getWidgetsData requires a view and a schema: + + >>> try: + ... result = getWidgetsData(view, IContent) + ... except Exception, e: + ... print 'getWidgetsData failed' + ... e + getWidgetsData failed + MissingInputError: ('foo', u'', 'the field is required') + MissingInputError: ('bar', u'', 'the field is required') + + We see that getWidgetsData raises a MissingInputError if a required + field does not have input from a widget.: + + >>> view.foo_widget.input = 'Input for foo' + >>> view.bar_widget.input = 'Input for bar' + >>> result = getWidgetsData(view, IContent) + + The result of getWidgetsData is a map of field names to widget values. + + >>> keys = result.keys(); keys.sort(); keys + ['bar', 'foo'] + >>> result['foo'] + 'Input for foo' + >>> result['bar'] + 'Input for bar' + + If a field is not required, however: + + >>> IContent['foo'].required = False + + we can omit input for its widget: + + >>> view.foo_widget.input = None + >>> result = getWidgetsData(view, IContent) + >>> 'foo' in result + False + + Note that when a widget indicates that is does not have input, its + results are omitted from getWidgetsData's return value. Users of + getWidgetsData should explicitly check for field values before + accessing them: + + >>> for name in IContent: + ... if name in result: + ... print (name, result[name]) + ('bar', 'Input for bar') + + You can also specify an optional 'names' argument (a tuple) to + request a subset of the schema fields: + + >>> result = getWidgetsData(view, IContent, names=('bar',)) + >>> result.keys() + ['bar'] + + >>> tearDown() + """ + + def test_widgetsErrorException(self): + """Documents and tests WidgetsError. + + XXX Move this test into zope.app.interfaces.tests.test_form when + that module is created. + + WidgetsError wraps one or more errors, which are specified as a + sequence in the 'errors' argument: + + >>> error = WidgetsError(('foo',)) + >>> error + str: foo + + WidgetsError also provides a 'widgetsData' attribute, which is a + map of valid field values, keyed by field name, that were obtained + in the same read operation that generated the errors: + + >>> error = WidgetsError(('foo',), widgetsData={'bar': 'Bar'}) + >>> error.widgetsData + {'bar': 'Bar'} + + The most typical use of this error is when reading a set of widget + values -- the read operation can generate more than one error, as well + as a set of successfully read values: + + >>> values = {'foo': 'Foo'} + >>> errors = [] + >>> widgetsData = {} + >>> for name in ('foo', 'bar'): # some list of values to read + ... try: + ... widgetsData[name] = values[name] # read operation + ... except Exception, e: + ... errors.append(e) # capture all errors + >>> if errors: + ... widgetsError = WidgetsError(errors, widgetsData) + ... raise widgetsError + Traceback (most recent call last): + WidgetsError: KeyError: 'bar' + + The handler of error can access all of the widget error as well as + the widget values read: + + >>> for error in widgetsError: + ... error.__class__.__name__ + 'KeyError' + >>> widgetsError.widgetsData + {'foo': 'Foo'} + """ + +def test_suite(): + return doctest.DocTestSuite() + +if __name__=='__main__': + main(defaultTest='test_suite')