[Zope3-checkins] CVS: Zope3/src/zodb/query - relation.py:1.1.2.1

Jeremy Hylton jeremy@zope.com
Fri, 25 Apr 2003 19:00:02 -0400


Update of /cvs-repository/Zope3/src/zodb/query
In directory cvs.zope.org:/tmp/cvs-serv4854/query

Added Files:
      Tag: jeremy-query-branch
	relation.py 
Log Message:
Add rough prototype of inter-object relationships.


=== Added File Zope3/src/zodb/query/relation.py ===
"""Automatic relationship management

A relationship associates two classes.  Each class in the relationship
has an attribute that is associated with the other class.  The
relationship can be one-to-one, one-to-many, or many-to-one.

A relationship is created by creating Relation descriptors in the two
associated classes.  After the classes are created, the Relation
properties are connected to each other.

The relationship is used just like any other attribute, except that
the Relation objects collaborate to provide referential integrity.  If
one side of the relationship is modified, the other side is, too.

A single-valued relationship is represented as a simple object
reference.  A many-valued relationship is represented as a container.
"""

# XXX The current implementation only handles many-to-many relationships.

from UserDict import UserDict

class Relation(object):
    """Descriptor for instance variable that is one end of relationship."""

    # Implementation details:

    # Most of the work is deferred to either the RelationDict or the
    # RelationManager.  (XXX Maybe the manager isn't necessary and all
    # its logic could be in the relation.)

    # A descriptor bound to the name foo will create an _r_foo attribute
    # for each instance, when the _r_ attribute provides the low-level
    # object.
    
    def __init__(self):
        self.clsinit = False
        self.manager = RelationManager()

    def __get__(self, obj, cls=None):
        if obj is None:
            # The manager needs to know what class contains the
            # descriptor.  I think the only way to know is to wait
            # until the descriptor is first used.
            if not self.clsinit:
                self.manager.setclass(cls)
                self.clsinit = True
            return self.manager
        else:
            return self.manager.get(obj)

class RelationDict(UserDict):

    # Dict may not be appropriate for all relationships.  Sometimes
    # a list may be needed.  Probably want to let the user decide.

    def __init__(self, obj, manager):
        self.obj = obj
        self.manager = manager
        UserDict.__init__(self)

    def add(self, obj):
        self[obj] = 1
        other = getattr(obj, self.manager.name)
        other[self.obj] = 1

class RelationManager:

    def __init__(self):
        self.cls = None
        self.other = None
        self.many = None
        self.name = None
        self.attr = None

    def __repr__(self):
        return "<relation %s-%s to %s-%s.%s>" % (
            self.many and "N" or "1",
            self.cls.__name__,
            self.other.many and "N" or "1",
            self.other.cls.__name__,
            self.name)

    def setclass(self, cls):
        self.cls = cls

    def register(self, other, many):
        # Called by many2many below to establish connection
        # between two Relation objects.
        self.other = other
        self.name = find_attribute_name(other)
        self.many = many
        self.attr = "_r_" + self.name

    def get(self, obj):
        # Get the relationship stored from the object.
        rel = getattr(obj, self.attr, None)
        if rel is None:
            rel = RelationDict(obj, self)
            setattr(obj, self.attr, rel)
        return rel

def find_attribute_name(r):
    # XXX We actually have to find the objects in the class dicts
    # to figure out what name they are using
    for name in r.cls.__dict__:
        v = r.cls.__dict__[name]
        if isinstance(v, Relation):
            if r == v.manager:
                return name

def many2many(r1, r2):
    """Connection to Relations in a many-to-many relationship."""
    r1.register(r2, True)
    r2.register(r1, True)

class SoftwareProject(object):

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

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

class Developer(object):

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

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.name)
    
many2many(SoftwareProject.developers, Developer.projects)

if __name__ == "__main__":
    zope3 = SoftwareProject("Zope3")
    jim = Developer("Jim Fulton")
    stevea = Developer("Steve Alexander")

    zope3.developers.add(jim)
    zope3.developers.add(stevea)
    assert stevea in zope3.developers
    assert zope3 in stevea.projects