Call for suggestions: Declaring data entry forms using Python classes

Carlos Ribeiro carribeiro at gmail.com
Thu Sep 23 18:36:19 CEST 2004


Hello all,

I'm posting this to the list with the intention to form a group of
people interested in this type of solution. I'm not going to spam the
list with it, unless for occasional and relevant announcements. If
you're interested, drop me a note. But if for some reason you think
that this discussion is fine here at the c.l.py, please let me know.

** LONG POST AHEAD **

I'm doing good progress on my form declarative language library. The
idea (for those who haven't read about it) is to be able to specify a
form declaration in the following format:

class ComplexForm(Form):
    class Header(Form):
        nickname = TextBox(length=15, default="")
        password = TextBox(length=10, default="", password=True)
        name     = TextBox(length=40, default="")
    class Comment(Form):
        comments = TextBox(length=200, default="", multiline=True)

This is an experimental approach, with the intention to allow one to
write the form description in pure Python code, with the least
syntactic clutter possible. The main alternative is to use a data
driven approach -- feeding dicts or XML based representation to a
constructor -- but as I said, that's not the goal of this experiment.

I'm already able to properly build the example mentioned above. The
result is a conventional class filled with all attributes, and an
extra <_fields> member which is a ordered list of field object
instances. The metaclass constructor checks for some issues:

1) For the purposes of GUI description, field attributes are
order-dependant. But the dict passed to the metaclass constructor
isn't ordered. My first solution used stack information (using the
inspect module) to solve this problem. Thomas Heller and Andrew Dalke
proposed a simpler solution using a global counter that I've included
in the current incarnation (I'm wondering why did I try that hack
before, this solution is *much* better).

2) The inner classes (Header and Comment, in the example above) are
converted to *instances* of the same name during the construction of
the outer class (ComplexForm, in the example). The goal was to achieve
consistency; if some field members are classes and other instances,
then you have to check types while iterating the _fields structure to
render the GUI -- that's not what I had in mind.

Building GUIs

I'm just starting to work on this part. A renderer object will take
the form as a parameter, and will build the GUI accordingly with the
structure contained there.

To build the GUI, the renderer has to iterate over its fields. It's a
recursive process, because a form can contain other forms, either
declared directly as classes as in the example above, or through
normal attributes. I''m working on a generic iterator interface for
the form, which would allow to proceed without recursion on the
renderer side, but I'm not sure if this is the best approach. I'm
still thinkering with this part of the code. Suggestions are welcome.

#=======================
Enough talk, that's the code. Use it as you want, but please, give it
proper attribution if you're going to use it elsewhere.
#=======================

from inspect import isclass

# auxiliar functions

def getmyposition():
    """
    Returns the position of the caller, relative to the sequence of 
    statements that were executed during class creation.
    
    Three possible methods were evaluated: using a global counter; using 
    a timestamp; or using the line number retrieved from the call stack.
    For all purposes here, a simple counter is enough. It doesn't need 
    to be thread aware, because all that matters is the relative 
    ordering of statements that, by definition, will be executed within 
    the same thread during the class initialization.
    """
    counter = 0
    while True:
        counter += 1
        yield counter

# decorates getmyposition to hide the internal static var
getmyposition = getmyposition().next

def getfields(dct):
    """
    takes a dictionary of class attributes and returns a decorated list 
    containing all valid field instances and their relative position.
    """
    for fname, fobj in dct.items():
        if isinstance(fobj,Component):
            print "found box: ", fname, fobj._mypos
            yield (fobj._mypos, (fname, fobj))
        elif isclass(fobj) and issubclass(fobj,Component):
            print "found container: ", fname, fobj._mypos
            # substitutes the contained class for its instance
            yield (fobj._mypos, (fname, fobj()))
        else:
            print "unknown: ", fname, fobj, type(fobj)
            # returns the original object with tag order = 0
            yield (0, (fname, fobj))
            
def makefieldsdict(dct):
    # build the field list and sort it
    fields = list(getfields(dct))
    fields.sort()
    # undecorate the list and return it converted to dict; the _field
    # attribute is added automatically to refer to the sorted list
    sorted_field_list = [field[1] for field in fields]
    field_dict = dict(sorted_field_list)
    field_dict['_fields'] = [field for field in sorted_field_list
                             if isinstance(field[1],Component)]
    return field_dict
    
# metaclasses

class Container(type):
    def __new__(cls, name, bases, dct):
        # creates the class using only the processed field list
        newclass = type.__new__(cls, name, bases, makefieldsdict(dct))
        newclass._mypos = getmyposition()
        return newclass
        
# Component is the ancestor of all elements in a
# form, including containers and boxes

class Component(object):
    pass
    
# BoxElement is the ancestor of all elements that are
# instantiated as objects inside the container classes

class Box(Component):
    def __init__(self):
        self._mypos = getmyposition()

# a form is a container of other components
        
class Form(Component):
    __metaclass__ = Container
    # I'm trying several versions of the iterator interface, I really don't
    # know which one is going to be useful in real cases -- I'll have to write 
    # more code an try some alternatives first
    
    def iterfields1(self):
        # this iterator returns field objects without recursion
        for fname, fobj in self._fields:
            yield (fname, fobj)

    def iterfields2(self, _level=0):
        # this iterator recursively returns tuples of 
        # (parent, level, iscontainer, name, component)
        for fname, fobj in self._fields:
            if hasattr(fobj, '_fields'):
                yield (self, _level, True, fname, fobj)
                for parent, level, iscont, name, field in
fobj.iterfields2(_level=_level+1):
                    yield (parent, level, iscont, name, field)
            else:
                yield (self, _level, False, fname, fobj)

    def iterfields3(self):
        # this iterator returns a nested list representation that is
        # built recursively
        for fname, fobj in self._fields:
            if hasattr(fobj,'_fields'):
                yield (self, fname, list(fobj.iterfields3()))
            else:
                yield (self, fname, fobj)
                
# data entry controls

class TextBox(Box):
    def __init__(self, length=40, default="", password=False, multiline=False):
        Box.__init__(self)
        self._length    = length
        self._default   = default
        self._password  = password
        self._multiline = multiline

# forms

class UserForm(Form):
    nickname = TextBox(length=15, default="")
    password = TextBox(length=10, default="", password=True)
    name     = TextBox(length=40, default="")

class ComplexForm(Form):
    class Header(Form):
        nickname = TextBox(length=15, default="")
        password = TextBox(length=10, default="", password=True)
        name     = TextBox(length=40, default="")
    class Comment(Form):
        comments = TextBox(length=200, default="", multiline=True)
        summary  = TextBox(length=200, default="", multiline=True)

# simple test for the iterators
x = ComplexForm()
print
print "ComplexForm.iterfields1"
print "\n".join([str(item) for item in x.iterfields1()])
print
print "ComplexForm.iterfields2"
print "\n".join([str(item) for item in x.iterfields2()])
print
print "ComplexForm.iterfields3"
print "\n".join([str(item) for item in x.iterfields3()])
print


-- 
Carlos Ribeiro
Consultoria em Projetos
blog: http://rascunhosrotos.blogspot.com
blog: http://pythonnotes.blogspot.com
mail: carribeiro at gmail.com
mail: carribeiro at yahoo.com



More information about the Python-list mailing list