[Zope-CVS] SVN: zope.agxassociation/trunk/src/zope/agxassociation/ Initial partial association support.

Jim Fulton jim at zope.com
Tue Sep 27 09:05:47 EDT 2005


Log message for revision 38652:
  Initial partial association support.
  

Changed:
  A   zope.agxassociation/trunk/src/zope/agxassociation/__init__.py
  A   zope.agxassociation/trunk/src/zope/agxassociation/association.py
  A   zope.agxassociation/trunk/src/zope/agxassociation/association.txt
  A   zope.agxassociation/trunk/src/zope/agxassociation/interfaces.py
  A   zope.agxassociation/trunk/src/zope/agxassociation/tests.py

-=-
Added: zope.agxassociation/trunk/src/zope/agxassociation/__init__.py
===================================================================
--- zope.agxassociation/trunk/src/zope/agxassociation/__init__.py	2005-09-27 13:00:12 UTC (rev 38651)
+++ zope.agxassociation/trunk/src/zope/agxassociation/__init__.py	2005-09-27 13:05:47 UTC (rev 38652)
@@ -0,0 +1 @@
+#


Property changes on: zope.agxassociation/trunk/src/zope/agxassociation/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zope.agxassociation/trunk/src/zope/agxassociation/association.py
===================================================================
--- zope.agxassociation/trunk/src/zope/agxassociation/association.py	2005-09-27 13:00:12 UTC (rev 38651)
+++ zope.agxassociation/trunk/src/zope/agxassociation/association.py	2005-09-27 13:05:47 UTC (rev 38652)
@@ -0,0 +1,179 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Association support
+
+
+$Id$
+"""
+
+from zope import schema, proxy
+import zope.schema.interfaces # allow schema.interfaces to be used
+
+class readproperty(object):
+
+    def __init__(self, func):
+        self.func = func
+
+    def __get__(self, inst, class_):
+        if inst is None:
+            return self
+
+        return self.func(inst)
+
+class Association(schema.Field):
+
+    @readproperty
+    def target(self):
+        names = self.target_name.split('.')
+        
+        target_module = __import__('.'.join(names[:-1]), {}, {}, ['*'])
+        self.target = getattr(target_module, names[-1])
+        
+        return self.target
+
+    def __init__(self, target, inverse=None, cardinality=None, **kw):
+        if isinstance(target, str):
+            self.target_name = target
+        else:
+            self.target = target
+
+        self.inverse = inverse
+
+        if cardinality is None:
+            cardinality = SINGLE
+        self.cardinality = cardinality
+
+        super(Association, self).__init__(**kw)
+
+    def _validate(self, value):
+        super(Association, self)._validate(value)
+        if not self.target.providedBy(value):
+            raise schema.interfaces.WrongType(self.target, value)
+
+    def inverse_add(self, ob, value):
+        """Add an inverse reference if the association has an inverse
+        """
+        if not self.inverse:
+            return
+        inverse = self.target[self.inverse]
+        inverse.add(value, ob, self)
+        
+    def add(self, ob, value, inverse):
+        """Add a forward reference to provide an inverse.
+
+        They key is that we *don't add an inverse reference, as this has
+        already been done.
+        """
+        self.cardinality.add(self, ob, value, inverse)
+
+class SingleCardinality:
+
+    def add(self, field, ob, value, inverse):
+        try:
+            old = field.get(ob)
+        except AttributeError:
+            # XXX need more direct way to test
+            old = field.missing_value
+            
+        if old is value:
+            # Nothing to do
+            return
+
+        if old != field.missing_value:
+            # remove the old inverse ref
+            inverse.remove(old, ob)
+
+        field.set(ob, value)
+            
+SINGLE = SingleCardinality()
+
+class Property(object):
+
+    def __init__(self, field, name=None):
+        self.field = field
+        if name is None:
+            name = field.__name__
+        self.__name__ = name
+
+class SingleProperty(Property):
+
+    def __get__(self, inst, class_):
+        if inst is None:
+            return self
+
+        try:
+            return inst.__dict__[self.__name__]
+        except KeyError:
+            raise AttributeError(self.__name__)
+
+    def __set__(self, inst, value):
+        self.field.validate(value)
+        old = inst.__dict__.get(self.__name__, self.field.missing_value)
+        if old != self.field.missing_value:
+            self.field.inverse_remove(ob, old)
+        inst.__dict__[self.__name__] = value
+        self.field.inverse_add(inst, value)
+
+    def __delete__(self, inst):
+        old = inst.__dict__.get(self.__name__, self)
+        if old is self:
+            raise AttributeError(self.__name__)
+        if old != self.field.missing_value:
+            self.field.inverse_remove(ob, old)
+        del inst.__dict__[self.__name__]
+        
+
+class SetCardinality:
+
+    def add(self, field, ob, value, inverse):
+        set = field.get(ob)
+        set.add(value)
+
+
+class SetProxy(proxy.ProxyBase):
+
+    __slots__ = 'inst', 'field'
+
+    def add(self, x):
+        set = proxy.getProxiedObject(self)
+        if x in set:
+            return
+        self.field.validate(x)
+        set.add(x)
+        self.field.inverse_add(self.inst, x)
+
+class SetProperty(Property):
+
+    def __get__(self, inst, class_):
+        if inst is None:
+            return self
+
+        set = inst.__dict__.get(self.__name__)
+        if set is None:
+            raise AttributeError(self.__name__)
+
+        set = SetProxy(set)
+        set.inst = inst
+        set.field = self.field
+
+        return set
+
+    def __set__(self, inst, set):
+        inst.__dict__[self.__name__] = set
+
+    def __delete__(self, inst):
+        del inst.__dict__[self.__name__]
+
+
+


Property changes on: zope.agxassociation/trunk/src/zope/agxassociation/association.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zope.agxassociation/trunk/src/zope/agxassociation/association.txt
===================================================================
--- zope.agxassociation/trunk/src/zope/agxassociation/association.txt	2005-09-27 13:00:12 UTC (rev 38651)
+++ zope.agxassociation/trunk/src/zope/agxassociation/association.txt	2005-09-27 13:05:47 UTC (rev 38652)
@@ -0,0 +1,124 @@
+Association Fields
+==================
+
+Association fields provide a Python reaization of (a subset of) UML
+associations. Associations model connections between objects.
+Association fields are used for "intrinsic" associations, which are
+associations that are implemented directly by objects.  (Intrinsic
+references are in contrast to "extrinsic" references, which are
+maintained by the system to support system concerns.)
+
+Let's create an association between people and organizations.
+To do that we'll start with a person schema:
+
+    >>> from zope import interface
+    >>> from zope.agxassociation import association
+
+    >>> class IPerson(interface.Interface):
+    ...     organization = association.Association(
+    ...        target = 'zope.agxassociation.association_txt.IOrganization',
+    ...        inverse = 'members',
+    ...        )
+
+There are some things to note:
+
+- The association is with objects of some other type.  In this case,
+  we want the association to be with IOrganization objects, but we
+  haven't defined IOrganization yet.  We use a dotted name to allow
+  the actual definition of the interface to be defered until needed.
+
+- We used the `inverse` option to specify that this is a two way
+  association and to specify the attribute in the organization schema
+  that will be used to refer back to the person.  This allows us to
+  automate updating the other side of an association when one side is
+  updated. 
+
+Now let's define IOrganization:
+
+    >>> class IOrganization(interface.Interface):
+    ...     members = association.Association(
+    ...        target = IPerson,
+    ...        inverse = 'organization',
+    ...        cardinality = association.SetCardinality()
+    ...        )
+
+Note:
+
+- We didn't need to use a dotted name in IOrganization, because
+  IPerson already exists. 
+
+- We specified a cardinality for the association.  There are a number
+  of ways to express cardinality.  The default is one to one
+  (association.SINGLE). Here we want a one to many association, and we
+  want to use a set API.  We use a SetCardinality to specify this. 
+
+Let's create implementations of these schemas:
+
+    >>> class Named(object):
+    ...
+    ...     def __init__(self, name=''):
+    ...         self.name = name
+    ...
+    ...     def __repr__(self):
+    ...         return "%s(%r)" % (self.__class__.__name__, self.name)
+
+    >>> class Person(Named):
+    ...     interface.implements(IPerson)
+    ...
+    ...     organization = association.SingleProperty(IPerson['organization'])
+    
+
+    >>> class Organization(Named):
+    ...     interface.implements(IOrganization)
+    ...
+    ...     members = association.SetProperty(IOrganization['members'])
+    ...
+    ...     def __init__(self, name=''):
+    ...         super(Organization, self).__init__(name)
+    ...         self.members = set()
+
+These classes use helper properties provided by the association
+module:
+
+- SingleProperty makes sure that when we assign valies to an
+  attribute, the assigned value is of the specified type.  In
+  addition, if an inverse attribute is specified in the schema field,
+  the inverse attribute is updated.  We passed the organization field
+  to the property to provide the attribute specification.
+
+- SetProperty provides an IAssociationSet implementation.  It uses set
+  proxies to make sure that the order side of the association is
+  updated when the set is updated.
+
+Now, with this in place, we can try to validate values for
+organization:
+
+    >>> IPerson['organization'].validate(22)
+    ... # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    WrongType: 
+    (<InterfaceClass zope.agxassociation.association_txt.IOrganization>, 22)
+
+    >>> IPerson['organization'].validate(Organization())
+
+If we assign an organization attribute, we see that the organization
+is updated:
+
+    >>> guido = Person('Guido')
+    >>> psf = Organization('PSF')
+    >>> guido.organization = psf
+    >>> psf.members
+    set([Person('Guido')])
+
+
+Similarly, if we add a person to an organization, their organization
+will be set:
+
+    >>> tim = Person('Tim')
+    >>> psf.members.add(tim)
+    >>> sorted(psf.members)
+    [Person('Guido'), Person('Tim')]
+
+    >>> tim.organization
+    Organization('PSF')


Property changes on: zope.agxassociation/trunk/src/zope/agxassociation/association.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zope.agxassociation/trunk/src/zope/agxassociation/interfaces.py
===================================================================
--- zope.agxassociation/trunk/src/zope/agxassociation/interfaces.py	2005-09-27 13:00:12 UTC (rev 38651)
+++ zope.agxassociation/trunk/src/zope/agxassociation/interfaces.py	2005-09-27 13:05:47 UTC (rev 38652)
@@ -0,0 +1,21 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Association interfaces and exceptions
+
+$Id$
+"""
+
+from zope.interface import Interface, Attribute
+
+class IAssociationSet(


Property changes on: zope.agxassociation/trunk/src/zope/agxassociation/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zope.agxassociation/trunk/src/zope/agxassociation/tests.py
===================================================================
--- zope.agxassociation/trunk/src/zope/agxassociation/tests.py	2005-09-27 13:00:12 UTC (rev 38651)
+++ zope.agxassociation/trunk/src/zope/agxassociation/tests.py	2005-09-27 13:05:47 UTC (rev 38652)
@@ -0,0 +1,37 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XXX short summary goes here.
+
+$Id$
+"""
+import unittest
+from zope.testing import doctest, module
+
+module_name = 'zope.agxassociation.association_txt'
+
+def setUp(test):
+    module.setUp(test, module_name)
+
+def tearDown(test):
+    module.tearDown(test, module_name)
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite('association.txt',
+                             setUp=setUp, tearDown=tearDown),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+


Property changes on: zope.agxassociation/trunk/src/zope/agxassociation/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native



More information about the Zope-CVS mailing list