# -*- 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'), ), {})
|