Python component model

fumanchu fumanchu at amor.org
Tue Oct 10 19:47:19 CEST 2006


Edward Diener No Spam wrote:
> OK, here is my idea of what such a component model envisages as a list
> of items. After this, unless I get some intelligent comments from people
> who might be interested in what I envision, or something very similar, I
> will be off to investigate it myself rather than do battle with the
> horde of people who will just tell me that Python, being a great
> language, does not need what I have suggested.

[This quote hacked up by me:]
> 1) Component property: This is a glorified attribute with a type that
> a) can be specified in a "static" manner, or discovered dynamically,
> b) has converters between a string and the actual value
> c) has a getter function to retrieve the value if it is readable and a
> setter function to set the value if it is writable.
> d) be either readable or writable or both.
> e) not any Python class attribute since a component has the right
> to specify only certain values as manipulatable in a design-time
> RAD environment.

Whenever you say "glorified attribute", your first thought should be
"Python descriptor" (but not your last--it's not a cure-all). They are
able to do all of (a, b, c, d, e) which I marked in your text above.
For example, here's a descriptor for
attributes-you-want-to-persist-in-a-database from my ORM, Dejavu (see
http://projects.amor.org/dejavu/browser/trunk/units.py#l290):


class UnitProperty(object):
    """Data descriptor for Unit data which will persist in storage."""

    def __init__(self, type=unicode, index=False, hints=None, key=None,
default=None):
        self.type = type
        self.index = index
        if hints is None: hints = {}
        self.hints = hints
        self.key = key
        self.default = default

    def __get__(self, unit, unitclass=None):
        if unit is None:
            # When calling on the class instead of an instance...
            return self
        else:
            return unit._properties[self.key]

    def __set__(self, unit, value):
        if self.coerce:
            value = self.coerce(unit, value)
        oldvalue = unit._properties[self.key]
        if oldvalue != value:
            unit._properties[self.key] = value

    def coerce(self, unit, value):
        if value is not None and not isinstance(value, self.type):
            # Try to cast the value to self.type.
            try:
                value = self.type(value)
            except Exception, x:
                x.args += (value, type(value))
                raise
        return value

    def __delete__(self, unit):
        raise AttributeError("Unit Properties may not be deleted.")

> a) can be specified in a "static" manner, or discovered dynamically,

The "component model" can either scan a class for instances of
UnitProperty or keep a registry of them in the class or elsewhere (via
a metaclass + add_property functions).

> b) has converters between a string and the actual value

Note the "coerce" function above. Something similar could be done for
serialization (which I can prove in my case because I use UnitProperty
to help produce SQL ;) but you could just as easily pickle
unit._properties and be done with it.

> c) has a getter function to retrieve the value if it is readable and a
> setter function to set the value if it is writable.
> d) be either readable or writable or both.

Descriptors that only have __get__ are read-only; if they have __set__
they are read-write.

> e) not any Python class attribute since a component has the right
> to specify only certain values as manipulatable in a design-time
> RAD environment.

Right. Descriptors allow the creator of a class to use "normal"
attributes (including functions) which don't participate in the
component model.

> 2) Component event: This is an type which encapsulates an array, or a
> list of callable objects with the same function signature, along with
> the functionality to add and remove elements from the array, as well as
> cycle through the array calling the callable objects as a particular
> event is triggered. A component event is an event source for a
> particular event. Component events have to be dicoverable by the Visual
> RAD system so that an object's appropriate event handler, an event sink,
> can be hooked to the component event itself, an event source, through a
> design time interface which propagates the connection at run-time.

This can be accomplished by creating a ComponentEvent descriptor whose
__get__ returns an object with a __call__ method. Here's a base class
for something similar (again, from Dejavu):

class UnitAssociation(object):
    """Non-data descriptor method to retrieve related Units via
attributes."""

    to_many = None

    def __init__(self, nearKey, farClass, farKey):
        # Since the keys will be used as kwarg keys, they must be
strings.
        self.nearKey = str(nearKey)
        self.farKey = str(farKey)

        self.nearClass = None
        self.farClass = farClass

    def __get__(self, unit, unitclass=None):
        if unit is None:
            # When calling on the class instead of an instance...
            return self
        else:
            m = types.MethodType(self.related, unit, unitclass)
            return m

    def __delete__(self, unit):
        raise AttributeError("Unit Associations may not be deleted.")

    def related(self, unit, expr=None, **kwargs):
        raise NotImplementedError

Subclasses override the "related" method, but a ComponentEvent class
could just as easily do:

    def run(self, *args, **kwargs):
        for sink in self.sinks:
            sink(*args, **kwargs)

> 3: Component serialization: A component which has its properties and
> events set by a visual design-time RAD environment needs to be
> serialized at design time and deserialized at run-time. This can be a
> default serialization of all component properties and events, or the
> component itself can participate in the serilization effort either
> wholly or partly.

In Dejavu, the UnitProperty class is your "component property" and the
Unit class is the component. The Unit class has a copy method:

    def __copy__(self):
        newUnit = self.__class__()
        for key in self.properties:
            if key in self.identifiers:
                prop = getattr(self.__class__, key)
                newUnit._properties[key] = prop.default
            else:
                newUnit._properties[key] = self._properties[key]
        newUnit.sandbox = None
        return newUnit

It wouldn't be hard to replace "newUnit._properties[key] =
self._properties[key]" with "dump(self._properties[key])".

> 4) Custom property and component editors: A component editor can present
> a property editor or an editor for an entire component which the visual
> design-time RAD environment can use to allow the programmer end-user of
> the component to set or get component property values. Normally a design
> time environment will present default property editors for each
> component property type, but a component can override this.

This is the hard part. I believe Dabo has done some work in this space,
but this is where the tight coupling comes in between code and tool, a
coupling which Python has traditionally resisted.

> 5) Custom type converters: A component should be able to specify a
> custom converter for any property to convert, in both directions or
> either direction, between the property's string value as seen by a
> property editor and the actual value of the component property's type.

A ComponentProperty descriptor could include a custom pair of methods
to get/set as string. This is often done in web frameworks which need
to coerce incoming string values to the correct type.

All of which is to say: nobody's done this yet because parts 1, 2, 3
and 5 are trivial to do with descriptors, but actually building a
visual RAD environment is too much work. ;)


Robert Brewer
System Architect
Amor Ministries
fumanchu at amor.org




More information about the Python-list mailing list