Viewing file: annotate.py (28.1 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# -*- test-case-name: formless.test -*- # Copyright (c) 2004 Divmod. # See LICENSE for details.
"""And the earth was without form, and void; and darkness was upon the face of the deep. """
import os import sys import inspect import warnings from zope.interface import implements from zope.interface.interface import InterfaceClass, Attribute
from nevow import util
from formless import iformless
class count(object): def __init__(self): self.id = 0 def next(self): self.id += 1 return self.id
nextId = count().next
class InputError(Exception): """A Typed instance was unable to coerce from a string to the appropriate type. """ def __init__(self, reason): self.reason = reason def __str__(self): return self.reason
class ValidateError(Exception): """A Binding instance was unable to coerce all it's arguments from a dictionary of lists of strings to the appropriate types.
One use of this is to raise from an autocallable if an input is invalid. For example, a password is incorrect. errors must be a dictionary mapping argument names to error messages to display next to the arguments on the form.
formErrorMessage is a string to display at the top of the form, not tied to any specific argument on the form.
partialForm is a dict mapping argument name to argument value, allowing you to have the form save values that were already entered in the form. """ def __init__(self, errors, formErrorMessage=None, partialForm=None): self.errors = errors self.formErrorMessage = formErrorMessage if partialForm is None: self.partialForm = {} else: self.partialForm = partialForm
def __str__(self): return self.formErrorMessage
class Typed(Attribute): """A typed value. Subclasses of Typed are constructed inside of TypedInterface class definitions to describe the types of properties, the parameter types to method calls, and method return types. @ivar label: The short label which will describe this parameter/proerties purpose to the user. @ivar description: A long description which further describes the sort of input the user is expected to provide. @ivar default: A default value that may be used as an initial value in the form.
@ivar required: Whether the user is required to provide a value
@ivar null: The value which will be produced if required is False and the user does not provide a value
@ivar unicode: Iff true, try to determine the character encoding of the data from the browser and pass unicode strings to coerce. """ implements(iformless.ITyped)
complexType = False strip = False label = None description = None default = '' required = False requiredFailMessage = 'Please enter a value' null = None unicode = False
__name__ = ''
def __init__( self, label=None, description=None, default=None, required=None, requiredFailMessage=None, null=None, unicode=None, **attributes):
self.id = nextId() if label is not None: self.label = label if description is not None: self.description = description if default is not None: self.default = default if required is not None: self.required = required if requiredFailMessage is not None: self.requiredFailMessage = requiredFailMessage if null is not None: self.null = null if unicode is not None: self.unicode = unicode self.attributes = attributes
def getAttribute(self, name, default=None): return self.attributes.get(name, default)
def coerce(self, val, configurable): raise NotImplementedError, "Implement in %s" % util.qual(self.__class__)
####################################### ## External API; your code will create instances of these objects #######################################
class String(Typed): """A string that is expected to be reasonably short and contain no newlines or tabs.
strip: remove leading and trailing whitespace. """
requiredFailMessage = 'Please enter a string.' # iff true, return the stripped value. strip = False
def __init__(self, *args, **kwargs): try: self.strip = kwargs['strip'] del kwargs['strip'] except KeyError: pass Typed.__init__(self, *args, **kwargs)
def coerce(self, val, configurable): if self.strip: val = val.strip() return val
class Text(String): """A string that is likely to be of a significant length and probably contain newlines and tabs. """
class Password(String): """Password is used when asking a user for a new password. The renderer user interface will possibly ask for the password multiple times to ensure it has been entered correctly. Typical use would be for registration of a new user.""" requiredFailMessage = 'Please enter a password.'
class PasswordEntry(String): """PasswordEntry is used to ask for an existing password. Typical use would be for login to an existing account.""" requiredFailMessage = 'Please enter a password.'
class FileUpload(Typed): requiredFailMessage = 'Please enter a file name.'
def coerce(self, val, configurable): return val.filename
class Integer(Typed):
requiredFailMessage = 'Please enter an integer.'
def coerce(self, val, configurable): if val is None: return None try: return int(val) except ValueError: if sys.version_info < (2,3): # Long/Int aren't integrated try: return long(val) except ValueError: raise InputError("'%s' is not an integer." % val) raise InputError("'%s' is not an integer." % val)
class Real(Typed):
requiredFailMessage = 'Please enter a real number.'
def coerce(self, val, configurable): # TODO: This shouldn't be required; check. # val should never be None, but always a string. if val is None: return None try: return float(val) except ValueError: raise InputError("'%s' is not a real number." % val)
class Boolean(Typed): def coerce(self, val, configurable): if val == 'False': return False elif val == 'True': return True raise InputError("'%s' is not a boolean" % val)
class FixedDigitInteger(Integer): def __init__(self, digits = 1, *args, **kw): Integer.__init__(self, *args, **kw) self.digits = digits self.requiredFailMessage = \ 'Please enter a %d digit integer.' % self.digits
def coerce(self, val, configurable): v = Integer.coerce(self, val, configurable) if len(str(v)) != self.digits: raise InputError("Number must be %s digits." % self.digits) return v
class Directory(Typed): requiredFailMessage = 'Please enter a directory name.'
def coerce(self, val, configurable): # TODO: This shouldn't be required; check. # val should never be None, but always a string. if val is None: return None if not os.path.exists(val): raise InputError("The directory '%s' does not exist." % val) return val
class Choice(Typed): """Allow the user to pick from a list of "choices", presented in a drop-down menu. The elements of the list will be rendered by calling the function passed to stringify, which is by default "str". """
requiredFailMessage = 'Please choose an option.'
def __init__(self, choices=None, choicesAttribute=None, stringify=str, valueToKey=str, keyToValue=None, keyAndConfigurableToValue=None, *args, **kw): """ Create a Choice.
@param choices: an object adaptable to IGettable for an iterator (such as a function which takes (ctx, data) and returns a list, a list itself, a tuple, a generator...)
@param stringify: a pretty-printer. a function which takes an object in the list of choices and returns a label for it.
@param valueToKey: a function which converts an object in the list of choices to a string that can be sent to a client.
@param keyToValue: a 1-argument convenience version of keyAndConfigurableToValue
@param keyAndConfigurableToValue: a 2-argument function which takes a string such as one returned from valueToKey and a configurable, and returns an object such as one from the list of choices. """
Typed.__init__(self, *args, **kw) self.choices = choices if choicesAttribute: self.choicesAttribute = choicesAttribute if getattr(self, 'choicesAttribute', None): warnings.warn( "Choice.choicesAttribute is deprecated. Please pass a function to choices instead.", DeprecationWarning, stacklevel=2) def findTheChoices(ctx, data): return getattr(iformless.IConfigurable(ctx).original, self.choicesAttribute) self.choices = findTheChoices
self.stringify = stringify self.valueToKey=valueToKey
if keyAndConfigurableToValue is not None: assert keyToValue is None, 'This should be *obvious*' self.keyAndConfigurableToValue = keyAndConfigurableToValue elif keyToValue is not None: self.keyAndConfigurableToValue = lambda x,y: keyToValue(x) else: self.keyAndConfigurableToValue = lambda x,y: str(x)
def coerce(self, val, configurable): """Coerce a value with the help of an object, which is the object we are configuring. """ return self.keyAndConfigurableToValue(val, configurable)
class Radio(Choice): """Type influencing presentation! horray!
Show the user radio button choices instead of a picklist. """
class Any(object): """Marker which indicates any object type. """
class Object(Typed): complexType = True def __init__(self, interface=Any, *args, **kw): Typed.__init__(self, *args, **kw) self.iface = interface
def __repr__(self): if self.iface is not None: return "%s(interface=%s)" % (self.__class__.__name__, util.qual(self.iface)) return "%s(None)" % (self.__class__.__name__,)
class List(Object): implements(iformless.IActionableType)
complexType = True def __init__(self, actions=None, header='', footer='', separator='', *args, **kw): """Actions is a list of action methods which may be invoked on one or more of the elements of this list. Action methods are defined on a TypedInterface and declare that they take one parameter of type List. They do not declare themselves to be autocallable in the traditional manner. Instead, they are passed in the actions list of a list Property to declare that the action may be taken on one or more of the list elements. """ if actions is None: actions = [] self.actions = actions self.header = header self.footer = footer self.separator = separator Object.__init__(self, *args, **kw)
def coerce(self, data, configurable): return data
def __repr__(self): if self.iface is not None: return "%s(interface=%s)" % (self.__class__.__name__, util.qual(self.iface)) return self.__class__.__name__ + "()"
def attachActionBindings(self, possibleActions): ## Go through and replace self.actions, which is a list of method ## references, with the MethodBinding instance which holds ## metadata about this function. act = self.actions for method, binding in possibleActions: if method in act: act[act.index(method)] = binding
def getActionBindings(self): return self.actions
class Dictionary(List): pass
class Table(Object): pass
class Request(Typed): """Marker that indicates that an autocallable should be passed the request when called. Including a Request arg will not affect the appearance of the rendered form.
>>> def doSomething(request=formless.Request(), name=formless.String()): ... pass >>> doSomething = formless.autocallable(doSomething) """ complexType = True ## Don't use the regular form
class Context(Typed): """Marker that indicates that an autocallable should be passed the context when called. Including a Context arg will not affect the appearance of the rendered form.
>>> def doSomething(context=formless.Context(), name=formless.String()): ... pass >>> doSomething = formless.autocallable(doSomething) """ complexType = True ## Don't use the regular form
class Button(Typed): def coerce(self, data, configurable): return data
class Compound(Typed): complexType = True def __init__(self, elements=None, *args, **kw): assert elements, "What is the sound of a Compound type with no elements?" self.elements = elements Typed.__init__(self, *args, **kw)
def __len__(self): return len(self.elements)
def coerce(self, data, configurable): return data
class Method(Typed): def __init__(self, returnValue=None, arguments=(), *args, **kw): Typed.__init__(self, *args, **kw) self.returnValue = returnValue self.arguments = arguments
class Group(Object): pass
def autocallable(method, action=None, visible=False, **kw): """Describe a method in a TypedInterface as being callable through the UI. The "action" paramter will be used to label the action button, or the user interface element which performs the method call. Use this like a method adapter around a method in a TypedInterface: >>> class IFoo(TypedInterface): ... def doSomething(): ... '''Do Something ... ... Do some action bla bla''' ... return None ... doSomething = autocallable(doSomething, action="Do it!!") """ method.autocallable = True method.id = nextId() method.action = action method.attributes = kw return method
####################################### ## Internal API; formless uses these objects to keep track of ## what names are bound to what types #######################################
class Binding(object): """Bindings bind a Typed instance to a name. When TypedInterface is subclassed, the metaclass looks through the dict looking for all properties and methods. If a properties is a Typed instance, a Property Binding is constructed, passing the name of the binding and the Typed instance. If a method has been wrapped with the "autocallable" function adapter, a Method Binding is constructed, passing the name of the binding and the Typed instance. Then, getargspec is called. For each keyword argument in the method definition, an Argument is constructed, passing the name of the keyword argument as the binding name, and the value of the keyword argument, a Typed instance, as the binding typeValue. One more thing. When an autocallable method is found, it is called with None as the self argument. The return value is passed the Method Binding when it is constructed to keep track of what the method is supposed to return. """ implements(iformless.IBinding)
label = None description = ''
def __init__(self, name, typedValue, id=0): self.id = id self.name = name self.typedValue = iformless.ITyped(typedValue)
# pull these out to remove one level of indirection... if typedValue.description is not None: self.description = typedValue.description if typedValue.label is not None: self.label = typedValue.label if self.label is None: self.label = nameToLabel(name) self.default = typedValue.default self.complexType = typedValue.complexType
def __repr__(self): return "<%s %s=%s at 0x%x>" % (self.__class__.__name__, self.name, self.typedValue.__class__.__name__, id(self))
def getArgs(self): """Return a *copy* of this Binding. """ return (Binding(self.name, self.original, self.id), )
def getViewName(self): return self.original.__class__.__name__.lower()
def configure(self, boundTo, results): raise NotImplementedError, "Implement in %s" % util.qual(self.__class__)
def coerce(self, val, configurable): if hasattr(self.original, 'coerce'): return self.original.coerce(val) return val
class Argument(Binding): pass
class Property(Binding): action = 'Change' def configure(self, boundTo, results): ## set the property! setattr(boundTo, self.name, results[self.name])
class MethodBinding(Binding): typedValue = None def __init__(self, name, typeValue, id=0, action="Call", attributes = {}): Binding.__init__(self, name, typeValue, id) self.action = action self.arguments = typeValue.arguments self.returnValue = typeValue.returnValue self.attributes = attributes
def getAttribute(self, name): return self.attributes.get(name, None)
def configure(self, boundTo, results): bound = getattr(boundTo, self.name) return bound(**results)
def getArgs(self): """Make sure each form post gets a unique copy of the argument list which it can use to keep track of values given in partially-filled forms """ return self.typedValue.arguments[:]
class ElementBinding(Binding): """An ElementBinding binds a key to an element of a container. For example, ElementBinding('0', Object()) indicates the 0th element of a container of Objects. When this ElementBinding is bound to the list [1, 2, 3], resolving the binding will result in the 0th element, the object 1. """ pass
class GroupBinding(Binding): """A GroupBinding is a way of naming a group of other Bindings. The typedValue of a GroupBinding should be a Configurable. The Bindings returned from this Configurable (usually a TypedInterface) will be rendered such that all fields must/may be filled out, and all fields will be changed at once upon form submission. """ def __init__(self, name, typedValue, id=0): """Hack to prevent adaption to ITyped while the adapters are still being registered, because we know that the typedValue should be a Group when we are constructing a GroupBinding. """ self.id = id self.name = name self.typedValue = Group(typedValue)
# pull these out to remove one level of indirection... self.description = typedValue.description if typedValue.label: self.label = typedValue.label else: self.label = nameToLabel(name) self.default = typedValue.default self.complexType = typedValue.complexType
def configure(self, boundTo, group): print "CONFIGURING GROUP BINDING", boundTo, group
def _sorter(x, y): return cmp(x.id, y.id)
class _Marker(object): pass
def caps(c): return c.upper() == c
def nameToLabel(mname): labelList = [] word = '' lastWasUpper = False for letter in mname: if caps(letter) == lastWasUpper: # Continuing a word. word += letter else: # breaking a word OR beginning a word if lastWasUpper: # could be either if len(word) == 1: # keep going word += letter else: # acronym # we're processing the lowercase letter after the acronym-then-capital lastWord = word[:-1] firstLetter = word[-1] labelList.append(lastWord) word = firstLetter + letter else: # definitely breaking: lower to upper labelList.append(word) word = letter lastWasUpper = caps(letter) if labelList: labelList[0] = labelList[0].capitalize() else: return mname.capitalize() labelList.append(word) return ' '.join(labelList)
def labelAndDescriptionFromDocstring(docstring): if docstring is None: docstring = '' docs = filter(lambda x: x, [x.strip() for x in docstring.split('\n')]) if len(docs) > 1: return docs[0], '\n'.join(docs[1:]) else: return None, '\n'.join(docs)
_typedInterfaceMetadata = {}
class MetaTypedInterface(InterfaceClass): """The metaclass for TypedInterface. When TypedInterface is subclassed, this metaclass' __new__ method is invoked. The Typed Binding introspection described in the Binding docstring occurs, and when it is all done, there will be three attributes on the TypedInterface class: - __methods__: An ordered list of all the MethodBinding instances produced by introspecting all autocallable methods on this TypedInterface
- __properties__: An ordered list of all the Property Binding instances produced by introspecting all properties which have Typed values on this TypedInterface
- __spec__: An ordered list of all methods and properties These lists are sorted in the order that the methods and properties appear in the TypedInterface definition. For example: >>> class Foo(TypedInterface): ... bar = String() ... baz = Integer() ... ... def frotz(): pass ... frotz = autocallable(frotz) ... ... xyzzy = Float() ... ... def blam(): pass ... blam = autocallable(blam)
Once the metaclass __new__ is done, the Foo class instance will have three properties, __methods__, __properties__, and __spec__, """ __methods__ = property(lambda self: _typedInterfaceMetadata[self, '__methods__']) __id__ = property(lambda self: _typedInterfaceMetadata[self, '__id__']) __properties__ = property(lambda self: _typedInterfaceMetadata[self, '__properties__']) __spec__ = property(lambda self: _typedInterfaceMetadata[self, '__spec__']) name = property(lambda self: _typedInterfaceMetadata[self, 'name']) label = property(lambda self: _typedInterfaceMetadata[self, 'label']) description = property(lambda self: _typedInterfaceMetadata[self, 'description']) default = property(lambda self: _typedInterfaceMetadata.get((self, 'default'), 'DEFAULT')) complexType = property(lambda self: _typedInterfaceMetadata.get((self, 'complexType'), True))
def __new__(cls, name, bases, dct): rv = cls = InterfaceClass.__new__(cls, name, bases, dct) _typedInterfaceMetadata[cls, '__id__'] = nextId() _typedInterfaceMetadata[cls, '__methods__'] = methods = [] _typedInterfaceMetadata[cls, '__properties__'] = properties = [] possibleActions = [] actionAttachers = [] for key, value in dct.items(): if key[0] == '_': continue
if isinstance(value, MetaTypedInterface): ## A Nested TypedInterface indicates a GroupBinding properties.append(GroupBinding(key, value, value.__id__))
## zope.interface doesn't like these del dct[key] setattr(cls, key, value) elif callable(value): names, _, _, typeList = inspect.getargspec(value)
_testCallArgs = ()
if typeList is None: typeList = []
if len(names) == len(typeList) + 1: warnings.warn( "TypeInterface method declarations should not have a 'self' parameter", DeprecationWarning, stacklevel=2) del names[0] _testCallArgs = (_Marker,)
if len(names) != len(typeList): ## Allow non-autocallable methods in the interface; ignore them continue
argumentTypes = [ Argument(n, argtype, argtype.id) for n, argtype in zip(names[-len(typeList):], typeList) ]
result = value(*_testCallArgs)
label = None description = None if getattr(value, 'autocallable', None): # autocallables have attributes that can set label and description label = value.attributes.get('label', None) description = value.attributes.get('description', None)
adapted = iformless.ITyped(result, None) if adapted is None: adapted = Object(result)
# ITyped has label and description we can use if label is None: label = adapted.label if description is None: description = adapted.description
defaultLabel, defaultDescription = labelAndDescriptionFromDocstring(value.__doc__) if defaultLabel is None: # docstring had no label, try the action if it is an autocallable if getattr(value, 'autocallable', None): if label is None and value.action is not None: # no explicit label, but autocallable has action we can use defaultLabel = value.action
if defaultLabel is None: # final fallback: use the function name as label defaultLabel = nameToLabel(key)
if label is None: label = defaultLabel if description is None: description = defaultDescription
theMethod = Method( adapted, argumentTypes, label=label, description=description )
if getattr(value, 'autocallable', None): methods.append( MethodBinding( key, theMethod, value.id, value.action, value.attributes)) else: possibleActions.append((value, MethodBinding(key, theMethod))) else: if not value.label: value.label = nameToLabel(key) if iformless.IActionableType.providedBy(value): actionAttachers.append(value) properties.append( Property(key, value, value.id) ) for attacher in actionAttachers: attacher.attachActionBindings(possibleActions) methods.sort(_sorter) properties.sort(_sorter) _typedInterfaceMetadata[cls, '__spec__'] = spec = methods + properties spec.sort(_sorter) _typedInterfaceMetadata[cls, 'name'] = name
# because attributes "label" and "description" would become Properties, # check for ones with an underscore prefix. _typedInterfaceMetadata[cls, 'label'] = dct.get('_label', None) _typedInterfaceMetadata[cls, 'description'] = dct.get('_description', None) defaultLabel, defaultDescription = labelAndDescriptionFromDocstring(dct.get('__doc__')) if defaultLabel is None: defaultLabel = nameToLabel(name) if _typedInterfaceMetadata[cls, 'label'] is None: _typedInterfaceMetadata[cls, 'label'] = defaultLabel if _typedInterfaceMetadata[cls, 'description'] is None: _typedInterfaceMetadata[cls, 'description'] = defaultDescription
return rv
####################################### ## External API; subclass this to create a TypedInterface #######################################
TypedInterface = MetaTypedInterface('TypedInterface', (InterfaceClass('TypedInterface'), ), {})
|