1# -*- test-case-name: formless.test -*-
2# Copyright (c) 2004 Divmod.
3# See LICENSE for details.
4
5
6"""And the earth was without form, and void; and darkness was upon the face of the deep.
7"""
8
9import os
10import sys
11import inspect
12import warnings
13from zope.interface import implements
14from zope.interface.interface import InterfaceClass, Attribute
15
16from nevow import util
17
18
19from formless import iformless
20
21
22class count(object):
23    def __init__(self):
24        self.id = 0
25    def next(self):
26        self.id += 1
27        return self.id
28
29nextId = count().next
30
31
32class InputError(Exception):
33    """A Typed instance was unable to coerce from a string to the
34    appropriate type.
35    """
36    def __init__(self, reason):
37        self.reason = reason
38
39    def __str__(self):
40        return self.reason
41
42
43class ValidateError(Exception):
44    """A Binding instance was unable to coerce all it's arguments from a
45    dictionary of lists of strings to the appropriate types.
46
47    One use of this is to raise from an autocallable if an input is invalid.
48    For example, a password is incorrect.
49
50    errors must be a dictionary mapping argument names to error messages
51    to display next to the arguments on the form.
52
53    formErrorMessage is a string to display at the top of the form, not tied to
54    any specific argument on the form.
55
56    partialForm is a dict mapping argument name to argument value, allowing
57    you to have the form save values that were already entered in the form.
58    """
59    def __init__(self, errors, formErrorMessage=None, partialForm=None):
60        self.errors = errors
61        self.formErrorMessage = formErrorMessage
62        if partialForm is None:
63            self.partialForm = {}
64        else:
65            self.partialForm = partialForm
66
67    def __str__(self):
68        return self.formErrorMessage
69
70
71
72class Typed(Attribute):
73    """A typed value. Subclasses of Typed are constructed inside of
74    TypedInterface class definitions to describe the types of properties,
75    the parameter types to method calls, and method return types.
76
77    @ivar label: The short label which will describe this
78        parameter/proerties purpose to the user.
79
80    @ivar description: A long description which further describes the
81        sort of input the user is expected to provide.
82
83    @ivar default: A default value that may be used as an initial
84        value in the form.
85
86    @ivar required: Whether the user is required to provide a value
87
88    @ivar null: The value which will be produced if required is False
89        and the user does not provide a value
90
91    @ivar unicode: Iff true, try to determine the character encoding
92        of the data from the browser and pass unicode strings to
93        coerce.
94    """
95    implements(iformless.ITyped)
96
97    complexType = False
98    strip = False
99    label = None
100    description = None
101    default = ''
102    required = False
103    requiredFailMessage = 'Please enter a value'
104    null = None
105    unicode = False
106
107    __name__ = ''
108
109    def __init__(
110        self,
111        label=None,
112        description=None,
113        default=None,
114        required=None,
115        requiredFailMessage=None,
116        null=None,
117        unicode=None,
118        **attributes):
119
120        self.id = nextId()
121        if label is not None:
122            self.label = label
123        if description is not None:
124            self.description = description
125        if default is not None:
126            self.default = default
127        if required is not None:
128            self.required = required
129        if requiredFailMessage is not None:
130            self.requiredFailMessage = requiredFailMessage
131        if null is not None:
132            self.null = null
133        if unicode is not None:
134            self.unicode = unicode
135        self.attributes = attributes
136
137    def getAttribute(self, name, default=None):
138        return self.attributes.get(name, default)
139
140    def coerce(self, val, configurable):
141        raise NotImplementedError, "Implement in %s" % util.qual(self.__class__)
142
143
144#######################################
145## External API; your code will create instances of these objects
146#######################################
147
148class String(Typed):
149    """A string that is expected to be reasonably short and contain no
150    newlines or tabs.
151
152    strip: remove leading and trailing whitespace.
153    """
154
155    requiredFailMessage = 'Please enter a string.'
156    # iff true, return the stripped value.
157    strip = False
158
159    def __init__(self, *args, **kwargs):
160        try:
161            self.strip = kwargs['strip']
162            del kwargs['strip']
163        except KeyError:
164            pass
165        Typed.__init__(self, *args, **kwargs)
166
167    def coerce(self, val, configurable):
168        if self.strip:
169            val = val.strip()
170        return val
171
172
173class Text(String):
174    """A string that is likely to be of a significant length and
175    probably contain newlines and tabs.
176    """
177
178
179class Password(String):
180    """Password is used when asking a user for a new password. The renderer
181    user interface will possibly ask for the password multiple times to
182    ensure it has been entered correctly. Typical use would be for
183    registration of a new user."""
184    requiredFailMessage = 'Please enter a password.'
185
186
187class PasswordEntry(String):
188    """PasswordEntry is used to ask for an existing password. Typical use
189    would be for login to an existing account."""
190    requiredFailMessage = 'Please enter a password.'
191
192
193class FileUpload(Typed):
194    requiredFailMessage = 'Please enter a file name.'
195
196    def coerce(self, val, configurable):
197        return val.filename
198
199
200class Integer(Typed):
201
202    requiredFailMessage = 'Please enter an integer.'
203
204    def coerce(self, val, configurable):
205        if val is None:
206            return None
207        try:
208            return int(val)
209        except ValueError:
210            if sys.version_info < (2,3): # Long/Int aren't integrated
211                try:
212                    return long(val)
213                except ValueError:
214                    raise InputError("'%s' is not an integer." % val)
215
216            raise InputError("'%s' is not an integer." % val)
217
218
219class Real(Typed):
220
221    requiredFailMessage = 'Please enter a real number.'
222
223    def coerce(self, val, configurable):
224        # TODO: This shouldn't be required; check.
225        # val should never be None, but always a string.
226        if val is None:
227            return None
228        try:
229            return float(val)
230        except ValueError:
231            raise InputError("'%s' is not a real number." % val)
232
233
234class Boolean(Typed):
235    def coerce(self, val, configurable):
236        if val == 'False':
237            return False
238        elif val == 'True':
239            return True
240        raise InputError("'%s' is not a boolean" % val)
241
242
243class FixedDigitInteger(Integer):
244
245    def __init__(self, digits = 1, *args, **kw):
246        Integer.__init__(self, *args, **kw)
247        self.digits = digits
248        self.requiredFailMessage = \
249            'Please enter a %d digit integer.' % self.digits
250
251    def coerce(self, val, configurable):
252        v = Integer.coerce(self, val, configurable)
253        if len(str(v)) != self.digits:
254            raise InputError("Number must be %s digits." % self.digits)
255        return v
256
257
258class Directory(Typed):
259
260    requiredFailMessage = 'Please enter a directory name.'
261
262    def coerce(self, val, configurable):
263        # TODO: This shouldn't be required; check.
264        # val should never be None, but always a string.
265        if val is None:
266            return None
267        if not os.path.exists(val):
268            raise InputError("The directory '%s' does not exist." % val)
269        return val
270
271
272class Choice(Typed):
273    """Allow the user to pick from a list of "choices", presented in a drop-down
274    menu. The elements of the list will be rendered by calling the function
275    passed to stringify, which is by default "str".
276    """
277
278    requiredFailMessage = 'Please choose an option.'
279
280    def __init__(self, choices=None, choicesAttribute=None, stringify=str,
281                 valueToKey=str, keyToValue=None, keyAndConfigurableToValue=None,
282                 *args, **kw):
283        """
284        Create a Choice.
285
286        @param choices: an object adaptable to IGettable for an iterator (such
287        as a function which takes (ctx, data) and returns a list, a list
288        itself, a tuple, a generator...)
289
290        @param stringify: a pretty-printer.  a function which takes an object
291        in the list of choices and returns a label for it.
292
293        @param valueToKey: a function which converts an object in the list of
294        choices to a string that can be sent to a client.
295
296        @param keyToValue: a 1-argument convenience version of
297        keyAndConfigurableToValue
298
299        @param keyAndConfigurableToValue:  a 2-argument function which takes a string such as
300        one returned from valueToKey and a configurable, and returns an object
301        such as one from the list of choices.
302        """
303
304        Typed.__init__(self, *args, **kw)
305        self.choices = choices
306        if choicesAttribute:
307            self.choicesAttribute = choicesAttribute
308        if getattr(self, 'choicesAttribute', None):
309            warnings.warn(
310                "Choice.choicesAttribute is deprecated. Please pass a function to choices instead.",
311                DeprecationWarning,
312                stacklevel=2)
313            def findTheChoices(ctx, data):
314                return getattr(iformless.IConfigurable(ctx).original, self.choicesAttribute)
315            self.choices = findTheChoices
316
317        self.stringify = stringify
318        self.valueToKey=valueToKey
319
320        if keyAndConfigurableToValue is not None:
321            assert keyToValue is None, 'This should be *obvious*'
322            self.keyAndConfigurableToValue = keyAndConfigurableToValue
323        elif keyToValue is not None:
324            self.keyAndConfigurableToValue = lambda x,y: keyToValue(x)
325        else:
326            self.keyAndConfigurableToValue = lambda x,y: str(x)
327
328
329    def coerce(self, val, configurable):
330        """Coerce a value with the help of an object, which is the object
331        we are configuring.
332        """
333        return self.keyAndConfigurableToValue(val, configurable)
334
335
336class Radio(Choice):
337    """Type influencing presentation! horray!
338
339    Show the user radio button choices instead of a picklist.
340    """
341
342
343class Any(object):
344    """Marker which indicates any object type.
345    """
346
347
348class Object(Typed):
349    complexType = True
350    def __init__(self, interface=Any, *args, **kw):
351        Typed.__init__(self, *args, **kw)
352        self.iface = interface
353
354    def __repr__(self):
355        if self.iface is not None:
356            return "%s(interface=%s)" % (self.__class__.__name__, util.qual(self.iface))
357        return "%s(None)" % (self.__class__.__name__,)
358
359
360
361class List(Object):
362    implements(iformless.IActionableType)
363
364    complexType = True
365    def __init__(self, actions=None, header='', footer='', separator='', *args, **kw):
366        """Actions is a list of action methods which may be invoked on one
367        or more of the elements of this list. Action methods are defined
368        on a TypedInterface and declare that they take one parameter
369        of type List. They do not declare themselves to be autocallable
370        in the traditional manner. Instead, they are passed in the actions
371        list of a list Property to declare that the action may be taken on
372        one or more of the list elements.
373        """
374        if actions is None:
375            actions = []
376        self.actions = actions
377        self.header = header
378        self.footer = footer
379        self.separator = separator
380        Object.__init__(self, *args, **kw)
381
382    def coerce(self, data, configurable):
383        return data
384
385    def __repr__(self):
386        if self.iface is not None:
387            return "%s(interface=%s)" % (self.__class__.__name__, util.qual(self.iface))
388        return self.__class__.__name__ + "()"
389
390    def attachActionBindings(self, possibleActions):
391        ## Go through and replace self.actions, which is a list of method
392        ## references, with the MethodBinding instance which holds
393        ## metadata about this function.
394        act = self.actions
395        for method, binding in possibleActions:
396            if method in act:
397                act[act.index(method)] = binding
398
399    def getActionBindings(self):
400        return self.actions
401
402class Dictionary(List):
403    pass
404
405
406class Table(Object):
407    pass
408
409
410class Request(Typed):
411    """Marker that indicates that an autocallable should be passed the
412    request when called. Including a Request arg will not affect the
413    appearance of the rendered form.
414
415    >>> def doSomething(request=formless.Request(), name=formless.String()):
416    ...     pass
417    >>> doSomething = formless.autocallable(doSomething)
418    """
419    complexType = True ## Don't use the regular form
420
421
422class Context(Typed):
423    """Marker that indicates that an autocallable should be passed the
424    context when called. Including a Context arg will not affect the
425    appearance of the rendered form.
426
427    >>> def doSomething(context=formless.Context(), name=formless.String()):
428    ...     pass
429    >>> doSomething = formless.autocallable(doSomething)
430    """
431    complexType = True ## Don't use the regular form
432
433
434class Button(Typed):
435    def coerce(self, data, configurable):
436        return data
437
438
439class Compound(Typed):
440    complexType = True
441    def __init__(self, elements=None, *args, **kw):
442        assert elements, "What is the sound of a Compound type with no elements?"
443        self.elements = elements
444        Typed.__init__(self, *args, **kw)
445
446    def __len__(self):
447        return len(self.elements)
448
449    def coerce(self, data, configurable):
450        return data
451
452
453class Method(Typed):
454    def __init__(self, returnValue=None, arguments=(), *args, **kw):
455        Typed.__init__(self, *args, **kw)
456        self.returnValue = returnValue
457        self.arguments = arguments
458
459
460class Group(Object):
461    pass
462
463
464def autocallable(method, action=None, visible=False, **kw):
465    """Describe a method in a TypedInterface as being callable through the
466    UI. The "action" paramter will be used to label the action button, or the
467    user interface element which performs the method call.
468
469    Use this like a method adapter around a method in a TypedInterface:
470
471    >>> class IFoo(TypedInterface):
472    ...     def doSomething():
473    ...         '''Do Something
474    ...
475    ...         Do some action bla bla'''
476    ...         return None
477    ...     doSomething = autocallable(doSomething, action="Do it!!")
478    """
479    method.autocallable = True
480    method.id = nextId()
481    method.action = action
482    method.attributes = kw
483    return method
484
485
486#######################################
487## Internal API; formless uses these objects to keep track of
488## what names are bound to what types
489#######################################
490
491
492class Binding(object):
493    """Bindings bind a Typed instance to a name. When TypedInterface is subclassed,
494    the metaclass looks through the dict looking for all properties and methods.
495
496    If a properties is a Typed instance, a Property Binding is constructed, passing
497    the name of the binding and the Typed instance.
498
499    If a method has been wrapped with the "autocallable" function adapter,
500    a Method Binding is constructed, passing the name of the binding and the
501    Typed instance. Then, getargspec is called. For each keyword argument
502    in the method definition, an Argument is constructed, passing the name
503    of the keyword argument as the binding name, and the value of the
504    keyword argument, a Typed instance, as the binding typeValue.
505
506    One more thing. When an autocallable method is found, it is called with
507    None as the self argument. The return value is passed the Method
508    Binding when it is constructed to keep track of what the method is
509    supposed to return.
510    """
511    implements(iformless.IBinding)
512
513    label = None
514    description = ''
515
516    def __init__(self, name, typedValue, id=0):
517        self.id = id
518        self.name = name
519        self.typedValue = iformless.ITyped(typedValue)
520
521        # pull these out to remove one level of indirection...
522        if typedValue.description is not None:
523            self.description = typedValue.description
524        if typedValue.label is not None:
525            self.label = typedValue.label
526        if self.label is None:
527            self.label = nameToLabel(name)
528        self.default = typedValue.default
529        self.complexType = typedValue.complexType
530
531    def __repr__(self):
532        return "<%s %s=%s at 0x%x>" % (self.__class__.__name__, self.name, self.typedValue.__class__.__name__, id(self))
533
534    def getArgs(self):
535        """Return a *copy* of this Binding.
536        """
537        return (Binding(self.name, self.original, self.id), )
538
539    def getViewName(self):
540        return self.original.__class__.__name__.lower()
541
542    def configure(self, boundTo, results):
543        raise NotImplementedError, "Implement in %s" % util.qual(self.__class__)
544
545    def coerce(self, val, configurable):
546        if hasattr(self.original, 'coerce'):
547            return self.original.coerce(val)
548        return val
549
550class Argument(Binding):
551    pass
552
553
554class Property(Binding):
555    action = 'Change'
556    def configure(self, boundTo, results):
557        ## set the property!
558        setattr(boundTo, self.name, results[self.name])
559
560
561class MethodBinding(Binding):
562    typedValue = None
563    def __init__(self, name, typeValue, id=0, action="Call", attributes = {}):
564        Binding.__init__(self, name, typeValue,  id)
565        self.action = action
566        self.arguments = typeValue.arguments
567        self.returnValue = typeValue.returnValue
568        self.attributes = attributes
569
570    def getAttribute(self, name):
571        return self.attributes.get(name, None)
572
573    def configure(self, boundTo, results):
574        bound = getattr(boundTo, self.name)
575        return bound(**results)
576
577    def getArgs(self):
578        """Make sure each form post gets a unique copy of the argument list which it can use to keep
579        track of values given in partially-filled forms
580        """
581        return self.typedValue.arguments[:]
582
583
584class ElementBinding(Binding):
585    """An ElementBinding binds a key to an element of a container.
586    For example, ElementBinding('0', Object()) indicates the 0th element
587    of a container of Objects. When this ElementBinding is bound to
588    the list [1, 2, 3], resolving the binding will result in the 0th element,
589    the object 1.
590    """
591    pass
592
593
594class GroupBinding(Binding):
595    """A GroupBinding is a way of naming a group of other Bindings.
596    The typedValue of a GroupBinding should be a Configurable.
597    The Bindings returned from this Configurable (usually a TypedInterface)
598    will be rendered such that all fields must/may be filled out, and all
599    fields will be changed at once upon form submission.
600    """
601    def __init__(self, name, typedValue, id=0):
602        """Hack to prevent adaption to ITyped while the adapters are still
603        being registered, because we know that the typedValue should be
604        a Group when we are constructing a GroupBinding.
605        """
606        self.id = id
607        self.name = name
608        self.typedValue = Group(typedValue)
609
610        # pull these out to remove one level of indirection...
611        self.description = typedValue.description
612        if typedValue.label:
613            self.label = typedValue.label
614        else:
615            self.label = nameToLabel(name)
616        self.default = typedValue.default
617        self.complexType = typedValue.complexType
618
619    def configure(self, boundTo, group):
620        print "CONFIGURING GROUP BINDING", boundTo, group
621
622
623def _sorter(x, y):
624    return cmp(x.id, y.id)
625
626
627class _Marker(object):
628    pass
629
630
631def caps(c):
632    return c.upper() == c
633
634
635def nameToLabel(mname):
636    labelList = []
637    word = ''
638    lastWasUpper = False
639    for letter in mname:
640        if caps(letter) == lastWasUpper:
641            # Continuing a word.
642            word += letter
643        else:
644            # breaking a word OR beginning a word
645            if lastWasUpper:
646                # could be either
647                if len(word) == 1:
648                    # keep going
649                    word += letter
650                else:
651                    # acronym
652                    # we're processing the lowercase letter after the acronym-then-capital
653                    lastWord = word[:-1]
654                    firstLetter = word[-1]
655                    labelList.append(lastWord)
656                    word = firstLetter + letter
657            else:
658                # definitely breaking: lower to upper
659                labelList.append(word)
660                word = letter
661        lastWasUpper = caps(letter)
662    if labelList:
663        labelList[0] = labelList[0].capitalize()
664    else:
665        return mname.capitalize()
666    labelList.append(word)
667    return ' '.join(labelList)
668
669
670def labelAndDescriptionFromDocstring(docstring):
671    if docstring is None:
672        docstring = ''
673    docs = filter(lambda x: x, [x.strip() for x in docstring.split('\n')])
674    if len(docs) > 1:
675        return docs[0], '\n'.join(docs[1:])
676    else:
677        return None, '\n'.join(docs)
678
679
680class MetaTypedInterface(InterfaceClass):
681    """The metaclass for TypedInterface. When TypedInterface is subclassed,
682    this metaclass' __new__ method is invoked. The Typed Binding introspection
683    described in the Binding docstring occurs, and when it is all done, there will
684    be three attributes on the TypedInterface class:
685
686     - __methods__: An ordered list of all the MethodBinding instances
687       produced by introspecting all autocallable methods on this
688       TypedInterface
689
690     - __properties__: An ordered list of all the Property Binding
691       instances produced by introspecting all properties which have
692       Typed values on this TypedInterface
693
694     - __spec__: An ordered list of all methods and properties
695
696    These lists are sorted in the order that the methods and properties appear
697    in the TypedInterface definition.
698
699    For example:
700
701    >>> class Foo(TypedInterface):
702    ...     bar = String()
703    ...     baz = Integer()
704    ...
705    ...     def frotz(): pass
706    ...     frotz = autocallable(frotz)
707    ...
708    ...     xyzzy = Float()
709    ...
710    ...     def blam(): pass
711    ...     blam = autocallable(blam)
712
713    Once the metaclass __new__ is done, the Foo class instance will have three
714    properties, __methods__, __properties__, and __spec__,
715    """
716
717    def __new__(cls, name, bases, dct):
718        rv = cls = InterfaceClass.__new__(cls)
719        cls.__id__ = nextId()
720        cls.__methods__ = methods = []
721        cls.__properties__ = properties = []
722        cls.default = 'DEFAULT'
723        cls.complexType = True
724        possibleActions = []
725        actionAttachers = []
726        for key, value in dct.items():
727            if key[0] == '_': continue
728
729            if isinstance(value, MetaTypedInterface):
730                ## A Nested TypedInterface indicates a GroupBinding
731                properties.append(GroupBinding(key, value, value.__id__))
732
733                ## zope.interface doesn't like these
734                del dct[key]
735                setattr(cls, key, value)
736            elif callable(value):
737                names, _, _, typeList = inspect.getargspec(value)
738
739                _testCallArgs = ()
740
741                if typeList is None:
742                    typeList = []
743
744                if len(names) == len(typeList) + 1:
745                    warnings.warn(
746                        "TypeInterface method declarations should not have a 'self' parameter",
747                        DeprecationWarning,
748                        stacklevel=2)
749                    del names[0]
750                    _testCallArgs = (_Marker,)
751
752                if len(names) != len(typeList):
753                    ## Allow non-autocallable methods in the interface; ignore them
754                    continue
755
756                argumentTypes = [
757                    Argument(n, argtype, argtype.id) for n, argtype in zip(names[-len(typeList):], typeList)
758                ]
759
760                result = value(*_testCallArgs)
761
762                label = None
763                description = None
764                if getattr(value, 'autocallable', None):
765                    # autocallables have attributes that can set label and description
766                    label = value.attributes.get('label', None)
767                    description = value.attributes.get('description', None)
768
769                adapted = iformless.ITyped(result, None)
770                if adapted is None:
771                    adapted = Object(result)
772
773                # ITyped has label and description we can use
774                if label is None:
775                    label = adapted.label
776                if description is None:
777                    description = adapted.description
778
779                defaultLabel, defaultDescription = labelAndDescriptionFromDocstring(value.__doc__)
780                if defaultLabel is None:
781                    # docstring had no label, try the action if it is an autocallable
782                    if getattr(value, 'autocallable', None):
783                        if label is None and value.action is not None:
784                            # no explicit label, but autocallable has action we can use
785                            defaultLabel = value.action
786
787                if defaultLabel is None:
788                    # final fallback: use the function name as label
789                    defaultLabel = nameToLabel(key)
790
791                if label is None:
792                    label = defaultLabel
793                if description is None:
794                    description = defaultDescription
795
796                theMethod = Method(
797                    adapted, argumentTypes, label=label, description=description
798                )
799
800                if getattr(value, 'autocallable', None):
801                    methods.append(
802                        MethodBinding(
803                            key, theMethod, value.id, value.action, value.attributes))
804                else:
805                    possibleActions.append((value, MethodBinding(key, theMethod)))
806            else:
807                if not value.label:
808                    value.label = nameToLabel(key)
809                if iformless.IActionableType.providedBy(value):
810                    actionAttachers.append(value)
811                properties.append(
812                    Property(key, value, value.id)
813                )
814        for attacher in actionAttachers:
815            attacher.attachActionBindings(possibleActions)
816        methods.sort(_sorter)
817        properties.sort(_sorter)
818        cls.__spec__ = spec = methods + properties
819        spec.sort(_sorter)
820        cls.name = name
821
822        # because attributes "label" and "description" would become Properties,
823        # check for ones with an underscore prefix.
824        cls.label = dct.get('_label', None)
825        cls.description = dct.get('_description', None)
826        defaultLabel, defaultDescription = labelAndDescriptionFromDocstring(dct.get('__doc__'))
827        if defaultLabel is None:
828            defaultLabel = nameToLabel(name)
829        if cls.label is None:
830            cls.label = defaultLabel
831        if cls.description is None:
832            cls.description = defaultDescription
833
834        return rv
835
836
837#######################################
838## External API; subclass this to create a TypedInterface
839#######################################
840
841TypedInterface = MetaTypedInterface('TypedInterface', (InterfaceClass('TypedInterface'), ), {})
842
843