[ZODB-Dev] Re: [Zope3-dev] PROPOSAL: ZODB Relationships

Roché Compaan roche at upfrontsystems.co.za
Sun May 11 20:30:18 EDT 2003


I played around a bit and have some proof of concept code honouring the
important things in the proposal as well as what MOF brings to the
table, hopefully. We need to beat the code it into shape though ;-) I
didn't define interfaces now but will when the code settles. I split up
multiplicity into the parameters lower- and upperBound so that we can
give them defaults. Questions I have are nested in the code:


# We should provide an interface for storage types and users must be
# able to pass in their own storage types. Jeremy mentioned this.
# I just use OOBTree and PersistentList for proof of concept
from BTrees.OOBTree import OOBTree
from ZODB.PersistentList import PersistentList

class AssociationError(Exception):
    pass
# Or is their a built-in Exception better suited?


class Association:

    def __init__(self, name, fromEnd, toEnd):
        self.name = name
        self.fromEnd = fromEnd
        self.toEnd = toEnd

        # If the AssociationEnd doesn't know its Association then it is
        # not possible for the Reference to find the Association. It
        # feels funny to just set it on the AssociationEnds, but we
        # create AssociationEnd instances before the Association
        self.fromEnd.association = self
        self.toEnd.association = self

        # We set 'otherEnd' when isNavigable is true but I don't really
        # use it. Code to illustrate use cases would help.
        if self.fromEnd.isNavigable:
            self.fromEnd.otherEnd = toEnd
        if self.toEnd.isNavigable:
            self.toEnd.otherEnd = fromEnd

    def add(self, fromObj, toObj):
        self.fromEnd._add(fromObj, toObj)
        self.toEnd._add(toObj, fromObj)

    def delete(self, fromObj, toObj):
        self.fromEnd._delete(fromObj, toObj)
        self.toEnd._delete(toObj, fromObj)

    def __getattr__(self, name):
        if name == self.fromEnd.name:
            return self.fromEnd
        elif name == self.toEnd.name:
            return self.toEnd

class AssociationEnd:

    def __init__(self, name, lowerBound=1, upperBound=None, navigable=True):
        self.name = name
        self.lowerBound = lowerBound
        self.upperBound = upperBound
        self.isNavigable = navigable
        self.otherEnd = None

        # We maintain both directions of the mapping. Not sure yet but
        # maybe we can get away with just a '_fromMap'
        self._fromMap = OOBTree()
        self._toMap = OOBTree()

    def _addToMap(self, map, key, value):
        if not map.has_key(key):
            if (map == self._fromMap and
                self.upperBound and len(map) == self.upperBound):
                raise AssociationError, 'Upper bound exceeded'
            map[key] = PersistentList()
        map[key].append(value)

    def _deleteFromMap(self, map, key, value):
        if map.has_key(key) and value in map[key]:
            map[key].remove(value)
            if len(map[key]) == 0:
                del map[key]

    def _add(self, fromObj, toObj):
        self._addToMap(self._fromMap, fromObj, toObj)
        self._addToMap(self._toMap, toObj, fromObj)

    def _delete(self, fromObj, toObj):
        self._deleteFromMap(self._fromMap, fromObj, toObj)
        self._deleteFromMap(self._toMap, toObj, fromObj)

    def get(self, toObj):
        return self._toMap.get(toObj, [])


class Reference(object):

    def __init__(self, associationEnd):
        self.associationEnd = associationEnd
    
    def __get__(self, obj, cls=None):
        references = self.associationEnd.get(obj)
        if self.associationEnd.upperBound == 1:
            return references[0]
        else:
            return references

    def __set__(self, obj, value):
        if self.associationEnd.upperBound == 1:
            value = [value]
        # remove existing associations
        association = self.associationEnd.association
        current_values = self.associationEnd.get(obj)
        for toObj in current_values:
            association.delete(obj, toObj)
        # add the new ones
        for toObj in value:
            association.add(obj, toObj)


########################
# But does it work? Let's test.

teacher = AssociationEnd('teacher', upperBound=1)
courses = AssociationEnd('courses') 
teaches = Association('teaches', teacher, courses)

class Teacher(object):

    courses = Reference(teaches.courses)

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.name)

class Course(object):

    teacher = Reference(teaches.teacher)

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.name)

monty = Teacher('monty')
brian = Teacher('brian')
zope101 = Course('zope101')
python101 = Course('python101')

# Testing associations
teaches.add(monty, zope101)
teaches.add(monty, python101)
assert teaches.teacher.get(zope101) == [monty]
assert teaches.courses.get(monty) == [zope101, python101]
teaches.delete(monty, zope101)
assert teaches.teacher.get(zope101) == []
assert teaches.courses.get(monty) == [python101]

# this should raise an AssociationError
try:
    teaches.add(brian, [zope101])
except AssociationError:
    print 'Exception raised'

# Testing references
assert monty.courses == [python101]
monty.courses = [zope101, python101]
python101.teacher == brian

# Check if the association was updated
assert teaches.courses.get(monty) == [zope101, python101]
monty.courses = [zope101]
assert zope101.teacher == monty

-- 
Roché Compaan
Upfront Systems                 http://www.upfrontsystems.co.za



More information about the ZODB-Dev mailing list