3

I'm trying to do the following, define two classes whose instances mutually reference one another, like Users and Groups in the following exemple. A User can belong to several groups and a Group can contains several users. The actual data is stored in a database and there it is a simple matter of many-to-many relationship using foreign keys. No problem at all.

Afterward the data is loaded through an ORM and stored in instances of python objects. Still no problem at all as the ORM used (SQLAlchemy) manage backrefs.

Now I want to check that the python objects comply to some interface using zope.interface and zope.schema. That's where I get into troubles.

import zope.schema as schema
from zope.interface import Interface, implements

class IGroup(Interface):
    name = schema.TextLine(title=u"Group's name")
#    user_list = schema.List(title = u"List of Users in this group", value_type = sz.Object(IUser))

class IUser(Interface):
    name = schema.TextLine(title=u"User's name")
    group_list = schema.List(title = u"List of Groups containing that user",
        value_type = schema.Object(IGroup))

IGroup._InterfaceClass__attrs['user_list'] = zs.List(title = u"List of Users in this group", required = False, value_type = zs.Object(IUser))

class Group(object):
    implements(IGroup)

    def __init__(self, name):
        self.name = name
        self.user_list = []

class User(object):
    implements(IUser)

    def __init__(self, name):
        self.name = name
        self.group_list = []

alice = User(u'Alice')
bob = User(u'Bob')
chuck = User(u'Chuck')
group_users = Group(u"Users")
group_auditors = Group(u"Auditors")
group_administrators = Group(u"Administrators")

def add_user_in_group(user, group):
    user.group_list.append(group)
    group.user_list.append(user)

add_user_in_group(alice, group_users)
add_user_in_group(bob, group_users)
add_user_in_group(chuck, group_users)
add_user_in_group(chuck, group_auditors)
add_user_in_group(chuck, group_administrators)

for x in [alice, bob, chuck]:
    errors = schema.getValidationErrors(IUser, x)
    if errors: print errors
    print "User ", x.name, " is in groups ", [y.name for y in x.group_list]

for x in [group_users, group_auditors, group_administrators]:
    errors = schema.getValidationErrors(IGroup, x)
    if errors: print errors
    print "Group ", x.name, " contains users ", [y.name for y in x.user_list]

My problem is the commented line. I can't define IGroup using IUser because at that time IUser is not yet defined. I've found a workaround completing the definition of IGroup after the definition of IUser but that is not satisfying at all, because IUser and IGroup are defined in different source files and part of IGroup is defined in the file defining IUser.

Is there any proper way to do that using zope.schema ?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
kriss
  • 23,497
  • 17
  • 97
  • 116

2 Answers2

3

Modify the field after definition:

#imports elided

class IFoo(Interface):
    bar = schema.Object(schema=Interface)

class IBar(Interface):
    foo = schema.Object(schema=IFoo)

IFoo['bar'].schema = IBar

Martijn's answer seems a bit more graceful and self-documenting, but this is a bit more succinct. Neither is perfect (compared to say, Django's solution of using string names for foreign keys) -- pick your poison.

IMHO, it would be nice to specify a dotted name to an interface instead of an identifier. You could pretty easily create a subclass of schema.Object to this end for your own use, should you find that approach useful.

sdupton
  • 1,869
  • 10
  • 9
  • 1
    As I said in my comment I do not believe Martijn's answer is really working at all (and my workaround certainly does not work). I'm not sure about yours. Rationale: as I understand it IBar is defined using a copy of fields of IFoo at the time of definition, hence when changing IFoo afterward I do not change the embedded IFoo. However I'm not really sure of that... – kriss Feb 24 '12 at 03:22
  • 1
    Yes, in Python the original class is monkey-patched here, so all consumers of IFoo at any point during runtime after import of your interfaces module will have an Object field with correctly bound schema for validation and introspection. This technique has been used in multiple production projects with consistent success. – sdupton Feb 24 '12 at 07:05
2

You could define a base, or abstract, interface for IUser:

class IAbstractUser(Interface):
    name = schema.TextLine(title=u"User's name")

class IGroup(Interface):
    name = schema.TextLine(title=u"Group's name")
    user_list = schema.List(
        title=u"List of Users in this group", 
        value_type=schema.Object(IAbstractUser))

class IUser(IAbstractUser):
    group_list = schema.List(
        title=u"List of Groups containing that user",
        value_type=schema.Object(IGroup))

Because IUser is a subclass of IAbstractUser, objects implementing the former also satisfy the latter interface.

Edit: You can always still apply sdupton's dynamic after-the-fact alteration of the IGroup interface after you defined IUser:

IGroup['user_list'].value_type.schema = IUser

I'd still use the Abstract interface pattern to facilitate better code documentation.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    if I understand correctly the answer, the user_list schema will not check it's items are IUser implementations (ie: with name and group_list) but only that they are IAbstractUser (ie: with a name but no check of the group_list). I guess it's already what my current workaround is doing anyway and your proposal is much cleaner. Could it be there is no way way to describe the actual circular-reference without entering an actual loop ? – kriss Feb 23 '12 at 21:59
  • 1
    Indeed, user_list schema will not check for IUser, only IAbstractUser, which is the compromize. You could combine this with @sdupton's solution and alter the `user_list` dynamically to only only accept IUser after defining IUser, to tighten your validators further. This is the nature of circular references in Python, you need the one half before the other half and then tie them together. – Martijn Pieters Feb 24 '12 at 07:54