diff --git a/src/zc/relation/tokens.txt b/src/zc/relation/tokens.txt index e6d1b40..3e70a4e 100644 --- a/src/zc/relation/tokens.txt +++ b/src/zc/relation/tokens.txt @@ -60,9 +60,89 @@ Now we can create some classes. In the README example, the setup was a bit of a toy. This time we will be just a bit more practical. We'll also expect to be operating within the ZODB, with a root and transactions. [#ZODB]_ +.. [#ZODB] Here we will set up a ZODB instance for us to use. + + >>> from ZODB.tests.util import DB + >>> db = DB() + >>> conn = db.open() + >>> root = conn.root() + Here's how we will dump and load our relations: use a "registry" object, similar to an intid utility. [#faux_intid]_ +.. [#faux_intid] Here's a simple persistent keyreference. Notice that it is + not persistent itself: this is important for conflict resolution to be + able to work (which we don't show here, but we're trying to lean more + towards real usage for this example). + + >>> class Reference(object): # see zope.app.keyreference + ... def __init__(self, obj): + ... self.object = obj + ... def __cmp__(self, other): + ... # this doesn't work during conflict resolution. See + ... # zope.app.keyreference.persistent, 3.5 release, for current + ... # best practice. + ... if not isinstance(other, Reference): + ... raise ValueError('can only compare with Reference objects') + ... if self.object._p_jar is None or other.object._p_jar is None: + ... raise ValueError( + ... 'can only compare when both objects have connections') + ... return cmp( + ... (self.object._p_jar.db().database_name, self.object._p_oid), + ... (other.object._p_jar.db().database_name, other.object._p_oid), + ... ) + ... + + Here's a simple integer identifier tool. + + >>> import persistent + >>> import BTrees + >>> class Registry(persistent.Persistent): # see zope.app.intid + ... def __init__(self, family=BTrees.family32): + ... self.family = family + ... self.ids = self.family.IO.BTree() + ... self.refs = self.family.OI.BTree() + ... def getId(self, obj): + ... if not isinstance(obj, persistent.Persistent): + ... raise ValueError('not a persistent object', obj) + ... if obj._p_jar is None: + ... self._p_jar.add(obj) + ... ref = Reference(obj) + ... id = self.refs.get(ref) + ... if id is None: + ... # naive for conflict resolution; see zope.app.intid + ... if self.ids: + ... id = self.ids.maxKey() + 1 + ... else: + ... id = self.family.minint + ... self.ids[id] = ref + ... self.refs[ref] = id + ... return id + ... def __contains__(self, obj): + ... if (not isinstance(obj, persistent.Persistent) or + ... obj._p_oid is None): + ... return False + ... return Reference(obj) in self.refs + ... def getObject(self, id, default=None): + ... res = self.ids.get(id, None) + ... if res is None: + ... return default + ... else: + ... return res.object + ... def remove(self, r): + ... if isinstance(r, (int, long)): + ... self.refs.pop(self.ids.pop(r)) + ... elif (not isinstance(r, persistent.Persistent) or + ... r._p_oid is None): + ... raise LookupError(r) + ... else: + ... self.ids.pop(self.refs.pop(Reference(r))) + ... + >>> registry = root['registry'] = Registry() + + >>> import transaction + >>> transaction.commit() + In this implementation of the "dump" method, we use the cache just to show you how you might use it. It probably is overkill for this job, and maybe even a speed loss, but you can see the idea. @@ -83,11 +163,6 @@ and maybe even a speed loss, but you can see the idea. Now we can create a relation catalog to hold these items. >>> import zc.relation.catalog - >>> import transaction - >>> from ZODB.tests.util import DB - >>> db = DB() - >>> conn = db.open() - >>> root = conn.root() >>> catalog = root['catalog'] = zc.relation.catalog.Catalog(dump, load) >>> transaction.commit() @@ -178,6 +253,15 @@ Let's add a couple of search indexes in too, of the hierarchy looking up... PLEASE NOTE: the search index looking up is not a good idea practically. The index is designed for looking down [#verifyObjectTransitive]_. +.. [#verifyObjectTransitive] The TransposingTransitiveMembership indexes + provide ISearchIndex. + + >>> from zope.interface.verify import verifyObject + >>> import zc.relation.interfaces + >>> index = list(catalog.iterSearchIndexes())[0] + >>> verifyObject(zc.relation.interfaces.ISearchIndex, index) + True + Let's create and add a few organizations. We'll make a structure like this [#silliness]_:: @@ -300,6 +384,41 @@ Likewise, `tokenizeRelations` is the mirror image of `resolveRelationTokens`. The other token-related methods are as follows [#show_remaining_token_methods]_: +.. [#show_remaining_token_methods] For what it's worth, here are some small + examples of the remaining token-related methods. + + These two are the singular versions of `tokenizeRelations` and + `resolveRelationTokens`. + + `tokenizeRelation` returns a token for the given relation. + + >>> catalog.tokenizeRelation(orgs['Zookd Corp Management']) == ( + ... registry.getId(orgs['Zookd Corp Management'])) + True + + `resolveRelationToken` returns a relation for the given token. + + >>> catalog.resolveRelationToken(registry.getId( + ... orgs['Zookd Corp Management'])) is orgs['Zookd Corp Management'] + True + + The "values" ones are a bit lame to show now, since the only value + we have right now is not tokenized but used straight up. But here + goes, showing some fascinating no-ops. + + `tokenizeValues`, returns an iterable of tokens for the values of + the given index name. + + >>> list(catalog.tokenizeValues((1,2,3), 'part')) + [1, 2, 3] + + `resolveValueTokens` returns an iterable of values for the tokens of + the given index name. + + >>> list(catalog.resolveValueTokens((1,2,3), 'part')) + [1, 2, 3] + + - `tokenizeValues`, which returns an iterable of tokens for the values of the given index name; - `resolveValueTokens`, which returns an iterable of values for the tokens of @@ -413,6 +532,15 @@ Now let's create and add some roles. Now we can begin to do searches [#real_value_tokens]_. + +.. [#real_value_tokens] We can also show the values token methods more + sanely now. + + >>> original = sorted((orgs['Zookd Devs'], orgs['Ynod SAs'])) + >>> tokens = list(catalog.tokenizeValues(original, 'organization')) + >>> original == sorted(catalog.resolveValueTokens(tokens, 'organization')) + True + What are all the role settings for ophelia? >>> sorted(catalog.findRelations({'principal_id': 'ophelia'})) @@ -712,7 +840,19 @@ values. >>> list(res) ['publisher', 'reviewer', 'user manager', 'writer'] -[#verifyObjectIntransitive]_ Now we can change and remove relations--both organizations and roles--and +[#verifyObjectIntransitive]_ + +.. [#verifyObjectIntransitive] The Intransitive search index provides + ISearchIndex and IListener. + + >>> from zope.interface.verify import verifyObject + >>> import zc.relation.interfaces + >>> verifyObject(zc.relation.interfaces.ISearchIndex, index) + True + >>> verifyObject(zc.relation.interfaces.IListener, index) + True + +Now we can change and remove relations--both organizations and roles--and have the index maintain correct state. Given the current state of organizations-- @@ -849,6 +989,111 @@ in the same catalog. Now we will fix up the the organization catalog [#compare_copy]_. +.. [#compare_copy] Before we modify them, let's look at the copy we made. + The copy should currently behave identically to the original. + + >>> len(org_catalog) + 38 + >>> len(role_catalog) + 38 + >>> indexed = list(org_catalog) + >>> len(indexed) + 38 + >>> orgs['Zookd Devs'] in indexed + True + >>> for r in indexed: + ... if r not in role_catalog: + ... print 'bad' + ... break + ... else: + ... print 'good' + ... + good + >>> org_names = set(dir(org_catalog)) + >>> role_names = set(dir(role_catalog)) + >>> org_names - role_names + set([]) + >>> role_names - org_names + set(['org_catalog']) + + >>> def checkYnodDevsParts(catalog): + ... res = sorted(catalog.findRelations(t({None: orgs['Ynod Devs']}))) + ... if res != [ + ... orgs["Bet Proj"], orgs["Y3L4 Proj"], orgs["Ynod Devs"]]: + ... print "bad", res + ... + >>> checkYnodDevsParts(org_catalog) + >>> checkYnodDevsParts(role_catalog) + + >>> def checkOpheliaRoles(catalog): + ... res = sorted(catalog.findRelations({'principal_id': 'ophelia'})) + ... if repr(res) != ( + ... "[, " + + ... ", " + + ... "]"): + ... print "bad", res + ... + >>> checkOpheliaRoles(org_catalog) + >>> checkOpheliaRoles(role_catalog) + + >>> def checkOpheliaWriterOrganizations(catalog): + ... res = sorted(catalog.findRelations({None: zc.relation.catalog.Any( + ... catalog.findValueTokens( + ... 'organization', {'principal_id': 'ophelia', + ... 'role_id': 'writer'}))})) + ... if repr(res) != ( + ... '[, ' + + ... ', ' + + ... ', ' + + ... ', ' + + ... ', ' + + ... ', ' + + ... ']'): + ... print "bad", res + ... + >>> checkOpheliaWriterOrganizations(org_catalog) + >>> checkOpheliaWriterOrganizations(role_catalog) + + >>> def checkPrincipalsWithRolesInZookdDevs(catalog): + ... org_id = registry.getId(orgs['Zookd Devs']) + ... res = sorted(catalog.findValueTokens( + ... 'principal_id', + ... {'organization': zc.relation.catalog.any( + ... org_id, *catalog.findRelationTokens({'part': org_id}))})) + ... if res != ['abe', 'ignas', 'karyn', 'lettie', 'nancy', 'ophelia']: + ... print "bad", res + ... + >>> checkPrincipalsWithRolesInZookdDevs(org_catalog) + >>> checkPrincipalsWithRolesInZookdDevs(role_catalog) + + >>> def checkOpheliaRolesInZookdNbd(catalog): + ... res = sorted(catalog.findValueTokens( + ... 'role_id', { + ... 'organization': registry.getId(orgs['Zookd Nbd']), + ... 'principal_id': 'ophelia'})) + ... if res != ['publisher', 'reviewer', 'writer']: + ... print "bad", res + ... + >>> checkOpheliaRolesInZookdNbd(org_catalog) + >>> checkOpheliaRolesInZookdNbd(role_catalog) + + >>> def checkAbeRolesInZookdNbd(catalog): + ... res = sorted(catalog.findValueTokens( + ... 'role_id', { + ... 'organization': registry.getId(orgs['Zookd Nbd']), + ... 'principal_id': 'abe'})) + ... if res != ['publisher', 'user manager', 'writer']: + ... print "bad", res + ... + >>> checkAbeRolesInZookdNbd(org_catalog) + >>> checkAbeRolesInZookdNbd(role_catalog) + + >>> org_catalog.removeDefaultQueryFactory(None) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + LookupError: ('factory not found', None) + >>> org_catalog.removeValueIndex('organization') >>> org_catalog.removeValueIndex('role_id') >>> org_catalog.removeValueIndex('principal_id') @@ -863,8 +1108,42 @@ Now we will fix up the the organization catalog [#compare_copy]_. This also shows using the `removeDefaultQueryFactory` and `removeSearchIndex` methods [#removeDefaultQueryFactoryExceptions]_. -Now we will set up the role catalog [#copy_unchanged]_. - +.. [#removeDefaultQueryFactoryExceptions] You get errors by removing query + factories that are not registered. + + >>> org_catalog.removeDefaultQueryFactory(factory2) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + LookupError: ('factory not found', ) + +Now we will set up the role catalog [#copy_unchanged]_. + + +.. [#copy_unchanged] Changes to one copy should not affect the other. That + means the role_catalog should still work as before. + + >>> len(org_catalog) + 13 + >>> len(list(org_catalog)) + 13 + + >>> len(role_catalog) + 38 + >>> indexed = list(role_catalog) + >>> len(indexed) + 38 + >>> orgs['Zookd Devs'] in indexed + True + >>> orgs['Zookd Devs'] in role_catalog + True + + >>> checkYnodDevsParts(role_catalog) + >>> checkOpheliaRoles(role_catalog) + >>> checkOpheliaWriterOrganizations(role_catalog) + >>> checkPrincipalsWithRolesInZookdDevs(role_catalog) + >>> checkOpheliaRolesInZookdNbd(role_catalog) + >>> checkAbeRolesInZookdNbd(role_catalog) + >>> role_catalog.removeValueIndex('part') >>> for ix in list(role_catalog.iterSearchIndexes()): ... role_catalog.removeSearchIndex(ix) @@ -964,293 +1243,6 @@ TODO make sure that intransitive copy looks the way we expect [#administrivia]_ -.. ......... .. -.. Footnotes .. -.. ......... .. - -.. [#ZODB] Here we will set up a ZODB instance for us to use. - - >>> from ZODB.tests.util import DB - >>> db = DB() - >>> conn = db.open() - >>> root = conn.root() - -.. [#faux_intid] Here's a simple persistent keyreference. Notice that it is - not persistent itself: this is important for conflict resolution to be - able to work (which we don't show here, but we're trying to lean more - towards real usage for this example). - - >>> class Reference(object): # see zope.app.keyreference - ... def __init__(self, obj): - ... self.object = obj - ... def __cmp__(self, other): - ... # this doesn't work during conflict resolution. See - ... # zope.app.keyreference.persistent, 3.5 release, for current - ... # best practice. - ... if not isinstance(other, Reference): - ... raise ValueError('can only compare with Reference objects') - ... if self.object._p_jar is None or other.object._p_jar is None: - ... raise ValueError( - ... 'can only compare when both objects have connections') - ... return cmp( - ... (self.object._p_jar.db().database_name, self.object._p_oid), - ... (other.object._p_jar.db().database_name, other.object._p_oid), - ... ) - ... - - Here's a simple integer identifier tool. - - >>> import persistent - >>> import BTrees - >>> class Registry(persistent.Persistent): # see zope.app.intid - ... def __init__(self, family=BTrees.family32): - ... self.family = family - ... self.ids = self.family.IO.BTree() - ... self.refs = self.family.OI.BTree() - ... def getId(self, obj): - ... if not isinstance(obj, persistent.Persistent): - ... raise ValueError('not a persistent object', obj) - ... if obj._p_jar is None: - ... self._p_jar.add(obj) - ... ref = Reference(obj) - ... id = self.refs.get(ref) - ... if id is None: - ... # naive for conflict resolution; see zope.app.intid - ... if self.ids: - ... id = self.ids.maxKey() + 1 - ... else: - ... id = self.family.minint - ... self.ids[id] = ref - ... self.refs[ref] = id - ... return id - ... def __contains__(self, obj): - ... if (not isinstance(obj, persistent.Persistent) or - ... obj._p_oid is None): - ... return False - ... return Reference(obj) in self.refs - ... def getObject(self, id, default=None): - ... res = self.ids.get(id, None) - ... if res is None: - ... return default - ... else: - ... return res.object - ... def remove(self, r): - ... if isinstance(r, (int, long)): - ... self.refs.pop(self.ids.pop(r)) - ... elif (not isinstance(r, persistent.Persistent) or - ... r._p_oid is None): - ... raise LookupError(r) - ... else: - ... self.ids.pop(self.refs.pop(Reference(r))) - ... - >>> registry = root['registry'] = Registry() - - >>> import transaction - >>> transaction.commit() - -.. [#verifyObjectTransitive] The TransposingTransitiveMembership indexes - provide ISearchIndex. - - >>> from zope.interface.verify import verifyObject - >>> import zc.relation.interfaces - >>> index = list(catalog.iterSearchIndexes())[0] - >>> verifyObject(zc.relation.interfaces.ISearchIndex, index) - True - -.. [#silliness] In _2001: A Space Odyssey_, many people believe the name HAL - was chosen because it was ROT25 of IBM.... I cheat a bit sometimes and - use ROT1 because the result sounds better. - -.. [#show_remaining_token_methods] For what it's worth, here are some small - examples of the remaining token-related methods. - - These two are the singular versions of `tokenizeRelations` and - `resolveRelationTokens`. - - `tokenizeRelation` returns a token for the given relation. - - >>> catalog.tokenizeRelation(orgs['Zookd Corp Management']) == ( - ... registry.getId(orgs['Zookd Corp Management'])) - True - - `resolveRelationToken` returns a relation for the given token. - - >>> catalog.resolveRelationToken(registry.getId( - ... orgs['Zookd Corp Management'])) is orgs['Zookd Corp Management'] - True - - The "values" ones are a bit lame to show now, since the only value - we have right now is not tokenized but used straight up. But here - goes, showing some fascinating no-ops. - - `tokenizeValues`, returns an iterable of tokens for the values of - the given index name. - - >>> list(catalog.tokenizeValues((1,2,3), 'part')) - [1, 2, 3] - - `resolveValueTokens` returns an iterable of values for the tokens of - the given index name. - - >>> list(catalog.resolveValueTokens((1,2,3), 'part')) - [1, 2, 3] - -.. [#real_value_tokens] We can also show the values token methods more - sanely now. - - >>> original = sorted((orgs['Zookd Devs'], orgs['Ynod SAs'])) - >>> tokens = list(catalog.tokenizeValues(original, 'organization')) - >>> original == sorted(catalog.resolveValueTokens(tokens, 'organization')) - True - -.. [#verifyObjectIntransitive] The Intransitive search index provides - ISearchIndex and IListener. - - >>> from zope.interface.verify import verifyObject - >>> import zc.relation.interfaces - >>> verifyObject(zc.relation.interfaces.ISearchIndex, index) - True - >>> verifyObject(zc.relation.interfaces.IListener, index) - True - -.. [#compare_copy] Before we modify them, let's look at the copy we made. - The copy should currently behave identically to the original. - - >>> len(org_catalog) - 38 - >>> len(role_catalog) - 38 - >>> indexed = list(org_catalog) - >>> len(indexed) - 38 - >>> orgs['Zookd Devs'] in indexed - True - >>> for r in indexed: - ... if r not in role_catalog: - ... print 'bad' - ... break - ... else: - ... print 'good' - ... - good - >>> org_names = set(dir(org_catalog)) - >>> role_names = set(dir(role_catalog)) - >>> org_names - role_names - set([]) - >>> role_names - org_names - set(['org_catalog']) - - >>> def checkYnodDevsParts(catalog): - ... res = sorted(catalog.findRelations(t({None: orgs['Ynod Devs']}))) - ... if res != [ - ... orgs["Bet Proj"], orgs["Y3L4 Proj"], orgs["Ynod Devs"]]: - ... print "bad", res - ... - >>> checkYnodDevsParts(org_catalog) - >>> checkYnodDevsParts(role_catalog) - - >>> def checkOpheliaRoles(catalog): - ... res = sorted(catalog.findRelations({'principal_id': 'ophelia'})) - ... if repr(res) != ( - ... "[, " + - ... ", " + - ... "]"): - ... print "bad", res - ... - >>> checkOpheliaRoles(org_catalog) - >>> checkOpheliaRoles(role_catalog) - - >>> def checkOpheliaWriterOrganizations(catalog): - ... res = sorted(catalog.findRelations({None: zc.relation.catalog.Any( - ... catalog.findValueTokens( - ... 'organization', {'principal_id': 'ophelia', - ... 'role_id': 'writer'}))})) - ... if repr(res) != ( - ... '[, ' + - ... ', ' + - ... ', ' + - ... ', ' + - ... ', ' + - ... ', ' + - ... ']'): - ... print "bad", res - ... - >>> checkOpheliaWriterOrganizations(org_catalog) - >>> checkOpheliaWriterOrganizations(role_catalog) - - >>> def checkPrincipalsWithRolesInZookdDevs(catalog): - ... org_id = registry.getId(orgs['Zookd Devs']) - ... res = sorted(catalog.findValueTokens( - ... 'principal_id', - ... {'organization': zc.relation.catalog.any( - ... org_id, *catalog.findRelationTokens({'part': org_id}))})) - ... if res != ['abe', 'ignas', 'karyn', 'lettie', 'nancy', 'ophelia']: - ... print "bad", res - ... - >>> checkPrincipalsWithRolesInZookdDevs(org_catalog) - >>> checkPrincipalsWithRolesInZookdDevs(role_catalog) - - >>> def checkOpheliaRolesInZookdNbd(catalog): - ... res = sorted(catalog.findValueTokens( - ... 'role_id', { - ... 'organization': registry.getId(orgs['Zookd Nbd']), - ... 'principal_id': 'ophelia'})) - ... if res != ['publisher', 'reviewer', 'writer']: - ... print "bad", res - ... - >>> checkOpheliaRolesInZookdNbd(org_catalog) - >>> checkOpheliaRolesInZookdNbd(role_catalog) - - >>> def checkAbeRolesInZookdNbd(catalog): - ... res = sorted(catalog.findValueTokens( - ... 'role_id', { - ... 'organization': registry.getId(orgs['Zookd Nbd']), - ... 'principal_id': 'abe'})) - ... if res != ['publisher', 'user manager', 'writer']: - ... print "bad", res - ... - >>> checkAbeRolesInZookdNbd(org_catalog) - >>> checkAbeRolesInZookdNbd(role_catalog) - -.. [#removeDefaultQueryFactoryExceptions] You get errors by removing query - factories that are not registered. - - >>> org_catalog.removeDefaultQueryFactory(factory2) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - LookupError: ('factory not found', ) - - >>> org_catalog.removeDefaultQueryFactory(None) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - LookupError: ('factory not found', None) - -.. [#copy_unchanged] Changes to one copy should not affect the other. That - means the role_catalog should still work as before. - - >>> len(org_catalog) - 13 - >>> len(list(org_catalog)) - 13 - - >>> len(role_catalog) - 38 - >>> indexed = list(role_catalog) - >>> len(indexed) - 38 - >>> orgs['Zookd Devs'] in indexed - True - >>> orgs['Zookd Devs'] in role_catalog - True - - >>> checkYnodDevsParts(role_catalog) - >>> checkOpheliaRoles(role_catalog) - >>> checkOpheliaWriterOrganizations(role_catalog) - >>> checkPrincipalsWithRolesInZookdDevs(role_catalog) - >>> checkOpheliaRolesInZookdNbd(role_catalog) - >>> checkAbeRolesInZookdNbd(role_catalog) - .. [#administrivia] You can add listeners multiple times. @@ -1283,3 +1275,12 @@ TODO make sure that intransitive copy looks the way we expect ... LookupError: ('index not found', ) + +.. ......... .. +.. Footnotes .. +.. ......... .. + + +.. [#silliness] In _2001: A Space Odyssey_, many people believe the name HAL + was chosen because it was ROT25 of IBM.... I cheat a bit sometimes and + use ROT1 because the result sounds better.