Call for suggestions: Declaring data entry forms using Pythonclasses

C

Carlos Ribeiro

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: (e-mail address removed)
mail: (e-mail address removed)
 
B

Bengt Richter

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.
No comment for now other than suggesting replacing pass with def __repr__ in

class Component(object):
def __repr__(self): return '<Comp %r>' % type(self).__name__

for a more userfriendly printout. Interesting stuff. I wonder about
one-dimensional position vs composing with various top/left/right/bottom/fill
aggregation indicator info etc.

Also wonder if just plain defining your own python-inspired but not python-constrained
language for expressing what you want to express might not be a viable alternative,
given the nice tools for language processing that are part of the open python kit.

IOW, you are using python in a certain way in order to be able to "spell" your
form-descriptions in a certain way. How would you spell them if you were not
constrained by python syntax?

Regards,
Bengt Richter
 
C

Carlos Ribeiro

No comment for now other than suggesting replacing pass with def __repr__ in

class Component(object):
def __repr__(self): return '<Comp %r>' % type(self).__name__

for a more userfriendly printout. Interesting stuff. I wonder about
one-dimensional position vs composing with various top/left/right/bottom/fill
aggregation indicator info etc.

I'm doing stuff like this right now. My debug display was awful, it's
nicer after some __repr__() customization. I've also changed some
class names and moved the basic metaclass stuff in a module of their
own, called "metacontainer". It will make easier to reuse the basic
engine while I'm testing it.
IOW, you are using python in a certain way in order to be able to "spell" your
form-descriptions in a certain way. How would you spell them if you were not
constrained by python syntax?

I regard this as an experiment. I don't know if it will be successful
or not, but I have to try it first ;-). I have a hunch that it will be
more usable in the long run, even with all constraints imposed by
Python syntax, than to define yet another language.

BTW, one can think about the constraints as problems, but they have a
definitive advantage: they limit the scope of the problem to a much
more manageable size. I've seen plenty of projects for description
languages go nowhere, partly because people would spend a lot of time
arguing on how should things be structured :)

Finally, I ask you to think about it for a while. It's still not
usable, but it may be really soon. I'm confident that, once one starts
using it, it will feel more natural than to have to work with yet
another language (as Tanebaum once said, there's a nice thing about
standards -- there are so many to choose from).

--
Carlos Ribeiro
Consultoria em Projetos
blog: http://rascunhosrotos.blogspot.com
blog: http://pythonnotes.blogspot.com
mail: (e-mail address removed)
mail: (e-mail address removed)
 
A

Andrew Dalke

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

Codewise this is probably cleaner

import itertools
getmyposition = itertools.count().next

More than that requires I know how to use metaclasses. :)

If it works like I think it does then it's pretty neat.

Andrew
(e-mail address removed)
 
P

Paul Paterson

Carlos said:
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)

[snip]

By a strange coincidence, this intermediate representation is eerily
similar to the one used in vb2py when converting Visual Basic forms to
Python ones (implemented in PythonCard).

The process is,

1. Read VB form
2. Write intermediate representation (see below)
3. Convert intermediate to a PythonCard (wxWidgets based) form

The intermediate representation looks like,

class vbobj_frmForm(resource.Form):
Caption = "Form1"
ClientHeight = 3090
ClientLeft = 60
ClientTop = 450
ClientWidth = 4680

class vbobj_btnCheckLike(resource.CommandButton):
Caption = "Like ?"
Height = 375
Left = 1440
TabIndex = 2
Top = 1560
Width = 1815

class vbobj_txtTwo(resource.TextBox):
Height = 405
Left = 240
TabIndex = 1
Text = "Text2"
Top = 840
Width = 4095

class vbobj_txtOne(resource.TextBox):
Height = 375
Left = 240
TabIndex = 0
Text = "Text1"
Top = 240
Width = 4095


I've found that the inner class approach works pretty well although I
must admit that the implementation of the transformation in vb2py is
pretty ugly!

I'm very interested in where you are headed with this - what is your
target UI? Are you planning on releasing this under an OS license?

Regards,

Paul


================================
vb2Py - Visual Basic to Python Conversion
http://vb2py.sourceforge.net
 
C

Carlos Ribeiro

By a strange coincidence, this intermediate representation is eerily
similar to the one used in vb2py when converting Visual Basic forms to
Python ones (implemented in PythonCard).

I don't think it is a coincidence; I think that's because this is the
natural way to represent this structure in Python. If you look at the
way Delphi forms are stored when you choose text format (the default
for newer versions), it's strikingly similar -- just change "class"
for "object". I assume VB internals are structured in the same way.
The intermediate representation looks like,

[...long sample...]

I think that my current code can be easily adapted for your needs. I'm
now working on a generic renderer interface. I'm figuring out how to
make it general enough but still simple to sue. The current
implementation is already able to walk the class attributes in the
order of the original declaration, which is important to preserve some
behavior (specially the implicit tab ordering that is normally used in
GUI design).

As for your last question: yes, it's open source, I just haven't yet
figured out which license to choose from -- either BSD or LGPL, I
think. Part of the problem is that I don't have a nice host to put it
for download yet. I this point BSD would be nice -- it's too little
code to make a fusss about it being used inside some other product --
but if the library gets bigger I tend to go towards LGPL.

--
Carlos Ribeiro
Consultoria em Projetos
blog: http://rascunhosrotos.blogspot.com
blog: http://pythonnotes.blogspot.com
mail: (e-mail address removed)
mail: (e-mail address removed)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,982
Messages
2,570,186
Members
46,740
Latest member
JudsonFrie

Latest Threads

Top