[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