SVN: zope.agxassociation/trunk/src/zope/agxassociation/ Initial partial association support.

Jim Fulton <[email protected]>
Newsgroups gmane.comp.web.zope.public-cvs
Message-ID <[email protected]>
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

_______________________________________________
Zope-CVS maillist  -  [email protected]
http://mail.zope.org/mailman/listinfo/zope-cvs

Zope CVS instructions: http://dev.zope.org/CVS
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.