[Zope3-checkins] SVN: Zope3/branches/f12gsprint-widget/src/zope/widget/ Added doctests, fleshed out interface and implementation.

Dylan Reinhardt zope at dylanreinhardt.com
Fri Sep 2 16:13:03 EDT 2005


Log message for revision 38248:
  Added doctests, fleshed out interface and implementation.
  

Changed:
  U   Zope3/branches/f12gsprint-widget/src/zope/widget/interfaces.py
  A   Zope3/branches/f12gsprint-widget/src/zope/widget/tests.py
  A   Zope3/branches/f12gsprint-widget/src/zope/widget/widget.py
  A   Zope3/branches/f12gsprint-widget/src/zope/widget/widget.txt

-=-
Modified: Zope3/branches/f12gsprint-widget/src/zope/widget/interfaces.py
===================================================================
--- Zope3/branches/f12gsprint-widget/src/zope/widget/interfaces.py	2005-09-02 19:50:56 UTC (rev 38247)
+++ Zope3/branches/f12gsprint-widget/src/zope/widget/interfaces.py	2005-09-02 20:13:02 UTC (rev 38248)
@@ -22,7 +22,13 @@
 from zope.interface import Attribute, Interface, implements
 from zope.schema import Bool
 
+# TODO: dots in names
 
+class ConversionError(ValueError):
+    """ Value could not be converted to correct type """
+    
+class InvalidStateError(RuntimeError):
+    """ This widget's state has been invalidated by a call to setValue()"""
 
 class IBaseWidget(IView):
     """Generically describes the behavior of a widget.
@@ -48,9 +54,6 @@
 
         Hint may be translated for the request.""")
 
-    visible = Attribute(
-        """A flag indicating whether or not the widget is visible.""")
-
     required = Bool(
         title=u"Required",
         description=u"""If True, widget should be displayed as requiring input.
@@ -69,8 +72,7 @@
     def initialize(prefix=None, value=None, state=None):
         """ Initialize widget and set its value.
 
-        If any widget method is called before initialize is called,
-        raise RuntimeError.
+        Initialize must be called before any other method besides __init__.
 
         If prefix is passed in, the prefix attribute of the widget is set.
         Prefix must be None or be a string.  See prefix attribute for more 
@@ -85,22 +87,21 @@
         from the getState method of a widget of the same class.  
         
         Only one of value or state may be passed, passing both raises
-        ValueError.        
+        TypeError.        
         
-        If the widget's value is not valid, its error attribute will contain an
-        appropriate ValidationError.  If a widget wishes to add an object to
-        message, that may be done here.
+        If the widget's value is not valid, its error attribute will contain 
+        either a ConversionError or zope.schema.interfaces.ValidationError.  If 
+        a widget wishes to add an object to message, that may be done here.
         """        
         
     def getState():
-        """ Return a pickleable object representing the state of the widget as 
-        currently rendered.
+        """ If the widget has been viewed previously, returns a non-None, 
+        picklable object representing the widget's state, unless setValue has 
+        been called, in which case it raises InvalidStateError.  Otherwise, 
+        returns None.
 
-        This object can later be passed in to initialize to restore the
+        A state object can later be passed in to initialize to restore the
         state of the widget.
-        
-        If the widget has never been rendered, getState should return None.
-        If the widget *has* been rendered, returning None would be an error.
         """
         
     def hasState():
@@ -124,7 +125,7 @@
         Prefix name may be None, or a string.  Any other value raises 
         ValueError.  When prefixes are concatenated with the widget name, a dot 
         is used as a delimiter; a trailing dot is neither required nor suggested
-        in the prefix itself.""")
+        in the prefix itself. """)
         
     error = Attribute(""" An exception created by initialize or setValue.  If 
         this value is not None, it is raised when getValue is called.

Added: Zope3/branches/f12gsprint-widget/src/zope/widget/tests.py
===================================================================
--- Zope3/branches/f12gsprint-widget/src/zope/widget/tests.py	2005-09-02 19:50:56 UTC (rev 38247)
+++ Zope3/branches/f12gsprint-widget/src/zope/widget/tests.py	2005-09-02 20:13:02 UTC (rev 38248)
@@ -0,0 +1,10 @@
+import unittest
+from zope.testing import doctest
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite(
+            'widget.txt'),))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest = 'test_suite')
\ No newline at end of file

Added: Zope3/branches/f12gsprint-widget/src/zope/widget/widget.py
===================================================================
--- Zope3/branches/f12gsprint-widget/src/zope/widget/widget.py	2005-09-02 19:50:56 UTC (rev 38247)
+++ Zope3/branches/f12gsprint-widget/src/zope/widget/widget.py	2005-09-02 20:13:02 UTC (rev 38248)
@@ -0,0 +1,141 @@
+# simple implementation of new widget API
+
+from zope import interface
+from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
+from zope.schema.interfaces import ValidationError
+
+import zope.widget.interfaces
+
+class BaseInputWidget(object):
+    """base simple widget"""
+    
+    interface.implements(zope.widget.interfaces.IInputWidget)
+    __doc__ = zope.widget.interfaces.IInputWidget.__doc__
+    
+    _name = None
+    name = property(lambda self: self._name)
+
+    _prefix = None
+    def prefix(self, value):
+        self._prefix = value
+        if value is None:
+            self._name = self.context.__name__
+        else:
+            self._name = '.'.join((value, self.context.__name__))
+    prefix = property(lambda self: self._prefix, prefix)
+    
+    _error = None
+    error = property(lambda self: self._error)
+
+    _message = None
+    message = property(lambda self: self._message)
+    
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+        self.label = context.title
+        self.hint = context.description
+        self.required = context.required
+        
+    _valueForced = False
+    _initialized = False    
+    _state = None
+    def initialize(self, prefix=None, value=None, state=None):
+        self._initialized = True
+        self.prefix = prefix
+        if state is None:
+            state = self._calculateStateFromRequest()
+        else:
+            if value is not None:
+                raise TypeError('May pass only one of value and state')
+        self._state = state
+        if value is None:
+            if self._state is None:
+                value = self.context.default
+            else:
+                value = self._calculateValueFromState()
+            self.setValue(value)
+            self._valueForced = False
+        else:
+            self.setValue(value)
+            
+    def getState(self):
+        if not self._initialized:
+            raise RuntimeError('Initialize widget first')
+        if self._state is not None and self._valueForced:
+            raise zope.widget.interfaces.InvalidStateError()
+        return self._state
+    
+    def hasState(self):
+        if not self._initialized:
+            raise RuntimeError('Initialize widget first')
+        return self._state is not None
+    
+    _value = None
+    def getValue(self):
+        if not self._initialized:
+            raise RuntimeError('Initialize widget first')
+        if self.error is not None:
+            raise self.error
+        return self._value
+    
+    def setValue(self, value):
+        if not self._initialized:
+            raise RuntimeError('Initialize widget first')
+        try:
+            self.context.validate(value)
+        except ValidationError, e:
+            self._error = e
+        else:
+            self._error = None
+        self._message = None
+        self._value = value
+        self._valueForced = True
+
+    def __call__(self):
+        raise NotImplementedError
+        
+    def _calculateStateFromRequest(self):
+        """return the widget's state object if it was rendered previously, or
+        None"""
+        return self.request.form.get(self.name)
+
+    def _calculateValueFromState(self):
+        """return the current value on the basis of the _state attribute"""
+        raise NotImplementedError
+    
+
+class AdvancedBaseInputWidget(BaseInputWidget):
+    """base advanced widget"""
+
+    def _calculateStateFromRequest(self):
+        res = {}
+        name = self.name
+        len_name = len(name)
+        prename = name + "."
+        for n, v in self.request.form.items():
+            if n == name or n.startswith(prename):
+                res[n[len_name:]] = v
+        return res or None
+
+class TextLineWidget(BaseInputWidget):
+    
+    #template = namedtemplate.NamedTemplate('default')    
+
+    def __call__(self):
+        if self._valueForced:
+            value = self.getValue()
+        else:
+            value = self._state
+        return self.template(value=value)
+
+    #("""<input type="text" value=%(value)s name=%(name)s """
+    #            """id=%(name)s size="20" />""")
+    
+    def _calculateValueFromState(self):
+        try:
+            value = unicode(self._state)
+        except ValueError, v: # XXX
+            e = zope.widget.interfaces.ConversionError(v)
+        else:
+            return value
\ No newline at end of file

Added: Zope3/branches/f12gsprint-widget/src/zope/widget/widget.txt
===================================================================
--- Zope3/branches/f12gsprint-widget/src/zope/widget/widget.txt	2005-09-02 19:50:56 UTC (rev 38247)
+++ Zope3/branches/f12gsprint-widget/src/zope/widget/widget.txt	2005-09-02 20:13:02 UTC (rev 38248)
@@ -0,0 +1,298 @@
+=====
+Widget API
+=====
+
+[...]
+
+
+=====================
+
+
+
+
+
+
+    >>> from zope.widget import widget, interfaces
+
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.schema import TextLine
+    >>> request = TestRequest()
+    >>> field = TextLine(__name__='test_name', title=u'Test Title', 
+    ...                  min_length=2, description=u'Test Description')
+    >>> context = object()
+    >>> field = field.bind(context)
+    >>> w = widget.TextLineWidget(field, request)
+    
+After creating a widget, the next step that must be taken is to initialize it.
+
+This is a separate step because the adapter machinery looks up the widget by
+context and request and instantiates it.  The widget must then be told how to
+obtain its value using initialize().
+
+The initialize method has three optional arguments: prefix, value and state.
+
+A widget that is initialized without any of these arguments will use the field
+default value and will have a name equivalent to the field name.
+
+    >>> w.initialize()
+    >>> w.prefix # None
+    >>> w.name
+    'test_name'
+    >>> w.getState() # None
+    >>> w.hasState()
+    False
+
+Because we have set no value for the widget, nor was any available in the 
+request, a required field will raise a RequiredMissing is getValue is called.
+
+    >>> w.getValue()
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+
+If the previous request rendered the field, the state will be gathered from it
+during initialize; if value is not passed explicitly to initialize, it will
+be set from the gathered state.
+
+    >>> request = TestRequest()
+    >>> request.form['test_name'] = u'Joe'
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize()
+    >>> w.hasState()
+    True
+    >>> w.getState() is not None
+    True
+    >>> w.getValue()
+    u'Joe'
+    
+A widget that is initialized with a prefix will prepend that prefix, plus a dot,
+to the field name.
+
+    >>> request = TestRequest()
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize(prefix='test_prefix')
+    >>> w.prefix
+    'test_prefix'
+    >>> w.name
+    'test_prefix.test_name'
+    >>> w.hasState()
+    False
+
+The prefix is also used for finding the previous information in the request, if 
+any.
+
+    >>> request = TestRequest()
+    >>> request.form['test_prefix.test_name'] = u'Joe'
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize(prefix='test_prefix')
+    >>> w.getValue()
+    u'Joe'
+    >>> w.hasState()
+    True
+    >>> w.getState() is not None
+    True
+    
+Prefix may be set directly.  It affects the read-only name attribute.
+
+    >>> w.prefix = 'new_prefix'
+    >>> w.name
+    'new_prefix.test_name'
+    >>> w.prefix = None
+    >>> w.name
+    'test_name'
+
+If a widget has been initialized, it will already have state gathered from the
+request, whether or not the prefix is the same.  This can be useful if the
+widget was drawn with one prefix, with valuable data, but needs to be redrawn
+with a new prefix.
+
+    >>> w.getValue()
+    u'Joe'
+    >>> w.hasState()
+    True
+    >>> w.getState() is not None
+    True
+
+A widget that is initialized with a value will attempt to set that value if
+it is valid.
+    
+    >>> request = TestRequest()
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize(value=u'test value')
+    >>> w.getValue()
+    u'test value'
+    >>> w.hasState()
+    False
+    >>> w.getState() # None    
+    
+State objects are black boxes.  The only contract they have is that they are
+pickleable.  Do not rely on their values, except for their intended use.
+    
+It is possible to keep a widget's state and use it to initilize a new widget
+of the same class.  This affects the value and whatever rendering the widget
+supports.  It does not affect the prefix.
+
+    >>> request = TestRequest()
+    >>> request.form['test_prefix.test_name'] = u'Joe'
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize(prefix='test_prefix')
+    >>> s = w.getState()
+    
+    >>> request = TestRequest()
+    >>> w2 = widget.TextLineWidget(field, request)
+    >>> w2.initialize(state=s)
+    >>> w2.hasState()
+    True
+    
+    >>> w.getState() == w2.getState()
+    True
+    >>> w.getValue() == w2.getValue()
+    True
+    >>> w.prefix != w2.prefix
+    True
+    >>> w.name != w2.name
+    True
+    
+A widget that is initialized with an invalid or unconvertable value will 
+store an exception object in the error attribute.  Calling getValue raises
+that error.
+    
+    >>> from zope.schema.interfaces import ValidationError
+    >>> request = TestRequest()
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize(value=5)
+    >>> isinstance(w.error, ValidationError)
+    True
+    >>> w.getState() # None
+    >>> w.hasState()
+    False
+    >>> w.getValue()
+    Traceback (most recent call last):
+    ...
+    WrongType: (5, <type 'unicode'>)
+    
+    
+    
+    >>> request = TestRequest()
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize()
+    >>> w.setValue(5)
+    >>> isinstance(w.error, ValidationError)
+    True
+    >>> w.getState() # None
+    >>> w.hasState()
+    False
+    >>> w.getValue()
+    Traceback (most recent call last):
+    ...
+    WrongType: (5, <type 'unicode'>)
+    
+
+    >>> request = TestRequest()
+    >>> request.form['test_name'] = 't'  # violates min length
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize()
+    >>> isinstance(w.error, ValidationError)
+    True
+    >>> w.getState() is not None
+    True
+    >>> w.hasState()
+    True
+    >>> w.getValue()
+    Traceback (most recent call last):
+    ...    
+    TooShort: (u't', 2)
+        
+    
+Attempting to pass both a value and a state to initialize will raise a
+TypeError.
+
+    >>> request = TestRequest()
+    >>> w3 = widget.TextLineWidget(field, request)
+    >>> w3.initialize(state=s, value='foo')
+    Traceback (most recent call last):
+    ...
+    TypeError: May pass only one of value and state
+    
+Note that if the widget has state and you pass in a value to initialize or 
+call setValue, calling getState will raise an InvalidStateError.
+
+    >>> request = TestRequest()
+    >>> request.form['test_prefix.test_name'] = u'Joe'  # widget has state
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize(prefix='test_prefix', value='foo')
+    >>> s = w.getState()
+    Traceback (most recent call last):
+    ...
+    InvalidStateError
+
+    >>> request = TestRequest()
+    >>> request.form['test_prefix.test_name'] = u'Joe'  # widget has state
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize(prefix='test_prefix')
+    >>> w.setValue('foo')
+    >>> s = w.getState()
+    Traceback (most recent call last):
+    ...
+    InvalidStateError
+
+    
+
+Until the widget has been initialized, the default implementation raises
+RuntimeError if another method is called as a help to the developer.
+
+    >>> w = widget.TextLineWidget(field, TestRequest())
+    >>> w.setValue('foo')
+    Traceback (most recent call last):
+    ...
+    RuntimeError: Initialize widget first
+    
+    >>> w.getValue()
+    Traceback (most recent call last):
+    ...
+    RuntimeError: Initialize widget first
+
+    >>> w.getState()
+    Traceback (most recent call last):
+    ...
+    RuntimeError: Initialize widget first
+
+    >>> w.hasState()
+    Traceback (most recent call last):
+    ...
+    RuntimeError: Initialize widget first
+
+    
+Widget objects have a label and a hint attribute.  Translation of these
+values is left to the template or presentation code.  Widget objects also have a
+required attribute.  
+
+    >>> request = TestRequest()
+    >>> w = widget.TextLineWidget(field, request)
+    >>> w.initialize()
+    >>> w.label == field.title
+    True
+    >>> w.label
+    u'Test Title'
+    >>> w.hint == field.description
+    True
+    >>> w.hint
+    u'Test Description'
+    >>> w.required == field.required
+    True
+    >>> w.required
+    True
+
+All three can be changed by setting the property.    
+    
+    >>> w.label = 'test label'
+    >>> w.label
+    'test label'
+    >>> w.hint = 'test hint'
+    >>> w.hint
+    'test hint'
+    >>> w.required = False
+    >>> w.required
+    False
+    
+# XXX remember to show __call__ and message



More information about the Zope3-Checkins mailing list