Defining circular references using zope.schema

2019-05-14 19:30发布

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 ?

2条回答
祖国的老花朵
2楼-- · 2019-05-14 20:09

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.

查看更多
再贱就再见
3楼-- · 2019-05-14 20:17

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.

查看更多
登录 后发表回答